天天看點

一道面試題引發的對JavaScript類型轉換的思考

最近群裡有人發了下面這題:

實作一個函數,運算結果可以滿足如下預期結果:

對于一個好奇的切圖仔來說,忍不住動手嘗試了一下,看到題目首先想到的是會用到高階函數以及 <code>array.prototype.reduce()</code>。

高階函數(higher-order function):高階函數的意思是它接收另一個函數作為參數。在 javascript 中,函數是一等公民,允許函數作為參數或者傳回值傳遞。

得到了下面這個解法:

驗證了一下,發現錯了:

上面的解法,隻有在 <code>add()()</code> 情形下是正确的。而當鍊式操作的參數多于兩個或者少于兩個的時候,無法傳回結果。

而這個也是這題的一個難點所在,<code>add()</code>的時候,如何既傳回一個值又傳回一個函數以供後續繼續調用?

後來經過高人指點,通過重寫函數的 <code>valueof</code> 方法或者 <code>tostring</code> 方法,可以得到其中一種解法:

嗯?第一眼看到這個解法的時候,我是懵逼的。因為我感覺 <code>fn.valueof()</code> 從頭到尾都沒有被調用過,但是驗證了下結果:

神奇的對了!那麼玄機必然是在上面的 <code>fn.valueof = function() {}</code> 内了。為何會是這樣呢?這個方法是在函數的什麼時刻執行的?且聽我一步一步道來。

<a></a>

先來簡單了解下這兩個方法:

javascript 調用 valueof() 方法用來把對象轉換成原始類型的值(數值、字元串和布爾值)。但是我們很少需要自己調用此函數,valueof 方法一般都會被 javascript 自動調用。

記住上面這句話,下面我們會細說所謂的自動調用是什麼意思。

tostring() 方法傳回一個表示該對象的字元串。

每個對象都有一個 tostring() 方法,當對象被表示為文本值時或者當以期望字元串的方式引用對象時,該方法被自動調用。

這裡先記住,valueof() 和 tostring() 在特定的場合下會自行調用。

好,鋪墊一下,先了解下 javascript 的幾種原始類型,除去 object 和 symbol,有如下幾種原始類型:

number

string

boolean

undefined

null

在 javascript 進行對比或者各種運算的時候會把對象轉換成這些類型,進而進行後續的操作,下面逐一說明: 

在某個操作或者運算需要字元串而該對象又不是字元串的時候,會觸發該對象的 string 轉換,會将非字元串的類型嘗試自動轉為 string 類型。系統内部會自動調用 <code>tostring</code> 函數。舉個例子:

轉換規則:

如果 <code>tostring</code> 方法存在并且傳回原始類型,傳回 <code>tostring</code> 的結果。

如果 <code>tostring</code> 方法不存在或者傳回的不是原始類型,調用 <code>valueof</code> 方法,如果 <code>valueof</code> 方法存在,并且傳回原始類型資料,傳回 <code>valueof</code> 的結果。

其他情況,抛出錯誤。

上面的例子實際上是:

其中,<code>obj.tostring()</code> 的值為 <code>"[object object]"</code>。

假設是數組:

上面 <code>+ arr</code> ,由于這裡是個字元串加操作,後面的 <code>arr</code> 需要轉化為一個字元串類型,是以其實是調用了 <code>+ arr.tostring()</code> 。

但是,我們可以自己改寫對象的 <code>tostring</code>,<code>valueof</code> 方法:

上面 <code>alert(obj + '1')</code> ,obj 會自動調用自己的 <code>obj.tostring()</code> 方法轉化為原始類型,如果我們不重寫它的 <code>tostring</code> 方法,将輸出 <code>[object object]1</code> ,這裡我們重寫了 <code>tostring</code> ,而且傳回了一個原始類型字元串 <code>111</code> ,是以最終 alert 出了 1111。

上面的轉化規則寫了,<code>tostring</code> 方法需要存在并且傳回原始類型,那麼如果傳回的不是一個原始類型,則會去繼續尋找對象的 <code>valueof</code> 方法:

下面我們嘗試證明如果在一個對象嘗試轉換為字元串的過程中,如果 <code>tostring()</code> 方法不可用的時候,會發生什麼。

這個時候系統會再去調用 <code>valueof()</code> 方法,下面我們改寫對象的 <code>tostring</code> 和 <code>valueof</code>:

