一道面試題引發(fā)的對(duì)javascript類型轉(zhuǎn)換的思考
最近群里有人發(fā)了下面這題:
實(shí)現(xiàn)一個(gè)函數(shù),運(yùn)算結(jié)果可以滿足如下預(yù)期結(jié)果:
add(1)(2) // 3 add(1, 2, 3)(10) // 16 add(1)(2)(3)(4)(5) // 15
對(duì)于一個(gè)好奇的切圖仔來說,忍不住動(dòng)手嘗試了一下,看到題目首先想到的是會(huì)用到高階函數(shù)以及 Array.prototype.reduce()
高階函數(shù)(Higher-order function):高階函數(shù)的意思是它接收另一個(gè)函數(shù)作為參數(shù)。在 javascript 中,函數(shù)是一等公民,允許函數(shù)作為參數(shù)或者返回值傳遞。
得到了下面這個(gè)解法:
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; }); } }
驗(yàn)證了一下,發(fā)現(xiàn)錯(cuò)了:
add(1)(2) // 3 add(1, 2)(3) // 6 add(1)(2)(3) // Uncaught TypeError: add(...)(...) is not a function(…)
上面的解法,只有在 add()() 情形下是正確的。而當(dāng)鏈?zhǔn)讲僮鞯膮?shù)多于兩個(gè)或者少于兩個(gè)的時(shí)候,無法返回結(jié)果。
而這個(gè)也是這題的一個(gè)難點(diǎn)所在,add()的時(shí)候,如何既返回一個(gè)值又返回一個(gè)函數(shù)以供后續(xù)繼續(xù)調(diào)用?
后來經(jīng)過高人指點(diǎn),通過重寫函數(shù)的 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; }
嗯?第一眼看到這個(gè)解法的時(shí)候,我是懵逼的。因?yàn)槲腋杏X fn.valueOf() 從頭到尾都沒有被調(diào)用過,但是驗(yàn)證了下結(jié)果:
add(1) // 1 add(1,2)(3) //6 add(1)(2)(3)(4)(5) // 15
神奇的對(duì)了!那么玄機(jī)必然是在上面的 fn.valueOf = function() {} 內(nèi)了。為何會(huì)是這樣呢?這個(gè)方法是在函數(shù)的什么時(shí)刻執(zhí)行的?且聽我一步一步道來。
valueOf 和 toString
先來簡單了解下這兩個(gè)方法:
Object.prototype.valueOf()
用 MDN 的話來說,valueOf() 方法返回指定對(duì)象的原始值。
JavaScript 調(diào)用 valueOf() 方法用來把對(duì)象轉(zhuǎn)換成原始類型的值(數(shù)值、字符串和布爾值)。但是我們很少需要自己調(diào)用此函數(shù),valueOf 方法一般都會(huì)被 JavaScript 自動(dòng)調(diào)用。
記住上面這句話,下面我們會(huì)細(xì)說所謂的自動(dòng)調(diào)用是什么意思。
Object.prototype.toString()
toString() 方法返回一個(gè)表示該對(duì)象的字符串。
每個(gè)對(duì)象都有一個(gè) toString() 方法,當(dāng)對(duì)象被表示為文本值時(shí)或者當(dāng)以期望字符串的方式引用對(duì)象時(shí),該方法被自動(dòng)調(diào)用。
這里先記住,valueOf() 和 toString() 在特定的場合下會(huì)自行調(diào)用。
原始類型
好,鋪墊一下,先了解下 javascript 的幾種原始類型,除去 Object 和 Symbol,有如下幾種原始類型:
- Number
- String
- Boolean
- Undefined
- Null
在 JavaScript 進(jìn)行對(duì)比或者各種運(yùn)算的時(shí)候會(huì)把對(duì)象轉(zhuǎn)換成這些類型,從而進(jìn)行后續(xù)的操作,下面逐一說明:
String 類型轉(zhuǎn)換
在某個(gè)操作或者運(yùn)算需要字符串的時(shí)候,會(huì)觸發(fā) Object 的 String 轉(zhuǎn)換,舉個(gè)例子:
var obj = {name: 'Coco'}; var str = '123' + obj; console.log(str); // 123[object Object]
轉(zhuǎn)換規(guī)則:
- 如果 toString 方法存在并且返回原始類型,返回 toString 的結(jié)果。
- 如果 toString 方法不存在或者返回的不是“原始類型”,調(diào)用valueOf 方法,如果 valueOf 方法存在,并且返回“原始類型”數(shù)據(jù),返回 valueOf 的結(jié)果。
- 其他情況,拋出錯(cuò)誤。
上面的例子實(shí)際上是:
var obj = {name: 'Coco'}; var str = '123' + obj.toString();
其中,obj.toString() 的值為 "[object Object]"。
假設(shè)是數(shù)組:
var arr = [1, 2]; var str = '123' + arr; console.log(str); // 1231,2
上面 + arr 其實(shí)是調(diào)用了 + arr.toString() 。
但是,我們可以自己改寫對(duì)象的 toString,valueOf 方法:
var obj = { toString: function() { console.log('調(diào)用了 obj.toString'); return '111'; } } alert(obj); // 調(diào)用了 obj.toString // 111
上面 alert(obj) ,obj 會(huì)自動(dòng)調(diào)用自己的 obj.toString() 方法轉(zhuǎn)化為原始類型,如果我們不重寫它的 toString 方法,將輸出 [object Object] ,這里我們重寫了 toString ,而且返回了一個(gè)原始類型字符串 111 ,所以最終 alert 出了 111。
上面的轉(zhuǎn)化規(guī)則寫了,toString 方法需要存在并且返回原始類型,那么如果返回的不是一個(gè)原始類型,則會(huì)去繼續(xù)尋找對(duì)象的 valueOf 方法:
下面我們嘗試證明如果 toString() 方法不可用的時(shí)候系統(tǒng)會(huì)調(diào)用 valueOf() 方法,下面我們改寫對(duì)象的 valueOf:
var obj = { toString: function() { console.log('調(diào)用了 obj.toString'); return {}; }, valueOf: function() { console.log('調(diào)用了 obj.valueOf') return '110'; } } alert(obj); // 調(diào)用了 obj.toString // 調(diào)用了 obj.valueOf // 110
從結(jié)果可以看到,當(dāng) toString 不可用的時(shí)候,系統(tǒng)會(huì)再嘗試 valueOf 方法,如果 valueOf 方法存在,并且返回原始類型(String、Number、Boolean)數(shù)據(jù),返回valueOf的結(jié)果。
那么如果,toString 和 valueOf 返回的都不是原始類型呢?看下面這個(gè)例子:
var obj = { toString: function() { console.log('調(diào)用了 obj.toString'); return {}; }, valueOf: function() { console.log('調(diào)用了 obj.valueOf') return {}; } } alert(obj); // 調(diào)用了 obj.toString // 調(diào)用了 obj.valueOf // Uncaught TypeError: Cannot convert object to primitive value
可以發(fā)現(xiàn),如果 toString 和 valueOf 方法均不可用的情況下,系統(tǒng)會(huì)直接返回一個(gè)錯(cuò)誤。
Number 類型轉(zhuǎn)換
上面描述的是 String 類型的轉(zhuǎn)換,很多時(shí)候也會(huì)發(fā)生 Number 類型的轉(zhuǎn)換:
- 調(diào)用 Number() 函數(shù),強(qiáng)制進(jìn)行 Number 類型轉(zhuǎn)換
- 調(diào)用 Math.sqrt() 這類參數(shù)需要 Number 類型的方法
- obj == 1 ,進(jìn)行對(duì)比的時(shí)候
- obj + 1 , 進(jìn)行運(yùn)算的時(shí)候
與 String 類型轉(zhuǎn)換相似,但是 Number 類型剛好反過來,先查詢自身的 valueOf 方法,再查詢自己 toString 方法:
- 如果 valueOf 存在,且返回原始類型數(shù)據(jù),返回 valueOf 的結(jié)果。
- 如果 toString 存在,且返回原始類型數(shù)據(jù),返回 toString 的結(jié)果。
- 其他情況,拋出錯(cuò)誤。
按照上述步驟,分別嘗試一下:
var obj = { valueOf: function() { console.log('調(diào)用 valueOf'); return 5; } } console.log(obj + 1); // 調(diào)用 valueOf // 6 var obj = { valueOf: function() { console.log('調(diào)用 valueOf'); return {}; }, toString: function() { console.log('調(diào)用 toString'); return 10; } } console.log(obj + 1); // 調(diào)用 valueOf // 調(diào)用 toString // 11 var obj = { valueOf: function() { console.log('調(diào)用 valueOf'); return {}; }, toString: function() { console.log('調(diào)用 toString'); return {}; } } console.log(obj + 1); // 調(diào)用 valueOf // 調(diào)用 toString // Uncaught TypeError: Cannot convert object to primitive value
Boolean 轉(zhuǎn)換
什么時(shí)候會(huì)進(jìn)行布爾轉(zhuǎn)換呢:
- 布爾比較時(shí)
- if(obj) , while(obj) 等判斷時(shí)
簡單來說,除了下述 6 個(gè)值轉(zhuǎn)換結(jié)果為 false,其他全部為 true:
- undefined
- null
- -0
- 0或+0
- NaN
- ''(空字符串)
Boolean(undefined) // false Boolean(null) // false Boolean(0) // false Boolean(NaN) // false Boolean('') // false
Function 轉(zhuǎn)換
好,最后回到我們一開始的題目,來講講函數(shù)的轉(zhuǎn)換。
我們定義一個(gè)函數(shù)如下:
function test() { var a = 1; console.log(1); }
如果我們僅僅是調(diào)用 test 而不是 test() ,看看會(huì)發(fā)生什么?
可以看到,這里把我們定義的 test 函數(shù)的重新打印了一遍,其實(shí),這里自行調(diào)用了函數(shù)的 valueOf 方法:
我們改寫一下 test 函數(shù)的 valueOf 方法。
test.valueOf = function() { console.log('調(diào)用 valueOf 方法'); return 2; } test; // 輸出如下: // 調(diào)用 valueOf 方法 // 2
與 Number 轉(zhuǎn)換類似,如果函數(shù)的 valueOf 方法返回的不是一個(gè)原始類型,會(huì)繼續(xù)找到它的 toString 方法:
test.valueOf = function() { console.log('調(diào)用 valueOf 方法'); return {}; } test.toString= function() { console.log('調(diào)用 toString 方法'); return 3; } test; // 輸出如下: // 調(diào)用 valueOf 方法 // 調(diào)用 toString 方法 // 3
破題
再看回我正文開頭那題的答案,正是運(yùn)用了函數(shù)會(huì)自行調(diào)用 valueOf 方法這個(gè)技巧,并改寫了該方法。我們稍作改變,變形如下:
function add () { console.log('進(jìn)入add'); var args = Array.prototype.slice.call(arguments); var fn = function () { var arg_fn = Array.prototype.slice.call(arguments); console.log('調(diào)用fn'); return add.apply(null, args.concat(arg_fn)); } fn.valueOf = function () { console.log('調(diào)用valueOf'); return args.reduce(function(a, b) { return a + b; }) } return fn; }
當(dāng)調(diào)用一次 add 的時(shí)候,實(shí)際是是返回 fn 這個(gè) function,實(shí)際是也就是返回 fn.valueOf();
add(1); // 輸出如下: // 進(jìn)入add // 調(diào)用valueOf // 1
其實(shí)也就是相當(dāng)于:
[1].reduce(function(a, b) { return a + b; }) // 1
當(dāng)鏈?zhǔn)秸{(diào)用兩次的時(shí)候:
add(1)(2); // 輸出如下: // 進(jìn)入add // 調(diào)用fn // 進(jìn)入add // 調(diào)用valueOf // 3
當(dāng)鏈?zhǔn)秸{(diào)用三次的時(shí)候:
add(1)(2)(3); // 輸出如下: // 進(jìn)入add // 調(diào)用fn // 進(jìn)入add // 調(diào)用fn // 進(jìn)入add // 調(diào)用valueOf // 6
可以看到,這里其實(shí)有一種循環(huán)。只有最后一次調(diào)用才真正調(diào)用到 valueOf,而之前的操作都是合并參數(shù),遞歸調(diào)用本身,由于最后一次調(diào)用返回的是一個(gè) fn 函數(shù),所以最終調(diào)用了函數(shù)的 fn.valueOf,并且利用了 reduce 方法對(duì)所有參數(shù)求和。
除了改寫 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; }
這里有個(gè)規(guī)律,如果只改寫 valueOf() 或是 toString() 其中一個(gè),會(huì)優(yōu)先調(diào)用被改寫了的方法,而如果兩個(gè)同時(shí)改寫,則會(huì)像 String 轉(zhuǎn)換規(guī)則一樣,優(yōu)先查詢 valueOf() 方法,在 valueOf() 方法返回的是非原始類型的情況下再查詢 toString() 方法。
如果你能認(rèn)真讀完,相信會(huì)有所收獲。
以上就是本文的全部內(nèi)容,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,同時(shí)也希望多多支持腳本之家!
- 詳解JS中的this、apply、call、bind(經(jīng)典面試題)
- 關(guān)于javascript作用域的常見面試題分享
- 10道典型的JavaScript面試題
- 80%應(yīng)聘者都不及格的JS面試題
- Javascript前端經(jīng)典的面試題及答案
- JavaScript中最常見的三個(gè)面試題解析
- JS搜狐面試題分析
- JavaScript面試題(指針、帽子和女朋友)
- 關(guān)于js原型的面試題講解
- JavaScript面試題大全(推薦)
- AngularJS 面試題集錦
- js前端面試題及答案整理(一)
- 14 個(gè)折磨人的 JavaScript 面試題
- JS面試題---關(guān)于算法臺(tái)階的問題
- 一道優(yōu)雅面試題分析js中fn()和return fn()的區(qū)別
- 一道關(guān)于JavaScript變量作用域的面試題
- 一道常被人輕視的web前端常見面試題(JS)
- 最新Javascript程序員面試試題和解題方法
相關(guān)文章
vue 集成騰訊地圖實(shí)現(xiàn)api(附DEMO)
之前項(xiàng)目使用騰訊地圖,不利于開發(fā)者查找,這篇文章主要介紹了vue 集成騰訊地圖實(shí)現(xiàn)api,具有一定的參考價(jià)值,感興趣的可以了解下2021-07-07從js向Action傳中文參數(shù)出現(xiàn)亂碼問題的解決方法
Action獲取jsp表單中的中文參數(shù),只要整個(gè)項(xiàng)目都采用UTF-8編碼格式都不會(huì)出現(xiàn)亂碼問題;但JSP中用到JS,并從JS向Action傳中文參數(shù),就會(huì)出現(xiàn)中文亂的現(xiàn)象2013-12-12