javascript原生一步步實現bind分析

bind

官方描述

bind() 函數會建立一個新函數(稱爲綁定函數),新函數與被調函數(綁定函數的目標函數)具備相同的函數體(在 ECMAScript 5 規範中內置的call屬性)。當目標函數被調用時 this 值綁定到 bind() 的第一個參數,該參數不能被重寫。綁定函數被調用時,bind() 也接受預設的參數提供給原函數。一個綁定函數也能使用new操做符建立對象:這種行爲就像把原函數當成構造器。提供的 this 值被忽略,同時調用時的參數被提供給模擬函數。javascript

使用介紹

因爲javascript中做用域是由其運行時候所處的環境決定的,因此每每函數定義和實際運行的時候所處環境不同,那麼做用域也會發生相應的變化。
例以下面這個狀況:java

var id = 'window';
//定義一個函數,可是不當即執行
var test = function(){
    console.log(this.id)
}
test() // window
//把test做爲參數傳遞
var obj = {
    id:'obj',
    hehe:test
}
//此時test函數運行環境發生了改變
obj.hehe() // 'obj'
//爲了不這種狀況,javascript裏面有一個bind方法能夠在函數運行以前就綁定其做用域,修改以下

var id = 'window';
var test = function(){
    console.log(this.id)
}.bind(window)
var obj = {
    id:'obj',
    hehe:test
}
test() // window
obj.hehe() // window

上面介紹了bind方法的一個重要做用就是爲一個函數綁定做用域,可是bind方法在低版本瀏覽器不兼容,這裏咱們能夠手動實現一下。數組

