众所周知,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,
- Let str be the result of calling the [[Call]] internal method of toString, with O as the this value and an empty argument list.
- 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,
- Let val be the result of calling the [[Call]] internal method of valueOf, with O as the this value and an empty argument list.
- 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,
- Let val be the result of calling the [[Call]] internal method of valueOf, with O as the this value and an empty argument list.
- 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,
- Let str be the result of calling the [[Call]] internal method of toString, with O as the this value and an empty argument list.
- 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 除了在四则运算中大量用到外,关系运算中也经常使用。比如 == 操作。其他类型转换等相关知识留给后续文章吧。
至此,“拆箱”已经说完了。
本文完。