JavaScript 类型系统
类型动静态强调的是静态类型检测,类型强弱强调的是隐式类型转换,JavaScript 是一门动态类型(dynamically typed)且弱类型(weakly typed)语言,既不对类型进行静态检测,又允许隐式类型转换。
1 | let foo = 42; // foo 是 number |
1 | 10 + ''; // number 转成了 string |
数据类型
JavaScript 中一共有 8 种内置(Built-In)数据类型,其中包括 7 种原始类型和 1 种对象类型。
原始类型(Primitive types)
原始类型也被称为基本类型,表示不能再细分下去的类型,具有原子性。
1 | * string: 一串表示文本值的字符序列 |
按内存分配方式,由于这些类型被存储在栈内存中,也被称为值类型。又由于这些类型的值是不可变的,又被称为不可变类型,原始类型是不可变的,没有任何方法可以直接改变其值,只会生成新的值。
1 | const foo = 'hello'; |
内存模型图:
1 | stack(栈) | heap(堆) |
- Number
JavaScrip 使用 IEEE 754 标准的双精度(double) 64 位(64 比特 bit)浮点数来存储数字。该标准定义了浮点数的格式,最大最小范围,以及超过范围的舍入方式等规范。JavaScript 中的 Number
类型的实际上是双精度 64 位的浮点型。
这种表示方式使用 64 位二进制格式,共 8 个字节,其中,63 位存储符号(正负),52 到 62 存储指数,0 到 51 存储尾数(数字)。
1 | s | eee eeeeeeee | ffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff |
一个数字的范围只能在 -(2^53-1)
至 2^53-1
(-9007199254740991
~ 9007199254740991
,即二进制 0b五十三个1
)之间,如果十进制转为二进制,有限或无限超出了其表示范围,则会被舍入。这也是导致小数和大整数出现精度丢失的原因。
1 | // 小数 |
以 0.1 + 0.2
问题为例,十进制小数 0.1
和 0.2
正好被转成无限循环的二进制小数,为了满足规范,做舍入,最终导致精度损失。
1 | 0.1 => 0.0001 1001 1001 1001…(无限循环小数) |
1 | (1/2).toString(2) // 0.1 |
所有采用 IEEE-754 浮点数标准的语言都存在这个问题,只不过 JavaScript 是一门动态类型的语言,没有静态语音那样对浮点数有严格的数据类型(比如 Java 中的 flot
、double
),精度误差问题显得格外突出。
注:IEEE-754 中还有单精度浮点数标准。
对象类型(Object types)
1 | * object |
声明一个对象通常有以下几种方式:
1 | const foo = {}; // 字面量形式,推荐 |
由于 Array
、String
、Number
、Boolean
、Function
、Date
、RegExp
… 这些对象都是 Object
对象的子类,所以全部归类为 object
类型。
1 | Array.prototype.__proto__ === Object.prototype; // true |
按内存分配方式,由于对象类型被存储在堆内存中,也被称为引用类型。又由于值是可变的,又被称为可变类型。
1 | const foo = { |
1 | const foo = [1, 2, 3]; |
内存模型图:
1 | stack(栈) | heap(堆) |
- 包装对象
基本数据类型调用对象方法或访问属性时,,。在使用完这些方法和属性后,包装对象会被销毁,
包装对象即原始类型的“包装对象(Wapper object)”。string
、number
、boolean
这三种原始类型值在引用属性和方法时,JavaScript 引擎会创建一个临时包装对象 new String()
、new Number()
和 new Boolean()
,该对象具有对应的方法和属性,一旦引用结束,便会销毁这个临时包装对象,返回原始的基本数据类型。这三个对象是引擎内部用的,不要在代码中使用。
不是所有的原始类型都有包装对象,null
和 undefined
就没有包装对象,访问它们的属性会报类型错误。
1 | const n = 42 |
不要因为原始类型可以转化为对应的包装对象,而得出 “JavaScript 中一切皆对象” 这一错误的论断,原始类型和原始类型的包装对象是两个东西。
1 | typeof 'seymoe'; // 'string' |
1 | const foo = 'hello'; |
注:Number
、String
和 Boolean
只有作为构造函数调用(加 new
调用),才会返回包装对象,如果当作普通函数调用,返回的是字面量,这常用于将任意类型的值转为数值、字符串和布尔值。
类型检测
typeof
typeof
操作符被用来检测数据的类型,其返回一个字符串,代表操作数的类型。
1 | typeof undefined // 'undefined' |
typeof
能检测除 null
和值为 function
的 object
类型外的所有类型。null
由于历史设计失误,typeof
返回了 object
(如果从 Java
的角度所有的值皆对象,null
是 Object 的零值,也能自洽),而 function
由于很特殊(构造函数)typeof
特殊处理返回了 function
。
typeof
不能检测除 function
外的 object
子类型,如果想要检测子类型,则需要使用 instanceof
或 Object.prototype.toString
方法。
instanceof
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
1 | [] instanceof Array // true |
instanceof
用来检测类型有缺陷,首先它会检测整个原型链,另外,它不能在 iframe 中跨页面检测,因为不在同一个 window
对象下,原型链无法关联。
1 | [] instanceof Object; // true |
[].__proto__ === Array.prototype
,而又 Array
属于 Object
子类型,即 Array.prototype.__proto__ === Object.prototype
,最终 [].__proto__.__proto__ === Object.prototype
。
1 | const arr = [1, 2, 3]; |
实例化 data
时的 Array
跟 iframe 里的 Array
属于不同的 window
对象下,不是同一个类。
Object.prototype.toString
终极解决方案,不但能检测基本类型,还能检测 object 子类型。
1 | Object.prototype.toString.call('hello'); // '[object String]' |
类型转换
将值从一种类型转换为另一种类型被称为类型转换,JavaScript 中类型转换都属于强制类型转换,其又分为隐式强制类型转换和显式强制类型转换。参考 《You Dont Know JS》。
隐式类型转换
JavaScript 中常见的隐式类型转换有:
+
运算符
+
运算符中其他所有类型会被隐式转换(toString
)为字符串。
1 | '1' + 1 // 数字会隐式转换为字符串 |
- 逻辑运算符、条件语句
逻辑运算符、条件语句中其他所有类型都会被隐式转换布尔值。
1 | &&、|| |
- 算术运算、比较运算
在算术运算符 +
、-
、*
、/
和比较运算符 <
、>
、==
中布尔值会隐式转换为数字
1 | true + true // 2 |
1 | true + 0; // 1 |
1 | true == 1; |
显式类型转换
- toString
基础类型强制转为 string 类型在规范中明确说明了,也比较符合直觉。
1 | String(1); // "1" |
object 类型在强制转换为 string 类型的时候,调用的是该类型原型上的 toString
方法,而 object 的各个子类型基本都重写了 toString
方法,所以在进行 toString
操作的时候表现有差异。
1 | String({ a: 2 }); // "[object Object]" |
- toNumber
1 | ToNumber('42'); // 42 |