Javascript之bind

寫在最前

最近開始重新學習一波js,框架用久了有些時候覺得這樣子應該可以實現發現就真的實現了,但是爲什麼這麼寫好像又說不太清楚,之前讀了LucasHC以及冴羽的兩篇關於bind的文章感覺自己好像基礎知識都還給體育老師了哈哈哈,所以危機感爆棚,趕緊重頭複習一遍。本次主要圍繞bind是什麼;做了什麼;自己怎麼實現一個bind,這三個部分。其中會包含一些細節代碼的探究,往下看就知道。

所以bind是什麼

bind()方法創建一個新的函數, 當被調用時,將其this關鍵字設置爲提供的值,在調用新函數時,在任何提供之前提供一個給定的參數序列。

var result = fun.bind(thisArg[, arg1[, arg2[, ...]]]) 
result(newArg1, newArg2...)

沒看懂沒事接着往下看。

bind到底做了什麼

從上面的介紹中可以看出三點。首先調用bind方法會返回一個新的函數(這個新的函數的函數體應該和fun是一樣的)。同時bind中傳遞兩個參數,第一個是this指向,即傳入了什麼this就等於什麼。如下代碼所示:

this.value = 2
var foo = {
    value: 1
}
var bar = function() {
  console.log(this.value)
}
var result = bar.bind(foo)
bar() // 2
result() // 1,即this === foo

第二個參數爲一個序列,你可以傳遞任意數量的參數到其中。並且會預置到新函數參數之前。

this.value = 2
var foo = {
    value: 1
};
var bar = function(name, age, school) {
  console.log(name) // 'An'
  console.log(age) // 22
  console.log(school) // '家裏蹲大學'
}
var result = bar.bind(foo, 'An') //預置了部分參數'An'
result(22, '家裏蹲大學') //這個參數會和預置的參數合併到一起放入bar中

我們可以看出在最後調用result(22, '家裏蹲大學')的時候,其內部已經包含了在調用bind的時候傳入的'An'

一句話總結:調用bind,就會返回一個新的函數。這個函數裏面的this就指向bind的第一個參數,同時this後面的參數會提前傳給這個新的函數。調用該新的函數時,再傳遞的參數會放到預置的參數後一起傳遞進新函數。

自己實現一個bind

實現一個bind需要實現以下兩個功能

  • 返回一個函數,綁定this,傳遞預置參數
  • bind返回的函數可以作爲構造函數使用。故作爲構造函數時應使得this失效,但是傳入的參數依然有效

1、返回一個函數,綁定this,傳遞預置參數

this.value = 2
var foo = {
    value: 1
};
var bar = function(name, age, school) {
    console.log(name) // 'An'
    console.log(age) // 22
    console.log(school) // '家裏蹲大學'
    console.log(this.value) // 1
}
Function.prototype.bind = function(newThis) {
    var aArgs   = Array.prototype.slice.call(arguments, 1) //拿到除了newThis之外的預置參數序列
    var that = this
    return function() {
        return that.apply(newThis, aArgs.concat(Array.prototype.slice.call(arguments)))
        //綁定this同時將調用時傳遞的序列和預置序列進行合併
    }
}
var result = bar.bind(foo, 'An')
result(22, '家裏蹲大學')

這裏面有一個細節就是Array.prototype.slice.call(arguments, 1)這句話,我們知道arguments這個變量可以拿到函數調用時傳遞的參數,但不是一個數組,但是其具有一個length屬性。爲什麼如此調用就可以將其變爲純數組了呢。那麼我們就需要回到V8的源碼來進行分析。#這個版本的源碼爲早期版本,內容相對少一些。


function ArraySlice(start, end) {
  var len = ToUint32(this.length); 
  //需要傳遞this指向對象,那麼call(arguments),
  //便可將this綁定到arguments,拿到其length屬性。
  var start_i = TO_INTEGER(start);
  var end_i = len;
  
  if (end !== void 0) end_i = TO_INTEGER(end);
  
  if (start_i < 0) {
    start_i += len;
    if (start_i < 0) start_i = 0;
  } else {
    if (start_i > len) start_i = len;
  }
  
  if (end_i < 0) {
    end_i += len;
    if (end_i < 0) end_i = 0;
  } else {
    if (end_i > len) end_i = len;
  }
  
  var result = [];
  
  if (end_i < start_i)
    return result;
  
  if (IS_ARRAY(this))
    SmartSlice(this, start_i, end_i - start_i, len, result);
  else 
    SimpleSlice(this, start_i, end_i - start_i, len, result);
  
  result.length = end_i - start_i;
  
  return result;
};

