说说你对原型链继承和类继承的理解?
问题解析
JavaScript 中的继承经历了从原型链继承到 ES6 Class 继承的演变。理解这两种继承方式的区别和联系,对于掌握 JavaScript 的面向对象编程至关重要。ES6 Class 本质上是原型链继承的语法糖,但提供了更清晰、更易于理解的语法。
核心概念
原型链继承
原型链继承是 JavaScript 传统的继承方式,基于构造函数和原型对象实现。每个构造函数都有一个 prototype 属性,实例通过 __proto__ 链接到原型对象,形成原型链。
类继承(ES6 Class)
ES6 引入的 Class 语法提供了一种更简洁、更接近传统面向对象语言的继承写法,但本质上仍然是基于原型链的。
详细解答
一、原型链继承详解
1. 基本实现
// 父类构造函数
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
// 父类原型方法
Animal.prototype.sayName = function() {
console.log(`My name is ${this.name}`);
};
// 子类构造函数
function Dog(name, breed) {
// 借用构造函数继承实例属性
Animal.call(this, name);
this.breed = breed;
}
// 建立原型链继承(关键步骤)
Dog.prototype = Object.create(Animal.prototype);
// 修正 constructor 指向
Dog.prototype.constructor = Dog;
// 子类原型方法
Dog.prototype.bark = function() {
console.log('Woof! Woof!');
};
// 使用
const dog = new Dog('Buddy', 'Golden Retriever');
dog.sayName(); // "My name is Buddy"
dog.bark(); // "Woof! Woof!"
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
2. 原型链继承的演进
// 方式1:原型链继承(不推荐,引用类型共享)
function Sub1() {}
Sub1.prototype = new Parent();
// 方式2:借用构造函数(不推荐,无法继承原型方法)
function Sub2() {
Parent.call(this);
}
// 方式3:组合继承(推荐,但调用两次父类构造函数)
function Sub3() {
Parent.call(this); // 第二次调用
}
Sub3.prototype = new Parent(); // 第一次调用
// 方式4:寄生组合式继承(最佳实践)
function inherit(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}
function Sub4() {
Parent.call(this);
}
inherit(Sub4, Parent);
二、类继承详解(ES6)
1. 基本语法
// 父类
class Animal {
// 构造函数
constructor(name) {
this.name = name;
this.colors = ['black', 'white'];
}
// 实例方法
sayName() {
console.log(`My name is ${this.name}`);
}
// 静态方法
static isAnimal(obj) {
return obj instanceof Animal;
}
}
// 子类
class Dog extends Animal {
constructor(name, breed) {
// 调用父类构造函数,必须在使用 this 之前
super(name);
this.breed = breed;
}
// 子类方法
bark() {
console.log('Woof! Woof!');
}
// 重写父类方法
sayName() {
// 调用父类方法
super.sayName();
console.log(`I am a ${this.breed}`);
}
// 子类静态方法
static isDog(obj) {
return obj instanceof Dog;
}
}
// 使用
const dog = new Dog('Buddy', 'Golden Retriever');
dog.sayName();
// "My name is Buddy"
// "I am a Golden Retriever"
dog.bark(); // "Woof! Woof!"
console.log(Dog.isDog(dog)); // true
console.log(Animal.isAnimal(dog)); // true
2. 类的 getter 和 setter
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// getter
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
// setter
set fullName(name) {
[this.firstName, this.lastName] = name.split(' ');
}
}
const person = new Person('John', 'Doe');
console.log(person.fullName); // "John Doe"
person.fullName = 'Jane Smith';
console.log(person.firstName); // "Jane"
console.log(person.lastName); // "Smith"
3. 类的私有字段和方法
class BankAccount {
// 私有字段(ES2022)
#balance = 0;
constructor(initialBalance) {
this.#balance = initialBalance;
}
// 私有方法
#validateAmount(amount) {
return amount > 0;
}
deposit(amount) {
if (this.#validateAmount(amount)) {
this.#balance += amount;
return true;
}
return false;
}
withdraw(amount) {
if (this.#validateAmount(amount) && amount <= this.#balance) {
this.#balance -= amount;
return true;
}
return false;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// 无法访问私有成员
console.log(account.#balance); // SyntaxError
深入理解
原型链继承 vs 类继承对比
// ========== 原型链继承 ==========
function Animal1(name) {
this.name = name;
}
Animal1.prototype.speak = function() {
console.log(`${this.name} makes a sound`);
};
function Dog1(name, breed) {
Animal1.call(this, name);
this.breed = breed;
}
Dog1.prototype = Object.create(Animal1.prototype);
Dog1.prototype.constructor = Dog1;
Dog1.prototype.bark = function() {
console.log(`${this.name} barks`);
};
// ========== 类继承 ==========
class Animal2 {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog2 extends Animal2 {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log(`${this.name} barks`);
}
}
// ========== 本质相同 ==========
const dog1 = new Dog1('Buddy', 'Golden');
const dog2 = new Dog2('Buddy', 'Golden');
console.log(dog1.__proto__ === Dog1.prototype); // true
console.log(dog2.__proto__ === Dog2.prototype); // true
console.log(Dog1.prototype.__proto__ === Animal1.prototype); // true
console.log(Dog2.prototype.__proto__ === Animal2.prototype); // true
ES6 Class 的本质
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
static createAnonymous() {
return new Person('Anonymous');
}
}
// 本质上等同于:
function PersonES5(name) {
this.name = name;
}
PersonES5.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
PersonES5.createAnonymous = function() {
return new PersonES5('Anonymous');
};
// 验证
console.log(typeof Person); // "function"
console.log(Person.prototype); // {constructor: ƒ, sayHello: ƒ}
console.log(Person === Person.prototype.constructor); // true
继承中的 super
class Parent {
constructor(value) {
this.value = value;
}
static staticMethod() {
return 'Parent static';
}
instanceMethod() {
return 'Parent instance';
}
}
class Child extends Parent {
constructor(value, extra) {
// super 作为函数调用:调用父类构造函数
super(value);
this.extra = extra;
}
instanceMethod() {
// super 作为对象使用:访问父类原型
return `${super.instanceMethod()} + Child`;
}
static staticMethod() {
// super 访问父类静态方法
return `${super.staticMethod()} + Child`;
}
}
const child = new Child('value', 'extra');
console.log(child.instanceMethod()); // "Parent instance + Child"
console.log(Child.staticMethod()); // "Parent static + Child"
最佳实践
1. 优先使用 ES6 Class
// ✅ 推荐:使用 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. 正确处理 constructor
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
// ✅ 必须先调用 super()
super(name);
// ❌ 在 super() 之前使用 this 会报错
this.age = age;
}
}
// 如果子类不需要额外构造逻辑,可以省略 constructor
class SimpleChild extends Parent {
// 自动继承父类 constructor
}
3. 使用私有字段保护数据
class Counter {
#count = 0; // 私有字段
increment() {
this.#count++;
return this;
}
decrement() {
this.#count--;
return this;
}
getCount() {
return this.#count;
}
}
// 方法链式调用
const counter = new Counter();
counter.increment().increment().decrement();
console.log(counter.getCount()); // 1
4. 抽象基类模式
// 抽象基类(不能被直接实例化)
class Animal {
constructor(name) {
if (new.target === Animal) {
throw new Error('Animal 是抽象类,不能直接实例化');
}
this.name = name;
}
// 抽象方法(子类必须实现)
speak() {
throw new Error('子类必须实现 speak 方法');
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} says Woof!`);
}
}
// const animal = new Animal('Test'); // Error
const dog = new Dog('Buddy');
dog.speak(); // "Buddy says Woof!"
面试要点
-
原型链继承和 Class 继承的区别?
- 语法层面:Class 更清晰、更接近传统面向对象语言
- 本质层面:Class 是原型链继承的语法糖,底层实现相同
- 功能层面:Class 提供了 super、static、私有字段等特性
-
ES6 Class 的本质是什么?
- Class 本质上还是函数
- Class 的 prototype 属性与构造函数相同
- extends 本质上是建立原型链
-
super 的作用?
- 在 constructor 中:调用父类构造函数
- 在实例方法中:访问父类原型上的方法
- 在静态方法中:访问父类的静态方法
- 必须在子类 constructor 中使用 this 之前调用 super()
-
如何实现多重继承?
- JavaScript 不支持多重继承
- 可以通过 Mixin 模式实现类似效果
- 使用组合优于继承
-
Class 中的私有字段是如何实现的?
- ES2022 引入
#前缀语法 - 私有字段只能在类内部访问
- 是语言层面的私有,不是约定
- ES2022 引入
-
什么时候使用原型链继承而不是 Class?
- 需要兼容非常旧的浏览器时
- 需要更灵活地操作原型时
- 大多数情况下推荐使用 Class