众所周知,JS 中共有 7 种数据类型:Undefined
、Null
、Boolean
、Number
、String
、Symbol
和 Object
。前 6 者是基本类型,Object
是引用类型。
《类型转换之装箱操作》一文中说,因为 JS 是弱类型语言,我们可以像对待引用类型一样对基本类型数据进行引用类型“才该有的”属性获取操作。
比如,如下的代码并不会报错:
var a = 1;
a.x = 2;
上述代码运行过程中,发生了“装箱操作”,通过阅读《ECMA-262》规范,我们知道浏览器内部是调用 ToObject
操作来实现的,它把基本类型包装成相应的引用类型。例如把 1
包装成了 new Number(1)
。
本文的主题关注相反的操作:对引用类型进行那些基本类型“才该有的”操作时会怎样?即,“拆箱操作”。
比如,如下的代码并不会报错:
var a = 1;
var b = {};
console.log(a - b);
对普通对象进行减法操作时,对象需要转化为数字类型。《Ecma-262 Edition 5.1》第11.6.2节对减法操作符规范如下:
The production AdditiveExpression : AdditiveExpression - MultiplicativeExpression is evaluated as follows:
- Let lref be the result of evaluating AdditiveExpression.
- Let lval be GetValue(lref).
- Let rref be the result of evaluating MultiplicativeExpression.
- Let rval be GetValue(rref).
- Let lnum be ToNumber(lval).
- Let rnum be ToNumber(rval).
- Return the result of applying the subtraction operation to lnum and rnum. See the note below 11.6.3.
上述操作中第 5、6 步比较关键,调用了内部操作 ToNumber
:
Argument Type | Result |
---|---|
Undefined | NaN |
Null | +0 |
Boolean | The result is 1 if the argument is true. The result is +0 if the argument is false. |
Number | The result equals the input argument (no conversion). |
String | See grammar and note below. |
Object | Apply the following steps: 1. Let primValue be ToPrimitive(input argument, hint Number). 2. Return ToNumber(primValue). |
最后一行,处理Object
时,经历两步:1. ToPrimitive
。2. ToNumber
。
而 ToPrimitive
操作正与 ToObject
相对,表示转化为基本类型:
Input Type | Result |
---|---|
Undefined | The result equals the input argument (no conversion). |
Null | The result equals the input argument (no conversion). |
Boolean | The result equals the input argument (no conversion). |
Number | The result equals the input argument (no conversion). |
String | The result equals the input argument (no conversion). |
Object | Return a default value for the Object. The default value of an object is retrieved by calling the [[DefaultValue]] internal method of the object, passing the optional hint PreferredType. The behaviour of the [[DefaultValue]] internal method is defined by this specification for all native ECMAScript objects in 8.12.8. |
最后一行说,对象转化为基本类型时,是获取的对象的默认值。使用的是内部[[DefaultValue]](hint)
,规范原文引用如下(补充:本文中的英文都可以不看的,我都会仔细说明的):
When the [[DefaultValue]] internal method of O is called with hint String, the following steps are taken:
- Let toString be the result of calling the [[Get]] internal method of object O with argument "toString".
- If IsCallable(toString) is true then,
1. Let str be the result of calling the [[Call]] internal method of toString, with O as the this value and an empty argument list.
2. If str is a primitive value, return str.- Let valueOf be the result of calling the [[Get]] internal method of object O with argument "valueOf".
- If IsCallable(valueOf) is true then,
1. Let val be the result of calling the [[Call]] internal method of valueOf, with O as the this value and an empty argument list.
2. If val is a primitive value, return val.- Throw a TypeError exception.
When the [[DefaultValue]] internal method of O is called with hint Number, the following steps are taken:
- Let valueOf be the result of calling the [[Get]] internal method of object O with argument "valueOf".
- If IsCallable(valueOf) is true then,
1. Let val be the result of calling the [[Call]] internal method of valueOf, with O as the this value and an empty argument list.
2. If val is a primitive value, return val.- Let toString be the result of calling the [[Get]] internal method of object O with argument "toString".
- If IsCallable(toString) is true then,
1. Let str be the result of calling the [[Call]] internal method of toString, with O as the this value and an empty argument list.
2. If str is a primitive value, return str. Throw a TypeError exception.- When the [[DefaultValue]] internal method of O is called with no hint, then it behaves as if the hint were Number, unless O is a Date object (see 15.9.6), in which case it behaves as if the hint were String.
When the [[DefaultValue]] internal method of O is called with no hint, then it behaves as if the hint were Number, unless O is a Date object (see 15.9.6), in which case it behaves as if the hint were String.
上述算法是说,根据 hint
值采取不同的处理方式,比如 hint
是 String
时,优先调用对象的 toString
方法,如果返回值是基本类型值,返回该值,否则调用对象的 valueOf
方法,如果返回值是基本类型值,返回该值。否则报错。
而 hint
是 Number
时,顺序是反过来的,优先调用 valueOf
,如果其返回值不是基本类型,再调用 toString
。另外,除了日期对象外,如果没传 hint
的话,其默认值是 Number
,因此 JS 中类型转化时,更偏爱 Number
。
下面我们举几个例子看看:
var a = {
toString() {
return 3
},
valueOf() {
return '30'
}
};
console.log(a - 5); // 25
这里使用的是减法操作,此时 hint
是 Number
,因此先调用对象 a
的 valueOf
方法,其返回值 '30'
是字符串类型,是基本类型。因此 a - 5
变成了 '30' - 5
。
再看:
var a = {
toString() {
return {}
},
valueOf: null
};
console.log(a - 5); // Uncaught TypeError: Cannot convert object to primitive value
对象 a
,其方法 valueOf
不是函数,因而看其 toString
方法,而该方法返回的是一个空对象,不是基本类型。因而报错。
再如:
var o = {
toString() {
return 'now is: '
},
valueOf: function() {
return "时间是:"
}
};
var d = new Date();
console.log(o + d); // 时间是:Mon May 06 2019 13:56:39 GMT+0800 (中国标准时间)
这里使用了加法操作:
The production AdditiveExpression : AdditiveExpression + MultiplicativeExpression is evaluated as follows:
- Let lref be the result of evaluating AdditiveExpression.
- Let lval be GetValue(lref).
- Let rref be the result of evaluating MultiplicativeExpression.
- Let rval be GetValue(rref).
- Let lprim be ToPrimitive(lval).
- Let rprim be ToPrimitive(rval).
- If Type(lprim) is String or Type(rprim) is String, then Return the String that is the result of concatenating ToString(lprim) followed by ToString(rprim)
- Return the result of applying the addition operation to ToNumber(lprim) and ToNumber(rprim). See the Note below 11.6.3.
其中第 5、6 步直接获取加号两边的基本类型。此时没有都传递 hint
,o 是普通对象,因此默认 hint
是 Number
,使用的 valueOf
的返回值。而 d 是日期对象,默认 hint
是 String
,优先调用的是其 toString
方法。然后根据第 7 步,采用的是字符串拼接方法。
加法操作,这里再举一例:
var o = {
toString: function() {
return 2
}
};
console.log(o + o); // 4
这里不过多解释了。
ToPrimitive
除了在四则运算中大量用到外,关系运算中也经常使用。比如 ==
操作。其他类型转换等相关知识留给后续文章吧。
至此,“拆箱”已经说完了。
本文完。