【JS基础系列】数据类型和类型转换

387 阅读4分钟

JS的数据类型

Undefined、Null、Boolean、String、Number、Symbol、Bigint、Object

Undefined

Undefined 类型表示未定义,它的类型只有一个值,就是 undefined。

undefined是全局对象的一个属性。也就是说,它是全局作用域的一个变量。undefined的最初值就是原始数据类型undefined。undefined是一个不能被配置(non-configurable),不能被重写(non-writable),不能被枚举(non-enumerable)的的属性。

值为undefined的情况:

  • 一个没有被赋值的变量的类型是undefined
  • 一个函数如果没有使用return语句指定返回值,就会返回一个undefined值

因为 JavaScript 的代码 undefined 是一个变量,而并非是一个关键字,这是 JavaScript 语言公认的设计失误之一,所以,为了避免无意中被篡改,建议使用 void 0 来获取 undefined 值。

Null

Null 表示的是:“定义了但是为空”。所以,在实际编程时,我们一般不会把变量赋值为 undefined,这样可以保证所有值为 undefined 的变量,都是从未赋值的自然状态。

Null 类型也只有一个值,就是 null,它的语义表示空值,与 undefined 不同,null 是 JavaScript 关键字,所以在任何代码中,你都可以放心用 null 关键字来获取 null 值。

Boolean

Boolean 类型有两个值, true 和 false,它用于表示逻辑意义上的真和假,同样有关键字 true 和 false 来表示两个值。这个类型很简单,我就不做过多介绍了。

String

  1. String用于表示文本数据,String的最大长度是 2^53 - 1,但是,这个所谓最大长度,并不是字符数。因为 String 的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。

现行的字符集国际标准,字符是以 Unicode 的方式表示的,每一个 Unicode 的码点表示一个字符,理论上,Unicode 的范围是无限的。UTF 是 Unicode 的编码方式,规定了码点在计算机中的表示方法,常见的有 UTF16 和 UTF8。 Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 - U+FFFF)的码点被称为基本字符区域(BMP)。

JavaScript 字符串把每个 UTF16 单元当作一个字符来处理,所以处理非 BMP(超出 U+0000 - U+FFFF 范围)的字符时,应该格外小心。

Number

Number 类型表示我们通常意义上的“数字”。这个数字大致对应数学中的有理数。当然,在计算机中,有一定的精度限制。

JavaScript 中的 Number 类型有 18437736874454810627(即 2^64-2^53+3) 个值。

JavaScript 中的 Number 类型基本符合 IEEE 754-2008 规定的双精度浮点数规则,但是 JavaScript 为了表达几个额外的语言场景(比如不让除以 0 出错,而引入了无穷大的概念),规定了几个例外情况:

  • NaN,占用了 9007199254740990,这原本是符合 IEEE 规则的数字;
  • Infinity,无穷大;
  • -Infinity,负无穷大。 ⚠️ JavaScript 中有 +0 和 -0,在加法类运算中它们没有区别,但是除法的场合则需要特别留意区分,“忘记检测除以 -0,而得到负无穷大”的情况经常会导致错误,而区分 +0 和 -0 的方式,正是检测 1/x 是 Infinity 还是 -Infinity。

根据双精度浮点数的定义,Number 类型中有效的整数范围是 -0x1fffffffffffff 至 0x1fffffffffffff,所以 Number 无法精确表示此范围外的整数。

同样根据浮点数的定义,非整数的 Number 类型无法用 ==(=== 也不行) 来比较。

主要原因是小数的二进制表示时就有误差。

1.因为十进制转二进制的小数部分的原则是乘2取整顺序表达,这边会发现0.1 0.2 0.3这三个数都不能有限表达,会产生无限位数。

2.固定位数二进制无法表示无限循环序列(截断部分会进行进位或者舍去,这边会产生误差)

console.log( 0.1 + 0.2 == 0.3);

// false
// 浮点数运算的精度问题导致等式左右的结果并不是严格相等

