寫于2018年11月30日
,發布在掘金上閱讀量近一萬,現在發布到微信公眾號申明原創。相對比較基礎的知識,雖然日常開發可能用得比較少,各種源碼中有很多call
和apply
,需要掌握。
前言
這是面試官問系列的第三篇,旨在幫助讀者提升
JS
基礎知識,包含new、call、apply、this、繼承
相關知識。面試官問系列
文章如下:感興趣的讀者可以點擊閱讀。
1.面試官問:能否模擬實現JS的new操作符
2.面試官問:能否模擬實現JS的bind方法
3.面試官問:能否模擬實現JS的call和apply方法
4.面試官問:JS的this指向
5.面試官問:JS的繼承
之前寫過兩篇《面試官問:能否模擬實現JS
的new
操作符》和《面試官問:能否模擬實現JS
的bind
方法》
其中模擬bind
方法時是使用的call
和apply
修改this
指向。但面試官可能問:能否不用call
和apply
來實現呢。意思也就是需要模擬實現call
和apply
的了。
附上之前寫文章寫過的一段話:已經有很多模擬實現
call
和apply
的文章,為什么自己還要寫一遍呢。學習就好比是座大山,人們沿著不同的路登山,分享著自己看到的風景。你不一定能看到別人看到的風景,體會到別人的心情。只有自己去登山,才能看到不一樣的風景,體會才更加深刻。
先通過MDN
認識下call
和apply
MDN 文檔:Function.prototype.call()
語法
fun.call(thisArg, arg1, arg2, ...)
thisArg
在fun
函數運行時指定的this
值。需要注意的是,指定的this
值并不一定是該函數執行時真正的this
值,如果這個函數處于非嚴格模式下,則指定為null
和undefined
的this
值會自動指向全局對象(瀏覽器中就是window
對象),同時值為原始值(數字,字符串,布爾值)的this
會指向該原始值的自動包裝對象。
arg1, arg2, ...
指定的參數列表
返回值
返回值是你調用的方法的返回值,若該方法沒有返回值,則返回undefined
。
MDN 文檔:Function.prototype.apply()
func.apply(thisArg, [argsArray])
thisArg
可選的。在 func
函數運行時使用的 this
值。請注意,this
可能不是該方法看到的實際值:如果這個函數處于非嚴格模式下,則指定為 null
或 undefined
時會自動替換為指向全局對象,原始值會被包裝。
argsArray
可選的。一個數組或者類數組對象,其中的數組元素將作為單獨的參數傳給 func
函數。如果該參數的值為 null
或 undefined
,則表示不需要傳入任何參數。從ECMAScript 5
開始可以使用類數組對象。
返回值
調用有指定this值和參數的函數的結果。直接先看例子1
call
和 apply
的異同
相同點:
1、call
和apply
的第一個參數thisArg
,都是func
運行時指定的this
。而且,this
可能不是該方法看到的實際值:如果這個函數處于非嚴格模式下,則指定為 null
或 undefined
時會自動替換為指向全局對象,原始值會被包裝。
2、都可以只傳遞一個參數。
不同點: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: '若川',doSth: doSth,
};
student.doSth(1, 2); // this === student // true // '若川' // [1, 2]
doSth.apply(student, [1, 2]); // this === student // true // '若川' // [1, 2]
可以得出結論1:在對象student
上加一個函數doSth
,再執行這個函數,這個函數里的this
就指向了這個對象。那也就是可以在thisArg
上新增調用函數,執行后刪除這個函數即可。知道這些后,我們試著容易實現第一版本:
// 瀏覽器環境 非嚴格模式function getGlobalObject(){returnthis;
}
Function.prototype.applyFn = function apply(thisArg, argsArray){ // `apply` 方法的 `length` 屬性是 `2`。// 1.如果 `IsCallable(func)` 是 `false`, 則拋出一個 `TypeError` 異常。if(typeofthis !== 'function'){thrownewTypeError(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 !== newObject(argsArray)){thrownewTypeError('CreateListFromArrayLike called on non-object');}if(typeof thisArg === 'undefined' || thisArg === null){// 在外面傳入的 thisArg 值會修改并成為 this 值。// ES3: thisArg 是 undefined 或 null 時它會被替換成全局對象 瀏覽器里是windowthisArg = getGlobalObject();}// ES3: 所有其他值會被應用 ToObject 并將結果作為 this 值,這是第三版引入的更改。thisArg = newObject(thisArg);var __fn = '__fn';thisArg[__fn] = this;// 9.提供 thisArg 作為 this 值并以 argList 作為參數列表,調用 func 的 [[Call]] 內部方法,返回結果var result = thisArg[__fn](...argsArray);delete thisArg[__fn];return result;
};
實現第一版后,很容易找出兩個問題:
[ ] 1.
__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: '若川',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: '若川', doSth: 'doSth' }
[ ] 2.使用了
ES6
擴展符...
解決方案一:采用eval
來執行函數。
eval
把字符串解析成代碼執行。
MDN 文檔:eval
語法
eval(string)
參數
string
表示JavaScript
表達式,語句或一系列語句的字符串。表達式可以包含變量以及已存在對象的屬性。
返回值
執行指定代碼之后的返回值。如果返回值為空,返回undefined
解決方案二:但萬一面試官不允許用eval
呢,畢竟eval
是魔鬼。可以采用new Function()
來生成執行函數。MDN 文檔:Function
語法
newFunction ([arg1[, arg2[, ...argN]],] functionBody)
參數
arg1, arg2, ... argN
被函數使用的參數的名稱必須是合法命名的。參數名稱是一個有效的JavaScript
標識符的字符串,或者一個用逗號分隔的有效字符串的列表;例如“×”
,“theValue”
,或“A,B”
。
functionBody
一個含有包括函數定義的JavaScript
語句的字符串。
接下來看兩個例子:
簡單例子:
var sum = newFunction('a', 'b', 'return a + b');
console.log(sum(2, 6));
// 稍微復雜點的例子:var student = {name: '若川',doSth: function(argsArray){console.log(argsArray);console.log(this.name);}
};
// var result = student.doSth(['若川i', 18]);// 用new Function()生成函數并執行返回結果var result = newFunction('return arguments[0][arguments[1]](arguments[2][0], arguments[2][1])')(student, 'doSth', ['若川i', 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;
}
你可能不知道在ES3、ES5
中 undefined
是能修改的
可能大部分人不知道。ES5
中雖然在全局作用域下不能修改,但在局部作用域中也是能修改的,不信可以復制以下測試代碼在控制臺執行下。雖然一般情況下是不會的去修改它。
function test(){varundefined = 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(){returnthis;
}
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(typeofthis !== 'function'){thrownewTypeError(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 !== newObject(argsArray)){thrownewTypeError('CreateListFromArrayLike called on non-object');}if(typeof thisArg === 'undefined' || thisArg === null){// 在外面傳入的 thisArg 值會修改并成為 this 值。// ES3: thisArg 是 undefined 或 null 時它會被替換成全局對象 瀏覽器里是windowthisArg = getGlobalObject();}// ES3: 所有其他值會被應用 ToObject 并將結果作為 this 值,這是第三版引入的更改。thisArg = newObject(thisArg);var __fn = '__' + newDate().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 = (newFunction(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);returnthis.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: '若川',age: 18,doSth: 'doSth',__fn: 'doSth',
};
var firstArg = student;
var result = doSth.applyFn(firstArg, [1, {name: '若川i'}]);
var result2 = doSth.callFn(firstArg, 1, {name: '若川i'});
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的對象的lengthvar m = %_ArgumentsLength(); // push的參數個數for (var i = 0; i < m; i++) {this[ i + n ] = %_Arguments( i ); // 復制元素 (1)}this.length = n + m; // 修正length屬性的值 (2)returnthis.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(){returnthis;
}
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(typeofthis !== 'function'){thrownewTypeError(this + ' is not a function');}if(typeof argsArray === 'undefined' || argsArray === null){argsArray = [];}if(argsArray !== newObject(argsArray)){thrownewTypeError('CreateListFromArrayLike called on non-object');}if(typeof thisArg === 'undefined' || thisArg === null){thisArg = getGlobalObject();}thisArg = newObject(thisArg);var __fn = '__' + newDate().getTime();var originalVal = thisArg[__fn];var hasOriginalVal = thisArg.hasOwnProperty(__fn);thisArg[__fn] = this;var code = generateFunctionCode(argsArray.length);var result = (newFunction(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];}returnthis.applyFn(thisArg, argsArray);
}
學習源碼整體架構系列
1.學習 jQuery 源碼整體架構,打造屬于自己的 js 類庫
2.學習 underscore 源碼整體架構,打造屬于自己的函數式編程類庫
3.學習 lodash 源碼整體架構,打造屬于自己的函數式編程類庫
4.學習 sentry 源碼整體架構,打造屬于自己的前端異常監控SDK
5.學習 vuex 源碼整體架構,打造屬于自己的狀態管理庫
6.學習 axios 源碼整體架構,打造屬于自己的請求庫
微信公眾號
作者:常以若川為名混跡于江湖。前端路上 | PPT 愛好者 | 所知甚少,唯善學。
博客:https://lxchuan12.cn/posts/
,閱讀體驗可能更好些。
主要發布
前端 | PPT | 生活 | 效率
相關的文章,長按掃碼關注。歡迎加我微信lxchuan12
(注明來源,基本來者不拒),拉您進【前端視野交流群】,長期交流學習~
小提醒:若川視野公眾號原創文章合集在菜單欄中間【原創精選】按鈕,歡迎點擊閱讀
由于公眾號限制外鏈,點擊閱讀原文,或許閱讀體驗更佳,覺得文章不錯,可以點個在看呀^_^另外歡迎留言交流~