Function.prototype.call
,手寫系列,萬文面試系列,必會系列必包含的內容,足見其在前端的份量。
本文基於MDN 和 ECMA 標準,和你們一塊兒重新認識call
。javascript
涉及知識點:前端
面試官的問題:
麻煩你手寫一下Function.prototype.call
java
ES6的拓展運算符
版本Function.prototype.call = function(context) {
context = context || window;
context["fn"] = this;
let arg = [...arguments].slice(1);
context["fn"](...arg);
delete context["fn"];
}
複製代碼
這個版本,應該不是面試官想要的真正答案。不作太多解析。node
eval
的版本Function.prototype.call = function (context) {
context = (context == null || context == undefined) ? window : new Object(context);
context.fn = this;
var arr = [];
for (var i = 1; i < arguments.length; i++) {
arr.push('arguments[' + i + ']');
}
var r = eval('context.fn(' + arr + ')');
delete context.fn;
return r;
}
複製代碼
這個版本值得完善的地方面試
this
是否是函數沒有進行判斷eval
必定會被容許執行嗎在咱們真正開始寫Function.prototype.call
以前,仍是先來看看MDN和 ECMA是怎麼定義她的。算法
function.call(thisArg, arg1, arg2, ...)
複製代碼
thisArg
編程
可選的。在 function 函數運行時使用的 this 值。請注意,this可能不是該方法看到的實際值:若是這個函數處於非嚴格模式下,則指定爲 null 或 undefined 時會自動替換爲指向全局對象,原始值會被包裝。
arg1, arg2, ...
指定的參數列表。小程序
這裏透露了幾個信息,我已經加粗標註:瀏覽器
window
。固然MDN這裏說是window也沒太大問題。我想補充的是 nodejs
也實現了 ES標準。因此咱們實現的時候,是否是要考慮到 nodejs
環境呢。Object(val)
,即完成了對原始值val
的包裝。在 Function.prototype.call() - JavaScript | MDN的底部羅列了ES規範版本,每一個版本都有call實現的
說明。安全
咱們實現的,是要基於ES的某個版原本實現的。
由於ES的版本不一樣,實現的細節可能不同,實現的環境也不同。
規範版本 | 狀態 | 說明 |
---|---|---|
ECMAScript 1st Edition (ECMA-262) | Standard | 初始定義。在 JavaScript 1.3 中實現。 |
ECMAScript 5.1 (ECMA-262) Function.prototype.call |
Standard | |
ECMAScript 2015 (6th Edition, ECMA-262) Function.prototype.call |
Standard | |
ECMAScript (ECMA-262) Function.prototype.call |
Living Standard |
在ES3標準中關於call
的規範說明在11.2.3 Function Calls
, 直接搜索就能查到。
咱們今天主要是基於2009年ES5標準下來實現Function.prototype.call
,有人可能會說,你這,爲嘛不在 ES3標準下實現,由於ES5下能涉及更多的知識點。
(context == null || context == undefined) ? window : new Object(context)
上面代碼的 undefined
不必定是可靠的。
引用一段MDN的話:
在現代瀏覽器(JavaScript 1.8.5/Firefox 4+),自ECMAscript5標準以來undefined是一個不能被配置(non-configurable),不能被重寫(non-writable)的屬性。即使事實並不是如此,也要避免去重寫它。
在沒有交代上下文的狀況使用 void 0
比直接使用 undefined
更爲安全。
有些同窗可能沒見過undefined被改寫的狀況,沒事,來一張圖:
void
這個一元運算法除了這個 準備返回 undefined
外, 還有另外兩件常見的用途:
a標籤的href,就是什麼都不作
<a href="javascript:void(0);">
IIFE當即執行
;void function(msg){
console.log(msg)
}("你好啊");
複製代碼
固然更直接的方式是:
;(function(msg){
console.log(msg)
})("你好啊");
複製代碼
瀏覽器環境:
typeof self == 'object' && self.self === self
複製代碼
nodejs環境:
typeof global == 'object' && global.global === global
複製代碼
如今已經有 globalThis, 在高版本瀏覽器和nodejs裏面都支持。
顯然,在咱們的這個場景下,還不能用,可是其思想能夠借鑑:
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
複製代碼
Strict mode 嚴格模式,是ES5引入的特性。那咱們怎麼驗證你的環境是否是支持嚴格模式呢?
var hasStrictMode = (function(){
"use strict";
return this == undefined;
}());
複製代碼
正常狀況都會返回true
,放到IE8裏面執行:
在非嚴格模式下,函數的調用上下文(this的值)是全局對象。在嚴格模式下,調用上下文是undefined。
知道是否是支持嚴格模式,還不夠,咱們還要知道咱們是否是處於嚴格模式下。
以下的代碼能夠檢測,是否是處於嚴格模式:
var isStrict = (function(){
return this === undefined;
}());
複製代碼
這段代碼在支持嚴格模式的瀏覽器下和nodejs
環境下都是工做的。
var r = eval('context.fn(' + arr + ')');
delete context.fn;
複製代碼
如上的代碼直接刪除了context上的fn
屬性,若是原來的context上有fn
屬性,那會不會丟失呢?
咱們採用eval
版本的call
, 執行下面的代碼
var context = {
fn: "i am fn",
msg: "i am msg"
}
log.call(context); // i am msg
console.log("msg:", context.msg); // i am msg
console.log("fn:", context.fn); // fn: undedined
複製代碼
能夠看到context的fn
屬性已經被幹掉了,是破壞了入參,產生了不應產生的反作用。
與反作用對應的是函數式編程中的 純函數。
對應的咱們要採起行動,基本兩種思路:
均可以,不過以爲 方案2更簡單和容易實現:
基本代碼以下:
var ctx = new Object(context);
var propertyName = "__fn__";
var originVal;
var hasOriginVal = ctx.hasOwnProperty(propertyName)
if(hasOriginVal){
originVal = ctx[propertyName]
}
...... // 其餘代碼
if(hasOriginVal){
ctx[propertyName] = originVal;
}
複製代碼
eval
的實現,基本以下基於標準ECMAScript 5.1 (ECMA-262) Function.prototype.call
When the call method is called on an object func with argument thisArg and optional arguments arg1, arg2 etc, the following steps are taken:
1. If IsCallable(func) is false, then throw a TypeError exception.
2. Let argList be an empty List.
3. If this method was called with more than one argument then in left to right
order starting with arg1 append each argument as the last element of argList
4. Return the result of calling the [[Call]] internal method of func, providing
thisArg as the this value and argList as the list of arguments.
The length property of the call method is 1.
NOTE The thisArg value is passed without modification as the this value. This is a
change from Edition 3, where a undefined or null thisArg is replaced with the
global object and ToObject is applied to all other values and that result is passed
as the this value.
複製代碼
對咱們比較重要的是 1
和 Note
:
看看咱們的基礎實現
var hasStrictMode = (function () {
"use strict";
return this == undefined;
}());
var isStrictMode = function () {
return this === undefined;
};
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
function isFunction(fn){
return typeof fn === "function";
}
function getContext(context) {
var isStrict = isStrictMode();
if (!hasStrictMode || (hasStrictMode && !isStrict)) {
return (context === null || context === void 0) ? getGlobal() : Object(context);
}
// 嚴格模式下, 妥協方案
return Object(context);
}
Function.prototype.call = function (context) {
// 不能夠被調用
if (typeof this !== 'function') {
throw new TypeError(this + ' is not a function');
}
// 獲取上下文
var ctx = getContext(context);
// 更爲穩妥的是建立惟一ID, 以及檢查是否有重名
var propertyName = "__fn__" + Math.random() + "_" + new Date().getTime();
var originVal;
var hasOriginVal = isFunction(ctx.hasOwnProperty) ? ctx.hasOwnProperty(propertyName) : false;
if (hasOriginVal) {
originVal = ctx[propertyName]
}
ctx[propertyName] = this;
// 採用string拼接
var argStr = '';
var len = arguments.length;
for (var i = 1; i < len; i++) {
argStr += (i === len - 1) ? 'arguments[' + i + ']' : 'arguments[' + i + '],'
}
var r = eval('ctx["' + propertyName + '"](' + argStr + ')');
// 還原現場
if (hasOriginVal) {
ctx[propertyName] = originVal;
} else {
delete ctx[propertyName]
}
return r;
}
複製代碼
當前版依舊存在問題,
Obeject
進行了封裝。會致使嚴格模式下傳遞非對象的時候,this的指向是不許的, 不得以的妥協。 哪位同窗有更好的方案,敬請指導。
因此完美的解決方法,就是產生一個UID.
eval
的執行,可能會被 Content-Security-Policy 阻止大體的提示信息以下:
[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an
allowed source of script in the following Content Security Policy directive: "script-src ......... 複製代碼
前面兩條都應該還能接受,至於第三條,咱們不能妥協。
這就得請出下一位嘉賓, new Function
。
new Function ([arg1[, arg2[, ...argN]],] functionBody)
其基本格式如上,最後一個爲函數體。
舉個簡單的例子:
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6));
// expected output: 8
複製代碼
咱們call
的參數個數是不固定,思路就是從arguments
動態獲取。
這裏咱們的實現借用面試官問:可否模擬實現JS的call和apply方法 實現方法:
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;
}
複製代碼
var hasStrictMode = (function () {
"use strict";
return this == undefined;
}());
var isStrictMode = function () {
return this === undefined;
};
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
function isFunction(fn){
return typeof fn === "function";
}
function getContext(context) {
var isStrict = isStrictMode();
if (!hasStrictMode || (hasStrictMode && !isStrict)) {
return (context === null || context === void 0) ? getGlobal() : Object(context);
}
// 嚴格模式下, 妥協方案
return Object(context);
}
function generateFunctionCode(argsLength){
var code = 'return arguments[0][arguments[1]](';
for(var i = 0; i < argsLength; i++){
if(i > 0){
code += ',';
}
code += 'arguments[2][' + i + ']';
}
code += ')';
// return arguments[0][arguments[1]](arg1, arg2, arg3...)
return code;
}
Function.prototype.call = function (context) {
// 不能夠被調用
if (typeof this !== 'function') {
throw new TypeError(this + ' is not a function');
}
// 獲取上下文
var ctx = getContext(context);
// 更爲穩妥的是建立惟一ID, 以及檢查是否有重名
var propertyName = "__fn__" + Math.random() + "_" + new Date().getTime();
var originVal;
var hasOriginVal = isFunction(ctx.hasOwnProperty) ? ctx.hasOwnProperty(propertyName) : false;
if (hasOriginVal) {
originVal = ctx[propertyName]
}
ctx[propertyName] = this;
var argArr = [];
var len = arguments.length;
for (var i = 1; i < len; i++) {
argArr[i - 1] = arguments[i];
}
var r = new Function(generateFunctionCode(argArr.length))(ctx, propertyName, argArr);
// 還原現場
if (hasOriginVal) {
ctx[propertyName] = originVal;
} else {
delete ctx[propertyName]
}
return r;
}
複製代碼
評論區最精彩:
Symbol
由於是基於ES5的標準來寫,若是使用Symbol
,那拓展運算符也可使用。 考察的知識面天然少不少。
這樣子的話,可能真的無能爲力了。
感謝虛鯤菜菜子的指正,其文章手寫 call 與 原生 Function.prototype.call 的區別 推薦你們細讀。
以下的代碼,嚴格模式下會報錯,非嚴格模式複製不成功:
"use strict";
var context = {
a: 1,
log(msg){
console.log("msg:", msg)
}
};
Object.freeze(context);
context.fn = function(){
};
console.log(context.fn);
VM111 call:12 Uncaught TypeError: Cannot add property fn, object is not extensible
at VM49 call:12
複製代碼
這種狀況怎麼辦呢,我能想到的是兩種方式:
Obect.create
這也算是一種妥協方法,畢竟鏈路仍是變長了。
"use strict";
var context = {
a: 1,
log(msg){
console.log("msg:", msg)
}
};
Object.freeze(context);
var ctx = Object.create(context);
ctx.fn = function(){
}
console.log("fn:", typeof ctx.fn); // fn: function
console.log("ctx.a", ctx.a); // ctx.a 1
console.log("ctx.fn", ctx.fn); // ctx.fn ƒ (){}
複製代碼
回顧一下依舊存在的問題
Object
進行了封裝基礎數據類型會致使嚴格模式下傳遞非對象的時候,this的指向是不許的, 不得以的妥協。 哪位同窗有更好的方案,敬請指導。
雖然說咱們把臨時的屬性名變得難以重名,可是若是重名,而函數調用中真調用了此方法,可能會致使異常行爲
小程序等環境可能禁止使用eval
和new Function
對象被凍結,call
執行函數中的this
不是真正傳入的上下文對象。
因此,我仍是修改標題爲三千文字,也沒寫好 Function.prototype.call
一個手寫call
涉及到很多的知識點,本人水平有限,若有遺漏,敬請諒解和補充。
當面試官問題的時候,你要清楚本身面試的崗位,是P6,P7仍是P8。
是高級開發仍是前端組長,抑或是前端負責人。
崗位不同,面試官固然指望的答案也不同。
寫做不易,您的支持就是我前行的最大動力。
Function.prototype.call() - JavaScript | MDN
Strict mode - JavaScript | MDN
ECMAScript 5 Strict Mode
ES合集
手寫call、apply、bind實現及詳解
call、apply、bind實現原理
面試官問:可否模擬實現JS的call和apply方法