Welcome toVigges Developer Community-Open, Learning,Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
261 views
in Technique[技术] by (71.8m points)

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();

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

尝试答一下。

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

  • 为什么判断 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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to Vigges Developer Community for programmer and developer-Open, Learning and Share
...