简明深入Function.prototype.bind


面试场景

对于这个话题,我面试时常会按照以下的顺序来对候选人进行提问:

  • 说一下对Javascript中的 this 关键字的理解。– 答对,30分
  • 如果能说出来 this 的作用,那么,手写两段程序,说明 this 啥时候指向原对象,啥时候指向 window 。– 答对,50分
  • 有什么方式可以改变 this 的指向? – 答对,70分
  • 引出 Function.prototype.bind , 是否有用过,怎么用 – 答对,80分
  • 写一个 Function.prototype.bindpolyfill (浏览器兼容方案)。 – 写得基本正确能运行,100分
  • 能考虑到constructor模式调用 – 120分!

下面是我的一些理解。

this关键字

this 关键字表示当前调用函数或方法的所有者。this的值取决于它的 调用模式

  • 对于一个全局函数,表示的就是 window 对象;
  • 对于一个对象的方法,表示的是该对象的实例;
  • 在一个事件句柄中,表示的是接收到该事件的元素。
  • 对于一个类的实例,表示的是该类的对象。

举例说明。

函数调用模式

function cat () {
  console.log(this);
  this.name = 'miaomiao';
}

// => window

即上面的第1条规则。

方法调用模式

var cat = {
  name: 'miaomiao',
  sleep: function () {
    console.log(this);
    return this.name + 'is sleeping';
  }
}

cat.sleep();
// => cat

即上面的第2条规则。

事件句柄模式

<a id="lk" href="https://www.baidu.com">click me</a>
var link = document.getElementById('lk');
        link.addEventListener('click', function (e) {
            e.preventDefault();
            console.log(this);
            console.log(this.href);
        })
//=> <a id="lk" href="https://www.baidu.com">click me</a>
//=> https://www.baidu.com

即上面的第3条规则。

构造器调用模式

function cat () {
  this.name = 'miaomiao';
}

var myCat = new cat();
console.log(myCat.name);
console.log(window.name);
// => miaomiao, 这里this指向了cat
// => undefined, 说明没指向window.

即上面的第4条规则。

改变this的指向:apply/call

function getName (name) {
  console.log(this);
  this.name = name;
  console.log(this);
}
var foo = {name: 'hehe'}; 
getName('xixi');
// => Window
// => Window

getName.call(foo, 'xixi');

// => Object {name: "hehe"}
// => Object {name: "xixi"}

上面这个例子中,调用 getName.call(foo, 'xixi') 时,在getName函数中的this就指向了foo; 所以再执行this.name = name 后,foo 对象的 name 相应地做出改变。

改变this的指向:bind

function getName (name) {
  console.log(this);
  this.name = name;
  console.log(this);
}

var foo = {name: 'hehe'};

var bar = getName.bind(foo);
bar('xixi');

// => Object {name: "hehe"}
// => Object {name: "xixi"}

上面这个例子中,调用 getName.bind 后,就把this 绑定到了 foo 上。产生的结果和apply/call是一样的。

当然,上面这个例子是在 最新版的chrome下测试的。IE9以下的浏览器是不兼容的。

Function.prototype.bind 的具体浏览器兼容性请见这里

Polyfill: Function.prototype.bind

你要写一个兼容旧浏览器的Function.prototype.bind,总得知道它怎么调用吧!

调用如下:

fun.bind(thisArg[, arg1[, arg2[, …]]])

thisArg 表示要把this绑定到 thisArg;第二个以及以后的参数arg1, arg2, ... 加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

那么这个顺序具体是什么样的呢?

arg1, arg2…argN 在前,绑定函数运行时本身的参数在后。

看一个具体例子来验证:

function getName (name) {
  console.log(this);
  console.log(arguments);
  this.name = name;
  console.log(this);
}

var foo = {name: 'hehe'};

var bar = getName.bind(foo, 1, 2, 3);
bar('xixi');

// => Object {name: "hehe"}
// => [1, 2, 3, "xixi"]
// => Object {name: 1}

输出了[1, 2, 3, "xixi"]。验证了这个结论。

MDN的polyfill解释

下面是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 || this,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}

