JavaScript 类型系统

类型动静态强调的是静态类型检测,类型强弱强调的是隐式类型转换,JavaScript 是一门动态类型(dynamically typed)且弱类型(weakly typed)语言,既不对类型进行静态检测,又允许隐式类型转换。

1
2
3
let foo = 42; // foo 是 number
foo = 'hello'; // foo 是 string
foo = true; // foo 是 boolean
1
2
10 + ''; // number 转成了 string
!!1; // number 转成了 boolean

数据类型

JavaScript 中一共有 8 种内置(Built-In)数据类型,其中包括 7 种原始类型和 1 种对象类型。

原始类型(Primitive types)

原始类型也被称为基本类型,表示不能再细分下去的类型,具有原子性。

1
2
3
4
5
6
7
* string: 一串表示文本值的字符序列
* number: 整数或浮点数,还有一些特殊值(-Infinity、+Infinity、NaN)
* boolean: 包含两个值 true 和 false
* null: 只包含一个值 null
* undefined: 只包含一个值 undefined
* symbol: 一种实例是唯一且不可改变的数据类型
* bigInt

按内存分配方式,由于这些类型被存储在栈内存中,也被称为值类型。又由于这些类型的值是不可变的,又被称为不可变类型,原始类型是不可变的,没有任何方法可以直接改变其值,只会生成新的值。

1
2
3
4
5
6
7
8
9
const foo = 'hello';
foo.substr(1);
foo.toLowerCase(1);
foo[0] = 1;
console.log(foo); // hello

// 栈内存原始空间中的值并没有改变,只是开辟了一块新空间,将变量名指向新的空间
foo += 'world';
console.log(foo); // hello world

内存模型图:

1
2
3
4
stack(栈)         |  heap(堆)
---------------------------------
'hello' |
foo 'hello world' |
  • Number

JavaScrip 使用 IEEE 754 标准的双精度(double) 64 位(64 比特 bit)浮点数来存储数字。该标准定义了浮点数的格式,最大最小范围,以及超过范围的舍入方式等规范。JavaScript 中的 Number 类型的实际上是双精度 64 位的浮点型。

这种表示方式使用 64 位二进制格式,共 8 个字节,其中,63 位存储符号(正负),52 到 62 存储指数,0 到 51 存储尾数(数字)。

1
2
s | eee eeeeeeee | ffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff
1 11 52

一个数字的范围只能在 -(2^53-1)2^53-1-9007199254740991 ~ 9007199254740991,即二进制 0b五十三个1)之间,如果十进制转为二进制,有限或无限超出了其表示范围,则会被舍入。这也是导致小数和大整数出现精度丢失的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 小数
// 加
0.1 + 0.2 // 0.30000000000000004
2.22 + 0.1 // 2.3200000000000003
// 减
0.3 - 0.2 // 0.09999999999999998
1.5 - 1.2 // 0.30000000000000004
// 乘
19.9 * 100 // 1989.9999999999998,19.9 * 10 * 10 结果 位 1990
1306377.64 * 100 // 130637763.99999999
0.7 * 180 // 125.99999999999999
// 除
0.3 / 0.1 // 2.9999999999999996
0.69 / 10 // 0.06899999999999999
// toFixed 不会四舍五入
1.335.toFixed(2) // 1.33

// 大整数
// 加
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true
// 乘
99999999999999999 * 1 // 100000000000000000
// 比较
1000000000000000128 === 1000000000000000129 // true
9999999999999999 + 1 === 9999999999999999 // true

0.1 + 0.2 问题为例,十进制小数 0.10.2 正好被转成无限循环的二进制小数,为了满足规范,做舍入,最终导致精度损失。

1
2
0.1 => 0.0001 1001 1001 1001…(无限循环小数)
0.2 => 0.0011 0011 0011 0011…(无限循环小数)
1
2
(1/2).toString(2) // 0.1
(1/10).toString(2) // 0.0001100110011001100110011001100110011001100110011001101...

所有采用 IEEE-754 浮点数标准的语言都存在这个问题,只不过 JavaScript 是一门动态类型的语言,没有静态语音那样对浮点数有严格的数据类型(比如 Java 中的 flotdouble),精度误差问题显得格外突出。

