理解bind的实现原理

感觉应该坚持一个月至少产出一篇博客文章来, 碰巧在sf看到一个关于bind的问题,又回到去年看过MDN关于bind实现的polyfill,当时看得一脸懵逼,如今对原型链使用有进一步的认识了,于是记一下笔记(原来之前没做笔记)并总结一下。

首先关于bind的使用,就是将函数转变为一个新的函数,而这个新的函数在调用过程中,自带bgm,哦不,自带指定的this

我们知道,一个函数里,有两个很重要的概念,一个是arguments 函数参数对象,另外一个就是this,指向函数所属对象。

当我们声明一个函数时,在调用过程中,内部this往往指向于window(浏览器环境下),因为这个函数作为普通函数被直接调用时,属于全局调用,所以此时它的this指向全局对象即window. PS: 虽然我更想理解成普通函数a,可以用window.a来理解,但是如果是在自执行函数里面声明的function,对不起,它不在window里,所以这样理解无效。

而如果是一个对象里的函数,在调用这个对象调用自己成员函数的时候,其this指向于自己,但如果单独把这个函数抽离过来使用,this又不一定指向原来的对象了。

    var test = 'hi';
    var obj = {
        test:'hello',
        f:function(){
            console.log(this.test);
        }
    };
    var outf = obj.f;
    obj.f(); // hello
    outf();// hi

此时,bind就是意在让function自带this的神器。

    var test = 'hi';
    var obj = {
        test:'hello',
        f:function(){
            console.log(this.test);
        }
    };
    var outf = obj.f;
    
    var outbindf = obj.f.bind(obj);

    obj.f(); // hello
    outf();// hi
    
    outbindf();//居然是hello了

看起来bind很强大,但是低版本浏览器,特别是ie9-,就没有bind这个方法。

没事,没轮子我们自己造,我们可以通过给Function原型加上bind属性方法来实现,运用currying的方式(生成一个新函数)、闭包和apply来实现this的绑定。

同时,我们要考虑到当原函数作为构造函数来使用,bind产出的函数也当构造函数来new的时候,需要忽略绑定的this了,也就是bind的原始作用移除。

来看一下MDN的polyfill实现:

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,
                 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;
  };
}

按顺序看,首先第一点就是判断是否有bind了,如果有,就没下面什么事了(这么快就领便当)。

最外层的function内,this指向的即原函数本身,那么首先会判断这个原函数是不是可执行的,如果不是,bind了也没意义,于是报错。有点疑惑,既然都能访问到Function.prototype了,这个原函数难道还不是function么?这个问题这里有解答

其实就是这样:

var notAFunction = {};
var someObject = {};

Function.prototype.bind.call(notAFunction, someObject);

间接的通过call或者apply来调用bind时,内部this指向了notAFunction了!!!

后面变量分析:

aArags主要用于把bind时剩余的参数作为补充参数,在调用新函数时整合参数来调用。

fToBind为原函数,fNOP为中介,一个纯函数,并且在第19行处将其原型链指向到原函数的原型链,这个干吗用呢?

fBound就是新函数了,它其实内部是调用fToBind,并且绑定了this,并返回执行结果。

而在this的选取中,通过判断 原函数是否为fNOP实例来决定用this还是绑定指定对象。

fBound.prototype = new fNOP();这句使得,当new fBound()即new调用新函数时,其this即为fNOP的实例,从而达到新函数作为构造函数调用时,无视原来要绑定的this.

再回过头来,看看关于fNOP.prototype = this.prototype; ,因为最后fBound的原型链指向fNOP的实例,而fNOP的原型链又指向原函数的原型。 如此一来,当新函数产生的实例对象,也能访问到原函数的原型链去了。

具体例子如下:

Function.prototype.bind1 = function(ctx){
    var me = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var F = function(){};
    F.prototype = this.prototype;
    var bound = function(){
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.apply(this instanceof F ? this : ctx||this, finalArgs);
    }
    bound.prototype = new F();
    return bound;
}
//不做指向原函数的原型
Function.prototype.bind2 = function(ctx){
    var me = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var F = function(){};
    //F.prototype = this.prototype;
    var bound = function(){
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.apply(this instanceof F ? this : ctx||this, finalArgs);
    }
    bound.prototype = new F();
    return bound;
}

var simple = function(){
    console.log('simple called: test = ',this.test);
};

var obj = {
    test:"obj's test"
};

var bSimple = simple.bind1(obj);
bSimple();//simple called: test =  obj's test

var bSimple2 = simple.bind2(obj);
bSimple();//simple called: test =  obj's test

var son = function(){
    console.log('son called: test = ',this.test);
};

var father = function(){
    console.log('father called');
    this.test = 'father test';
};

son.prototype = new father();//father called

var son1 = son.bind1(obj);
new son1(); //son called: test =  father test

var son2 = son.bind2(obj);
new son2(); //son called: test =  undefined

如果原函数没有更多的原型链指向,直接新函数构造的实例是可以访问到原函数内的属性的,但是如果原函数还有其他原型指向,要想访问原型链上的属性,那bind的实现里就必须带上原函数的原型链处理。

这个bind实现基本满足了要求,还有其他更佳兼容的,可读从一道面试题的进阶,到“我可能看了假源码”(2)