MDN的实现:链接

本文实现了两个版本的bind:简略版和进阶版。第一章实现了简略版并揭示了简略版存在的问题,第二章深入研究了导致该问题的原理,以及如何解决。

1. 简略版

1.1 实现

备注:简略版不反对应用new调用新创建的构造函数。

Function.prototype.myBind = function (context, ...args) {  context = context || window;  let invokFn = this;  return function () {      // 将两次传进来的参数合并      let finalArgs = args.concat(...arguments);      return invokFn.call(context, ...finalArgs);  }}

这样,咱们就能够实现这样的成果:

let obj = {  name: "xiaofei"}function sayName(age, sex){  console.log(this.name, age+"岁", sex);}let boundSayName = sayName.myBind(obj, 18);boundSayName("男");    // xiaofei 18岁 男

1.2 问题

最开始说了,这个实现不反对new调用。上面咱们就来看一下如果用new调用这个绑定函数会有什么问题:

let obj = {  name: "xiaofei"}function sayName(age){  console.log(this.name);    / **(1)** /  this.age = age;}let boundSayName = sayName.myBind(obj, 18);let o = new boundSayName();    // xiaofei

这里打印的还是“xiaofei”,阐明下面标注为(1)处执行时的this依然指向obj

上面揭示代码存在的问题:

问题1:

如果咱们打印一下 objo,后果如下图:

能够看到,本该属于o实例对象的age属性跑到obj对象上了!其实这也很好了解,因为下面咱们剖析过,(1)处的代码this指向obj,那么它下一行的代码this.age = age也就相当于在给obj设置age属性。
问题2:

另外,咱们进一步摸索还能够发现一个问题:o是新绑定函数的实例,而不是旧函数的实例。

请读者认真想一想,这样的后果合不合理?

正当的后果应该是:o应该同时是这两个函数的实例,即下面的两行代码都应该返回true

2. 升级版

2.1 问题的实质

要钻研1.2中提出的问题,咱们首先得深刻了解new命令到底做了什么?

上面给出new命令的模仿实现代码(没接触过的读者倡议钻研一下,搞懂每一行代码在做什么):

function myNew(fn) {    let objTemp = {};    objTemp.__proto__ = fn.prototype;    let args = [].slice.call(arguments, 1);    let result = fn.call(objTemp, ...args);    /*(2)*/    return (typeof result === 'object' && result != null) ? result : objTemp;}

咱们来剖析一下new boundSayName()执行经验了什么:

首先,先执行new指令,当执行到(2)处代码时,用call执行boundSayName函数,这是第一次this指向产生了变动,此时this指向的是new命令底层生成的对象,也就是下面代码中的objTemp对象;

而后,boundSayName函数执行,也就是执行如下图所示中红框内的代码。咱们看红框中的最上面那行代码,它又一次地调用了call,使得this指向了context,也就是1.2节中所示代码中的obj

最初,调用invokFn函数,其实就是 sayName 函数(如下图所示),此时this指向为obj。这样,咱们就能解释1.2中所提出的问题了。然而,该如何解决这个问题?下一节给出解决方案。

2.2 解决

先来回顾一下:

Function.prototype.myBind = function (context, ...args) {  context = context || window;  let invokFn = this;  return function () {      // 将两次传进来的参数合并      let finalArgs = args.concat(...arguments);      return invokFn.call(context, ...finalArgs);  }}let obj = {  name: "xiaofei"}function sayName(age){  console.log(this.name);    / **(1)** /  this.age = age;}let boundSayName = sayName.myBind(obj, 18);let o = new boundSayName();    // xiaofeiconsole.log(o);    // {}console.log(obj);    // {name: "xiaofei", age: 18}o instanceof sayName; // falseo instanceof boundSayName;    // true

咱们实现了一个myBind办法,然而当绑定函数被new调用时,会存在两个问题:

  1. 实例oage属性跑到obj对象上了;
  2. 新生成的实例不是sayName的实例。

咱们想实现的最终后果:

  1. age属性在o实例上,obj对象上没有age属性;
  2. o既是sayName的实例,也是boundSayName的实例,即:o instanceof sayNameo instanceof boundSayName都返回true

(2.2.1) 咱们先来思考第一个问题。

思考一下上图中红框局部的this指向。能够分为两类:

  1. 当绑定函数间接调用时,即执行boundSayName()时,红框内代码执行时的this指向window
  2. new调用时,即执行new boundSayName()时,依据2.1节的剖析,此时this指向new命令底层生成的对象 objTemp


此外,在new命令底层操作中还有这么一步(如上图箭头所示),咱们将fn的原型赋予给了objTemp--proto--属性,也就是说:objTemp instanceof fn 应为true。而objTemp对应红框中的thisfn对应boundSayNameboundSayName对应红框中的boundFn,因而,咱们能够加这么一层判断 (this instanceof boundFn)?this:context,解释如下:

  • boundFnprototype呈现在this对象的原型链中,阐明此时是new调用的,此时call中传入this对象,也就是objTemp,也就是最终生成的实例;
  • 如果不是,阐明此时是一般执行(此时this指向windowwindow instanceof boundFn 显然返回false),call中就传入context对象,也就是obj对象。

代码如下:

Function.prototype.myBind = function (context, ...args) {  context = context || window;  let invokFn = this;  let boundFn = function () {    let finalArgs = args.concat(...arguments);     // 看这里!!就多了这里一行代码!!    return invokFn.call((this instanceof boundFn) ? this : context, ...finalArgs);  }  return boundFn;}

(2.2.2) 上面来看第二个问题:新生成的实例不是sayName的实例。

这个问题的解决思路就是:将sayNameprototype赋予给boundFn的原型的--proto--属性。这样新生成实例就会继承boundFn的原型,而这个原型的--proto--又指向sayName的原型。所以最终的成果就是:新生成的实例的原型链中既有boundFn的原型,又有sayName的原型。

残缺代码如下:

Function.prototype.myBind = function (context, ...args) {  context = context || window;  let invokFn = this;  let helperFn = function(){};    // 借助这个辅助函数将invokFn的prototype混入boundFn的原型链中.  helperFn.prototype = invokFn.prototype;  let boundFn = function () {      // 将两次传进来的参数合并      console.log(this instanceof invokFn);      let finalArgs = args.concat(...arguments);      return invokFn.call((this instanceof boundFn)?this:context, ...finalArgs);  }  boundFn.prototype = new helperFn();  return boundFn;}