zoukankan      html  css  js  c++  java
  • 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、方法二中加入了对原型链的维护。

  • 相关阅读:
    递归算法的时间复杂度分析
    MongoDB入门简单介绍
    关于用例须要多少文档以及业务用例等等
    Java连接redis的使用演示样例
    C++ String 转 char*
    MySQL和PostgreSQL 导入数据对照
    SSL连接建立过程分析(1)
    XTU OJ 1210 Happy Number (暴力+打表)
    Codeforces Round #258 (Div. 2)[ABCD]
    CreateFont具体解释
  • 原文地址:https://www.cnblogs.com/twodog/p/12134761.html
Copyright © 2011-2022 走看看