注:IEEE-754 中还有单精度浮点数标准。

对象类型(Object types)

1
* object

声明一个对象通常有以下几种方式:

1
2
3
4
const foo = {}; // 字面量形式,推荐
const foo = new Object(); // new 调用
const foo = Object(); // 与 new 调用相同
cosnt foo = Object.create(null); // 空对象

由于 ArrayStringNumberBooleanFunctionDateRegExp… 这些对象都是 Object 对象的子类,所以全部归类为 object 类型。

1
Array.prototype.__proto__ === Object.prototype; // true

按内存分配方式,由于对象类型被存储在堆内存中,也被称为引用类型。又由于值是可变的,又被称为可变类型。

1
2
3
4
5
6
const foo = {
msg: 'hello',
};
const bar = foo;
foo.msg = 'hello world';
bar; // {msg: 'hello world'}
1
2
3
4
const foo = [1, 2, 3];
foo[foo.length] = foo.length + 1; // [1, 2, 3, 4]
foo.push(5); // [1, 2, 3, 4, 5]
foo.length = 0; // []

内存模型图:

1
2
3
4
stack(栈)      |  heap(堆)
---------------------------------
foo 0x01 ---> | 0x01 hello world
bar 0x01 ---> |
  • 包装对象

基本数据类型调用对象方法或访问属性时,,。在使用完这些方法和属性后,包装对象会被销毁,

包装对象即原始类型的“包装对象(Wapper object)”。stringnumberboolean 这三种原始类型值在引用属性和方法时,JavaScript 引擎会创建一个临时包装对象 new String()new Number()new Boolean(),该对象具有对应的方法和属性,一旦引用结束,便会销毁这个临时包装对象,返回原始的基本数据类型。这三个对象是引擎内部用的,不要在代码中使用。

不是所有的原始类型都有包装对象,nullundefined 就没有包装对象,访问它们的属性会报类型错误。

1
2
3
4
5
6
7
const n = 42
console.log(n.toFixed(2)) // 作为基本类型的 n 没有 toFixed 方法,引擎会创建临时包装对象处理 toFixed 调用

// let temp = new Number(42)
// value = temp.toFixed(2)
// 删除 temp
// 返回 value

不要因为原始类型可以转化为对应的包装对象,而得出 “JavaScript 中一切皆对象” 这一错误的论断,原始类型和原始类型的包装对象是两个东西。

1
2
3
typeof 'seymoe'; // 'string'
typeof new String('seymoe'); // 'object'
typeof String('seymoe'); // 'string'
1
2
3
const foo = 'hello';
foo.bar = 'word'; // foo 是原始类型,添加属性是无效的
foo.bar; // 原始类型就是原始类型,如果万物皆对象这里不会返回 undefined

注:NumberStringBoolean 只有作为构造函数调用(加 new 调用),才会返回包装对象,如果当作普通函数调用,返回的是字面量,这常用于将任意类型的值转为数值、字符串和布尔值。

类型检测

typeof

typeof 操作符被用来检测数据的类型,其返回一个字符串,代表操作数的类型。

1
2
3
4
5
6
7
8
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof 1337 // 'number'
typeof 'foo' // 'string'
typeof {} // 'object'
typeof parseInt // 'function'
typeof Symbol() // 'symbol'
typeof 127n // 'bigint'

typeof 能检测除 null 和值为 functionobject 类型外的所有类型。null 由于历史设计失误,typeof 返回了 object(如果从 Java 的角度所有的值皆对象,null 是 Object 的零值,也能自洽),而 function 由于很特殊(构造函数)typeof 特殊处理返回了 function

typeof 不能检测除 function 外的 object 子类型,如果想要检测子类型,则需要使用 instanceofObject.prototype.toString 方法。

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

1
2
3
[] instanceof Array // true
({}) instanceof Object // true
(()=>{}) instanceof Function // true

instanceof 用来检测类型有缺陷,首先它会检测整个原型链,另外,它不能在 iframe 中跨页面检测,因为不在同一个 window 对象下,原型链无法关联。

1
[] instanceof Object; // true

