原來 JS 還存在這樣的拆箱轉換

程式猿何大叔發表於2019-01-29

在讀 Winter 大佬的《重學前端》欄目時,重溫了 JS 的「拆箱轉換」。「裝箱轉換」與「拆箱轉換」以前都是瞭解的,今天來看,自己所謂的瞭解也真是一知半解。在閱讀 Winter 老師寫的內容後,對「拆箱轉換」這個知識點還是不甚清楚,因此我再去深入地瞭解一番,參考資料詳見文末的「參考連結」。

被我們忽略的表象

首先,我們來看一下例子:

const a = {
    name: 'a',
    toString () {
        console.log(this);
        console.log('toString');
        return { name: 'toString' };
    },
    valueOf () {
        console.log(this);
        console.log('valueOf');
        return { name: 'valueOf' };
    }
};

a * 2;
// {name: "a", toString: ƒ, valueOf: ƒ}
// valueOf
// {name: "a", toString: ƒ, valueOf: ƒ}
// toString
// Uncaught TypeError: Cannot convert object to primitive value

a + "";
// {name: "a", toString: ƒ, valueOf: ƒ}
// valueOf
// {name: "a", toString: ƒ, valueOf: ƒ}
// toString
// Uncaught TypeError: Cannot convert object to primitive 

alert(a);
// {name: "a", toString: ƒ, valueOf: ƒ}
// toString
// {name: "a", toString: ƒ, valueOf: ƒ}
// valueOf
// Uncaught TypeError: Cannot convert object to primitive value
複製程式碼

可以看到,toStringvalueOf 的執行順序並不固定,而是根據某個條件來決定的,那麼是根據什麼呢?那就是在拆箱轉換時,呼叫了物件的 ToPrimitive 內部函式時,其會根據執行上下文,自動傳入一個轉換型別引數,暫時給它命名為 hint

ToPrimitive

在 JavaScript 標準中,規定了 ToPrimitive 函式,它是物件型別到基本型別轉換的實現者(即,拆箱轉換);但這是一個內部演算法,是程式語言在內部執行時遵循的一套規則。

物件到 String 和 Number 的轉換都遵循“先拆箱再轉換”的規則。通過拆箱轉換,把物件變成基本型別,再從基本型別轉換為對應的 String 或者 Number。

但是對於不同的操作,拆箱轉換的內部實現也有所區別,正如上面的例子所示。

「拆箱轉換」的呼叫規則及順序如下:

  1. 檢查物件中是否有使用者顯式定義的 [Symbol.toPrimitive] 方法,如果有,直接呼叫;
  2. 如果沒有,則執行原內部函式 ToPrimitive,然後判斷傳入的 hint 值,如果其值為 string,順序呼叫物件的 toStringvalueOf 方法(其中 toString 方法一定會執行,如果其返回一個基本型別值,則返回、終止運算,否則繼續呼叫 valueOf 方法);
  3. 如果判斷傳入的 hint 值不為 string,則就可能為 number 或者 default 了,均會順序呼叫物件的 valueOftoString 方法(其中 valueOf 方法一定會執行,如果其返回一個基本型別值,則返回、終止運算,否則繼續呼叫 toString 方法);

來看一下第一種情況:

const b = {
    [Symbol.toPrimitive] (hint) {
        console.log(`hint: ${hint}`);
        return {};
    },
    toString () {
        console.log('toString');
        return 1;
    },
    valueOf () {
        console.log('valueOf');
        return 2;
    }
};

alert(b); // hint: string 
b + ''; // hint: default
b + 500; // hint: default
+b; // hint: number
b * 1; // hint: number
複製程式碼

第二、三種情況:

const c = {
    toString () {
        console.log('toString');
        return 1;
    },
    valueOf () {
        console.log('valueOf');
        return 2;
    }
};

alert(c); // 列印出 toString 並 alert 出 1
c + '';  // 先後列印出 valueOf,"2"
c + 500; // 先後列印出 valueOf,502
+c; // 先後列印出 valueOf,2
c * 1; // 先後列印出 valueOf,2
複製程式碼

那麼關於 hint 可取的三種值,都有什麼含義?又什麼情況對應什麼值?

確定 hint 取值

string
當在希望是字串操作,也即發生物件到字串的轉換時,傳入內部函式 ToPrimitive 的引數值即為 string

// output
alert(obj);

// using object as a property key
anotherObj[obj] = 123;
複製程式碼

number
當在希望是數值操作,也即發生物件到數值的轉換時,傳入內部函式 ToPrimitive 的引數值即為 number

// explicit conversion
let num = Number(obj);

// maths (except binary plus)
let n = +obj; // unary plus
let delta = date1 - date2;

// less/greater comparison
let greater = user1 > user2;
複製程式碼

default
當在一些不確定需要將物件轉換成什麼基礎型別的場景下,傳入內部函式 ToPrimitive 的引數值即為 default

// binary plus
let total = car1 + car2;

// obj == string/number/symbol
if (user == 1) { ... };
複製程式碼

結語

如果親愛的讀者們在本文中發現了什麼錯誤,或者有什麼不同的意見,還請留言,一起討論,一起將隱藏的、晦澀的點提出來,然後解決掉。

參考連結


微信公眾號
覺得本文不錯的話,分享一下給小夥伴吧~

相關文章