里面有几个重要的点,我逐条说明:

  1. 首先判断是否原生支持,判断调用 bind 的是不是一个function类型等,略
  2. Array.prototype.slice.call(arguments,1) 这一句:
    • arguments 这个伪数组,除了第1个参数后的所有参数取出为一个数组。 - aArgs有啥用,留着后面合并啊,看上面的[1,2,3,”xixi”]的例子~
  3. fToBind = this : 当前这个待bind的对象;
  4. fNOP 我不知道它为啥要起这个名,是啥的缩写?这个东东要和下面的两句 fNOP = prototype = this.prototype , fBound.prototype = new fNOP() 一起看,这一看就明白,fNOP是当前待bind函数的一个原型副本, fBound 继承了 fNOP。 最后把 fBound 返回。fBound是个函数,因为bind的结果还是个函数嘛!
  5. 好了,中间这一句,总体看是这样:return fToBind.apply(this, arguments); 这个arguments就是用aArgs和绑定函数运行时的函数给concat组合起来了。

看了上面的分析可能有些童鞋会有疑问:

 为啥要搞个fNOP出来,然后 fBound去继承它?

 为什么不能直接 fBound.prototype = this.prototype , 然后返回fBound

哎,童鞋,你还是太单纯呐。

要是按你那么搞,因为prototype是个引用类型fBound又能被外界访问到的话,如果我来个fBound.prototype = shit 之类的,你让 this.prototype 情何以堪呐。

这里fNOP作为私有变量,就杜绝了改变this.prototype的可能性。

polyfill中的主要难点

好,那这里的一个__主要的难点__是:

this instanceof fNOP
    ? this
    : oThis || this

此时fNOP 它是一个空函数

  • 解读上面这一句的用意:
  1. 如果 this 是 fNOP 的实例,那么就是this
  2. 否则就是 oThis ,如果oThisnull或者undefined, 那么就是this

那么问题来了:

this instanceof fNOP 这是在干毛?

为什么我要去判断this是不是一个空函数的实例?

这判断有啥用?

我为啥不直接写成oThis || this ?

好,那我们就把MDN上polyfill的这段去掉。为了便于测试,我们把bind 改成 myBind :

Function.prototype.myBind = function (oThis) {
    if (typeof this !== "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 () {
          // 把那个instanceof判断去掉
          return fToBind.apply(oThis || this,
            aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };

好了看下面的例子:

function getName (name) {
  this.name = name;
}
var foo = {};

var bar = getName.myBind(foo);

bar('hehe');
console.log(foo.name);
// => hehe

这是显然的嘛。myBind函数也工作得很好。

那么,我们使用new 构造器来调用绑定后的函数bar,会发生啥呢?

在上面的程序下面继续:

var xixi = new bar('xixi');
console.log(foo.name);
// => xixi, foo的name变成了xixi!
console.log(xixi.name);
// => undefined

上面的例子显而易见,我们当然希望foo.name仍然是hehe, 而xixi.namexixi。 说明我们的myBind函数在这种情况下不好使了。

这说明啥?说明咱们用new构造器来调用bar时,绑定的this还是foo

我们在chrome下用原生的bind函数试一次:

function getName (name) {
  this.name = name;
}
var foo = {};

var bar = getName.bind(foo);

bar('hehe');
console.log(foo.name);
// => hehe

var xixi = new bar('xixi');
console.log(foo.name);
// => hehe   还是hehe
console.log(xixi.name);
// => xixi

可见,bind 函数正确绑定在了xixi上。

这样,我们再回头来看this instanceof fNOP 在对绑定函数执行new下调用会是啥结果:

那就相当于执行了这么一句:

new function(oThis){
  //...
  return fBound;
}();

显然,这时得到的是fBoundfBound是个啥呢?是个function(){…}。

那么执行this instanceof fNOP 相当于fBound instaceof fNOP

true !

如果为true 那么就绑定在oThisthis上,优先oThis;

即传进的参数,在我们的例子里面,就是xixi

所以可以得出结论:this instanceof fNOP 这个判断分支是为了在用new调用绑定函数的情况下而生的。

总结

Function.prototype.bind 可以引申出很多的知识点, 甚至可以认为:

熟悉了这些知识点, 你的js就不再是 小白 水平了。

写这篇文章前专门网上走了一圈,发现没有对MDN的polyfill的详细解释,所以才打算写~

希望俺这篇文章能让你有所收获~

以上。

关于此文章:

  • 作者:Clancy Zhu
  • 版权声明:自由转载-非商用-非衍生-保持署名(CC BY-NC-ND 3.0)