JavaScript 深入解剖bind內部機制

會會會發表於2019-04-17

接上篇文章JavaScript重識bind、call、apply

1、 先看一段程式碼:

function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 ); // obj1.a === 2
var baz = new bar(3);
console.log( obj1.a ); // 2 
console.log( baz.a ); // 3
複製程式碼

bar 被硬繫結到 obj1 上,但是 new bar(3) 沒有將obj1.a 修改為 3。相反,new 修改了硬繫結(到 obj1 的)呼叫 bar(..) 中的 this。因為使用了 new 繫結,我們得到了一個名字為 baz 的新物件,並且 baz.a 的值是 3。

2、手動實現的bind程式碼

if (!Function.prototype.bindNew) {
    Function.prototype.bindNew = function(oThis) {
        //一個函式去呼叫,也就是說bind,call,apply的this是個函式;
        //然後再去改變這個函式裡面的this;
        if (typeof this !== "function") {
         // 與 ECMAScript 5 最接近的
         // 內部 IsCallable 函式 
         throw new TypeError(
          "Function.prototype.bind - what is trying " +
         "to be bound is not callable"
         ); 
        }
        //這裡將初始化的引數快取起來;
        var aArgs = Array.prototype.slice.call( arguments, 1 ),
        // ftoBind 指向要bind的函式;
        fToBind = this,
        // 返回一個新函式
        fNOP = function(){}, 
        fBound = function(){
        //fToBind.apply 改變繫結this;
        // 執行的時候判斷,當前this等於fNOP並且傳入oThis,就設定成當前this,不然就改變成初始化傳入的oThis;
           return fToBind.apply( 
            (this instanceof fNOP && oThis ? this : oThis ),
            aArgs.concat(Array.prototype.slice.call( arguments ) )
            ); 
        };
        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();
        return fBound;
    };
}

複製程式碼

(後面會介紹為什麼要在 new 中使用硬繫結函 數)

3、解釋new操作符

1️⃣使用bindNew來模擬bind內部機制

下面是 new 修改 this 的相關程式碼:

this instanceof fNOP &&
oThis ? this : oThis ; 
// ... 以及:
fNOP.prototype = this.prototype; 
fBound.prototype = new fNOP();
複製程式碼

這段程式碼會判斷硬繫結函式是否是被 new 呼叫,如果是的話就會使用新建立 的 this 替換硬繫結的 this。 如果你這樣子呼叫:

function foo() {
    console.log("name: " + this.name);
}
var obj = { name: "obj" };
var obj2 = { name: "obj2" }, obj3 = { name: "obj3" };
var dd = foo.bindNew(obj2);
var dj = new dd();// name:undefined;   而不是name:obj2
複製程式碼

因為new操作修改了this的指向;this繫結的就是是新建立的物件-dj。 詳細解釋一下:

  • 1、dd 是foo.bindNew(obj2)執行後,返回的一個函式
  • 2、dd這個函式是:
// ftoBind 指向要bind的函式; 這裡是foo;
fToBind = this,
// 返回一個新函式
fNOP = function(){}, 
fBound = function(){
    //fToBind.apply 改變繫結this;
    // 執行的時候判斷,當前this等於fNOP並且傳入oThis,就設定成當前this,不然就改變成初始化傳入的oThis;
    return fToBind.apply( 
    (this instanceof fNOP && oThis ? this : oThis ),
        aArgs.concat(Array.prototype.slice.call( arguments ) )
    ); 
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
複製程式碼

注意 :

// fNOP的原型指向this的原型,this此時指向foo;
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
複製程式碼

這個程式碼使 fBound 為 fNOP 的例項;

  • 3、new dd()後,就執行fBound這個函式 裡面的程式碼:
return fToBind.apply( 
    (this instanceof fNOP && oThis ? this : oThis ),
        aArgs.concat(Array.prototype.slice.call( arguments ) )
複製程式碼

此時的 fToBind ,是之前執行 bindNew 指向的foo

此時 this,就是指向的new dd() 後返回的新例項; this instanceof fNOP === true

(this instanceof fNOP && oThis ? this : oThis ) 這個就返回 this; 那麼這個新物件上面是沒有obj這個屬性的,foo.apply,執行foo後,就列印出name:undefined;

2️⃣使用bind

上面是手寫bind然後來剖析bind內部的繫結機制;那麼我們實際檢測也會等到同樣的結果; 就是本文最開始的程式碼:

function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 ); // obj1.a === 2
var baz = new bar(3);
console.log( obj1.a ); // 2 
console.log( baz.a ); // 3
複製程式碼

這樣就明白了為什麼baz.a 是3,而不是2了,因為new後,改變了barthis指向;使其新new的例項 baz; foothis.a = something; 就將 baz.a = 3了; 這裡也可以得出結論,new 操作改變this繫結的優先順序高於硬繫結(bind,apply,call);

3️⃣new和bind的特性的應用

如果 new 中使用硬繫結函式,就可以預先設定函式的一些引數,這樣在使用 new 進行初始化時就可以只傳入其餘的引數。bind(..) 的功能之一就是可以把除了第一個 引數(第一個引數用於繫結 this)之外的其他引數都傳給下層的函式(這種技術稱為“部 分應用”,是“柯里化”的一種)。舉例來說:

function foo(p1,p2) { 
    this.val = p1 + p2;
}
// 之所以使用 null 是因為在本例中我們並不關心硬繫結的 this 是什麼 
// 反正使用 new 時 this 會被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" ); 
baz.val; // p1p2
複製程式碼

相關文章