文章講隱式強制型別轉換的路線是從基礎講到應用, 這條學習曲線比較平滑. 具體的路線為:
基本資料型別引出包裝型別的概念 ==> 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. 物件的兩個重要方法 valueOf
和 toString
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 個基本包裝型別的物件:
- 對於布林物件, 返回字串 "true" 或 "false", 根據其對應的基本資料型別的值而定.
- 對於數值物件, 返回在指定基數下該數的字串形式, 預設基數是 10, 即預設返回 十進位制 的數用引號包裹而成的字串.
- 對於字串物件, 返回對應基本資料型別的字串(和呼叫
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');}'
複製程式碼
- 其他物件
- 對於 RegExp 物件,返回該正規表示式的字串.
- 對於 Date 物件, 返回表示特定時間的字串.
- 對於 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
為引數建立出來的物件, 由於它的 prototype
為 null
, 所以沒有 toString
和 valueOf
方法
2.4 總結
夲節討論了兩個重要的函式 valueOf
和 toString
, 前者返回撥用者的基本型別的值, 後者可以將一個物件轉化為字串.
這兩個函式將在強制型別轉換過程中起到重要的作用.
注: 包裝和拆包裝過程也有其他名稱, 例如封裝(wrap)和解封(unwrap), 只是同一個過程的不同說法.