JavaScript中关于bind的polyfill实现问题

问题描述

今天在研究JS中bind polyfill实现的时候碰到一个问题:

Function.prototype.myBind = function (obj) {
  // 检查被绑定的是否为函数(假设此例为foo)
  if (typeof this !== 'function') {
    throw new TypeError('not a function');
  }
  // 将foo传给that保存
  var that = this,
    // 取出传入的参数
    oldArr = Array.prototype.slice.call(arguments, 1),
    fnVoid = function () {}, // 定义一个空函数用于之后的原型链绑定
    fnNew = function () {
      return that.apply(this instanceof fnVoid && obj ? this : obj, [
        // 判断this的指向,如果是使用new调用,则this绑定到新对象,此时新对象的原型为构造函数,即this instanceof fnVoid
        // 此时需要将新对象this传入that(foo)函数,将构造函数的属性绑定到新对象上
        // 如果不是使用new调用,则传入obj,将foo函数绑定到传入的obj上
        ...oldArr,
        ...arguments,
      ]);
    };
  // 为什么不直接 fnNew.prototype = new this(), 而要借用一个空函数呢
  fnVoid.prototype = this.prototype; 
  fnNew.prototype = new fnVoid(); 
  return fnNew;
};

如最后一个注释所述,为什么不直接 fnNew.prototype = new this(), 而要借用一个空函数呢?这样不是也能实现对原函数的继承吗?

还有就是突然想到之前看到的原型式继承,为什么子类的原型要继承至父类的一个实例,而不是直接继承至父类的原型呢?

function TypeA(name) {
  this.name = name;
}

function TypeB(age) {
  this.age = age;
}

TypeB.prototype = new TypeA();

小白刚入门,对很多问题的理解不是很深,感谢各位大佬能点拨点拨,感激不尽!!!!

阅读 4k
5 个回答

尝试答一下。

乍一看我也觉得奇怪,疑点有二:

  • 为什么判断 bind 返回的函数是否通过 new 调用要用 this instanceof fnVoid
  • 为什么引入 fnVoid 然后弄一堆 prototype 赋值

1. 我的第一直觉是应该这么干:

判断 new 调用使用 this instanceof fnNew, fnNew.prototype = this.prototype, 直接不需要 fnVoid 的存在:

Function.prototype.myBind = function (obj) {
  // ... 省略
  var fnNew = function () {
      return that.apply(this instanceof fnNew && obj ? this : obj, [
        ...oldArr,
        ...arguments,
      ]);
    };
  fnNew.prototype = this.prototype; 
  return fnNew;
};

妥妥的不是么?有问题吗?等等,确实是有的。

问题是返回的函数的 prototype 引用了原先函数的 prototype,意味着你如果添加新属性则会影响旧函数。

// 你定义了一个 A
function A(){}
A.prototype.a = function(){return 1;}

// 另一个人干了这件事
var B = A.myBind();
B.prototype.a = function(){return 2;}

var a = new A();
a.a(); // 见鬼,谁把老子的函数改了

2. 所以我们避免直接引用原先函数的 prototype

fnVoid 的作用就是用来当中间层用,那么不用 fnVoid,用 new this() 可以么?
试试:

Function.prototype.myBind = function (obj) {
  // ... 省略
  var fnNew = function () {
      return that.apply(this instanceof fnNew && obj ? this : obj, [
        ...oldArr,
        ...arguments,
      ]);
    };
  fnNew.prototype = new this(); 
  return fnNew;
};

function A(){ alert(1); }
var B = A.bind({});
// 还没干什么呢,A 就被执行了一遍?

你看, new this 是有副作用的,它会导致意外执行原函数本身。

3. 引入的 fnVoid 其实等价于 Object.create 的 polyfill

要我说,这个地方不引入 fnVoid 也是可以的,我们的目的其实就是让 fnNew 能间接引用到 原函数的 prototype.

Function.prototype.myBind = function (obj) {
  // ... 省略
  var fnNew = function () {
      return that.apply(this instanceof fnNew && obj ? this : obj, [
        ...oldArr,
        ...arguments,
      ]);
    };
  fnNew.prototype = Object.create(this.prototype); 
  return fnNew;
};

而 Object.create 的 polyfill 版本就是:

Object.create = function(o) {
  function F() {}
  F.prototype = o;
  return new F();
 };

这就是 fnVoid 的由来。空函数是为了搭配 new 使用继承 prototype 并且不产生副作用。本质和 ES6 的 Object.create 或者 Object.setPrototypeOf 是一个作用。


关于第二个原型问题,我也一并解释一下,篇幅比较长。

