JavaScript中bind方法的实现

3次阅读

共计 3168 个字符,预计需要花费 8 分钟才能阅读完成。

在讨论 bind 方法前,我们可以先看一个例子:
var getElementsByTagName = document.getElementsByTagName;
getElementsByTagName(‘body’);
这样在浏览器 (这里使用的是 chrome) 执行会报错:
原因也显而易见:上面的 getElementsByTagName 方法是 document.getElementsByTagName 的引用,但是在执行时 this 指向了 global 或 window 对象,而不是 document 对象。
解决办法也很简单,使用 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 方法。
我们先来看看 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 支持传参。
动态参数
我们来继续改造
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

正文完
 0