沿著平滑的曲線學會 JavaScript 中的隱式強制型別轉換(基礎篇)

mynull發表於2019-03-08

文章講隱式強制型別轉換的路線是從基礎講到應用, 這條學習曲線比較平滑. 具體的路線為:

基本資料型別引出包裝型別的概念 ==> valueOf 和 toString 方法 ==> ToPrimitive 抽象操作和 [[DefaultValue]] 操作 ==> 各種型別之間型別轉換的規律 ==> 常見引起隱式型別轉換的情況 ==> 多個例項練習以檢驗方法的有用性.

1. 從基本包裝型別講起

討論基本包裝型別的前提是瞭解基本資料型別(也可以稱為原始型別, 簡單資料型別等)。然後通過基本資料型別呼叫方法的行為, 引出基本包裝型別的概念和作用.

1.1 基本資料型別

已經知道 JavaScript 中共有6種基本資料型別,分別是:string,number,boolean,null,undefined,symbol.

基本型別既不是物件, 也沒有方法可供呼叫. 然而經常見到基本型別的變數呼叫方法的情況, 類如:

let index = 'ABCDE'.indexOf('CD');
console.log(index); // 2
複製程式碼

上例中 'ABCDE' 是一個字串的基本型別, 這個字串直接呼叫了indexOf 方法, 將該方法的返回值儲存在變數 index 中, 然後在第二行輸出. 可以看到這個例子能夠執行並且結果正確.

既然上面說原始型別沒有方法可供呼叫, 那麼字串 'ABCDE' 在呼叫 indexOf 方法時為什麼沒有出錯呢? 而且從執行結果來看, indexOf 這個方法確實被呼叫了, 那麼是誰呼叫了這個方法並且返回了正確的結果呢? 答案就是基本包裝型別. 下面引入基本包裝型別的相關內容.

1.2 基本包裝型別

關鍵詞: 包裝, 基本包裝型別, 基本包裝型別的物件

上一節討論到其實呼叫方法的並不是字串本身 , 而是基本包裝型別. 下面就應該具體討論基本包裝型別的相關內容了.

為了基本型別可以正常呼叫方法, 後臺會為這個基本型別的值自動建立一個對應的物件, 這個物件的型別就稱為基本包裝型別, 然後用這個物件去呼叫方法, 在呼叫完方法之後, 這個物件就會被銷燬( 用完就銷燬 )了. 這個從基本資料型別生成基本包裝型別物件的過程稱為包裝( box ).

簡單來說, 在基本型別呼叫方法的時候, 方法的真正呼叫者其實不是我們直接定義的基本型別, 而是後臺給我們建立的基本包裝型別的 物件 . 而且這個物件是臨時的、不會一直存在的、用完就會被銷燬的.

這個物件還有一個特點, 他是與基本型別的值相對應的. 然而:

並不是每一種基本型別都有對應的包裝型別

上節中提到的六種基本型別中的四種有其對應的包裝型別, 分別是:

string(字串) -> String, number(數值) -> Number,boolean(布林) -> Boolean,symbol(符號) -> Symbol

其中符號 -> 表示對應. 同時: 注意首字母的大寫( 物件名的首字母習俗預設大寫 ).

注意: 本文只討論 string,number,boolean 三種基本型別及其對應的包裝型別.

還需要了解的是, 不僅僅是系統可以隱式的建立包裝物件, 使用者也可以手動、顯式的建立一個基本包裝型別的物件, 方法為: 用關鍵字 new 加上對應的包裝型別的建構函式, 引數傳入基本型別的值即可, 例如:

let n = new Number(22);             // n 是 數值22對應的包裝物件
let str = new String('example');    // str 是字串 'example' 對應的包裝物件
let flag = new Boolean(false);      // flag 是布林值 false 對應的包裝物件
複製程式碼

至此, 再看第一節的這個例子, 可以再次想象系統建立包裝物件的大致過程:

let index = 'ABCDE'.indexOf('CD');
console.log(index); // 2
複製程式碼

