在JavaScript中,bind()函数仅在IE9+、Firefox4+、Chrome、Safari5.1+可得到原生支持。本文将深入探讨bind()函数并对两种兼容方法进行分析比较。由于本文将反复使用用到原型对象、原型、prototype、[[proto]],为使文章更加易读不致引起混淆,这里将对几者进行明确区分:
1、
原型
:每个函数本身也是一个对象,作为对象的函数拥有一个属性叫做原型,它是一个指针。
2、原型对象
:函数的原型(是一个指针)指向一个对象,这个对象便是原型对象。
3、prototype
:函数的prototype属性就是函数的原型(指针)。
4、[[proto]]
:实例拥有一个内部指针[[prototype]]指向原型对象,实例的原型也就是指实例的[[prototype]]属性。
5、当叙述原型的方法和原型对象的方法时,两者是同一个意思;
6、可以通过对象形式操作原型。
F.prototype.bind()
这样的写法表明F.prototype
虽然本质上是一个指针,但可以使用对象的.
这样的操作符,就好像F.prototype本来就是一个对象一样,实质上是通过指针访问了原型对象。
一、bind()方法从何而来?
第一个问题是,每个函数都可以使用bind函数,那么它究竟从何而来?
事实上,bind()来自函数的原型链,它是Function构造函数的原型对象上的一个方法,基于前面的区分,可以通过Function.prototype访问即Function构造函数的原型对象:
Function.prototype.bind()
由于每个函数都是Function构造函数的实例,因此会继承Function的原型对象的属性和方法。
第二个问题是,每个函数都有的方法一定是从原型链继承而来吗?
答案是否定的,因为每个函数都有call()和apply()方法,但call()和apply()却不是继承而来。`
二、与call()、apply()的区别
call()、apply()可以改变函数运行时的执行环境,foo.call()
、foo.apply()
这样的语句可以看作执行foo(),只不过foo()中的this指向了后面的第一个参数。
foo.bind({a:1})
却并不如此,执行该条语句仅仅得到了一个新的函数,新函数的this被绑定到了后面的第一个参数,亦即新的函数并没有执行。
function foo(){
return this;
}
var f1=foo.call({a:1});
var f2=foo.apply({a:2});
var f3=foo.bind({a:1});
console.log(f1); //{a:1}
console.log(f2); //{a:2}
console.log(f3); //function foo(){
// return this;
//}
console.log(foo()); //window对象
console.log(f3()); //{a: 1}
在上面的例子中,f1和f2都得到改变了执行环境的foo()函数运行后的返回值。f3得到的是另一个函数,函数体本身和foo()是一样的,但执行f3()却和执行foo()得到不同的结果,这是因为bind()函数使得f3中this绑定到一个特定的对象。
三、多个参数
例如:
var obj={
a:1
};
function foo(a,b){
this.a++;
return a+b;
}
var fo=foo.bind(obj,1,2);
console.log(fo()); //3
console.log(obj); //{a:2}
以上例子中,当执行foo()函数,将使得this指向的对象的a属性自加1,对于foo()函数而言,它的this指向window对象,也就是将使得window环境中的a变量自加1,然后同时a+b的值。
fo()函数则是由foo()调用bind()并传入三个参数onj、1和2得到的新函数,该函数的this指向传入的obj对象。当执行fo()函数,将使得obj的a属性自加1,然后返回bind()的后两个参数相加的结果。
四、 兼容方法1:使用apply——简洁的实现
Function.prototype.bind= function(obj){
if (Function.prototype.bind)
return Function.prototype.bind;
var _self = this, args = arguments;
return function() {
_self.apply(obj, Array.prototype.slice.call(args, 1));
}
}
分析:
首先,从总体结构而言,bind()是一个函数,故采用function定义。由于foo.bind()得到的仍然是一个函数,因而返回值是一个函数。
第二,在返回的函数中,需要执行一次改变了执行环境的原函数,使用apply(obj)达到将原函数的执行环境改为obj的目的。
第三,对于bind()函数而言,由于它是Function.prototype的一个属性,它的this将指向调用它的对象。例如,foo.bind(obj),则bind()函数内部的this指向foo()函数。但对于执行bind()后得到的新函数,它的this将指向全局对象,因此需要使用var _self = this
这样的参数传递。
第四,调用bind()得到的新函数需要接收执行bind()时传入的实际参数。因此,使用了args = arguments这样的赋值。需要将执行bind()时传入的参数进行分离,只获取第一个参数后面的参数,slice()方法可以达到这个目的。又由于arguments是类数组对象不是真正的数组,故而没有slice方法,使用call()以达到借用的目的。
最终,参见下例梳理如下:
var func=foo.bind(obj,...);
bind是Function构造函数的prototype指针指向的对象上的一个方法。当某个函数foo()调用它时,即foo.bind()
,将返回一个新的函数。当新的函数执行时,相当于执行一次foo()函数本身,只不过改变了foo()的执行环境为传入的obj,this也指向了传入的obj,传入bind的第一个实参以后的参数作为新函数执行的实际参数。
五、兼容方法2: 基于原型——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,
// 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
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;
};
}
分析:
首先,检测Function的原型对象上是否存在bind()方法,若不存在则赋值为一个函数。在函数内部,对调用bind()的对象类型进行了检测,如果是函数,则正常调用,否则抛出异常;
var foo={
a:1
};
var o={
b:1;
}
var f=foo.bind(o);
这就是说上面的调用是不被允许的,因为foo不是函数。
第二,fBound是最终得到的函数。 aArgs得到调用fBound时传入的第一个参数后面的参数,aArgs.concat(Array.prototype.slice.call(arguments)))
得到执行新函数时传入的实参。比如:
var obj={
a:1
};
function foo(a,b,c){
this.a++;
return a+b+c;
}
var fo=foo.bind(obj,1,2);
console.log(fo(3)); //6
console.log(obj); //{a:2}
在上面例子中,aArgs保存的是参数b、c,aArgs.concat(Array.prototype.slice.call(arguments)))
则得到调用fo()时传入的参数3。
第三,fToBind
的作用同前面第一种兼容方法的_self
。fBound
作为构造函数时,它的实例会继承fBound.prototype
。由于fBound.prototype
又是fNOP
的实例,因此fBound.prototype
会继承fNOP.prototype
的属性。fNOP.prototype
和this.prototype
指向了同一个原型对象,这里的this指向的是调用bind()的函数。这样形成的原型链中,fBound
的实例将继承得到fNOP.prototype
的属性,这便是原型链继承。
第四,this instanceof fNOP
实现对新函数调用方式的的判断。当新函数作为一般函数直接调用时,它的this指向绑定对象,显然this不是 fNOP
的实例。如:
var obj={
a:1
};
function foo(a,b){
this.a++;
return a+b;
}
var fo=foo.bind(obj,1,2);
fo();
console.log(obj.a); //2
上面例子中,foo()内部的this指向obj,显然obj不是fNOP的实例,因此this instanceof fNOP
返回false。
当新函数作为构造函数调用时,即new fo()
,它的this
将指向新创建的函数实例,由第三点所述原型链继承,实例的原型链上存在构造函数fNOP,故 this instanceof fNOP
将返回true
。
5、第五,fNOP.prototype = this.prototype
用于实现对bind()的调用者的原型链的继承
。这里,this指向bind()的调用者,因此这使得fNOP.prototype
指向调用者的原型对象。假使调用者也有原型链,那么这样新函数就也能继承原函数的原型链。当然,只有在调用者是一个函数时才能成立,因此需先判断this.prototype
是否返回true
。
六、两种兼容方法的比较
1、方法二中加入了对调用bind()的对象类型的检测,即若调用bind()的不是函数,将抛出异常;
2、方法二中实现了对调用bind()后得到的新函数的调用方式的检测,即新函数可以作为一般函数和构造函数调用,方法一只能作为一般函数调用。
3、方法二中加入了对原型链的维护。