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
- 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 更好的选择。
参数 | 结果 |
---|---|
undefined | NaN |
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"。
/**
* @obj 转换的对象
* @type 期望转换的类型
*/
ToPrimitive(obj, type)
// 实现如下:
// type 是 number
var objToNumber = Number(obj.valueOf().toString())
// type是 string
var objToString = String(obj.toString().valueOf())
value | toNumber | toString | toBoolean |
---|---|---|---|
NaN | NaN | "NaN" | false |
Infinity | Infinity | "Infinity" | true |
[] | 0 | "" | true |
[1] | 1 | "1" | true |
undefined | NaN | "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