1. 程式人生 > 實用技巧 >bind,call,apply的實現

bind,call,apply的實現

apply、call 和 bind 實現

在前面的文章中介紹過了,apply 和bind 的作用就是顯示改變函式執行的this的繫結。
apply 和 call 是執行函式並該改變this,二者的引數有所區別
而bind則是 返回一個待執行的新函式, 當新函式執行的時候改變了this的指向。
所以,bind的情況會與apply和call不同。

  1. 我們知道在js裡函式是可以被new的 因此情況會比 apply 和 call 多一種情況。
  2. bind 是可以預設引數,
    相同情況是:
    這三個函式對於沒有引數 都指向的window 對於原始型別 都會包裝成物件的形式

apply的實現

Function.prototype.myapply = function(context){
    context = context ? Object(context) : window;  // 確定物件
    var fn = Symbol(); 
    context[fn] = this;  // 繫結到當前物件上
    let result;
    let arr = [...arguments].slice(1);  // 獲取引數
    // 執行函式
    if (!arr.length) {
        result = context[fn]();
    } else {
        result = context[fn](arr); 
    }
    delete context[fn];  // 刪除繫結
    return result;
}

call的實現

Function.prototype.mycall = function(context){
    context = context ? Object(context) : window;
    let fn = Symbol();
    // 繫結到當前物件上
    context[fn] = this;
    // 獲取引數
    let args = [...arguments].slice(1);
    // 執行函式
    let result = context[fn](...args);
    // 刪除繫結
    delete context[fn] 
    return result;
}

bind的實現

如果參照 apply 和 call 的想法,實現方式如下:

Function.prototype.mybind = function(context, ...perAgrs){
    context = context ? Object(context) : window;
    let fn = this; // 記錄函式
    let fnBound = function(...args){
        fn.apply(context, perAgrs.concat(args) )
    }
    return fnBound;
}

此時我們正常的繫結函式是沒有問題的。
但是在 new 繫結後的函式時候就會出現問題

function fn(a){
    this.a = a;
}
let t1 = {a:2};
let t2 = {a:2};
let mybindFn = fn.mybind(t1);
let bindFn = fn.bind(t2);
let objmybindFn = new mybindFn(1);
let objbindFn = new bindFn(1);
let objn = new fn(1);
console.log(objmybindFn);  // fn {a: 1}
console.log(objbindFn);   // fnBound{}

我們會發現這兩結果是不一樣的,
objmybindFn是 fnBound例項化後的物件。
objbindFn則是 fn例項化後的物件。
其實對於objmybindFn的結果,我們也很容易理解,建構函式就是fnBound。而在fnBound的函式體內執行語句是fn.apply(context, perAgrs.concat(args) ) 所以new值設定在t2上了。
可是我們發現原生bind的實現並不是這樣的。他使用的建構函式是fn, 而且他也不會改變t1。你可以認為是直接new fn(2).
到此,我們需要解決兩個問題。

  1. 判斷 new 還是 函式執行,從而確定 fn.apply(context, perAgrs.concat(args) ) 的context是誰。
  2. 原型物件的改寫。
    解決方法:
  3. instanceof 可以做原型鏈的檢查, 判斷當前物件是都 new 出來的。 用於確定 context是誰。
  4. 重寫 fnBound 的原型物件(方法很多)
    1. 直接讓 fnBound.prototype = fn.prototype, 這樣在改寫 fnBound.prototype時候會影響 fn.prototype
    2. fnBound.prototype = new fn(),
    3. fnBound.prototype = Object.create(fn.prototype)
      其實是 2和3 是在 1的中間多加了一個物件, 但是原型鏈卻相連線,這樣在改寫fnBound.prototype的時候只會改寫創建出來的物件,但是訪問的時候卻可以通過原型鏈訪問到 fn.prototype
      因此注意構建出來的物件,不能覆蓋 fn.prototype 上的屬性和方法。

實現

Function.prototype.mybind = function(context, ...perAgrs){
    context = context ? Object(context) : window;
    let fn = this; // 記錄函式
    let fnBound = function(...args){
        fn.apply( this instanceof fnBound ? this : context, perAgrs.concat(args) )
    }
    fnBound.prototype = Object.create(fn.prototype);
    return fnBound;
}

依然存在問題

function fn(a){
    this.a = a;
}
let mybindFn = fn.mybind({a:2});
let bindFn = fn.bind({a:2});
let objmybindFn = new mybindFn(1);
let objbindFn = new bindFn(1);
let objn = new fn(1);
console.log(mybindFn.prototype) // {}
console.log(bindFn.prototype) // undefined

這裡你就會發現,其實我們實現的結果和bind還是有所差異。