JavaScript 继承面试题
1. 请解释 JavaScript 中的原型继承
Details
原型继承是 JavaScript 中实现继承的主要方式,它基于原型链机制。
原型继承的基本概念
在 JavaScript 中,每个对象都有一个原型(prototype),当访问对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到为止。
原型继承的实现
1. 使用构造函数和原型
javascript
// 父类构造函数
function Animal(name) {
this.name = name;
this.eat = function() {
console.log(`${this.name} is eating`);
};
}
// 父类原型方法
Animal.prototype.sleep = function() {
console.log(`${this.name} is sleeping`);
};
// 子类构造函数
function Dog(name, breed) {
// 调用父类构造函数
Animal.call(this, name);
this.breed = breed;
}
// 建立原型链
Dog.prototype = Object.create(Animal.prototype);
// 修复构造函数指向
Dog.prototype.constructor = Dog;
// 子类原型方法
Dog.prototype.bark = function() {
console.log(`${this.name} is barking`);
};
// 使用
const dog = new Dog("Buddy", "Golden Retriever");
dog.eat(); // Buddy is eating
dog.sleep(); // Buddy is sleeping
dog.bark(); // Buddy is barking2. 使用 ES6 类语法
ES6 引入了类语法,使继承更加简洁明了。
javascript
// 父类
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
sleep() {
console.log(`${this.name} is sleeping`);
}
}
// 子类
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
bark() {
console.log(`${this.name} is barking`);
}
}
// 使用
const dog = new Dog("Buddy", "Golden Retriever");
dog.eat(); // Buddy is eating
dog.sleep(); // Buddy is sleeping
dog.bark(); // Buddy is barking原型继承的特点
- 基于原型链:通过原型链实现属性和方法的继承。
- 共享原型方法:子类实例共享父类原型上的方法,节省内存。
- 动态性:可以在运行时修改原型,影响所有实例。
原型继承的优缺点
优点
- 灵活性:可以动态修改原型,实现方法的共享。
- 内存效率:原型上的方法被所有实例共享,节省内存。
- 简洁性:通过原型链实现继承,代码简洁。
缺点
- 复杂性:原型链的查找机制可能导致性能问题。
- 容易混淆:原型继承的概念对于初学者来说可能难以理解。
- 构造函数参数传递:在使用构造函数继承时,需要手动调用父类构造函数。
注意事项
- 原型链的长度:过长的原型链会影响属性查找性能。
- 原型方法的修改:修改原型会影响所有实例,需要谨慎操作。
- 构造函数的指向:使用
Object.create()后需要修复构造函数的指向。
2. 请解释 JavaScript 中的 ES6 类继承
Details
ES6 引入了类语法,使继承更加简洁明了,它是原型继承的语法糖。
ES6 类继承的基本语法
javascript
// 父类
class Parent {
constructor(name) {
this.name = name;
}
method() {
console.log(`Parent method called`);
}
}
// 子类
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
method() {
super.method(); // 调用父类方法
console.log(`Child method called`);
}
}ES6 类继承的特点
- 使用 extends 关键字:明确指定父类。
- 使用 super 关键字:调用父类的构造函数和方法。
- 简洁的语法:相比传统的原型继承,语法更加清晰。
- 支持静态方法继承:静态方法会被自动继承。
ES6 类继承的实现原理
ES6 类继承本质上是原型继承的语法糖,它在内部仍然使用原型链来实现继承。
javascript
// ES6 类继承的底层实现
function Parent(name) {
this.name = name;
}
Parent.prototype.method = function() {
console.log(`Parent method called`);
};
function Child(name, age) {
Parent.call(this, name); // 调用父类构造函数
this.age = age;
}
// 建立原型链
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.method = function() {
Parent.prototype.method.call(this); // 调用父类方法
console.log(`Child method called`);
};ES6 类继承的注意事项
必须调用 super():在子类构造函数中,必须先调用 super(),然后才能使用 this。
javascriptclass Child extends Parent { constructor(name, age) { // 错误:在调用 super() 之前使用 this // this.age = age; super(name); this.age = age; // 正确:在调用 super() 之后使用 this } }super 的使用:
- 在构造函数中,super() 调用父类构造函数。
- 在方法中,super.method() 调用父类的方法。
静态方法继承:
javascriptclass Parent { static staticMethod() { console.log('Static method'); } } class Child extends Parent { } Child.staticMethod(); // 输出: Static methodgetter 和 setter 继承:
javascriptclass Parent { get name() { return this._name; } set name(value) { this._name = value; } } class Child extends Parent { } const child = new Child(); child.name = 'John'; console.log(child.name); // 输出: John
ES6 类继承与传统原型继承的对比
| 特性 | ES6 类继承 | 传统原型继承 |
|---|---|---|
| 语法 | 简洁明了 | 较为复杂 |
| 构造函数调用 | 使用 super() | 使用 Parent.call(this) |
| 方法调用 | 使用 super.method() | 使用 Parent.prototype.method.call(this) |
| 静态方法继承 | 自动继承 | 需要手动设置 |
| 可读性 | 高 | 低 |
| 浏览器支持 | 现代浏览器 | 所有浏览器 |
示例:ES6 类继承的完整实现
javascript
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
sleep() {
console.log(`${this.name} is sleeping`);
}
static create(name) {
return new Animal(name);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log(`${this.name} is barking`);
}
// 重写父类方法
sleep() {
super.sleep();
console.log(`${this.name} (${this.breed) is sleeping soundly`);
}
// 静态方法
static create(name, breed) {
return new Dog(name, breed);
}
}
// 使用
const animal = Animal.create('Generic Animal');
animal.eat(); // Generic Animal is eating
const dog = Dog.create('Buddy', 'Golden Retriever');
dog.eat(); // Buddy is eating
dog.bark(); // Buddy is barking
dog.sleep(); // Buddy is sleeping
// Buddy (Golden Retriever) is sleeping soundly3. 请解释 JavaScript 中的组合继承
Details
组合继承是 JavaScript 中一种常用的继承模式,它结合了构造函数继承和原型继承的优点。
组合继承的基本概念
组合继承通过以下两个步骤实现:
- 使用构造函数继承(通过
call()或apply())继承实例属性。 - 使用原型继承(通过设置原型链)继承原型方法。
组合继承的实现
javascript
// 父类构造函数
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
// 父类原型方法
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类构造函数
function Child(name, age) {
// 继承父类实例属性
Parent.call(this, name);
this.age = age;
}
// 继承父类原型方法
Child.prototype = new Parent();
// 修复构造函数指向
Child.prototype.constructor = Child;
// 子类原型方法
Child.prototype.sayAge = function() {
console.log(this.age);
};
// 使用
const child1 = new Child('John', 10);
child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
child1.sayName(); // John
child1.sayAge(); // 10
const child2 = new Child('Jane', 12);
console.log(child2.colors); // ['red', 'blue', 'green'](不受 child1 的影响)
child2.sayName(); // Jane
child2.sayAge(); // 12组合继承的优缺点
优点
- 继承实例属性:通过构造函数继承,每个实例都有自己的实例属性副本,避免了引用类型属性的共享问题。
- 继承原型方法:通过原型继承,所有实例共享原型上的方法,节省内存。
- 方法重写:子类可以重写父类的方法,实现多态。
缺点
- 父类构造函数被调用两次:一次在创建子类原型时,一次在子类构造函数中。
- 原型上的实例属性:父类构造函数在创建子类原型时会在原型上创建实例属性,这些属性会被实例属性覆盖。
组合继承的优化
为了避免父类构造函数被调用两次,可以使用 Object.create() 来创建子类原型。
javascript
// 父类构造函数
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
// 父类原型方法
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类构造函数
function Child(name, age) {
// 继承父类实例属性
Parent.call(this, name);
this.age = age;
}
// 优化:使用 Object.create() 创建子类原型
Child.prototype = Object.create(Parent.prototype);
// 修复构造函数指向
Child.prototype.constructor = Child;
// 子类原型方法
Child.prototype.sayAge = function() {
console.log(this.age);
};组合继承与 ES6 类继承的关系
ES6 类继承本质上是组合继承的语法糖,它在内部使用了类似的机制:
- 使用
super()调用父类构造函数,继承实例属性。 - 使用原型链继承父类的方法。
javascript
// ES6 类继承
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 相当于 Parent.call(this, name)
this.age = age;
}
sayAge() {
console.log(this.age);
}
}组合继承的应用场景
组合继承适用于需要继承实例属性和原型方法的场景,特别是当父类有引用类型的实例属性时,组合继承可以确保每个实例都有自己的属性副本。
示例:组合继承的完整实现
javascript
// 父类:Person
function Person(name, age) {
this.name = name;
this.age = age;
this.friends = [];
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
// 子类:Student
function Student(name, age, grade) {
// 继承父类实例属性
Person.call(this, name, age);
this.grade = grade;
}
// 继承父类原型方法
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
// 子类原型方法
Student.prototype.study = function() {
console.log(`${this.name} is studying in grade ${this.grade}`);
};
// 重写父类方法
Student.prototype.greet = function() {
console.log(`Hello, my name is ${this.name} and I'm in grade ${this.grade}`);
};
// 使用
const student1 = new Student('John', 15, 10);
student1.friends.push('Jane');
student1.greet(); // Hello, my name is John and I'm in grade 10
student1.study(); // John is studying in grade 10
console.log(student1.friends); // ['Jane']
const student2 = new Student('Bob', 16, 11);
student2.friends.push('Alice');
student2.greet(); // Hello, my name is Bob and I'm in grade 11
student2.study(); // Bob is studying in grade 11
console.log(student2.friends); // ['Alice'](不受 student1 的影响)