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_(...)
执行时,会发生以下事情:
- 一个继承自
_Foo_.prototype
的新对象被创建。 - 使用指定的参数调用构造函数 _
Foo
,并将[this](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this)
绑定到新创建的对象。new _Foo_
等同于new Foo
()
,也就是没有指定参数列表,Foo
_ 不带任何参数调用的情况。 - 由构造函数返回的对象就是
new
表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)
所以,你那样写会存在问题
1.先回答第一个问题呢为什么要用一个空函数,
看了这个你想必会明白, 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,而你写的 就不是原型式继承
- 你写的这个继承
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,意味着你如果添加新属性则会影响旧函数。
// 你定义了一个 Afunction 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