的数据类型、基本数据类的包装器对象,以及对应的“装箱”机制,对一些违反直觉的语言表现进行了解释和查证。
在阅读本文前思考下面的代码,考虑它们的预期处理结果是什么。如果你能完全预料和理解这种表现,那么无需拨冗阅读本文了。如果一些表现使你疑惑,也许能从本文得到答案。
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' )console .log (v1) console .log (v2) console .log (v3) typeof v1 typeof v2 typeof v3 v1 instanceof String v2 instanceof String v3 instanceof String v1 === v2 v1 === v3 v1 == v2 v1 == v3 v1.__proto__ === String .prototype v2.__proto__ === String .prototype v3.__proto__ === String .prototype v1[1 ] = 'b' console .log (v1[1 ]) v2[1 ] = 'b' console .log (v2[1 ]) v3[1 ] = 'b' console .log (v3[1 ])
对于数字类型(Number)和布尔类型(Boolean)有以上类似的表现。
8 种数据类型
根据最新的 ECMAScript 标准,定义了 8 种数据类型:
7 种基本数据类型(Primitive value)
布尔值(Boolean),有两个值分别是:
true
和
false
null,一个表明 null 值的特殊关键字
undefined,表示变量未赋值,同样是一个特殊的关键字
数字(Number),表示整数或浮点数。使用双精度浮点类型
任意精度的整数(BigInt),用于大整数的计算。 日前已经成为 ES
的正式标准被主流浏览器实现
字符串(String)
代表(Symbol),ES6 中新增的类型。一般用以获取一个唯一的标识符
对象(Object)
标准中没列出数组、函数等其他数据类型,因为它们都被视为对象类型。作为
Javascript 开发者一定听过广为流传的“一切都是对象”这句话,是否意味着 7
种基本数据类型也是对象呢?若如此,标准中为何要将他们在对象类型外单独列出呢?若不是,为何可以调用字符串的
length
属性和
toLocaleLowerCase()
toLocaleUpperCase()
等方法?内置的
Number
String
Boolean
对象又是何用呢?
暂且不管“一切都是对象”这句话到底是什么意思,但需要明确的是
7
种基本类型既不是对象也没有属性和方法
1
!那如何解释
'a'.length
等对基本数据类型进行方法和属性的调用?这就涉及到了 JS
的基本类型包装器对象和“自动装箱”机制。在此之前,我们先弄明白文章开头例子里几种变量的声明方式,以及他们分别生成了什么类型的变量。
变量的声明
上文只是抽象地指出了哪些数据类型是基本数据类型,但没有给出具体的声明语法。
对于数字、字符串、布尔类型以及
undefined
和
null
,可以直接使用字面量(Literals)声明。例如
val1 =
1
,
val2 = 'a'
,
val3 = true
。其中
1
、
'a'
、
true
分别为数字字面量、字符串字面量和布尔字面量,由他们所赋值的变量
val1
、
val2
、
val3
即为数字型、字符串型、布尔型,属于基本数据类型。对于 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
。但在大多数情况下,这种语法都是没有必要的。(
Symbol
和
BigInt
的包装器对象仍可以通过其他方法手动生成,查看参考资料 [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
,即是因为在非严格判等时会进行隐式类型转换,调用了对象
v3
的
valueOf()
方法进行比较
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