[].__proto__ === Array.prototype,而又 Array 属于 Object 子类型,即 Array.prototype.__proto__ === Object.prototype,最终 [].__proto__.__proto__ === Object.prototype

1
2
3
4
5
6
7
const arr = [1, 2, 3];
window.frames[0].foo(arr);

// iframe
function foo(data) {
console.log(data instanceof Array); // false
}

实例化 data 时的 Array 跟 iframe 里的 Array 属于不同的 window 对象下,不是同一个类。

Object.prototype.toString

终极解决方案,不但能检测基本类型,还能检测 object 子类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Object.prototype.toString.call('hello'); // '[object String]'
Object.prototype.toString.call(1); // '[object Number]'
Object.prototype.toString.call(true); // '[object Boolean]'
Object.prototype.toString.call(null); // '[object Null]'
Object.prototype.toString.call(undefined); // '[object Undefined]'
Object.prototype.toString.call(Symbol()); // '[object Symbol]'
Object.prototype.toString.call(BigInt(2)) // "[object BigInt]"
Object.prototype.toString.call({}); // '[object Object]'
Object.prototype.toString.call([]); // '[object Array]'
Object.prototype.toString.call(() => {}); // '[object Function]'

Object.prototype.toString.call(new Date()); // '[object Date]'
Object.prototype.toString.call(new RegExp());
Object.prototype.toString.call(Math); // '[object Math]'
Object.prototype.toString.call(new Set()); // '[object Set]'
Object.prototype.toString.call(new WeakSet()); // '[object WeakSet]'
Object.prototype.toString.call(new Map()); // '[object Map]'
Object.prototype.toString.call(new WeakMap()); // '[object WeakMap]'

类型转换

将值从一种类型转换为另一种类型被称为类型转换,JavaScript 中类型转换都属于强制类型转换,其又分为隐式强制类型转换和显式强制类型转换。参考 《You Dont Know JS》

隐式类型转换

JavaScript 中常见的隐式类型转换有:

  • + 运算符

+ 运算符中其他所有类型会被隐式转换(toString)为字符串。

1
'1' + 1 // 数字会隐式转换为字符串
  • 逻辑运算符、条件语句

逻辑运算符、条件语句中其他所有类型都会被隐式转换布尔值。

1
2
3
4
&&、||
if ('1') {}
if (null) {}
while (1) {}
  • 算术运算、比较运算

在算术运算符 +-*/ 和比较运算符 <>== 中布尔值会隐式转换为数字

1
2
true + true // 2
true > false // true
1
2
3
4
5
6
7
8
9
10
11
12
13
true + 0; // 1
'1' + ['hello', 1]; // 1hello,1
{} + []; // 0
[] + {}; // [object Object]
const obj = {};
'2' + {}; // 2[object Object]

const obj = {
toString() {
return 'hello';
},
};
'2' + obj; // 2hello
1
2
3
4
5
true == 1;
false === 0; // false
false + false === 0; // true
[] == ![];
[undefined] == false;

显式类型转换

  • toString

基础类型强制转为 string 类型在规范中明确说明了,也比较符合直觉。

1
2
3
4
5
String(1); // "1"
String(true); // "true"
String(null); // "null"
String(undefined); // "undefined"
String(Symbol('hello')); // "Symbol(hello)"

object 类型在强制转换为 string 类型的时候,调用的是该类型原型上的 toString 方法,而 object 的各个子类型基本都重写了 toString 方法,所以在进行 toString 操作的时候表现有差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
String({ a: 2 }); // "[object Object]"
String([1, 2]); // "1,2"
String(/reg/g); // "/reg/g"

const arr = [1, 2];
arr.toString(); // "1,2"
String(arr); // "1,2"

// 重写toString
arr.toString = function() {
return this.join('/');
};
String(arr); // "1/2"
  • toNumber
1
2
3
4
5
6
7
8
ToNumber('42'); // 42
ToNumber('123px'); // NaN
ToNumber(''); // 0
ToNumber(' '); // 0
ToNumber(true); // 1
ToNumber(false); // 0
ToNumber(null); // 0
ToNumber(undefined); // NaN