實現 call、apply、bind 在之前 "一篇文章" 寫了這三個參數的區別,但是其實面試更常考察如何實現。其實所有的原生函數的 polyfill 如何實現,只需要考慮 4 點即可: 1. 基本功能 2. 原型 3. this 4. 返回值 call 1. call 的基本功能: call() ...
實現 call、apply、bind
在之前一篇文章寫了這三個參數的區別,但是其實面試更常考察如何實現。其實所有的原生函數的 polyfill 如何實現,只需要考慮 4 點即可:
- 基本功能
- 原型
- this
- 返回值
call
call 的基本功能:
call() 方法使用一個指定的 this 值和單獨給出的一個或多個參數來調用一個函數。
- 原型
不涉及原型鏈的轉移,不用管 - this
本質上,call 就是 this 的轉移 返回值
簡單實現:
Function.prototype.myCall = function(context = window, ...args) {
context.fn = this; // 先將fn掛在context上,
var res = context.fn(...args); // 然後通過context調用fn,使得fn中的this指向指到context上
delete context.fn; // 最後刪除掉context上的fn
return res; // 返回原函數的返回值
};
上面為了簡單,使用了 ES6 的剩餘參數和展開語法,基本用這個回答面試官就好了。當然,如果不讓使用剩餘參數,那就只能使用eval
或者new Function
的字元串拼接大法了,可以參考這篇模板引擎。
再就是 fn 可能會和 context 重名,整一個不會重名的 uniqueID 掛上去,執行完畢後刪除。
apply
之前提過 apply 和 call 區別,只有一些入參和性能上的區別。直接上代碼:
Function.prototype.myApply = function(context = window, args) {
context.fn = this; // 先將fn掛在context上,
var res = context.fn(...args); // 然後通過context調用fn,使得fn中的this指向指到context上
delete context.fn; // 最後刪除掉context上的fn
return res; // 返回原函數的返回值
};
bind
bind 有點不一樣,它會返回一個綁定了 this 的函數。
bind()方法創建一個新的函數,在 bind()被調用時,這個新函數的 this 被 bind 的第一個參數指定,其餘的參數將作為新函數的參數供調用時使用。
Function.prototype.myBind = function(context, ...args) {
var fn = this;
var newFn = function(...restArgs) {
// 使用call去調用fn,因為bind可能會bind一部分參數,所以把restArgs也傳進去
return fn.call(context, ...args, ...restArgs);
};
return newFn;
};
上面的函數基本上覆蓋了大部分場景,但是不能支持new
調用——
綁定函數自動適應於使用 new 操作符去構造一個由目標函數創建的新實例。當一個綁定函數是用來構建一個值的,原來提供的 this 就會被忽略。不過提供的參數列表仍然會插入到構造函數調用時的參數列表之前。
如果直接使用我們上面所寫的bind
,就會返回
function Person(age, name) {
this.name = name;
this.age = age;
}
var Age18Person = Person.myBind(null, 18);
var a = {};
var Age20Person = Person.myBind(a, 20);
var p18 = new Age18Person("test18"); // newFn {}
var p20 = new Age20Person("test20"); // newFn {}
// a {name: "test20", age: 20}
// window {name: "test18", age: 18}
顯然,返回了以newFn
生成的對象,並且,因為傳入的是null
,所以,對context
的賦值轉移到了window
。
這裡需要判斷是否被 new 調用,然後丟棄沒用的 context。
Function.prototype.myBind = function(context, ...args) {
var fn = this;
var newFn = function(...restArgs) {
// 如果是new構造,則使用new構造的實例
if (new.target) {
return fn.call(this, ...args, ...restArgs);
}
// 使用call去調用fn,因為bind可能會bind一部分參數,所以把restArgs也傳進去
return fn.call(context, ...args, ...restArgs);
};
return newFn;
};
再次調用上面的new
構造,發現實例的原型不是指向我們希望的 Person
var Age18Person = Person.myBind(null, 18);
var p18 = new Age18Person("test18"); // newFn {}
p instanceof Person; // false
p instanceof Age18Person; // false
記錄一下原型鏈,再來一遍
Function.prototype.myBind = function(context, ...args) {
var fn = this;
var newFn = function(...restArgs) {
// 如果是new構造,則使用new構造的實例
if (new.target) {
return fn.call(this, ...args, ...restArgs);
}
// 使用call去調用fn,因為bind可能會bind一部分參數,所以把restArgs也傳進去
return fn.call(context, ...args, ...restArgs);
};
// 綁定原型鏈
newFn.prototype = this.prototype;
return newFn;
};
但是這裡還有個問題,如果改了Age18Person
的prototype
,也會影響到Person
的prototype
。
所以,需要做一個中轉——
Function.prototype.myBind = function(context, ...args) {
var fn = this;
var newFn = function(...restArgs) {
// 如果是new構造,則使用new構造的實例
if (new.target) {
return fn.call(this, ...args, ...restArgs);
}
// 使用call去調用fn,因為bind可能會bind一部分參數,所以把restArgs也傳進去
return fn.call(context, ...args, ...restArgs);
};
var NOOP = function() {};
// 綁定原型鏈
NOOP.prototype = this.prototype;
newFn.prototype = new NOOP();
return newFn;
};
這樣基本上就算完成了,當然更推薦function-bind方案。
完