JS探幽——从数据类型到包装器对象

本文介绍了 JavaScript 的数据类型、基本数据类的包装器对象,以及对应的“装箱”机制,对一些违反直觉的语言表现进行了解释和查证。

在阅读本文前思考下面的代码,考虑它们的预期处理结果是什么。如果你能完全预料和理解这种表现,那么无需拨冗阅读本文了。如果一些表现使你疑惑,也许能从本文得到答案。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
const v1 = 'a'
const v2 = String('a')
const v3 = new String('a')

/*-- part 1 --*/

console.log(v1) // 'a'
console.log(v2) // 'a'
console.log(v3) // String {'a'}

typeof v1 // 'string'
typeof v2 // 'string'
typeof v3 // 'object'

v1 instanceof String // false
v2 instanceof String // false
v3 instanceof String // true

v1 === v2 // true
v1 === v3 // false

/*---------------*/

/*-- part 2 --*/

v1 == v2 // true
v1 == v3 // true

v1.__proto__ === String.prototype // true
v2.__proto__ === String.prototype // true
v3.__proto__ === String.prototype // true

v1[1] = 'b'
console.log(v1[1]) // undefined
v2[1] = 'b'
console.log(v2[1]) // undefined
v3[1] = 'b'
console.log(v3[1]) // 'b'

对于数字类型(Number)和布尔类型(Boolean)有以上类似的表现。

8 种数据类型

根据最新的 ECMAScript 标准,定义了 8 种数据类型:

  • 7 种基本数据类型(Primitive value)
    • 布尔值(Boolean),有两个值分别是:truefalse
    • null,一个表明 null 值的特殊关键字
    • undefined,表示变量未赋值,同样是一个特殊的关键字
    • 数字(Number),表示整数或浮点数。使用双精度浮点类型
    • 任意精度的整数(BigInt),用于大整数的计算。 日前已经成为 ES 的正式标准被主流浏览器实现
    • 字符串(String)
    • 代表(Symbol),ES6 中新增的类型。一般用以获取一个唯一的标识符
  • 对象(Object)

标准中没列出数组、函数等其他数据类型,因为它们都被视为对象类型。作为 Javascript 开发者一定听过广为流传的“一切都是对象”这句话,是否意味着 7 种基本数据类型也是对象呢?若如此,标准中为何要将他们在对象类型外单独列出呢?若不是,为何可以调用字符串的 length 属性和 toLocaleLowerCase() toLocaleUpperCase() 等方法?内置的 Number String Boolean 对象又是何用呢?

暂且不管“一切都是对象”这句话到底是什么意思,但需要明确的是 7 种基本类型既不是对象也没有属性和方法1!那如何解释 'a'.length 等对基本数据类型进行方法和属性的调用?这就涉及到了 JS 的基本类型包装器对象和“自动装箱”机制。在此之前,我们先弄明白文章开头例子里几种变量的声明方式,以及他们分别生成了什么类型的变量。

变量的声明

上文只是抽象地指出了哪些数据类型是基本数据类型,但没有给出具体的声明语法。

对于数字、字符串、布尔类型以及 undefinednull,可以直接使用字面量(Literals)声明。例如 val1 = 1val2 = 'a'val3 = true。其中 1'a'true 分别为数字字面量、字符串字面量和布尔字面量,由他们所赋值的变量 val1val2val3 即为数字型、字符串型、布尔型,属于基本数据类型。对于 BigInt 类型,可以在数字末尾加上 n 来使用字面量赋值:const theBiggestInt = 9007199254740991n

对于 Symbol 类型没有对应的字面量语法,则使用 Symbol() 函数返回一个 Symbol 类型的值,例如 const symbol1 = Symbol()。同样地,数字、字符串、布尔、BigInt 类型的值也可以通过直接调用函数 Number()String()Boolean()BigInt() 来获取。

以上两种方法所举例子中所声明的变量均为基本数据类型。而本文开头的示例代码中,还使用了 new 关键字 + 构造函数生成的变量的语法。在这种语法下所生成的变量则为对象类型。例如 new Number(1) 语法赋值的变量即为 Number 对象,属于对象数据类型。通过这种方式生成的对象也相应的被称为对应基本数据类型的包装器对象

需要注意的是,在 ES 标准中已不允许围绕基本数据类型显性地生成包装器对象。例如 new BigInt()new Symbol() 会报错 Uncaught TypeError: Symbol/BigInt is not a constructor。而 new Number()new String()new Boolean() 的语法由于历史原因被保留下来2。但在大多数情况下,这种语法都是没有必要的。(SymbolBigInt 的包装器对象仍可以通过其他方法手动生成,查看参考资料 [2] 2。)

至此,便可以解释示例代码中 part 1 的表现了。前两种声明方式生成了基本数据类型——字符串类型,第三种声明方式生成了包装器对象。因此才会有前两者的类型为 sting 而后者为 Object;同时前两者既不是对象,则使用 instanceof 找不到任何构造函数;最后基本数据类型完全判等时只比较值,故有前两者完全相等,而不等于第三者。

包装器对象和“装箱”

上文介绍了基本数据类型的包装器对象,即对基本数据类型进行“包装”生成对应的对象类型,以赋予其特定的属性和方法。除了 undefined 和 null 外,所有的基本类型都有对应的包装器对象:

  • Boolean
  • Number
  • String
  • Symbol
  • BigInt

使用包装对象的 valueOf() 方法则返回该包装对象的原始基本类型值。

使用 new 关键词可以显性地构造包装器对象,但在对于基本数据类型进行操作时包装器对象常常被隐性地构造。

例如在尝试对一个字符串变量进行属性读取和方法调用时,则首先将该字符串包装成一个 String 对象,然后再调用该对象的方法和属性。而该对象在被调用完成后即被销毁,因此原来基本类型变量的值并不受影响。这就解释了为何在设置 v1[1] = 'b'v1 的值仍为 'a'。以及调用 v1.__proto__ 仍能得到 String.prototype

这种在必要时对基本数据类型进行“包装”生成对象的机制常被成为“装箱”。同样地,当试图对包装对象进行基本数据类型的操作时,包装器的 valueOf() 方法会被调用,用其返回的基本类型值进行运算。这种机制有人称之为“解箱”。

示例代码中 v1 == v3 之所以为 true,即是因为在非严格判等时会进行隐式类型转换,调用了对象 v3valueOf() 方法进行比较3

思考

以下代码有如何表现呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const obj1 = {
valueOf() { return 1 },
toString() { return '2' }
}
const obj2 = {
toString() { return '2' }
}
const val1 = 1
const val2 = 2
const val3 = '2'

obj1 == val1
obj1 == val2
obj1 == val3

obj2 == val2
obj2 == val3

那么“一切皆是对象”是不是说 JavaScript 中的一切变量都可以看作对象来使用呢?或者说一切实现围绕对象进行呢?

参考资料

[1] Primitive - MDN Web Docs Glossary: Definitions of Web-related terms | MDN[EB/OL]. [2022-09-06]. https://developer.mozilla.org/en-US/docs/Glossary/Primitive.

[2] Symbol - JavaScript | MDN[EB/OL]. [2022-09-06]. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#description.

[3] Equality comparisons and sameness - JavaScript | MDN[EB/OL]. [2022-09-06]. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#loose_equality_using