JavaScript的原型继承机制是该语言最核心的特性之一,也是许多开发者容易混淆的概念。与传统的类继承不同,JS采用原型链(Prototype Chain)实现对象间的属性和方法共享。
每个JS对象都有一个隐藏的[[Prototype]]属性(可通过__proto__访问),指向其原型对象。当访问对象的属性时,如果当前对象没有该属性,JS引擎会沿着原型链向上查找。这种机制使得对象可以共享方法和属性,而不需要每个实例都单独创建副本。
javascript复制function Animal(name) {
this.name = name
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a noise`)
}
function Dog(name) {
Animal.call(this, name) // 调用父类构造函数
}
// 设置原型链
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
Dog.prototype.speak = function() {
console.log(`${this.name} barks`)
}
const d = new Dog('Mitzie')
d.speak() // Mitzie barks
关键点:
Object.create()会创建一个新对象,并将其[[Prototype]]指向传入的对象。这是ES5中实现原型继承的标准方式。
ES6引入了class语法糖,使原型继承的写法更接近传统面向对象语言。但要注意,这仅仅是语法糖,底层仍然是基于原型继承。
super关键字在类中有两种用法:
super()调用父类构造函数super.method()调用父类方法javascript复制class Animal {
constructor(name) {
this.name = name
}
speak() {
console.log(`${this.name} makes a noise`)
}
}
class Dog extends Animal {
constructor(name) {
super(name) // 必须在使用this前调用
}
speak() {
super.speak() // 调用父类方法
console.log(`${this.name} barks`)
}
}
const d = new Dog('Mitzie')
d.speak()
// 输出:
// Mitzie makes a noise
// Mitzie barks
注意事项:在派生类的构造函数中,必须先调用
super()才能使用this,否则会抛出ReferenceError。这是因为子类实例的创建依赖于父类构造函数的执行。
虽然super看起来像是一个普通的变量或关键字,但实际上它的行为非常特殊。根据ECMAScript规范:
super在方法中指向当前方法的[[HomeObject]].[[Prototype]][[HomeObject]]是函数被定义时确定的,静态绑定这种设计意味着super的查找是静态的,不会像this那样动态变化:
javascript复制class A {
say() {
console.log('A')
}
}
class B extends A {
say() {
console.log('B')
}
run() {
this.say() // 动态查找,输出取决于调用者
super.say() // 静态查找,总是输出A
}
}
class C extends B {
say() {
console.log('C')
}
}
new C().run()
// 输出:
// C (this.say()动态查找)
// A (super.say()静态查找)
在多层继承中,super会沿着类继承链向上查找:
javascript复制class A {
foo() { console.log('A') }
}
class B extends A {
foo() {
super.foo()
console.log('B')
}
}
class C extends B {
foo() {
super.foo()
console.log('C')
}
}
new C().foo()
// 输出:
// A
// B
// C
箭头函数没有自己的super绑定,它会从外层作用域继承super:
javascript复制class A {
foo() { return 'A' }
}
class B extends A {
foo() {
const arrow = () => super.foo()
return arrow()
}
}
console.log(new B().foo()) // 输出"A"
方法赋值会破坏super绑定,因为[[HomeObject]]只在方法定义时设置:
javascript复制class A {
foo() { return 'A' }
}
class B extends A {
foo() { return super.foo() }
}
const b = new B()
console.log(b.foo()) // "A"
// 动态赋值方法
b.foo = function() { return super.foo() }
console.log(b.foo()) // 抛出错误
解决方法是将方法定义为类方法,或使用箭头函数:
javascript复制// 方案1:保持为类方法
class B extends A {
foo() { return super.foo() }
}
// 方案2:使用箭头函数
class B extends A {
constructor() {
super()
this.foo = () => super.foo()
}
}
避免过深的原型链:每层原型查找都有性能开销,建议保持继承层次扁平化
优先使用组合而非继承:考虑使用对象组合而非类继承来共享行为
谨慎使用super:理解其静态绑定特性,避免在动态上下文中使用
方法定义优于动态赋值:确保方法在类中定义以保持正确的super绑定
考虑使用Proxy:对于需要动态继承行为的场景,Proxy可能是更好的选择
javascript复制// 组合优于继承的例子
const canSpeak = {
speak() {
console.log(`${this.name} makes a noise`)
}
}
class Dog {
constructor(name) {
this.name = name
Object.assign(this, canSpeak)
}
bark() {
this.speak()
console.log(`${this.name} barks`)
}
}
在实际项目中,理解原型继承和super的底层机制,可以帮助开发者写出更健壮、可维护的代码,同时避免常见的陷阱和性能问题。