十年前我刚接触JavaScript时,第一次看到__proto__这个属性简直一头雾水。直到在Chrome控制台里把玩了一个小时对象实例后,才突然意识到:原来这就是JavaScript实现继承的底层机制。不同于传统面向对象语言的类继承,JS通过原型链(Prototype Chain)实现了独特的继承体系。
每个JS函数创建时都会自动获得prototype属性(箭头函数除外),这个属性指向一个包含constructor指针的对象。当我们用new操作符创建实例时,实例内部的[[Prototype]](可通过__proto__访问)就会指向构造函数的prototype对象。查找属性时,如果实例自身没有,引擎就会沿着__proto__这条链向上查找——这就是原型链的核心原理。
javascript复制function Animal(name) {
this.name = name
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`)
}
const cat = new Animal('Tom')
cat.eat() // 查找路径:cat -> cat.__proto__ (Animal.prototype)
ES6引入的super在不同上下文中有截然不同的行为:
在派生类的constructor中,super作为函数调用时,相当于执行父类的constructor。这里有个隐藏的坑:在super()之前访问this会抛出ReferenceError。因为引擎要求必须先初始化父类实例,才能修饰这个实例。
javascript复制class Cat extends Animal {
constructor(name, color) {
// console.log(this) // 这里会报错!
super(name) // 相当于Animal.call(this, name)
this.color = color
}
}
在普通方法中,super作为对象使用时指向父类的prototype。但要注意,通过super调用父类方法时,方法内的this仍然指向当前子类实例。
javascript复制class Cat extends Animal {
eat() {
super.eat() // 相当于Animal.prototype.eat.call(this)
console.log('with fish flavor')
}
}
当super遇上原型链,会产生一些反直觉的现象。比如通过super访问属性时,引擎实际上执行的是Object.getPrototypeOf(ChildClass.prototype)查找:
javascript复制class Animal {}
Animal.prototype.food = 'meat'
class Cat extends Animal {
getFood() {
return super.food
}
}
console.log(new Cat().getFood()) // 'meat'
这个机制解释了为什么在对象字面量中,super指向的是对象的原型:
javascript复制const animal = { food: 'meat' }
const cat = {
getFood() {
return super.food
}
}
Object.setPrototypeOf(cat, animal)
console.log(cat.getFood()) // 'meat'
箭头函数没有自己的super绑定,它会捕获所在上下文的super:
javascript复制class Animal {
say() { return 'Hello' }
}
class Cat extends Animal {
say = () => super.say() + ' Meow'
// 等同于:
// constructor() {
// this.say = () => super.say() + ' Meow'
// }
}
在静态方法中,super指向父类本身而非prototype:
javascript复制class Animal {
static create() { return new this() }
}
class Cat extends Animal {
static create() {
const instance = super.create() // 调用Animal.create()
instance.name = 'Tom'
return instance
}
}
虽然JS不支持真正的多重继承,但可以通过混入模式(Mixin)配合super实现:
javascript复制const Flyable = Base => class extends Base {
fly() { console.log('Flying!') }
}
class Animal {}
class Bird extends Flyable(Animal) {
move() {
super.fly()
}
}
当父类方法是Proxy时,super调用会穿透Proxy:
javascript复制class Animal {
constructor() {
return new Proxy(this, {
get(target, key) {
console.log(`Accessing ${key}`)
return target[key]
}
})
}
}
class Cat extends Animal {
say() {
super.say() // 不会触发Proxy的get陷阱
}
}
将方法赋值给其他对象后,super引用会断裂:
javascript复制class Animal {
say() { return 'Hello' }
}
class Cat extends Animal {
say() { return super.say() + ' Meow' }
}
const alien = { say: new Cat().say }
alien.say() // 报错:无法读取undefined的say
使用Object.setPrototypeOf动态修改继承关系时,super仍然指向原始父类:
javascript复制class A { foo() { return 'A' } }
class B { foo() { return 'B' } }
class C extends A {}
Object.setPrototypeOf(C.prototype, B.prototype)
console.log(new C().foo()) // 仍然输出'A'
在generator方法中使用yield和super的组合:
javascript复制class Animal {
*sounds() {
yield 'Growl'
yield 'Howl'
}
}
class Cat extends Animal {
*sounds() {
yield* super.sounds()
yield 'Meow'
}
}
console.log([...new Cat().sounds()]) // ['Growl', 'Howl', 'Meow']
原型链查找和super调用都会带来性能开销。在V8引擎中,隐藏类(Hidden Class)机制会优化原型访问,但以下情况会导致优化失效:
__proto__或Object.setPrototypeOf)实测表明,在热代码路径中,直接引用比通过super调用快约15%:
javascript复制// 较慢的实现
class Optimized extends Animal {
eat() {
super.eat()
}
}
// 更快的实现
const parentEat = Animal.prototype.eat
class Optimized extends Animal {
eat() {
parentEat.call(this)
}
}
看Babel如何将ES6的class和super转译为ES5代码很有启发性。对于super.method()的调用,Babel会生成:
javascript复制// 原始代码
class Child extends Parent {
method() {
super.method()
}
}
// 编译后
var Child = (function(_Parent) {
_inherits(Child, _Parent)
function Child() {
_classCallCheck(this, Child)
return _possibleConstructorReturn(this, _getPrototypeOf(Child).apply(this, arguments))
}
_createClass(Child, [{
key: "method",
value: function method() {
_get(_getPrototypeOf(Child.prototype), "method", this).call(this)
}
}])
return Child
})(Parent)
关键点在于_getPrototypeOf(Child.prototype)获取父类prototype,然后通过.call(this)确保this绑定正确。
ECMAScript规范中关于super的算法描述非常精妙:
当解析super.method()时:
[[HomeObject]]是函数在定义时就确定的:
这个设计确保了super的静态绑定特性,使得方法被复制到其他对象后,super引用仍然指向原本的父类。