JavaScript 是通过「原型」来创建对象。下面记录了关于 JavaScript 对象、原型、继承相关知识。
原型
每一个对象都具有一个 prototype 属性,它用于实现「原型继承」和「属性共享」,它所指向的那个对象也就是这个对象的原型。
[[prototype]]隐式原型
任何一个对象都具有一个 [[prototype]] 内部属性,它可能是 null 或者是对象,不可以被外部直接访问,但是有些方法可以访问到。比如 getPrototypeOf(mdn)、setPrototypeOf(mdn)。
另外,改变一个对象的 [[Prototype]] 属性,这种行为在每一个 JavaScript 引擎和浏览器中都是一个非常慢且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的(来源 proto mdn )
proto
__proto__ 属性, 通过它访问的对象的内部[[Prototype]]。注意它只是一个被浏览器默认支持的行为,在 ECMAScript 2015 规范中被标准化为传统功能(位置),以确保 Web 浏览器的兼容性。
__proto__属性值指向它的构造函数的prototype属性值
Function.prototype.prototype
当一个函数被 new 运算符调用时,构造函数的 prototype属性会变成实例对象的 prototype。
注意:
- Generator 比较特殊,不可以被new 调用。
- 有些函数可能有原型,但在用 new 调用时会无条件抛出。 例如,Symbol() 和 BigInt() ,因为 Symbol.prototype 和 BigInt.prototype 仅旨在为原始值提供方法,但不应直接构造包装对象。
Object.prototype.constructor
prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
function P() {}
P.prototype.constructor === P // true
Class 的 prototype 属性和__proto__属性
Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
原型链
每一个对象都有他们的原型,它们的原型也是对象,因此就会形成一个回溯原型的链条,最终追溯到Object的原型null。这就是原型链。
Object.getPrototypeOf(Object.prototype) // null
继承
如何实现多重继承
JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。
function M1() {
this.hello = 'hello';
}
function M2() {
this.world = 'world';
}
function S() {
M1.call(this);
M2.call(this);
}
// 继承 M1
S.prototype = Object.create(M1.prototype); // todo 为什么用 object.create
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype);
// 指定构造函数
S.prototype.constructor = S; // todo 为什么要修改 constructor
var s = new S();
s.hello // 'hello'
s.world // 'world'
上面代码中有几个问题我们要讨论一下:
- 为什么用了 Object.create
为了避免重复调用「父类构造函数」,比如下面的代码
function M1() {
console.log("M1 constructor");
this.hello = "hello";
}
function S() {
M1.call(this);
}
S.prototype = new M1();
// S.prototype = Object.create(M1.prototype)
S.prototype.constructor = S;
var s = new S();
console.log(s.hello);
// 结果
// M1 constructor
// M1 constructor
// hello
- 为什么要修改 constructor
因为之前修改 prototype 的时候,已经覆盖了 constructor,如果不修改会导致 instanceof 出问题。
function M1() {
console.log("M1 constructor");
this.hello = "hello";
}
function S() {
M1.call(this);
}
S.prototype = new M1();
var s = new S();
const isInstance = s instanceof M1
console.log(isInstance); // true
- 为什么要执行父类构造函数
生成对象四步,其中一步是执行构造函数。
参考链接:
2 ES5 对象简介
4 网道 类的 prototype 属性和__proto__属性