眾所周知,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
除了在四則運算中大量用到外,關係運算中也經常使用。比如 ==
操作。其他型別轉換等相關知識留給後續文章吧。
至此,“拆箱”已經說完了。
本文完。