在讨论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