最近我读了 GitHub 上的一个仓库项目 《What the f*ck JavaScript?》,列举了 JavaScript 语言上比较怪癖的一些特性。东西比较多,看起来也有点杂。
其中有相当一部分内容涉及到类型转换——
- 如果根据转换形式分的话:分隐式和显式的
// 1. 隐式转换:使用 `==` 运算符比较
[] == 0 // -> true
// 2. 显式转换:比如下面显式调用了 `toString` 方法,转成字符串
[].toString() // -> ""
- 根据数据类型分的话:分基本类型转换和对象类型转换。
// 1. 基本类型转换
0 == +false;
// 2. 对象类型转换
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'
最开始我们接触这些东西的时候,有些摸不着头脑,感觉很烦的。
其实里面是有规则的,也不多。排除掉规则相对简单的基本类型转换,总结下来,实际困扰我们的核心点有两个:
- 何时发生隐式类型转换?
- 还有,对象的类型转换流程又是怎样的?
何时发生隐式类型转换?
通常发生在比较运算(==
、!=
、>
、<
)和算术运算(+
、-
、*
、/
、%
)中,而且运算符两边的操作数不是同一类型。
注: 相等运算符
==
有一条特别的规则,在规范中定义——就是null == undefined
的比较结果返回true
。
对象的类型转换流程又是怎样的?
当我们谈及对象的类型转换时,通常讲的是对象到基本类型的转换。当上述描述的场景发生时,对象转换规则就开始起作用了。
规则梳理起来并不复杂,就那么几条。因为一年前,我针对对象到基本类型转换做了简单的介绍,因此此次文章命名使用了“再谈”。
这次我会尽量详细的给大家介绍转换内部的机制,带领大家理解透对象到底是怎样转换的。
对象属性: [Symbol.toPrimitive]
对象发生类型转换时,首先会检查对象上是否存在 [Symbol.toPrimitive]
属性,如果存在的话就调用。该属性存在一些限制:它必须是一个函数,而且返回值必须是基本类型,否则就要报错:
1). 不是函数
2). 函数返回值不是基本类型
hint 值
[Symbol.toPrimitive]
在调用时,系统会自动给与一个参数 hint,这个 hint 可以理解为,此次对象发生转换的预期类型为何。
hint 有三种可能取值:'string'
、'number'
和 'default'
。
下面我们来演示下,这三种情况的发生场景。
var obj = {
[Symbol.toPrimitive](hint) {
console.log('[Symbol.toPrimitive]', hint)
}
}
undefined
// 第一种情况(期望类型是 number):
+obj
// [Symbol.toPrimitive] number
// NaN
// 第二种情况(期望类型是 string):
String(obj)
// [Symbol.toPrimitive] string
// "undefined"
// 第三种情况(未知):
obj + ':('
// [Symbol.toPrimitive] default
// "undefined:("
obj + 1
// [Symbol.toPrimitive] default
// NaN
[Symbol.toPrimitive]
方法里我们没有定义返回值,因此方法返回值是默认的 undefined
。
- 第一种情况:
+obj
->+undefined
->Number(undefined)
->NaN
。 - 第二种情况:
String(obj)
->String(undefined)
->"undefined"
。 - 第三种情况:
obj + ':('
->undefined + ':('
->"undefined:("
。obj + 1
->undefined + 1
->NaN
。
然而当对象不存在 [Symbol.toPrimitive]
的时候,转换规则又是怎样的呢?
这就关系到 Object.prototype
对象上的两个方法了:valueOf
和 toString
。
Object.prototype.valueOf / Object.prototype.toString
当对象上不存在 [Symbol.toPrimitive]
属性的时候,若发生类型转换,就要用到 Object.prototype
对象上的 valueOf
和 toString
两个方法了。
两个方法调用有先后,可能都会调用,也可以只调用一个就完成转换,返回结果。这跟方法返回值和 hint 值有关系。
具体是:
- 如果 hint 值为
'default'
,则与 hint 为'number'
时一样对待。 - 如果 hint 值为
'number'
,则- 先调用对象的
valueOf
方法,如果方法返回的是一个基本类型值,则对象的转换结果就是这个返回值; - 否则,接着调用对象的
toString
方法。
- 先调用对象的
- 如果 hint 值为
'string'
,则- 先调用对象的
toString
方法,如果方法返回的是一个基本类型值,则对象的转换结果就是这个返回值; - 否则,接着调用对象的
valueOf
方法。
- 先调用对象的
你可能要问了,如果两个方法的返回值都是对象的话,岂不是得不到对象最终的转换结果了?一点都没错,我们来试一下:
上图里,我们在对象 obj
上定义了 valueOf
和 toString
方法,在发生类型转换时,覆盖掉原型对象上的同名方法,以便我们能更加真切地感受到对象内部的实际的转换流程。
我们制造了一个极端情况,两个方法都没有返回基本类型只而是对象,结果呢?然后控制台就报错了,告诉我们不要这样玩。
需要说明的是——在 JavaScript 中,当我们在一个对象上调用 valueOf
方法的时候,实际上调用的是 Object.prototype.valueOf
这个原型方法。默认这个方法的返回值始终是调用对象自身,也就是说 valueOf
方法的返回的始终是对象,而非一个基本类型值。
注:
Date 对象除外,因为
Date.prototype
上定义的valueOf
方法覆盖掉了Object.protortype
上的。在 Date 对象上调用valueOf
方法,返回的是时间对象内部的时间戳表示。另外,
Date.prototype
也定义了自己的[Symbol.toPrimitive]
属性,默认是不可写入的。
这就得到了一个结论:对象发生到基本类型转换时,最终转换结果就是 obj.toString()
返回值!这就解释了下面代码里的输出结果:
var obj = {}
// (1)
obj + ' :)' // "[object Object] :)"
// (2)
obj - 1 // NaN
obj + ' :)'
->'[object Object]' + :)'
->'[object Object] :)'
obj - 1
->'[object Object]' - 1
->Number('[object Object]') - 1
->NaN - 1
->NaN
总结:对象转换算法步骤
总结下来,一个对象转换到基本类型的算法步骤如下:
- 首先,检查对象上是否有
[Symbol.toPrimitive]
属性:- 有的话,调用此方法(属性),此方法的返回值即对象最终的转换值,
- 没有的话,进入第二步。
- 检查当前转换的 hint 值:
- 如果为
'default'
/'number'
:- 先调用对象的
valueOf
方法,如果方法返回的是一个基本类型值,则对象的转换结果就是这个返回值, - 否则,接着调用对象的
toString
方法。
- 先调用对象的
- 如果为
'string'
:- 先调用对象的
toString
方法,如果方法返回的是一个基本类型值,则对象的转换结果就是这个返回值, - 否则,接着调用对象的
valueOf
方法。
- 先调用对象的
- 如果为
数组的转换
数组本质也是对象,因此上面的转换规则也适应于它。不同的是数组原型对象上定义了自己的实现方法:Array.prototype.toString
,这个方法覆盖掉了 Object.prototype
上的同名方法。
重写之后的 toString
方法的逻辑(类似)如下:
Array.prototype.toString = function () { return this.join() }
因为并未覆盖 valueOf
方法(也是返回对象本身,对应到这里就是数组实例),因此数组转换结果就是调用 array.join()
的返回结果。
下面举几个例子:
var emptyArray = []
var array1 = [123]
var array2 = ['hi', 'How are you']
// (1)
Number(emptyArray) // 0
// (2)
array1 - 100 // 23
// (3)
array2 + '?' // "hi,How are you?"
对应的转换过程如下:
emptyArray
首先会转换成空字符''
,Numbe('')
的结果就是0
,array1
会转换成字符串'123'
,'123' - 100
相当于Number('123') - 100
,也就是23
,array2
会转换成字符串'hi,How are you'
,'hi,How are you' + '?'
的结果自然是'hi,How are you?'
。
到这里的话,对象类型转换的内容就讲完了,希望能帮助到大家!
最后
感谢你花费宝贵的时间阅读这篇文章,希望你能有所收获。如果你觉得这篇文章让你的生活美好了一点点,欢迎给我点赞或者评论建议。谢谢!
(完)