從結果可以看到,當 <code>tostring</code> 不可用的時候,系統會再嘗試 <code>valueof</code> 方法,如果 <code>valueof</code> 方法存在,并且傳回原始類型(string、number、boolean)資料,傳回<code>valueof</code>的結果。

那麼如果,<code>tostring</code> 和 <code>valueof</code> 傳回的都不是原始類型呢?看下面這個例子:

可以發現,如果 <code>tostring</code> 和 <code>valueof</code> 方法均不可用的情況下,系統會直接傳回一個錯誤。

添加于 2017-03-07:在查證了 ecmascript5 官方文檔後,發現上面的描述有一點問題,object 類型轉換為 string 類型的轉換規則遠比上面複雜。轉換規則為:1.設原始值為調用 toprimitive 的結果;2.傳回 tostring(原始值)

上面描述的是 string 類型的轉換,很多時候也會發生 number 類型的轉換:

調用 number() 函數,強制進行 number 類型轉換

調用 math.sqrt() 這類參數需要 number 類型的方法

<code>obj == 1</code> ,進行對比的時候

<code>obj + 1</code> , 進行運算的時候

與 string 類型轉換相似,但是 number 類型剛好反過來,先查詢自身的 <code>valueof</code> 方法,再查詢自己 <code>tostring</code> 方法:

如果 <code>valueof</code> 存在,且傳回原始類型資料,傳回 <code>valueof</code> 的結果。

如果 <code>tostring</code> 存在,且傳回原始類型資料,傳回 <code>tostring</code> 的結果。

按照上述步驟,分别嘗試一下:

什麼時候會進行布爾轉換呢:

布爾比較時

if(obj) , while(obj) 等判斷時

簡單來說,除了下述 6 個值轉換結果為 false,其他全部為 true:

-0

0或+0

nan

”(空字元串)

好,最後回到我們一開始的題目,來講講函數的轉換。

我們定義一個函數如下:

如果我們僅僅是調用 <code>test</code> 而不是 <code>test()</code> ,看看會發生什麼?

一道面試題引發的對JavaScript類型轉換的思考

可以看到,這裡把我們定義的 test 函數的重新列印了一遍,其實,這裡自行調用了函數的 <code>valueof</code> 方法:

一道面試題引發的對JavaScript類型轉換的思考

我們改寫一下 test 函數的 <code>valueof</code> 方法。

與 number 轉換類似,如果函數的 <code>valueof</code> 方法傳回的不是一個原始類型,會繼續找到它的 <code>tostring</code> 方法:

再看回我正文開頭那題的答案,正是運用了函數會自行調用 <code>valueof</code> 方法這個技巧,并改寫了該方法。我們稍作改變,變形如下:

當調用一次 add 的時候,實際是是傳回 fn 這個 function,實際是也就是傳回 <code>fn.valueof()</code>;

其實也就是相當于:

當鍊式調用兩次的時候:

當鍊式調用三次的時候:

可以看到,這裡其實有一種循環。隻有最後一次調用才真正調用到 <code>valueof</code>,而之前的操作都是合并參數,遞歸調用本身,由于最後一次調用傳回的是一個 fn 函數,是以最終調用了函數的 <code>fn.valueof</code>,并且利用了 reduce 方法對所有參數求和。

除了改寫 <code>valueof</code> 方法,也可以改寫 <code>tostring</code> 方法,是以,如果你喜歡,下面這樣也可以:

這裡有個規律,如果隻改寫 <code>valueof()</code> 或是 <code>tostring()</code> 其中一個,會優先調用被改寫了的方法,而如果兩個同時改寫,則會像 number 類型轉換規則一樣,優先查詢 <code>valueof()</code> 方法,在 <code>valueof()</code> 方法傳回的是非原始類型的情況下再查詢 <code>tostring()</code> 方法。

像阮一峰老師所說的,“炫耀從來不是我寫作的動機,好奇才是”。本文行文過程也是我自己學習的一個過程,過程中我也遇到了很多困惑,是以即便查閱了官方文檔及大量的文章,但是錯誤及疏漏仍然在所難免,歡迎指正及給出更好的方法。

對于類型轉換,最好還是看看 ecmascript 規範,拒絕成為伸手黨,自己多嘗試。另外評論處有很多人提出了自己的疑問,值得一看。

到此本文結束,如果還有什麼疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

作者:chokcoco

來源:51cto

繼續閱讀