以前寫過兩篇《面試官問:可否模擬實現JS
的new
操做符》和《面試官問:可否模擬實現JS
的bind
方法》前端
其中模擬bind
方法時是使用的call
和apply
修改this
指向。但面試官可能問:可否不用call
和apply
來實現呢。意思也就是須要模擬實現call
和apply
的了。git
附上以前寫文章寫過的一段話:已經有不少模擬實現call
和apply
的文章,爲何本身還要寫一遍呢。學習就比如是座大山,人們沿着不一樣的路爬山,分享着本身看到的風景。你不必定能看到別人看到的風景,體會到別人的心情。只有本身去爬山,才能看到不同的風景,體會才更加深入。
MDN
認識下call
和apply
MDN 文檔:Function.prototype.call()
語法
github
fun.call(thisArg, arg1, arg2, ...)
thisArg
在fun
函數運行時指定的this
值。須要注意的是,指定的this
值並不必定是該函數執行時真正的this
值,若是這個函數處於非嚴格模式下,則指定爲null
和undefined
的this
值會自動指向全局對象(瀏覽器中就是window
對象),同時值爲原始值(數字,字符串,布爾值)的this
會指向該原始值的自動包裝對象。
arg1, arg2, ...
指定的參數列表
返回值
返回值是你調用的方法的返回值,若該方法沒有返回值,則返回undefined
。
面試
MDN 文檔:Function.prototype.apply()
chrome
func.apply(thisArg, [argsArray])
thisArg
可選的。在 func
函數運行時使用的 this
值。請注意,this
可能不是該方法看到的實際值:若是這個函數處於非嚴格模式下,則指定爲 null
或 undefined
時會自動替換爲指向全局對象,原始值會被包裝。
argsArray
可選的。一個數組或者類數組對象,其中的數組元素將做爲單獨的參數傳給 func
函數。若是該參數的值爲 null
或 undefined
,則表示不須要傳入任何參數。從ECMAScript 5
開始可使用類數組對象。
返回值
調用有指定this值和參數的函數的結果。
直接先看例子1segmentfault
call
和 apply
的異同相同點:
一、call
和apply
的第一個參數thisArg
,都是func
運行時指定的this
。並且,this
可能不是該方法看到的實際值:若是這個函數處於非嚴格模式下,則指定爲 null
或 undefined
時會自動替換爲指向全局對象,原始值會被包裝。
二、均可以只傳遞一個參數。
不一樣點:apply
只接收兩個參數,第二個參數能夠是數組也能夠是類數組,其實也能夠是對象,後續的參數忽略不計。call
接收第二個及之後一系列的參數。
看兩個簡單例子1和2**:設計模式
// 例子1:瀏覽器環境 非嚴格模式下 var doSth = function(a, b){ console.log(this); console.log([a, b]); } doSth.apply(null, [1, 2]); // this是window // [1, 2] doSth.apply(0, [1, 2]); // this 是 Number(0) // [1, 2] doSth.apply(true); // this 是 Boolean(true) // [undefined, undefined] doSth.call(undefined, 1, 2); // this 是 window // [1, 2] doSth.call('0', 1, {a: 1}); // this 是 String('0') // [1, {a: 1}]
// 例子2:瀏覽器環境 嚴格模式下 'use strict'; var doSth2 = function(a, b){ console.log(this); console.log([a, b]); } doSth2.call(0, 1, 2); // this 是 0 // [1, 2] doSth2.apply('1'); // this 是 '1' // [undefined, undefined] doSth2.apply(null, [1, 2]); // this 是 null // [1, 2]
typeof
有7
種類型(undefined number string boolean symbol object function
),筆者都驗證了一遍:更加驗證了相同點第一點,嚴格模式下,函數的this
值就是call
和apply
的第一個參數thisArg
,非嚴格模式下,thisArg
值被指定爲 null
或 undefined
時this
值會自動替換爲指向全局對象,原始值則會被自動包裝,也就是new Object()
。數組
從新認識了call
和apply
會發現:它們做用都是同樣的,改變函數裏的this
指向爲第一個參數thisArg
,若是明確有多少參數,那能夠用call
,不明確則可使用apply
。也就是說徹底能夠不使用call
,而使用apply
代替。
也就是說,咱們只須要模擬實現apply
,call
能夠根據參數個數都放在一個數組中,給到apply
便可。
瀏覽器
apply
既然準備模擬實現apply
,那先得看看ES5
規範。ES5規範 英文版
,ES5規範 中文版
。apply
的規範下一個就是call
的規範,能夠點擊打開新標籤頁去查看,這裏摘抄一部分。緩存
Function.prototype.apply (thisArg, argArray)
當以thisArg
和argArray
爲參數在一個func
對象上調用apply
方法,採用以下步驟:1.若是
IsCallable(func)
是false
, 則拋出一個TypeError
異常。
2.若是argArray
是null
或undefined
, 則返回提供thisArg
做爲this
值並以空參數列表調用func
的[[Call]]
內部方法的結果。
3.返回提供thisArg
做爲this
值並以空參數列表調用func
的[[Call]]
內部方法的結果。
4.若是Type(argArray)
不是Object
, 則拋出一個TypeError
異常。
5~8 略
9.提供thisArg
做爲this
值並以argList
做爲參數列表,調用func
的[[Call]]
內部方法,返回結果。apply
方法的length
屬性是2
。在外面傳入的
thisArg
值會修改併成爲this
值。thisArg
是undefined
或null
時它會被替換成全局對象,全部其餘值會被應用ToObject
並將結果做爲this
值,這是第三版引入的更改。
結合上文和規範,如何將函數裏的this
指向第一個參數thisArg
呢,這是一個問題。
這時候請出例子3:
// 瀏覽器環境 非嚴格模式下 var doSth = function(a, b){ console.log(this); console.log(this.name); console.log([a, b]); } var student = { name: '軒轅Rowboat', doSth: doSth, }; student.doSth(1, 2); // this === student // true // '軒轅Rowboat' // [1, 2] doSth.apply(student, [1, 2]); // this === student // true // '軒轅Rowboat' // [1, 2]
能夠得出結論1:在對象student
上加一個函數doSth
,再執行這個函數,這個函數裏的this
就指向了這個對象。那也就是能夠在thisArg
上新增調用函數,執行後刪除這個函數便可。
知道這些後,咱們試着容易實現初版本:
// 瀏覽器環境 非嚴格模式 function getGlobalObject(){ return this; } Function.prototype.applyFn = function apply(thisArg, argsArray){ // `apply` 方法的 `length` 屬性是 `2`。 // 1.若是 `IsCallable(func)` 是 `false`, 則拋出一個 `TypeError` 異常。 if(typeof this !== 'function'){ throw new TypeError(this + ' is not a function'); } // 2.若是 argArray 是 null 或 undefined, 則 // 返回提供 thisArg 做爲 this 值並以空參數列表調用 func 的 [[Call]] 內部方法的結果。 if(typeof argsArray === 'undefined' || argsArray === null){ argsArray = []; } // 3.若是 Type(argArray) 不是 Object, 則拋出一個 TypeError 異常 . if(argsArray !== new Object(argsArray)){ throw new TypeError('CreateListFromArrayLike called on non-object'); } if(typeof thisArg === 'undefined' || thisArg === null){ // 在外面傳入的 thisArg 值會修改併成爲 this 值。 // ES3: thisArg 是 undefined 或 null 時它會被替換成全局對象 瀏覽器裏是window thisArg = getGlobalObject(); } // ES3: 全部其餘值會被應用 ToObject 並將結果做爲 this 值,這是第三版引入的更改。 thisArg = new Object(thisArg); var __fn = '__fn'; thisArg[__fn] = this; // 9.提供 thisArg 做爲 this 值並以 argList 做爲參數列表,調用 func 的 [[Call]] 內部方法,返回結果 var result = thisArg[__fn](...argsArray); delete thisArg[__fn]; return result; };
__fn
同名覆蓋問題,thisArg
對象上有__fn
,那就被覆蓋了而後被刪除了。針對問題1
解決方案一:採用ES6
Sybmol()
獨一無二的。能夠原本就是模擬ES3
的方法。若是面試官不容許用呢。
解決方案二:本身用Math.random()
模擬實現獨一無二的key
。面試時能夠直接用生成時間戳便可。
// 生成UUID 通用惟一識別碼 // 大概生成 這樣一串 '18efca2d-6e25-42bf-a636-30b8f9f2de09' function generateUUID(){ var i, random; var uuid = ''; for (i = 0; i < 32; i++) { random = Math.random() * 16 | 0; if (i === 8 || i === 12 || i === 16 || i === 20) { uuid += '-'; } uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) .toString(16); } return uuid; } // 簡單實現 // '__' + new Date().getTime();
若是這個key
萬一這對象中仍是有,爲了保險起見,能夠作一次緩存操做。好比以下代碼:
var student = { name: '軒轅Rowboat', doSth: 'doSth', }; var originalVal = student.doSth; var hasOriginalVal = student.hasOwnProperty('doSth'); student.doSth = function(){}; delete student.doSth; // 若是沒有,`originalVal`則爲undefined,直接賦值新增了一個undefined,這是不對的,因此需判斷一下。 if(hasOriginalVal){ student.doSth = originalVal; } console.log('student:', student); // { name: '軒轅Rowboat', doSth: 'doSth' }
ES6
擴展符...
解決方案一:採用eval
來執行函數。
eval
把字符串解析成代碼執行。
MDN 文檔:eval
語法
eval(string)
參數
string
表示JavaScript
表達式,語句或一系列語句的字符串。表達式能夠包含變量以及已存在對象的屬性。
返回值
執行指定代碼以後的返回值。若是返回值爲空,返回undefined
解決方案二:但萬一面試官不容許用eval
呢,畢竟eval
是魔鬼。能夠採用new Function()
來生成執行函數。
MDN 文檔:Function
語法
new Function ([arg1[, arg2[, ...argN]],] functionBody)
參數
arg1, arg2, ... argN
被函數使用的參數的名稱必須是合法命名的。參數名稱是一個有效的JavaScript
標識符的字符串,或者一個用逗號分隔的有效字符串的列表;例如「×」
,「theValue」
,或「A,B」
。
functionBody
一個含有包括函數定義的JavaScript
語句的字符串。
接下來看兩個例子:
簡單例子: var sum = new Function('a', 'b', 'return a + b'); console.log(sum(2, 6));
// 稍微複雜點的例子: var student = { name: '軒轅Rowboat', doSth: function(argsArray){ console.log(argsArray); console.log(this.name); } }; // var result = student.doSth(['Rowboat', 18]); // 用new Function()生成函數並執行返回結果 var result = new Function('return arguments[0][arguments[1]](arguments[2][0], arguments[2][1])')(student, 'doSth', ['Rowboat', 18]); // 個數不定 // 因此能夠寫一個函數生成函數代碼: function generateFunctionCode(argsArrayLength){ var code = 'return arguments[0][arguments[1]]('; for(var i = 0; i < argsArrayLength; i++){ if(i > 0){ code += ','; } code += 'arguments[2][' + i + ']'; } code += ')'; // return arguments[0][arguments[1]](arg1, arg2, arg3...) return code; }
ES三、ES5
中 undefined
是能修改的可能大部分人不知道。ES5
中雖然在全局做用域下不能修改,但在局部做用域中也是能修改的,不信能夠複製如下測試代碼在控制檯執行下。雖然通常狀況下是不會的去修改它。
function test(){ var undefined = 3; console.log(undefined); // chrome下也是 3 } test();
因此判斷一個變量a
是否是undefined
,更嚴謹的方案是typeof a === 'undefined'
或者a === void 0;
這裏面用的是void
,void
的做用是計算表達式,始終返回undefined
,也能夠這樣寫void(0)
。
更多能夠查看韓子遲
的這篇文章:爲何用「void 0」代替「undefined」
解決了這幾個問題,比較容易實現以下代碼。
new Function()
模擬實現的apply
// 瀏覽器環境 非嚴格模式 function getGlobalObject(){ return this; } function generateFunctionCode(argsArrayLength){ var code = 'return arguments[0][arguments[1]]('; for(var i = 0; i < argsArrayLength; i++){ if(i > 0){ code += ','; } code += 'arguments[2][' + i + ']'; } code += ')'; // return arguments[0][arguments[1]](arg1, arg2, arg3...) return code; } Function.prototype.applyFn = function apply(thisArg, argsArray){ // `apply` 方法的 `length` 屬性是 `2`。 // 1.若是 `IsCallable(func)` 是 `false`, 則拋出一個 `TypeError` 異常。 if(typeof this !== 'function'){ throw new TypeError(this + ' is not a function'); } // 2.若是 argArray 是 null 或 undefined, 則 // 返回提供 thisArg 做爲 this 值並以空參數列表調用 func 的 [[Call]] 內部方法的結果。 if(typeof argsArray === 'undefined' || argsArray === null){ argsArray = []; } // 3.若是 Type(argArray) 不是 Object, 則拋出一個 TypeError 異常 . if(argsArray !== new Object(argsArray)){ throw new TypeError('CreateListFromArrayLike called on non-object'); } if(typeof thisArg === 'undefined' || thisArg === null){ // 在外面傳入的 thisArg 值會修改併成爲 this 值。 // ES3: thisArg 是 undefined 或 null 時它會被替換成全局對象 瀏覽器裏是window thisArg = getGlobalObject(); } // ES3: 全部其餘值會被應用 ToObject 並將結果做爲 this 值,這是第三版引入的更改。 thisArg = new Object(thisArg); var __fn = '__' + new Date().getTime(); // 萬一仍是有 先存儲一份,刪除後,再恢復該值 var originalVal = thisArg[__fn]; // 是否有原始值 var hasOriginalVal = thisArg.hasOwnProperty(__fn); thisArg[__fn] = this; // 9.提供 `thisArg` 做爲 `this` 值並以 `argList` 做爲參數列表,調用 `func` 的 `[[Call]]` 內部方法,返回結果。 // ES6版 // var result = thisArg[__fn](...args); var code = generateFunctionCode(argsArray.length); var result = (new Function(code))(thisArg, __fn, argsArray); delete thisArg[__fn]; if(hasOriginalVal){ thisArg[__fn] = originalVal; } return result; };
apply
模擬實現call
Function.prototype.callFn = function call(thisArg){ var argsArray = []; var argumentsLength = arguments.length; for(var i = 0; i < argumentsLength - 1; i++){ // argsArray.push(arguments[i + 1]); argsArray[i] = arguments[i + 1]; } console.log('argsArray:', argsArray); return this.applyFn(thisArg, argsArray); } // 測試例子 var doSth = function (name, age){ var type = Object.prototype.toString.call(this); console.log(typeof doSth); console.log(this === firstArg); console.log('type:', type); console.log('this:', this); console.log('args:', [name, age], arguments); return 'this--'; }; var name = 'window'; var student = { name: '軒轅Rowboat', age: 18, doSth: 'doSth', __fn: 'doSth', }; var firstArg = student; var result = doSth.applyFn(firstArg, [1, {name: 'Rowboat'}]); var result2 = doSth.callFn(firstArg, 1, {name: 'Rowboat'}); console.log('result:', result); console.log('result2:', result2);
細心的你會發現註釋了這一句argsArray.push(arguments[i + 1]);
,事實上push
方法,內部也有一層循環。因此理論上不使用push
性能會更好些。面試官也可能根據這點來問時間複雜度和空間複雜度的問題。
// 看看V8引擎中的具體實現: function ArrayPush() { var n = TO_UINT32( this.length ); // 被push的對象的length var m = %_ArgumentsLength(); // push的參數個數 for (var i = 0; i < m; i++) { this[ i + n ] = %_Arguments( i ); // 複製元素 (1) } this.length = n + m; // 修正length屬性的值 (2) return this.length; };
行文至此,就基本結束了,你可能還發現就是寫的非嚴格模式下,thisArg
原始值會包裝成對象,添加函數並執行,再刪除。而嚴格模式下仍是原始值這個沒有實現,並且萬一這個對象是凍結對象呢,Object.freeze({})
,是沒法在這個對象上添加屬性的。因此這個方法只能算是非嚴格模式下的簡版實現。最後來總結一下。
經過MDN
認識call
和apply
,閱讀ES5
規範,到模擬實現apply
,再實現call
。
就是使用在對象上添加調用apply
的函數執行,這時的調用函數的this
就指向了這個thisArg
,再返回結果。引出了ES6 Symbol
,ES6
的擴展符...
、eval
、new Function()
,嚴格模式等。
事實上,現實業務場景不須要去模擬實現call
和apply
,畢竟是ES3
就提供的方法。但面試官能夠經過這個面試題考察候選人不少基礎知識。如:call
、apply
的使用。ES6 Symbol
,ES6
的擴展符...
,eval
,new Function()
,嚴格模式,甚至時間複雜度和空間複雜度等。
讀者發現有不妥或可改善之處,歡迎指出。另外以爲寫得不錯,能夠點個贊,也是對筆者的一種支持。
// 最終版版 刪除註釋版,詳細註釋看文章 // 瀏覽器環境 非嚴格模式 function getGlobalObject(){ return this; } function generateFunctionCode(argsArrayLength){ var code = 'return arguments[0][arguments[1]]('; for(var i = 0; i < argsArrayLength; i++){ if(i > 0){ code += ','; } code += 'arguments[2][' + i + ']'; } code += ')'; return code; } Function.prototype.applyFn = function apply(thisArg, argsArray){ if(typeof this !== 'function'){ throw new TypeError(this + ' is not a function'); } if(typeof argsArray === 'undefined' || argsArray === null){ argsArray = []; } if(argsArray !== new Object(argsArray)){ throw new TypeError('CreateListFromArrayLike called on non-object'); } if(typeof thisArg === 'undefined' || thisArg === null){ thisArg = getGlobalObject(); } thisArg = new Object(thisArg); var __fn = '__' + new Date().getTime(); var originalVal = thisArg[__fn]; var hasOriginalVal = thisArg.hasOwnProperty(__fn); thisArg[__fn] = this; var code = generateFunctionCode(argsArray.length); var result = (new Function(code))(thisArg, __fn, argsArray); delete thisArg[__fn]; if(hasOriginalVal){ thisArg[__fn] = originalVal; } return result; }; Function.prototype.callFn = function call(thisArg){ var argsArray = []; var argumentsLength = arguments.length; for(var i = 0; i < argumentsLength - 1; i++){ argsArray[i] = arguments[i + 1]; } return this.applyFn(thisArg, argsArray); }
《JavaScript設計模式與開發實踐》- 第二章 第 2 章 this、call和apply
JS魔法堂:再次認識Function.prototype.call
不用call和apply方法模擬實現ES5的bind方法
JavaScript深刻之call和apply的模擬實現
做者:常以軒轅Rowboat若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
我的博客segmentfault
前端視野專欄,開通了前端視野專欄,歡迎關注
掘金專欄,歡迎關注
知乎前端視野專欄,開通了前端視野專欄,歡迎關注
github,歡迎follow
~