说说JavaScript中的继承?
问题解析
继承是面向对象编程的核心概念之一。JavaScript 作为一门基于原型的语言,其继承机制与 Java、C++ 等基于类的语言有很大不同。理解 JavaScript 的继承方式,对于编写可维护、可复用的代码至关重要。
核心概念
继承的本质
继承使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码。在子类继承父类的同时,可以重新定义某些属性,并重写某些方法,使其获得与父类不同的功能。
详细解答
1. 原型链继承
function Parent() {
this.name = 'Parent';
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child() {
this.age = 18;
}
// 继承 Parent
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child1 = new Child();
const child2 = new Child();
child1.sayName(); // "Parent"
// 问题:引用类型属性被共享
child1.colors.push('green');
console.log(child1.colors); // ["red", "blue", "green"]
console.log(child2.colors); // ["red", "blue", "green"] —— 也被修改了!
优点:
- 可以继承父类原型上的方法
缺点:
- 引用类型属性被所有实例共享
- 创建子类实例时不能向父类传参
2. 构造函数继承(借用构造函数)
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
// 借用 Parent 构造函数
Parent.call(this, name);
this.age = age;
}
const child1 = new Child('Alice', 18);
const child2 = new Child('Bob', 20);
// 引用类型属性独立
child1.colors.push('green');
console.log(child1.colors); // ["red", "blue", "green"]
console.log(child2.colors); // ["red", "blue"]
// 问题:无法继承父类原型上的方法
child1.sayName(); // TypeError: child1.sayName is not a function
优点:
- 引用类型属性独立,不被共享
- 可以向父类传参
缺点:
- 无法继承父类原型上的方法
- 每次创建实例都会调用一次父类构造函数
3. 组合继承
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
// 继承实例属性(第二次调用 Parent)
Parent.call(this, name);
this.age = age;
}
// 继承原型方法(第一次调用 Parent)
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
const child1 = new Child('Alice', 18);
const child2 = new Child('Bob', 20);
child1.sayName(); // "Alice"
child1.sayAge(); // 18
// 引用类型属性独立
child1.colors.push('green');
console.log(child1.colors); // ["red", "blue", "green"]
console.log(child2.colors); // ["red", "blue"]
优点:
- 结合了原型链继承和构造函数继承的优点
- 可以继承实例属性和原型方法
- 引用类型属性独立
缺点:
- 父类构造函数被调用两次
- 子类原型上有多余的父类实例属性
4. 原型式继承
// 基于现有对象创建新对象
function createObject(o) {
function F() {}
F.prototype = o;
return new F();
}
// ES5 Object.create 的实现
const person = {
name: 'Person',
colors: ['red', 'blue'],
sayName() {
console.log(this.name);
}
};
const person1 = Object.create(person);
person1.name = 'Alice';
person1.colors.push('green');
const person2 = Object.create(person);
console.log(person2.colors); // ["red", "blue", "green"] —— 共享引用类型
// 可以指定第二个参数,定义额外属性
const person3 = Object.create(person, {
name: { value: 'Bob' },
age: { value: 25, writable: true, enumerable: true }
});
优点:
- 简单,不需要构造函数
- 适合对象之间的浅复制
缺点:
- 引用类型属性共享
- 无法实现代码复用
5. 寄生式继承
function createAnother(original) {
// 通过某种方式创建对象(如 Object.create)
const clone = Object.create(original);
// 增强对象,添加方法
clone.sayHi = function() {
console.log('Hi!');
};
return clone;
}
const person = {
name: 'Person',
sayName() {
console.log(this.name);
}
};
const person1 = createAnother(person);
person1.sayName(); // "Person"
person1.sayHi(); // "Hi!"
优点:
- 在原型式继承基础上增强了对象
缺点:
- 引用类型属性共享
- 方法无法复用(每次创建新函数)
6. 寄生组合式继承(推荐)
function inheritPrototype(Child, Parent) {
// 创建父类原型的副本
const prototype = Object.create(Parent.prototype);
// 修正 constructor 指向
prototype.constructor = Child;
// 赋值给子类原型
Child.prototype = prototype;
}
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// 使用寄生组合式继承
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function() {
console.log(this.age);
};
const child = new Child('Alice', 18);
child.sayName(); // "Alice"
child.sayAge(); // 18
console.log(child instanceof Child); // true
console.log(child instanceof Parent); // true
优点:
- 只调用一次父类构造函数
- 避免了在子类原型上创建不必要的属性
- 原型链保持不变
- 效率最高
7. ES6 Class 继承
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
sayName() {
console.log(this.name);
}
// 静态方法
static isParent(obj) {
return obj instanceof Parent;
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数,必须在使用 this 之前调用
this.age = age;
}
sayAge() {
console.log(this.age);
}
// 重写父类方法
sayName() {
super.sayName(); // 调用父类方法
console.log(`I am ${this.age} years old`);
}
}
const child = new Child('Alice', 18);
child.sayName(); // "Alice" "I am 18 years old"
child.sayAge(); // 18
console.log(Child.isParent(child)); // true
本质:ES6 Class 是寄生组合式继承的语法糖
// Class 本质上仍然是原型链
console.log(typeof Child); // "function"
console.log(Child.prototype.__proto__ === Parent.prototype); // true
console.log(child.__proto__ === Child.prototype); // true
console.log(child.__proto__.__proto__ === Parent.prototype); // true
深入理解
继承方式对比
| 继承方式 | 优点 | 缺点 |
|---|---|---|
| 原型链继承 | 可以继承原型方法 | 引用类型共享,不能传参 |
| 构造函数继承 | 引用类型独立,可以传参 | 不能继承原型方法 |
| 组合继承 | 结合前两者优点 | 调用两次父类构造函数 |
| 原型式继承 | 简单,不需要构造函数 | 引用类型共享 |
| 寄生式继承 | 增强对象 | 引用类型共享,方法无法复用 |
| 寄生组合式继承 | 效率高,只调用一次父类构造函数 | 实现稍复杂 |
| ES6 Class | 语法清晰,易于理解 | 需要转译(旧浏览器) |
多重继承(Mixin)
// JavaScript 不支持多重继承,但可以通过 Mixin 实现
const Mixin1 = {
method1() {
console.log('method1');
}
};
const Mixin2 = {
method2() {
console.log('method2');
}
};
function mix(...mixins) {
return function(target) {
mixins.forEach(mixin => {
Object.assign(target.prototype, mixin);
});
};
}
@mix(Mixin1, Mixin2)
class MyClass {}
// 或使用函数方式
function applyMixins(targetClass, ...mixins) {
mixins.forEach(mixin => {
Object.getOwnPropertyNames(mixin).forEach(name => {
targetClass.prototype[name] = mixin[name];
});
});
}
class MyClass {}
applyMixins(MyClass, Mixin1, Mixin2);
const obj = new MyClass();
obj.method1(); // "method1"
obj.method2(); // "method2"
最佳实践
1. 现代项目推荐使用 ES6 Class
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
super.speak();
console.log(`${this.name} barks`);
}
}
2. 需要兼容旧浏览器时使用寄生组合式继承
function inherit(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inherit(Child, Parent);
3. 避免使用 __proto__
// ❌ 不推荐
obj.__proto__ = otherObj;
// ✅ 推荐
Object.setPrototypeOf(obj, otherObj);
// 或创建时指定
const obj = Object.create(otherObj);
面试要点
-
JavaScript 有哪些继承方式?
- 原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、ES6 Class 继承
-
组合继承的缺点是什么?
- 父类构造函数被调用两次
- 子类原型上有多余的父类实例属性
-
为什么寄生组合式继承是最佳方案?
- 只调用一次父类构造函数
- 避免了不必要的属性创建
- 原型链结构清晰
-
ES6 Class 继承的本质是什么?
- 是寄生组合式继承的语法糖
- 使用 extends 关键字建立原型链
- 使用 super 调用父类构造函数
-
super 的作用?
- 在构造函数中:调用父类构造函数
- 在方法中:调用父类原型上的方法
- 必须在子类构造函数中使用 this 之前调用 super()
-
如何判断一个对象是否是某个类的实例?
instanceof操作符Object.prototype.isPrototypeOf()constructor属性(不推荐,可能被修改)