JavaScript中bind方法的實現

在討論bind方法前,咱們能夠先看一個例子:javascript

var getElementsByTagName = document.getElementsByTagName;
getElementsByTagName('body');

這樣在瀏覽器(這裏使用的是chrome)執行會報錯:
圖片描述java

緣由也顯而易見:上面的getElementsByTagName方法是document.getElementsByTagName的引用,可是在執行時this指向了globalwindow對象,而不是document對象。chrome

解決辦法也很簡單,使用callbind方法來改變this數組

var getElementsByTagName = document.getElementsByTagName;
getElementsByTagName.call(document, 'body');

瀏覽器

var getElementsByTagName = document.getElementsByTagName;
getElementsByTagName.bind(document)('body');

上述兩種解決辦法也能夠看出callbind的區別:call方法是直接執行,而bind方法是返回一個新函數。閉包

實現

因爲bind方法是從ES5纔開始引入的,不是全部瀏覽器都支持,爲了實現兼容,須要本身實現bind方法。app

咱們先來看看bind方法的定義:函數

bind方法會建立一個新函數。當這個新函數被調用時, bind的第一個參數將做爲它運行時的 this(該參數不能被重寫), 以後的一序列參數將會在傳遞的實參前傳入做爲它的參數。
新函數也能使用 new操做符建立對象:這種行爲就像把原函數當成構造器,提供的 this值被忽略。

初步思路

  1. 由於bind方法不是當即執行函數,須要返回一個待執行的函數,這裏能夠利用閉包:return function(){}
  2. 做用域綁定:可使用applycall方法來實現;
  3. 參數傳遞:因爲參數的不肯定性,須要用apply傳遞數組;

根據上述思路,咱們先來實現一個簡單的customBind方法;測試

Function.prototype.customBind = function (context) {
    var self = this,
        /**
         * 因爲參數的不肯定性,咱們用 arguments 來處理
         * 這裏的 arguments 只是一個類數組對象,能夠用數組的 slice 方法轉化成標準格式數組
         * 除了做用域對象 self 之外,後面的全部參數都須要做爲數組進行參數傳遞
         */
        args = Array.prototype.slice.call(arguments, 1);
    // 返回新函數
    return function() {
        // 做用域綁定
        return self.apply(context, args);
    }
};

測試第一版

var testFn = function(obj, arg) {
    console.log('做用域對象屬性值:' + this.value);
    console.log('綁定函數時參數對象屬性值:' + obj.value);
    console.log('調用新函數參數值:' + arg);
}
var testObj = {
    value: 1
};
var newFn = testFn.customBind(testObj, {value: 2});
newFn('hello world');

// 執行結果:
// 做用域對象屬性值:1
// 綁定函數時參數對象屬性值:2
// 調用新函數參數值:undefined

從測試執行結果能夠看出,上面已經實現了做用域綁定,可是返回新函數newFn不支持傳參,只能在testFn綁定時傳參。
由於咱們最終須要使用的是newFn,因此咱們須要讓newFn支持傳參。this

動態參數

咱們來繼續改造

Function.prototype.customBind = function (context) {
    var fn = this,
        args = Array.prototype.slice.call(arguments, 1);
    return function() {
        // 將新函數執行時的參數 arguments 所有數組化,而後與綁定時傳參 arg 合併
        var newArgs = Array.prototype.slice.call(arguments);
        return fn.apply(context, args.concat(newArgs));
    }
};

測試動態參數

var testFn = function(obj, arg) {
    console.log('做用域對象屬性值:' + this.value);
    console.log('綁定函數時參數對象屬性值:' + obj.value);
    console.log('調用新函數參數值:' + arg);
}
var testObj = {
    value: 1
};
var newFn = testFn.customBind(testObj, {value: 2});
newFn('hello world');

// 執行結果:
// 做用域對象屬性值:1
// 綁定函數時參數對象屬性值:2
// 調用新函數參數值:hello world

能夠看出,綁定時傳的參數和新函數執行時傳的參數是合併在一塊兒造成完整參數的。

原型鏈

咱們再回到bind方法的定義第二條:新函數也能使用new操做符建立對象。
說明綁定後的新函數被new實例化以後,須要繼承原函數的原型鏈方法,且綁定過程當中提供的this被忽略(繼承原函數的this對象),可是參數仍是會使用。因此咱們須要一箇中轉的函數將原型鏈傳遞下去。

首先咱們須要明確new實例化過程,好比說var a = new b()

  1. 建立一個空對象a = {},而且this變量引用指向到這個空對象a
  2. 繼承被實例化函數的原型:a.__proto__ = b.prototype
  3. 被實例化方法bthis對象的屬性和方法將被加入到這個新的this引用的對象中:b的屬性和方法被加入的a裏面;
  4. 新建立的對象由this所引用:b.call(a)

接下來咱們實現原型鏈。

Function.prototype.customBind = function (context) {
    var self = this,
        args = Array.prototype.slice.call(arguments, 1);
    // 建立中轉函數
    var cacheFn = function() {};
    var newFn =  function() {
        var newArgs = Array.prototype.slice.call(arguments);
        /**
         * 這裏的 this 是指調用時的執行上下文
         * 若是是 new 操做,須要綁定 new 以後做用域,this 指向新的實例對象
         */
        return self.apply(this instanceof cacheFn ? this : context, args.concat(newArgs));
    };

    // 中轉原型鏈
    cacheFn.prototype = self.prototype;
    newFn.prototype = new cacheFn();

    return newFn;
};

測試原型鏈

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function() {
  return this.x + ',' + this.y;
};

var YAxisPoint = Point.customBind({}, 0);
var axisPoint = new YAxisPoint(5);
axisPoint.toString();   // "0,5"

axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new Point(1, 2) instanceof YAxisPoint; // true
相關文章
相關標籤/搜索