我在这里先声明一下几个术语,前端届对原型的称呼一直比较混乱:

  • 对象:JS 中一切非原始值皆对象,函数也是对象的一种
  • 原型:

    • 每个对象都有一个原型,原型本身也是一个普通的对象。
    • 使用点操作符或者 [ ] 访问属性时,会先检查对象本身的属性,如果不存在则会检查对象的原型,如果还不存在则会继续检查对象原型的原型直到原型的尽头 Object.prototype ,它没有原型,它的原型是 null,这个就是所谓的原型链。
    • 获取对象的原型可以用标准的 Object.getPrototypeOf(obj),或者非标准的 obj.__proto__, 以下我都用 Object.getPrototypeOf(obj).
    • 对象的默认 toString, valueOf 方法都来自 Object.prototype。
  • 函数:函数是一种特殊的对象,所以函数也有原型,通过 function 语句声明的函数的原型是 Function.prototype。函数的 call,apply,bind 方法都来自 Function.prototype 上。
  • 构造函数:除了箭头函数外,普通函数都可以当作构造函数用,用法就是使用 new 操作符。
  • 类:ES6 的类是一种特殊的函数,只能通过 new 操作符来使用,不能当普通函数来直接调用。
  • 函数.prototype: 函数对象还有一个特殊的属性,名字叫 prototype。这个我们还是别叫 函数的原型 了吧,因为函数作为对象看待它本身确实是有原型的,所以我们叫它函数的prototype属性,以区别于函数的原型,即:Object.getPrototypeOf(fn) vs fn.prototype.
  • new 一个构造函数/类的时候发生了什么:

    • 新建一个对象
    • 把对象的原型指向构造函数的 prototype 属性
    • 把对象当 this 运行一遍构造函数
  • 什么是继承?

    • JS 的继承发生在对象之间,而非类之间,就像现实中的儿子继承爸爸,不需要先有儿子类和爸爸类,让儿子类 extends 爸爸类然后实例化。JS 中的对象直接继承另一个对象。
    • JS 的继承是使用原型来实现的。对象属性查找会检查原型链,这就是继承概念的体现。
    • ES5 加入的 Object.create API 就是把一个对象当原型来创建另一个对象之用。
  • 基于 prototype 的继承模拟基于 class 的继承:

    • 函数的 prototype 属性就是用来模拟基于 class 的继承的,否则只要能从一个对象构造另一个对象那 prototype 继承就有了。
    • 当我们 new 一个 class/函数 时,其实我们是把 class/函数的prototype 属性当原型来构造新对象的。你把方法和属性定义在函数的prototype属性上,用起来和传统的类定义方法和字段一个感觉。

回到你的例子, TypeB.prototype = new TypeA();这一句其实是错误的,这里存在一个非预期的副作用,那就是 TypeA 的构造器莫名被调用了一次。这里其实是需要使用 Object.create 或者其 polyfill 即空函数+new 来实现的。

function TypeA(name) {
  this.name = name;
}

function TypeB(age) {
  this.age = age;
}

TypeB.prototype = new TypeA(); // 错误

TypeB.prototype = Object.create(TypeA.prototype);//
// 同时 TypeB 的原型要指向 TypeA 以便继承静态方法
Object.setPrototypeOf(TypeB, TypeA); 

TypeA.staticP = 1;
console.log(TypeB.staticP); // 1

因为里面要判断是否是 new 构造调用。

怎么判断呢?是 this instanceof fnVoid 。这就是为什么要有 fnVoid 的原因。没有 fnVoid ,这个判断做不了。

那么直接用 this instanceof that.prototype 不成嘛?这是不可以的,因为用 that 的实例调用这个函数,该判断为 true ,但是并不是 new 构造调用。无法准确判断。

https://stackoverflow.com/a/2...

if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; 
    }
    fBound.prototype = new fNOP();

我看MDN的是这样的,如果直接调用Function.prototype.bind没有prototype属性。
再看看new的机制
当代码 new _Foo_(...) 执行时,会发生以下事情:

  1. 一个继承自 _Foo_.prototype 的新对象被创建。
  2. 使用指定的参数调用构造函数 _Foo,并将 [this](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this) 绑定到新创建的对象。new _Foo_ 等同于 new Foo(),也就是没有指定参数列表,Foo_ 不带任何参数调用的情况。
  3. 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)

所以,你那样写会存在问题

1.先回答第一个问题呢为什么要用一个空函数,
image.png
看了这个你想必会明白, this.prototype 不一定存在,但是 bind之后的函数又可以被new,所以需要一个容器来承载,保证bind的函数是可以被new的

2.你说的原型式继承

  原型式继承有2种
  1. let a = Object.create(obj);
  2. function _create(obj) {
        let func = function () {};
        func.prototype = obj;
        return new func;
     }

这是原型式继承的模板
你会发现被继承的其实是一个Object,并不是function,所以依然需要一个容器来存这个prototype,而你写的 就不是原型式继承

  1. 你写的这个继承
function TypeA(name) {
  this.name = name;
}
function TypeB(age) {
  this.age = age;
}
TypeB.prototype = new TypeA();

这个其实就是一个继承的基本写法,可以看看红宝书 继承这一章,继承的各种方式都有自己的特点和优缺点

非常感谢各位大佬的详细解答,真的很感谢!!!

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题