JavaScript 语言的每一个值都属于某一种数据类型。JavaScript 语言规定了 7 种语言类型。语言类型广泛用于变量、函数参数、表达式、函数返回值等场合。根据最新的语言标
准,这 7 种语言类型是:
1. Undefined;
2. Null;
3. Boolean;
4. String;
5. Number;
6. Symbol;
7. Object。
除了 ES6 中新加入的 Symbol 类型,剩下 6 种类型都是我们日常开发中的老朋友了,但是,要想回答文章一开始的问题,我们需要重新认识一下这些老朋友,下面我们就来从简单到复杂,重新学习一下这些类型。
Undefined、Null
我们的第一个问题,为什么有的编程规范要求用 void 0 代替 undefined?现在我们就分别来看一下。
Undefined 类型表示未定义,它的类型只有一个值,就是 undefined。任何变量在赋值前是 Undefined 类型、值为 undefined,一般我们可以用全局变量 undefined(就是名为undefined 的这个变量)来表达这个值,或者 void 运算来把任意一个表达式变成undefined 值。但是呢,因为 JavaScript 的代码 undefined 是一个变量,而并非是一个关键字,这是JavaScript 语言公认的设计失误之一,所以,我们为了避免无意中被篡改,我建议使用void 0 来获取 undefined 值。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 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。Note:现行的字符集国际标准,字符是以 Unicode 的方式表示的,每一个Unicode 的码点表示一个字符,理论上,Unicode 的范围是无限的。UTF 是
Unicode 的编码方式,规定了码点在计算机中的表示方法,常见的有 UTF16和 UTF8。 Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 - U+FFFF)的码点被称为基本字符区域(BMP)。JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。JavaScript 字符串把每个 UTF16 单元当作一个字符来处理,所以处理非 BMP(超出U+0000 - U+FFFF 范围)的字符时,你应该格外小心。JavaScript 这个设计继承自 Java,最新标准中是这样解释的,这样设计是为了“性能和尽可能实现起来简单”。因为现实中很少用到 BMP 之外的字符。
Number
下面,我们来说说 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 类型无法用 ==(=== 也不行) 来比较,一段著名的代码,这也正是我们第三题的问题,为什么在 JavaScript 中,0.1+0.2 不能=0.3
console.log( 0.1 + 0.2 == 0.3);
console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);
检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。这段代码结果就是 true 了。
Symbol
Symbol 是 ES6 中引入的新类型,它是一切非字符串的对象 key 的集合,在 ES6 规范中,整个对象系统被用 Symbol 重塑。在后面的文章中,我会详细叙述 Symbol 跟对象系统。这里我们只介绍 Symbol 类型本身:它有哪些部分,它表示什么意思,以及如何创建 Symbol 类型。Symbol 可以具有字符串类型的描述,但是即使描述相同,Symbol 也不相等。我们创建 Symbol 的方式是使用全局的 Symbol 函数。例如:
var mySymbol = Symbol("my symbol");
一些标准中提到的 Symbol,可以在全局的 Symbol 函数的属性中找到。例如,我们可以使用 Symbol.iterator 来自定义 for…of 在对象上的行为:
var o = new Object o[Symbol.iterator] = function() { var v = 0 return { next: function() { return { value: v++, done: v > 10 } } } }; for(var v of o) console.log(v); // 0 1 2 3 ... 9
代码中我们定义了 iterator 之后,用 for(var v of o) 就可以调用这个函数,然后我们可以根据函数的行为,产生一个 for…of 的行为。这里我们给对象 o 添加了 Symbol.iterator 属性,并且按照迭代器的要求定义了一个 0 到10 的迭代器,之后我们就可以在 for of 中愉快地使用这个 o 对象啦。这些标准中被称为“众所周知”的 Symbol,也构成了语言的一类接口形式。它们允许编写与语言结合更紧密的 API。
Object
Object 是 JavaScript 中最复杂的类型,也是 JavaScript 的核心机制之一。Object 表示对象的意思,它是一切有形和无形物体的总称。下面我们来看一看,为什么给对象添加的方法能用在基本类型上?在 JavaScript 中,对象的定义是“属性的集合”。属性分为数据属性和访问器属性,二者都是 key-value 结构,key 可以是字符串或者 Symbol 类型。关于对象的机制,后面会有单独的一篇来讲述,这里我重点从类型的角度来介绍对象类型。提到对象,我们必须要提到一个概念:类。因为 C++ 和 Java 的成功,在这两门语言中,每个类都是一个类型,二者几乎等同,以至于很多人常常会把 JavaScript 的“类”与类型混淆。事实上,JavaScript 中的“类”仅仅是运行时对象的一个私有属性,而 JavaScript 中是无法自定义类型的。JavaScript 中的几个基本类型,都在对象类型中有一个“亲戚”。它们是:
Number;
String;
Boolean;
Symbol。
所以,我们必须认识到 3 与 new Number(3) 是完全不同的值,它们一个是 Number 类型, 一个是对象类型。Number、String 和 Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。JavaScript 语言设计上试图模糊对象和基本类型之间的关系,我们日常代码可以把对象的方法在基本类型上使用,比如:
console.log("abc".charAt(0)); //a
甚至我们在原型上添加方法,都可以应用于基本类型,比如以下代码,在 Symbol 原型上添加了 hello 方法,在任何 Symbol 类型变量都可以调用。
Symbol.prototype.hello = () => console.log("hello"); var a = Symbol("a"); console.log(typeof a); //symbol,a 并非对象 a.hello(); //hello,有效
所以我们文章开头的问题,答案就是. 运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。