可以發現第一行程式碼中後臺發生了包裝行為: 後臺根據字串型別的 'ABCDE' 包裝出了一個物件. 即: 在呼叫 indexOf 方法時, 後臺發現想呼叫這個方法的是一個基本型別的值, 但是這個值沒有這個方法可供呼叫, 於是為它生成了對應的包裝物件( 操作 1 ), 然後通過這個包裝物件來呼叫了 indexOf 方法( 操作 2 ), 而後將方法的返回值賦給了變數 index. 最後, 把這個包裝物件銷燬( 操作 3 ).

根據以上思路,可以大致模擬出上述過程的對應的程式碼:

// step 1. 建立 'ABCDE' 對應的基本包裝型別的物件: 
let temp = new String('ABCDE');

// step 2. 用包裝型別的物件 temp 呼叫 indexOf 方法, 並將返回值賦給 index 變數: 
let index = temp.indexOf('CD');

// step 3. 將 temp 物件銷燬
temp = null;
複製程式碼

1.3 總結

當一個基本資料型別想要呼叫方法時, 後臺會為它生成一個臨時的包裝物件, 利用這個物件去呼叫方法, 再將方法執行的結果返回, 隨後這個臨時物件被銷燬.

1.4 包裝物件的"拆包裝" box <-> unbox

從上面的內容瞭解到 包裝(box) 是根據一個基本型別的值生成一個對應型別的物件的過程, 與這個過程大致相反, 存在一種根據包裝物件生成基本型別值的過程, 可稱為 拆包裝 (unbox). 這個過程同樣即可以由後臺隱式的完成, 也可以手動的呼叫方法 valueOf 來做.

下一節開始討論 valueOf 這個方法, 同時引出另外一個同樣重要的方法 toString.

2. 物件的兩個重要方法 valueOftoString

2.1 基本包裝型別的拆包裝( unbox ) 用到的 valueOf 方法

基本包裝型別的拆包裝操作用到了包裝物件中的 valueOf 函式, 這個函式可以將一個物件轉換成一個基本型別的值.

對於 Boolean, Number 和 String 三者的基本包裝物件來說, 呼叫 valueOf 的返回值是各自對應的基本資料型別的值:

let n = new Number(22);     // 包裝基本數值資料 22
// 拆包裝出來的結果是對應的基本資料型別的值
console.log(n.valueOf() === 22);    // true

let str = new String('example');    // 包裝基本字串資料 'example'
// 拆包裝出來的結果是對應的基本資料型別的值
console.log(str.valueOf() === 'example');   // true, 

let flag = new Boolean(false);      // 包裝基本布林資料 false
// 拆包裝出來的結果是對應的基本資料型別的值
console.log(flag.valueOf() === false);  // true
複製程式碼

有時會發生後臺隱式拆包裝的情況, 包裝型別的物件會在後臺呼叫 valueOf 方法, 例如:

let a = new Number(1);
let b = a + 1;  //---> 這一行發生了隱式拆包裝操作: let b = a.valueOf() + 1;

console.log(b);  // b 為 2

console.log(typeof a);  // object
console.log(typeof b);  // number, b 的型別為 基本資料型別, 而不是包裝型別
複製程式碼

甚至一行程式碼中會發生包裝和解包裝兩種操作, 例如:

let num = 3.14159;
console.log(num.valueOf());   // 3.14159
複製程式碼

在上面程式碼塊的第二行程式碼中變數 num 要呼叫函式 valueOf , 此時 num 會被先包裝為 基本包裝型別的物件,而這個物件在呼叫 valueOf 方法時就發生瞭解包裝的操作.

2.2 其他物件的 valueOf 方法

不僅僅是基本包裝型別有 valueOf 方法, 許多 JavaScript 內建(build-in)物件都有該函式, 為使執行的結果與物件本身相符合, 大多物件都重寫了這個方法. 下面看一些其他物件的 valueOf 方法的行為有什麼特點.

以下列出常用內建物件的 valueOf 方法的返回值:

物件 返回值
Boolean, Number 和 String 三者 各自相對應的基本型別的值
Array,Function,Object 三者 其本身
Date 當前時間距 1970.01.01 午夜的毫秒數
Math 和 Error 沒有 valueOf 方法

