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

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

回答

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

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

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

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

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

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

尝试答一下。

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

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

以上是 JavaScript中关于bind的polyfill实现问题 的全部内容, 来源链接: utcz.com/a/38048.html

回到顶部