一道面試題引發的對 javascript 型別轉換的思考
最近群裡有人發了下面這題:
實現一個函式,運算結果可以滿足如下預期結果:
add(1)(2) // 3 add(1, 2, 3)(10) // 16 add(1)(2)(3)(4)(5) // 15
對於一個好奇的切圖仔來說,忍不住動手嘗試了一下,看到題目首先想到的是會用到高階函式以及 Array.prototype.reduce()
。
高階函式(Higher-order function):高階函式的意思是它接收另一個函式作為引數。在 javascript 中,函式是一等公民,允許函式作為引數或者返回值傳遞。
得到了下面這個解法:
function add() { var args = Array.prototype.slice.call(arguments); return function() { var arg2 = Array.prototype.slice.call(arguments); return args.concat(arg2).reduce(function(a, b){ return a + b; }); } }
驗證了一下,發現錯了:
add(1)(2) // 3 add(1, 2)(3) // 6 add(1)(2)(3) // Uncaught TypeError: add(...)(...) is not a function(…)
上面的解法,只有在 add()()
情形下是正確的。而當鏈式操作的引數多於兩個或者少於兩個的時候,無法返回結果。
而這個也是這題的一個難點所在,add()
的時候,如何既返回一個值又返回一個函式以供後續繼續呼叫?
後來經過高人指點,通過重寫函式的 valueOf
方法或者 toString
方法,可以得到其中一種解法:
function add () { var args = Array.prototype.slice.call(arguments); var fn = function () { var arg_fn = Array.prototype.slice.call(arguments); return add.apply(null, args.concat(arg_fn)); } fn.valueOf = function () { return args.reduce(function(a, b) { return a + b; }) } return fn; }
嗯?第一眼看到這個解法的時候,我是懵逼的。因為我感覺 fn.valueOf()
從頭到尾都沒有被呼叫過,但是驗證了下結果:
add(1) // 1 add(1,2)(3) //6 add(1)(2)(3)(4)(5) // 15
神奇的對了!那麼玄機必然是在上面的 fn.valueOf = function() {}
內了。為何會是這樣呢?這個方法是在函式的什麼時刻執行的?且聽我一步一步道來。
valueOf 和 toString
先來簡單瞭解下這兩個方法:
Object.prototype.valueOf()
用 MDN 的話來說,valueOf() 方法返回指定物件的原始值。
JavaScript 呼叫 valueOf() 方法用來把物件轉換成原始型別的值(數值、字串和布林值)。但是我們很少需要自己呼叫此函式,valueOf 方法一般都會被 JavaScript 自動呼叫。
記住上面這句話,下面我們會細說所謂的自動呼叫是什麼意思。
Object.prototype.toString()
toString() 方法返回一個表示該物件的字串。
每個物件都有一個 toString() 方法,當物件被表示為文字值時或者當以期望字串的方式引用物件時,該方法被自動呼叫。
這裡先記住,valueOf() 和 toString() 在特定的場合下會自行呼叫。
原始型別
好,鋪墊一下,先了解下 javascript 的幾種原始型別,除去 Object 和 Symbol,有如下幾種原始型別:
- Number
- String
- Boolean
- Undefined
- Null
在 JavaScript 進行對比或者各種運算的時候會把物件轉換成這些型別,從而進行後續的操作,下面逐一說明:
String 型別轉換
在某個操作或者運算需要字串而該物件又不是字串的時候,會觸發該物件的 String 轉換,會將非字串的型別嘗試自動轉為 String 型別。系統內部會自動呼叫 toString
函式。舉個例子:
var obj = {name: 'Coco'}; var str = '123' + obj; console.log(str); // 123[object Object]
轉換規則:
- 如果
toString
方法存在並且返回原始型別,返回toString
的結果。 - 如果
toString
方法不存在或者返回的不是原始型別,呼叫valueOf
方法,如果valueOf
方法存在,並且返回原始型別資料,返回valueOf
的結果。 - 其他情況,丟擲錯誤。
上面的例子實際上是:
var obj = {name: 'Coco'}; var str = '123' + obj.toString();
其中,obj.toString()
的值為 "[object Object]"
。
假設是陣列:
var arr = [1, 2]; var str = '123' + arr; console.log(str); // 1231,2
上面 + arr
,由於這裡是個字串加操作,後面的 arr
需要轉化為一個字串型別,所以其實是呼叫了 + arr.toString()
。
但是,我們可以自己改寫物件的 toString
,valueOf
方法:
var obj = { toString: function() { console.log('呼叫了 obj.toString'); return {}; }, valueOf: function() { console.log('呼叫了 obj.valueOf') return '110'; } } alert(obj); // 呼叫了 obj.toString // 呼叫了 obj.valueOf // 110
上面 alert(obj + '1')
,obj 會自動呼叫自己的 obj.toString()
方法轉化為原始型別,如果我們不重寫它的 toString
方法,將輸出 [object Object]1
,這裡我們重寫了 toString
,而且返回了一個原始型別字串 111
,所以最終 alert 出了 1111。
上面的轉化規則寫了,toString
方法需要存在並且返回原始型別,那麼如果返回的不是一個原始型別,則會去繼續尋找物件的 valueOf
方法:
下面我們嘗試證明如果在一個物件嘗試轉換為字串的過程中,如果 toString()
方法不可用的時候,會發生什麼。
這個時候系統會再去呼叫 valueOf()
方法,下面我們改寫物件的 toString
和 valueOf
:
var obj = { toString: function() { console.log('呼叫了 obj.toString'); return {}; }, valueOf: function() { console.log('呼叫了 obj.valueOf') return '110'; } } alert(obj); // 呼叫了 obj.toString // 呼叫了 obj.valueOf // 110
從結果可以看到,當 toString
不可用的時候,系統會再嘗試 valueOf
方法,如果 valueOf
方法存在,並且返回原始型別(String、Number、Boolean)資料,返回valueOf
的結果。
那麼如果,toString
和 valueOf
返回的都不是原始型別呢?看下面這個例子:
var obj = { toString: function() { console.log('呼叫了 obj.toString'); return {}; }, valueOf: function() { console.log('呼叫了 obj.valueOf') return {}; } } alert(obj); // 呼叫了 obj.toString // 呼叫了 obj.valueOf // Uncaught TypeError: Cannot convert object to primitive value
可以發現,如果 toString
和 valueOf
方法均不可用的情況下,系統會直接返回一個錯誤。
新增於 2017-03-07:在查證了 ECMAScript5 官方文件後,發現上面的描述有一點問題,Object 型別轉換為 String 型別的轉換規則遠比上面複雜。轉換規則為:1.設原始值為呼叫 ToPrimitive 的結果;2.返回 ToString(原始值) 。關於 ToPrimitive 和 ToString 的規則可以看看官方文件:ECMAScript5 — ToString
Number 型別轉換
上面描述的是 String 型別的轉換,很多時候也會發生 Number 型別的轉換:
- 呼叫 Number() 函式,強制進行 Number 型別轉換
- 呼叫 Math.sqrt() 這類引數需要 Number 型別的方法
obj == 1
,進行對比的時候obj + 1
, 進行運算的時候
與 String 型別轉換相似,但是 Number 型別剛好反過來,先查詢自身的 valueOf
方法,再查詢自己 toString
方法:
- 如果
valueOf
存在,且返回原始型別資料,返回valueOf
的結果。 - 如果
toString
存在,且返回原始型別資料,返回toString
的結果。 - 其他情況,丟擲錯誤。
按照上述步驟,分別嘗試一下:
var obj = { valueOf: function() { console.log('呼叫 valueOf'); return 5; } } console.log(obj + 1); // 呼叫 valueOf // 6
var obj = { valueOf: function() { console.log('呼叫 valueOf'); return {}; }, toString: function() { console.log('呼叫 toString'); return 10; } } console.log(obj + 1); // 呼叫 valueOf // 呼叫 toString // 11
var obj = { valueOf: function() { console.log('呼叫 valueOf'); return {}; }, toString: function() { console.log('呼叫 toString'); return {}; } } console.log(obj + 1); // 呼叫 valueOf // 呼叫 toString // Uncaught TypeError: Cannot convert object to primitive value
Boolean 轉換
什麼時候會進行布林轉換呢:
- 布林比較時
- if(obj) , while(obj) 等判斷時
簡單來說,除了下述 6 個值轉換結果為 false,其他全部為 true:
- undefined
- null
- -0
- 0或+0
- NaN
- ”(空字串)
Boolean(undefined) // false Boolean(null) // false Boolean(0) // false Boolean(NaN) // false Boolean('') // false
Function 轉換
好,最後回到我們一開始的題目,來講講函式的轉換。
我們定義一個函式如下:
function test() { var a = 1; console.log(1); }
如果我們僅僅是呼叫 test
而不是 test()
,看看會發生什麼?
可以看到,這裡把我們定義的 test 函式的重新列印了一遍,其實,這裡自行呼叫了函式的 valueOf
方法:
我們改寫一下 test 函式的 valueOf
方法。
test.valueOf = function() { console.log('呼叫 valueOf 方法'); return 2; } test; // 輸出如下: // 呼叫 valueOf 方法 // 2
與 Number 轉換類似,如果函式的 valueOf
方法返回的不是一個原始型別,會繼續找到它的 toString
方法:
test.valueOf = function() { console.log('呼叫 valueOf 方法'); return {}; } test.toString= function() { console.log('呼叫 toString 方法'); return 3; } test; // 輸出如下: // 呼叫 valueOf 方法 // 呼叫 toString 方法 // 3
破題
再看回我正文開頭那題的答案,正是運用了函式會自行呼叫 valueOf
方法這個技巧,並改寫了該方法。我們稍作改變,變形如下:
function add () { console.log('進入add'); var args = Array.prototype.slice.call(arguments); var fn = function () { var arg_fn = Array.prototype.slice.call(arguments); console.log('呼叫fn'); return add.apply(null, args.concat(arg_fn)); } fn.valueOf = function () { console.log('呼叫valueOf'); return args.reduce(function(a, b) { return a + b; }) } return fn; }
當呼叫一次 add 的時候,實際是是返回 fn 這個 function,實際是也就是返回 fn.valueOf()
;
add(1); // 輸出如下: // 進入add // 呼叫valueOf // 1
其實也就是相當於:
[1].reduce(function(a, b) { return a + b; }) // 1
當鏈式呼叫兩次的時候:
add(1)(2); // 輸出如下: // 進入add // 呼叫fn // 進入add // 呼叫valueOf // 3
當鏈式呼叫三次的時候:
add(1)(2)(3); // 輸出如下: // 進入add // 呼叫fn // 進入add // 呼叫fn // 進入add // 呼叫valueOf // 6
可以看到,這裡其實有一種迴圈。只有最後一次呼叫才真正呼叫到 valueOf
,而之前的操作都是合併引數,遞迴呼叫本身,由於最後一次呼叫返回的是一個 fn 函式,所以最終呼叫了函式的 fn.valueOf
,並且利用了 reduce 方法對所有引數求和。
除了改寫 valueOf
方法,也可以改寫 toString
方法,所以,如果你喜歡,下面這樣也可以:
function add () { var args = Array.prototype.slice.call(arguments); var fn = function () { var arg_fn = Array.prototype.slice.call(arguments); return add.apply(null, args.concat(arg_fn)); } fn.toString = function() { return args.reduce(function(a, b) { return a + b; }) } return fn; }
這裡有個規律,如果只改寫 valueOf()
或是 toString()
其中一個,會優先呼叫被改寫了的方法,而如果兩個同時改寫,則會像 Number 型別轉換規則一樣,優先查詢 valueOf()
方法,在 valueOf()
方法返回的是非原始型別的情況下再查詢 toString()
方法。
後記
像阮一峰老師所說的,“炫耀從來不是我寫作的動機,好奇才是”。本文行文過程也是我自己學習的一個過程,過程中我也遇到了很多困惑,所以即便查閱了官方文件及大量的文章,但是錯誤及疏漏仍然在所難免,歡迎指正及給出更好的方法。
對於型別轉換,最好還是看看 ECMAScript 規範,拒絕成為伸手黨,自己多嘗試。另外評論處有很多人提出了自己的疑問,值得一看。
到此本文結束,如果還有什麼疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。
相關文章
- 一道面試題引發的js資料型別傳參思考面試題JS資料型別
- 一道面試題引發的思考面試題
- 一道 JS 面試題引發的思考JS面試題
- 一道面試題目引發的思考面試題
- 一道賦值面試題引發的思考賦值面試題
- 'abc' 轉換成[a, b, c]一道面試題的思考面試題
- 一道面試題引發的思考:理解 new 運算子面試題
- [基礎] JavaScript 型別轉換及面試題JavaScript型別面試題
- 【API知識】型別轉換工具ConvertUtils引發的思考API型別
- 一道題引發的EventLoop思考OOP
- 一道排序題引發的思考排序
- 一道面試題引發的“血案”面試題
- ssd上一道題目引發的思考
- 一道數學題引發的思考薦
- 一道簡單的題目引發的思考
- 一道面試題引起的思考面試題
- 一道“史上最難”java面試題引發的執行緒安全思考Java面試題執行緒
- 一道單連結串列題引發的思考
- 記一個面試題引發的思考面試題
- JavaScript 型別轉換JavaScript型別
- 說說JavaScript的型別轉換JavaScript型別
- 淺談JavaScript的型別轉換JavaScript型別
- 一道前端面試題引發的學習前端面試題
- 瞭解JavaScript中的型別轉換JavaScript型別
- JavaScript中的強制型別轉換JavaScript型別
- 有趣的JavaScript隱式型別轉換JavaScript型別
- 【死磕JVM】一道面試題引發的“棧幀”!!!JVM面試題
- Swift-Optional Binding引發的值型別與引用型別的思考Swift型別
- 論一道面試題引起的思考(總結)面試題
- JavaScript的隱式型別轉換淺析JavaScript型別
- 神奇的JavaScript弱等價型別轉換JavaScript型別
- JavaScript隱式型別轉換JavaScript型別
- JavaScript 資料型別轉換JavaScript資料型別
- javascript資料型別轉換JavaScript資料型別
- 從一道前端面試題引發的原理性探究前端面試題
- 聊聊一道簡單的javascript面試題JavaScript面試題
- 一道java面試題分析及思考Java面試題
- JavaScript強制型別轉換的背後操作JavaScript型別