下面是實驗結果:

// 陣列呼叫 valueOf, 返回陣列本身
let array = [1, 'hello', false];
console.log(array.valueOf() === array);   // true

// 函式呼叫 valueOf, 返回函式本身
function foo(){}
console.log(foo.valueOf() === foo);   // true

// 物件呼叫 valueOf, 返回物件本身
let obj = {
    name: 'doug',
    age : 22
};
console.log(obj.valueOf() === obj);   // true

// 當前時間距1970年1月1日午夜的毫秒數
console.log(new Date().valueOf());   // 1551684737052

複製程式碼

總結: valueOf 方法可以將一個物件轉換為基本資料型別, 並不是每個物件都有此方法(例如: Math 和 Error 物件). 對於布林、數值和字串三者的基本包裝型別來說,呼叫此函式返回其對應的基本型別的值; 物件呼叫此函式的返回值是其本身 ( 由於陣列和函式本質上也是物件, 所以也返回其自身 ) .

提到 valueOf 方法就不得不想起另外一個對於型別轉換十分重要的方法 toString, 下節將會討論它.

2.3 可以將物件表示為字串的方法 toString()

每個內建的物件都有此方法,是從 Object 物件繼承而來的. 為使執行的結果與物件本身相符合, 大多數內建物件都重寫了該函式. 常見的物件呼叫 toString 方法的返回值如下:

  • 對於使用者建立的物件, 返回'[object object]'. (存在一個例外, 見最後部分)

  • 對於 Math 物件, 返回 "[object Math]":

// 自定義的物件
console.log({name: 'doug'}.toString());  // '[object Object]'

// Math 物件
console.log(Math.toString());  // '[object Math]'
複製程式碼
  • 對於 第一部分中提到的 3 個基本包裝型別的物件:
  1. 對於布林物件, 返回字串 "true" 或 "false", 根據其對應的基本資料型別的值而定.
  2. 對於數值物件, 返回在指定基數下該數的字串形式, 預設基數是 10, 即預設返回 十進位制 的數用引號包裹而成的字串.
  3. 對於字串物件, 返回對應基本資料型別的字串(和呼叫 valueOf 方法得到的結果相同) .
// 布林值的包裝型別的物件
console.log(new Boolean(false).toString());   // 'false'

// 數值包裝型別物件的物件
console.log(new Number(3.14159).toString());  // '3.14159'

// 字串包裝型別物件的物件
console.log(new String('str').toString());    // 'str'
複製程式碼
  • 對於陣列,返回所有項組成的字串, 各項之間用 "," 連線.
// 陣列
console.log([1, 'hello', false].toString());  // '1,hello,false'
複製程式碼
  • 對於 函式,toString方法返回一個字串,其中包含用於定義函式的源文字段.
// 函式
function foo(){console.log('hello foo');}
console.log(foo.toString());  
// 'function foo(){console.log('hello foo');}'
複製程式碼
  • 其他物件
  1. 對於 RegExp 物件,返回該正規表示式的字串.
  2. 對於 Date 物件, 返回表示特定時間的字串.
  3. 對於 Error 物件,返回包含錯誤內容的字串.
// 正則物件
console.log(new RegExp("a+b+c").toString());      // "/a+b+c/"

// 日期物件
console.log(new Date().toString()); 
// Mon Mar 04 2019 17:07:54 GMT+0800 (中國標準時間)


// Error 物件
console.log(new Error('fatal error').toString()); 
// 'Error: fatal error'
複製程式碼

並非每個物件都有 toString() 方法, 例如通過 Object.create 函式傳入null 為引數建立出來的物件, 由於它的 prototypenull, 所以沒有 toStringvalueOf 方法

2.4 總結

夲節討論了兩個重要的函式 valueOftoString , 前者返回撥用者的基本型別的值, 後者可以將一個物件轉化為字串.

這兩個函式將在強制型別轉換過程中起到重要的作用.

注: 包裝和拆包裝過程也有其他名稱, 例如封裝(wrap)和解封(unwrap), 只是同一個過程的不同說法.

相關文章