從源碼中可以看到通過call將arguments下的length屬性賦給slice後,便可通過start_i & end_i來獲得最後的數組,所以不需要傳遞進slice時就是一個純數組最後也可以得到一個數組變量。

2、bind返回的函數可以作爲構造函數使用

被用作構造函數時,this應指向new出來的實例,同時有prototype屬性,其指向實例的原型。

this.value = 2
var foo = {
  value: 1
};
var bar = function(name, age, school) {
  ...
  console.log('this.value', this.value)
}
Function.prototype.bind = function(newThis) {
  var aArgs   = Array.prototype.slice.call(arguments, 1)
  var that = this  //that始終指向bar
  var NoFunc = function() {}
  var resultFunc = function() {
    return that.apply(this instanceof that ? this : newThis, aArgs.concat(Array.prototype.slice.call(arguments)))
  } 
  NoFunc.prototype = that.prototype //that指向bar
  resultFunc.prototype = new NoFunc()
  return resultFunc
  
}
var result = bar.bind(foo, 'An')
result.prototype.name = 'Lsc' // 有prototype屬性
var person = new result(22, '家裏蹲大學')
console.log('person', person.name) //'Lsc'

上面這段模擬代碼做了兩件重要的事。

1.給返回的函數模擬一個prototype屬性。,因爲通過構造函數new出來的實例可以查詢到原型上定義的屬性和方法

var NoFunc = function() {}
...
NoFunc.prototype = that.prototype //that指向bar
resultFunc.prototype = new NoFunc()
return resultFunc

通過上面代碼可以看出,that始終指向bar。同時返回的函數已經繼承了that.prototype即bar.prototype。爲什麼不直接讓返回的函數的prototype屬性resultFunc.prototype等於爲bar(that).prototype呢,這是因爲任何new出來的實例都可以訪問原型鏈。如果直接賦值那麼new出來的對象可以直接修改bar函數的原型鏈,這也就是是原型鏈污染。所以我們採用繼承的方式(將構造函數的原型鏈賦值爲父級構造函數的實例),讓new出來的對象的原型鏈與bar脫離關係。

2.判斷當前被調用時,this是用於普通的bind還是用於構造函數從而更改this指向。

如何判斷當前this指向了哪裏呢,通過第一點我們已經知道,通過bind方法返回的新函數已經有了原型鏈,剩下需要我們做的就是改變this的指向就可以模擬完成了。通過什麼來判斷當前被調用是以何種姿勢呢。答案是instanceof

instanceof 運算符用來測試一個對象在其原型鏈中是否存在一個構造函數的 prototype 屬性。

// 定義構造函數
function C(){} 
function D(){} 
var o = new C();
// true,因爲 Object.getPrototypeOf(o) === C.prototype
o instanceof C; 
// false,因爲 D.prototype不在o的原型鏈上
o instanceof D; 

從上面可以看出,instanceof可以判斷出一個對象是否是由這個函數new出來的,如果是new出來的,那麼這個對象的原型鏈應爲該函數的prototype.
所以我們來看這段關鍵的返回的函數結構:

var resultFunc = function() {
    return that.apply(this instanceof that ? 
        this : 
        newThis, 
        aArgs.concat(Array.prototype.slice.call(arguments)))
  } 

在這其中我們要先認清this instanceof that中的this是bind函數被調用後,返回的新函數中的this。所以這個this可能執行在普通的作用域環境,同時也可能被new一下從而改變自己的指向。再看that,that始終指向了bar,同時其原型鏈that.prototype是一直存在的。所以如果現在這個新函數要做new操作,那麼this指向了新函數,那麼this instanceof that === true, 所以在apply中傳入this爲指向,即指向新函數。如果是普通調用,那麼this不是被new出來的,即新函數不是作爲構造函數,this instanceof that === false就很顯而易見了。這個時候是正常的bind調用。將調用的第一個參數作爲this的指向即可。

完整代碼(MDN下的實現)

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          return fToBind.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; 
    }
    fBound.prototype = new fNOP();
    return fBound;
  };
}

可以看到,其首先做了當前是否支持bind的判定,不支持再實行兼容。同時判斷調用這個方法的對象是否是個函數,如果不是則報錯。

同時這個模擬的方法也有一些缺陷,可關注MDN上的Polyfill部分

小結

模擬bind實現最大的一個缺陷是,模擬出來的函數中會一直存在prototype屬性,但是原生的bind作爲構造函數是沒有prototype的,這點打印一下即可知。不過這樣子new出來的實例沒有原型鏈,那麼它的意義是什麼呢。如果哪天作者知道了意義會更新在這裏的=。= 如果說錯的地方歡迎指正,一起交流哈哈。原文會放到github中。作者會不定期更新查缺補漏記。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章