此文首发于 lijing0906.github.io
前面已经讨论过bind()的用法,这篇文章一步一步模拟实现bind()。
bind特点
- 可以指定this
- 返回一个函数
- 可以传入参数
模拟实现第一步
Function.prototype.bind2 = function(context) {
var self = this; // this是调用bind2的对象
return function() { // 返回一个函数
return self.apply(context); // 把this绑定给传入的上下文对象
}
}
// 测试
var foo = {
value: 2
}
var bar = function () {
console.log(this.value);
}
var bf = bar.bind2(foo);
console.log(bf()); // 2
模拟实现第二步----传参
先看一个例子,在bind()的时候可以传参,在调用函数的又可以传参:
var foo = {
value: 2
}
function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bf = bar.bind(foo, 'Jane');
bf(20); // 2 Jane 20
// bar.bind(foo, 'Jane')(20); // 2 Jane 20 把bind和调用写在一起
可以用arguments进行处理:
Function.prototype.bind2 = function (context) {
var self = this;
// 获取bind时传入的参数,因为bind的第一个参数是指定的this,因此从第二个参数开始截取
var args = Array.prototype.slice.call(arguments, 1);
return function() {
// 获取返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(context, args.concat(bindArgs)); // 合并两次获取到的参数
}
}
// 测试
var foo = {
value: 2
}
function bar(name, age) {
return {
value: this.value,
name: name,
age: age
}
}
var bf = bar.bind2(foo, 'Jane');
bf(20); // { value: 2, name: "Jane", age: 20 }
模拟实现第三步
到上面的第二步,大部分的功能已经实现,但是还有一个难点,bind()有一个特性:
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的this值被忽略,同时调用时的参数被提供给模拟函数。
也就是说,当bind()返回的函数作为构造函数的时候,bind时指定的this值会失效,但传入的参数依然生效。
var value = 2;
var foo = {
value: 1
}
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'Jane');
var obj = new bindFoo(20); // undefined "Jane" 20
console.log(obj.habit); // "shopping"
console.log(obj.friend); // "kevin"
上面的例子中,运行结果this.value输出是undefined,这个值既不是全局的value也不是foo对象的value,这说明bind的this失效了,而new操作符生成了一个新的对象,这个时候this指向的是obj。
OK,我们可以通过修改返回函数的原型来实现这种效果:
Function.prototype.bind2 = function(context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function() {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this指向实例,此时`this instanceof fBound`结果为true,可以让实例获得来自绑定函数的值,即上例中实例会具有habit属性
// 当作为普通函数时,this指向window,此时结果为false,将绑定函数的this指向context
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
}
// 修改返回函数的prototype为绑定函数的prototype,实例就可以继承绑定函数的原型中的值,即上例中obj可以获取到bar原型上的friend
fBound.prototype = this.prototype;
return fBound;
}
// 测试直接用上面的例子把bind改成bind2即可测试,效果跟原生的bind一样
模拟实现第四步
上面实现中fBound.prototype = this.prototype有一个缺点,直接修改fBound.prototype的时候,也会直接修改this.prototype。
可以用一个空函数来中转:
Function.prototype.bind2 = function(context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function() {};
var fBound = function() {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this指向实例,此时`this instanceof fBound`结果为true,可以让实例获得来自绑定函数的值,即上例中实例会具有habit属性
// 当作为普通函数时,this指向window,此时结果为false,将绑定函数的this指向context
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
}
// 修改返回函数的prototype为绑定函数的prototype,实例就可以继承绑定函数的原型中的值,即上例中obj可以获取到bar原型上的friend
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
模拟实现第五步
这一步要做一个判断:如果调用bind()的不是函数,需要抛出异常:
if (typeof this !== 'function') {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
最终代码
Function.prototype.bind2 = function(context) {
if (typeof this !== 'function') {
throw new Error('Function.prototype.bind - what is trying to be bound is not callable');
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function() {};
var fBound = function() {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
}