// 正确的比较方法是使用 JavaScript 提供的最小精度值,检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。
 console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);

Symbol

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。

对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。但是要注意,Symbol 值作为对象属性名时,不能用点运算符。

Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

创建 Symbol 的方式是使用全局的 Symbol 函数。

var mySymbol = Symbol("my symbol");

typeof mySymbol
// "symbol"

var mySymbol2 = Symbol("my symbol");

mySymbol === mySymbol2; // false

mySymbol.toString() // "Symbol(my symbol)"

Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值。Symbol.for()为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。

Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false

我们可以使用 Symbol.toStringTag 来自定义 Object.prototype.toString 的行为:

var o = { [Symbol.toStringTag]: "MyObject" };

console.log(o + "");
// [object MyObject]

Object.prototype.toString.call(o);
// "[object MyObject]"

Object

Object 表示对象,在 JavaScript 中,对象的定义是“属性的集合”。属性分为数据属性和访问器属性,二者都是 key-value 结构,key 可以是字符串或者 Symbol 类型。

⚠️ 3 与 new Number(3) 是完全不同的值,它们一个是 Number 类型, 一个是对象类型。

  • Number、String 和 Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。
  • Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。

类型转换

ToNumber

字符串到数字的类型转换,存在一个语法结构,类型转换支持十进制、二进制、八进制和十六进制。

JavaScript 支持的字符串语法还包括正负号科学计数法,可以使用大写或者小写的 e 来表示。

parseInt 在不传入第二个参数的情况下,只支持 16 进制前缀“0x”,而且会忽略非数字字符,也不支持科学计数法。所以在任何环境下,都建议传入 parseInt 的第二个参数,而 parseFloat 则直接把原字符串作为十进制来解析,它不会引入任何的其他进制。

多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。

参数结果
undefinedNaN
null+0
布尔值true被转换为1,false转换为0
数字无需转换
字符串由字符串解析为数字,无法解析的,为NaN,例如"324"被转换为324,"324a"则被转换为NaN

ToString

参数结果
undefined"undefined"
null"null"
布尔值"true"或者"false"
数字转换为字符串
字符串无需转换

装箱转换

每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的 call 方法来强迫产生装箱。

每一类装箱对象皆有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:

var symbolObject = Object(Symbol("a"));
console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]

在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。

⚠️:call 本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。

拆箱转换

类型转换的内部实现是通过ToPrimitive ( input [ , PreferredType ] )方法进行转换的,这个方法的作用就是将input转换成一个非对象类型。

  • 参数preferredType是可选的,它的作用是,指出了input被期待转成的类型。如果不传preferredType进来,默认的是'number'。

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 preferredType 的值是 "string",那就先执行 toString() , 如果拿到基本类型,就返回基本类型的值,如果没有拿到基本类型的值,就再执行 valueOf() 。preferredType 的值是 “number”,则先执行 valueOf() , 后执行 toString() 。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。只有 Date 对象,preferredType 默认为 "string",其他情况下,都是 "number"。

ToPrimitive

/**
* @obj 转换的对象
* @type 期望转换的类型
*/
ToPrimitive(obj, type)

// 实现如下:
// type 是 number
var objToNumber = Number(obj.valueOf().toString())
// type是 string
var objToString = String(obj.toString().valueOf())

valuetoNumbertoStringtoBoolean
NaNNaN"NaN"false
InfinityInfinity"Infinity"true
[]0""true
[1]1"1"true
undefinedNaN"undefined"false
{}NaN"[object Object]"true
function(){}NaN"function(){}"true

在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
};
// 把o转换成字符串,o会先调用toString方法
console.log(String(o));
// toString
// valueOf
// TypeError 转换失败

// o+0, o会先调用valueOf方法
console.log(o + 1);
// valueOf
// toString
// TypeError 转换失败

// 修改toPrimitive
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}

console.log(o + "");
// toPrimitive
// hello