在討論bind
方法前,咱們能夠先看一個例子:javascript
var getElementsByTagName = document.getElementsByTagName; getElementsByTagName('body');
這樣在瀏覽器(這裏使用的是chrome
)執行會報錯:java
緣由也顯而易見:上面的getElementsByTagName
方法是document.getElementsByTagName
的引用,可是在執行時this
指向了global
或window
對象,而不是document
對象。chrome
解決辦法也很簡單,使用call
或bind
方法來改變this
:數組
var getElementsByTagName = document.getElementsByTagName; getElementsByTagName.call(document, 'body');
或瀏覽器
var getElementsByTagName = document.getElementsByTagName; getElementsByTagName.bind(document)('body');
上述兩種解決辦法也能夠看出call
和bind
的區別:call
方法是直接執行,而bind
方法是返回一個新函數。閉包
因爲bind
方法是從ES5
纔開始引入的,不是全部瀏覽器都支持,爲了實現兼容,須要本身實現bind
方法。app
咱們先來看看bind
方法的定義:函數
bind
方法會建立一個新函數。當這個新函數被調用時,bind
的第一個參數將做爲它運行時的this
(該參數不能被重寫), 以後的一序列參數將會在傳遞的實參前傳入做爲它的參數。
新函數也能使用new
操做符建立對象:這種行爲就像把原函數當成構造器,提供的this
值被忽略。
bind
方法不是當即執行函數,須要返回一個待執行的函數,這裏能夠利用閉包:return function(){}
;apply
或call
方法來實現;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()
:
a = {}
,而且this
變量引用指向到這個空對象a
;a.__proto__ = b.prototype
;b
的this
對象的屬性和方法將被加入到這個新的this
引用的對象中:b
的屬性和方法被加入的a
裏面;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