javascript
一道面试题引发的对JavaScript类型转换的思考
最近群里有人發(fā)了下面這題:
實現(xiàn)一個函數(shù),運算結(jié)果可以滿足如下預(yù)期結(jié)果:
add(1)(2) // 3 add(1, 2, 3)(10) // 16 add(1)(2)(3)(4)(5) // 15對于一個好奇的切圖仔來說,忍不住動手嘗試了一下,看到題目首先想到的是會用到高階函數(shù)以及?Array.prototype.reduce()。
高階函數(shù)(Higher-order function):高階函數(shù)的意思是它接收另一個函數(shù)作為參數(shù)。在 javascript 中,函數(shù)是一等公民,允許函數(shù)作為參數(shù)或者返回值傳遞。
得到了下面這個解法:
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;});} }驗證了一下,發(fā)現(xiàn)錯了:
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ù)多于兩個或者少于兩個的時候,無法返回結(jié)果。
而這個也是這題的一個難點所在,add()的時候,如何既返回一個值又返回一個函數(shù)以供后續(xù)繼續(xù)調(diào)用?
后來經(jīng)過高人指點,通過重寫函數(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; }嗯?第一眼看到這個解法的時候,我是懵逼的。因為我感覺?fn.valueOf()?從頭到尾都沒有被調(diào)用過,但是驗證了下結(jié)果:
add(1) // 1 add(1,2)(3) //6 add(1)(2)(3)(4)(5) // 15神奇的對了!那么玄機必然是在上面的?fn.valueOf = function() {}?內(nèi)了。為何會是這樣呢?這個方法是在函數(shù)的什么時刻執(zhí)行的?且聽我一步一步道來。
valueOf 和 toString
先來簡單了解下這兩個方法:
Object.prototype.valueOf()
用?MDN?的話來說,valueOf() 方法返回指定對象的原始值。
JavaScript 調(diào)用 valueOf() 方法用來把對象轉(zhuǎn)換成原始類型的值(數(shù)值、字符串和布爾值)。但是我們很少需要自己調(diào)用此函數(shù),valueOf 方法一般都會被 JavaScript 自動調(diào)用。
記住上面這句話,下面我們會細(xì)說所謂的自動調(diào)用是什么意思。
Object.prototype.toString()
toString() 方法返回一個表示該對象的字符串。
每個對象都有一個 toString() 方法,當(dāng)對象被表示為文本值時或者當(dāng)以期望字符串的方式引用對象時,該方法被自動調(diào)用。
這里先記住,valueOf() 和 toString() 在特定的場合下會自行調(diào)用。
原始類型
好,鋪墊一下,先了解下 javascript 的幾種原始類型,除去 Object 和 Symbol,有如下幾種原始類型:
- Number
- String
- Boolean
- Undefined
- Null
在 JavaScript 進行對比或者各種運算的時候會把對象轉(zhuǎn)換成這些類型,從而進行后續(xù)的操作,下面逐一說明:?
String 類型轉(zhuǎn)換
在某個操作或者運算需要字符串而該對象又不是字符串的時候,會觸發(fā)該對象的 String 轉(zhuǎn)換,會將非字符串的類型嘗試自動轉(zhuǎn)為 String 類型。系統(tǒng)內(nèi)部會自動調(diào)用?toString?函數(shù)。舉個例子:
var obj = {name: 'Coco'}; var str = '123' + obj; console.log(str); // 123[object Object]轉(zhuǎn)換規(guī)則:
上面的例子實際上是:
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?,由于這里是個字符串加操作,后面的?arr?需要轉(zhuǎn)化為一個字符串類型,所以其實是調(diào)用了?+ arr.toString()?。
但是,我們可以自己改寫對象的?toString,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上面?alert(obj + '1')?,obj 會自動調(diào)用自己的?obj.toString()?方法轉(zhuǎn)化為原始類型,如果我們不重寫它的?toString?方法,將輸出?[object Object]1?,這里我們重寫了?toString?,而且返回了一個原始類型字符串?111?,所以最終 alert 出了 1111。
上面的轉(zhuǎn)化規(guī)則寫了,toString?方法需要存在并且返回原始類型,那么如果返回的不是一個原始類型,則會去繼續(xù)尋找對象的?valueOf?方法:
下面我們嘗試證明如果在一個對象嘗試轉(zhuǎn)換為字符串的過程中,如果?toString()?方法不可用的時候,會發(fā)生什么。
這個時候系統(tǒng)會再去調(diào)用?valueOf()?方法,下面我們改寫對象的?toString?和?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?不可用的時候,系統(tǒng)會再嘗試?valueOf?方法,如果?valueOf?方法存在,并且返回原始類型(String、Number、Boolean)數(shù)據(jù),返回valueOf的結(jié)果。
那么如果,toString?和?valueOf?返回的都不是原始類型呢?看下面這個例子:
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)會直接返回一個錯誤。
添加于 2017-03-07:在查證了 ECMAScript5 官方文檔后,發(fā)現(xiàn)上面的描述有一點問題,Object 類型轉(zhuǎn)換為 String 類型的轉(zhuǎn)換規(guī)則遠(yuǎn)比上面復(fù)雜。轉(zhuǎn)換規(guī)則為:1.設(shè)原始值為調(diào)用 ToPrimitive 的結(jié)果;2.返回 ToString(原始值) 。關(guān)于 ToPrimitive 和 ToString 的規(guī)則可以看看官方文檔:ECMAScript5 — ToString
Number 類型轉(zhuǎn)換
上面描述的是 String 類型的轉(zhuǎn)換,很多時候也會發(fā)生 Number 類型的轉(zhuǎn)換:
- 調(diào)用 Number() 函數(shù),強制進行 Number 類型轉(zhuǎn)換
- 調(diào)用 Math.sqrt() 這類參數(shù)需要 Number 類型的方法
- obj == 1?,進行對比的時候
- obj + 1?, 進行運算的時候
與 String 類型轉(zhuǎn)換相似,但是 Number 類型剛好反過來,先查詢自身的?valueOf?方法,再查詢自己?toString?方法:
按照上述步驟,分別嘗試一下:
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 valueBoolean 轉(zhuǎn)換
什么時候會進行布爾轉(zhuǎn)換呢:
- 布爾比較時
- if(obj) , while(obj) 等判斷時
簡單來說,除了下述 6 個值轉(zhuǎn)換結(jié)果為 false,其他全部為 true:
- undefined
- null
- -0
- 0或+0
- NaN
- ”(空字符串)
Function 轉(zhuǎn)換
好,最后回到我們一開始的題目,來講講函數(shù)的轉(zhuǎn)換。
我們定義一個函數(shù)如下:
function test() {var a = 1;console.log(1); }如果我們僅僅是調(diào)用?test?而不是?test()?,看看會發(fā)生什么?
可以看到,這里把我們定義的 test 函數(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?方法返回的不是一個原始類型,會繼續(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破題
再看回我正文開頭那題的答案,正是運用了函數(shù)會自行調(diào)用?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('調(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 的時候,實際是是返回 fn 這個 function,實際是也就是返回?fn.valueOf();
add(1); // 輸出如下: // 進入add // 調(diào)用valueOf // 1其實也就是相當(dāng)于:
[1].reduce(function(a, b) {return a + b; }) // 1當(dāng)鏈?zhǔn)秸{(diào)用兩次的時候:
add(1)(2); // 輸出如下: // 進入add // 調(diào)用fn // 進入add // 調(diào)用valueOf // 3當(dāng)鏈?zhǔn)秸{(diào)用三次的時候:
add(1)(2)(3); // 輸出如下: // 進入add // 調(diào)用fn // 進入add // 調(diào)用fn // 進入add // 調(diào)用valueOf // 6可以看到,這里其實有一種循環(huán)。只有最后一次調(diào)用才真正調(diào)用到?valueOf,而之前的操作都是合并參數(shù),遞歸調(diào)用本身,由于最后一次調(diào)用返回的是一個 fn 函數(shù),所以最終調(diào)用了函數(shù)的?fn.valueOf,并且利用了 reduce 方法對所有參數(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; }這里有個規(guī)律,如果只改寫?valueOf()?或是?toString()?其中一個,會優(yōu)先調(diào)用被改寫了的方法,而如果兩個同時改寫,則會像 Number 類型轉(zhuǎn)換規(guī)則一樣,優(yōu)先查詢?valueOf()?方法,在?valueOf()?方法返回的是非原始類型的情況下再查詢?toString()?方法。
后記
像阮一峰老師所說的,“炫耀從來不是我寫作的動機,好奇才是”。本文行文過程也是我自己學(xué)習(xí)的一個過程,過程中我也遇到了很多困惑,所以即便查閱了官方文檔及大量的文章,但是錯誤及疏漏仍然在所難免,歡迎指正及給出更好的方法。
對于類型轉(zhuǎn)換,最好還是看看 ECMAScript 規(guī)范,拒絕成為伸手黨,自己多嘗試。另外評論處有很多人提出了自己的疑問,值得一看。
到此本文結(jié)束,如果還有什么疑問或者建議,可以多多交流,原創(chuàng)文章,文筆有限,才疏學(xué)淺,文中若有不正之處,萬望告知。
from:?http://developer.51cto.com/art/201703/534335.htm
總結(jié)
以上是生活随笔為你收集整理的一道面试题引发的对JavaScript类型转换的思考的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaScript中的遍历详解
- 下一篇: 你不知道的JavaScript错误和调用