前面提到过,JavaScript模块会从运行时、文法和执行过程三个角度去剖析JS的知识体系,本篇就从运行时的角度去看JavaScript的类型系统。
运行时类型是代码实际执行过程中我们用到的类型。所有的类型数据都会属于7个类型之一。从变量、参数、返回值到表达式中间结果,任何JavaScript代码运行过程中产生的数据,都具有运行时类型。
类型
JavaScript语言的每一个值都属于某一种数据类型,JavaScript语言规定了7种语言类型,语言类型广泛用于变量、函数参数、表达式、函数返回值等场合。这7种语言类型是:
- Undefined
- Null
- Boolean
- String
- Number
- Symbol
- Object 其中,Symbol类型是ES6种新加入的。
Undefined、Null
Undefined类型表示未定义,它的类型只有一个值,就是undefined。任何变量在赋值前是Undefined类型,值为undefined。
Null类型也只有一个值,就是null,它的语义表示空值。
Undefined跟null有一定的表意差别,null表示“定义了但是为空”。在实际编程中,一般不会把变量赋值为undefined,这样可以保证所有值为undefined的变量都是从未赋值的自然状态。
Boolean
Boolean类型有两个值,true和false,它用于表示逻辑意义上的真和假。
String
String用于表示文本数据,最大长度是2^53 - 1。String的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。
JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。
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); // false
这里输出的结果是false,说明两边不相等的,这是浮点运算的特点,浮点数运算的精度问题导致等式左右的结果并不是严格相等,而是相差了个微小的值。
所以实际上,这里错误的不是结论而是比较的方法,正确的比较方法是使用JavaScript提供的最小精度值(Number.EPSILON):
console.log(Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON); // true
检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。这段代码结果就是 true了。
Symbol
Symbol 是 ES6 中引入的新类型,它是一切非字符串的对象key的集合,在ES6规范中,整个对象系统被用Symbol 重塑。
Symbol 可以具有字符串类型的描述,但是即使描述相同,Symbol也不相等。
Object
Object 是 JavaScript 中最复杂的类型,也是 JavaScript 的核心机制之一。Object表示对象的意思,它是一切有形和无形物体的总称。
在 JavaScript 中,对象的定义是“属性的集合”。属性分为数据属性和访问器属性,二者都是key-value结构,key可以是字符串或者 Symbol类型。
提到对象,我们必须要提到一个概念:类。JavaScript 中的“类”仅仅是运行时对象的一个私有属性,而JavaScript中是无法自定义类型的。
JavaScript中的几个基本类型,都在对象类型中有一个“亲戚”。它们是:
- Number;
- String;
- Boolean;
- Symbol。
所以,我们必须认识到 3 与 new Number(3) 是完全不同的值,它们一个是 Number 类型, 一个是对象类型。
Number、String和Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。
Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。
类型转换
因为JS是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉,但是如果不去理解类型转换的严格定义,很容易造成一些代码中的判断失误。
其中最为臭名昭著的是JS中的“ == ”运算,因为试图实现跨类型的比较,它的规则复杂到几乎没人可以记住。“==”的规则属于设计失误,并非语言中有价值的部分,很多实践中推荐禁止使用“==”,而要求程序员进行显式地类型转换后,用"==="比较。
其它运算,如加减乘除大于小于,也都会涉及类型转换。幸好的是,实际上大部分类型转换规则是非常简单的,如下表所示:
在上表中,较为复杂的部分是Number和String之间的转换,以及对象跟基本类型之间的转换。
补充阅读
事实上,“类型”在 JavaScript 中是一个有争议的概念。一方面,标准中规定了运行时数据类型; 另一方面,JS语言中提供了 typeof 这样的运算,用来返回操作数的类型,但 typeof 的运算结果,与运行时类型的规定有很多不一致的地方。我们可以看下表来对照一下。
在表格中,多数项是对应的,但是请注意object——Null和function——Object是特例,我们理解类型的时候需要特别注意这个区别。
从一般语言使用者的角度来看,毫无疑问,我们应该按照 typeof 的结果去理解语言的类型系统。但JS之父本人也在多个场合表示过,typeof 的设计是有缺陷的,只是现在已经错过了修正它的时机。