bind()函数的深入理解及两种兼容方法分析

在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的作用同前面第一种兼容方法的_selffBound作为构造函数时,它的实例会继承fBound.prototype。由于fBound.prototype又是fNOP的实例,因此fBound.prototype会继承fNOP.prototype的属性。fNOP.prototypethis.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、方法二中加入了对原型链的维护。

原文地址:https://www.cnblogs.com/twodog/p/12134761.html