拆分一下關鍵思路

  1. 由於bind方法不會當即執行函數,須要返回一個待執行的函數(這裏用到閉包,能夠返回一個函數)return function(){}瀏覽器

  2. 做用域綁定,這裏可使用apply或者call方法來實現 xx.call(yy)/xx.apply(yy)閉包

  3. 參數傳遞,因爲參數的不肯定性,須要用apply傳遞數組(實例更明瞭xx.apply(yy,[...Array...]),若是用call就不太方便了,由於call後面的參數須要一個個列出來app

實現

有了上述的思路,大體的雛形已經明瞭了,代碼應該也很容易實現ide

綁定做用域,綁定傳參

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

測試函數

var test = function(a,b){
    console.log('做用域綁定 '+ this.value)
    console.log('testBind參數傳遞 '+ a.value2)
    console.log('調用參數傳遞 ' + b)
}
var obj = {
    value:'ok'
}
var fun_new = test.testBind(obj,{value2:'also ok'})

fun_new ('hello bind')
// 做用域綁定 ok
// testBind參數傳遞 also ok
// 調用參數傳遞  undefined

動態參數

上面已經實現了bind方法的做用域綁定,可是美中不足的是,既然咱們返回的是一個函數,調用的時候應該支持傳遞參數,很顯然,上面的 fun_new 調用的時候並不支持傳參,只能在 testBind 綁定的時候傳遞參數,由於咱們最終調用的是這個返回函數測試

function(){
        return _this.apply(that,args)
    }    

這裏面的args在綁定的時候就已經肯定了,調用的時候值已經固定,
咱們並無處理這個function傳遞的參數。

咱們對其進行改造this

return function(){
        return _this.apply(that,
            args.concat(Array.prototype.slice.apply(arguments,[0]))
        )
    }

這裏的 Array.prototype.slice.apply(arguments,[0]) 指的是這個返回函數執行的時候傳遞的一系列參數,因此是從第一個參數開始 [0] ,以前的args = slice.apply(arguments,[1])指的是 testBind方法執行時候傳遞的參數,因此從第二個開始 [1],兩則有本質區別,不能搞混,只有二者合併了以後纔是返回函數的完整參數

因此有以下實現

Function.prototype.testBind = function(that){
    var _this = this,
        slice = Array.prototype.slice,
        args = slice.apply(arguments,[1]);
    return function(){
        return _this.apply(that,
                    args.concat(Array.prototype.slice.apply(arguments,[0]))
                )
    }    
}

測試

var test = function(a,b){
    console.log('做用域綁定 '+ this.value)
    console.log('testBind參數傳遞 '+ a.value2)
    console.log('調用參數傳遞 ' + b)
}
var obj = {
    value:'ok'
}
var fun_new = test.testBind(obj,{value2:'also ok'})

fun_new ('hello bind')
// 做用域綁定 ok
// testBind參數傳遞 also ok
// 調用參數傳遞  hello bind

在以上2種傳參方式中,bind的優先級高,從 args.concat(Array.prototype.slice.apply(arguments,[0])) 也能夠看出來,bind的參數在數組前面。

原型鏈

官方文檔上有一句話:

A bound function may also be constructed using the new operator: doing
so acts as though the target function had instead been constructed.
The provided this value is ignored, while prepended arguments are
provided to the emulated function.

說明綁定事後的函數被new實例化以後,須要繼承原函數的原型鏈方法,且綁定過程當中提供的this被忽略(繼承原函數的this對象),可是參數仍是會使用。
這裏就須要一箇中轉函數把原型鏈傳遞下去

fNOP = function () {} //建立一箇中轉函數
fNOP.prototype = this.prototype;
xx.prototype = new fNOP() 
修改以下
Function.prototype.testBind = function(that){
    var _this = this,
        slice = Array.prototype.slice,
        args = slice.apply(arguments,[1]),
        fNOP = function () {},
        //因此調用官方bind方法以後 有一個name屬性值爲 'bound '
        bound = function(){
            return _this.apply(that,
                args.concat(Array.prototype.slice.apply(arguments,[0]))
            )
        }    

    fNOP.prototype = _this.prototype;

    bound.prototype = new fNOP();

    return bound;
}

並且bind方法的第一個參數this是能夠不傳的,須要分2種狀況

  • 直接調用bind以後的方法

var f = function () { console.log('不傳默認爲'+this)  };f.bind()()
// 不傳默認爲 Window

因此直接調用綁定方法時候 apply(that, 建議改成 apply(that||window,,其實不改也能夠,由於不傳默認指向window

  • 使用new實例化被綁定的方法

容易糊塗,重點在於弄清楚標準的bind方法在new的時候作的事情,而後就能夠清晰的實現

這裏咱們須要看看 new 這個方法作了哪些操做 好比說 var a = new b()

  1. 建立一個空對象 a = {},而且this變量引用指向到這個空對象a

  2. 繼承被實例化函數的原型 :a.__proto__ = b.prototype

  3. 被實例化方法bthis對象的屬性和方法將被加入到這個新的 this 引用的對象中: b的屬性和方法被加入的 a裏面

  4. 新建立的對象由 this 所引用 :b.call(a)

經過以上能夠得知,若是是var after_new = new bindFun(); 因爲這種行爲是把原函數當成構造器,那麼那麼最終實例化以後的對象 this須要繼承自原函數, 而這裏的 bindFun 目前是

function(){
            return _this.apply(that || window,
                args.concat(Array.prototype.slice.apply(arguments,[0]))
            )
        }

這裏apply的做用域是綁定的that || window,在執行 testBind()的時候就已經固定,並無把原函數的this對象繼承過來,不符合咱們的要求,咱們須要根據apply的特性解決這個問題:

在一個子構造函數中,你能夠經過調用父構造函數的 `apply/call` 方法來實現繼承

例如
function Product(name, price) {
  this.name = name;
  this.price = price;

  if (price < 0) {
    throw RangeError('Cannot create product ' +
                      this.name + ' with a negative price');
  }
}

function Food(name, price) {
  Product.call(this, name, price); 
  this.category = 'food';
}

//等同於(其實就是把Product放在Food內部執行了一次)
function Food(name, price) { 
    this.name = name;
    this.price = price;
    if (price < 0) {
        throw RangeError('Cannot create product ' +
                this.name + ' with a negative price');
    }

    this.category = 'food'; 
}

因此在new新的實例的時候實時將這個新的this對象 進行 apply 繼承原函數的 this 對象,就能夠達到 new 方法裏面的第 3 步的結果

apply(that||window,
//修改成 若是是new的狀況,須要綁定new以後的做用域,this指向新的實例對象
apply(isNew ? this : that||window,  ==>

Function.prototype.testBind = function(that){
    var _this = this,
        slice = Array.prototype.slice,
        args = slice.apply(arguments,[1]),
        fNOP = function () {},
        //因此調用官方bind方法以後 有一個name屬性值爲 'bound '
        bound = function(){
            return _this.apply(isNew ? this : that||window,
                args.concat(Array.prototype.slice.apply(arguments,[0]))
            )
        }    

    fNOP.prototype = _this.prototype;

    bound.prototype = new fNOP();

    return bound;
}

這裏的 isNew 是區分 bindFun 是直接調用仍是被 new 以後再調用,經過原型鏈的繼承關係能夠知道,
bindFun 屬於 after_new的父類,因此 after_new instanceof bindFun 爲 true,同時
bindFun.prototype = new fNOP() 原型繼承; 因此 fNOP 也是 after_new的父類, after_new instanceof fNOP 爲 true

最終結果

Function.prototype.testBind = function(that){
        var _this = this,
            slice = Array.prototype.slice,
            args = slice.apply(arguments,[1]),
            fNOP = function () {},
            bound = function(){
                //這裏的this指的是調用時候的環境
                return _this.apply(this instanceof  fNOP ? this : that||window,
                    args.concat(Array.prototype.slice.apply(arguments,[0]))
                )
            }    
        fNOP.prototype = _this.prototype;
    
        bound.prototype = new fNOP();
    
        return bound;
    }

我看到有些地方寫的是

this instanceof fNOP && that ? this : that || window,

我我的以爲這裏有點不正確,若是綁定時候不傳參數,那麼that就爲空,那不管怎樣就只能綁定 window做用域了。

以上是我的看法,不對的地方望指導,謝謝!

相關文章
相關標籤/搜索