理解 JavaScript 的对象继承方法
tips:博主不定期分享前端知识与算法。
公众号:FE Corner
wx小程序:FE Corner
在 JavaScript 中,对象继承是一个核心概念,它允许对象从其他对象继承属性和方法,从而实现代码的重用和组织。在这篇文章中,我们将详细探讨几种常见的对象继承方式,帮助你在实际开发中选择最合适的继承方法。
1. 原型链继承
原型链继承是 JavaScript 中最基本的继承方式。通过将子对象的 prototype 赋值为父对象的实例,子对象可以继承父对象的属性和方法。
function Parent() {
this.name = 'Parent';
}
Parent.prototype.greet = function() {
console.log('Hello from ' + this.name);
};
function Child() {
this.name = 'Child';
}
Child.prototype = new Parent();
const child = new Child();
child.greet(); // 输出 "Hello from Child"
- 优点:
实现简单,适合属性和方法较少的对象。 - 缺点:
不能传参给父构造函数。
所有子实例共享父对象的引用类型属性,可能导致数据污染。
2. 构造函数继承
构造函数继承通过在子对象的构造函数中调用父对象的构造函数,实现属性的继承。这种方式允许我们在创建子对象时传递参数给父构造函数。
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name); // 继承父类属性
this.age = age;
}
const child = new Child('Child', 10);
console.log(child.name); // 输出 "Child"
console.log(child.age); // 输出 "10"
- 优点:
支持向父对象传递参数。
避免了共享引用类型属性的问题。 - 缺点:
不能继承父对象的原型方法。
3. 组合继承
组合继承结合了原型链继承和构造函数继承的优点。它通过原型链继承父对象的原型方法,并通过构造函数继承父对象的属性。
function Parent(name) {
this.name = name;
}
Parent.prototype.greet = function() {
console.log('Hello from ' + this.name);
};
function Child(name, age) {
Parent.call(this, name); // 继承属性
this.age = age;
}
Child.prototype = new Parent(); // 继承方法
Child.prototype.constructor = Child;
const child = new Child('Child', 10);
child.greet(); // 输出 "Hello from Child"
console.log(child.age); // 输出 "10"
- 优点:
结合了两种继承方式的优势。 - 缺点:
父构造函数会被调用两次,可能导致性能问题。
4. 原型式继承
原型式继承基于一个现有对象创建一个新对象,将其作为新对象的原型。这种方法适合轻量级的对象创建场景。
const parent = {
name: 'Parent',
greet: function() {
console.log('Hello from ' + this.name);
}
};
const child = Object.create(parent);
child.name = 'Child';
child.greet(); // 输出 "Hello from Child"
优点:
简单易用,适合轻量级对象。
缺点:
共享引用类型的属性可能引发问题。
5. 寄生式继承
寄生式继承在原型式继承的基础上,进一步增强了新对象。它允许你在继承的基础上添加额外的方法和属性。
function createChild(original) {
const clone = Object.create(original);
clone.greet = function() {
console.log('Hello from ' + this.name);
};
return clone;
}
const parent = { name: 'Parent' };
const child = createChild(parent);
child.name = 'Child';
child.greet(); // 输出 "Hello from Child"
- 优点:
可以在继承的过程中增强新对象。 - 缺点:
增加了一层函数调用,可能影响性能。
6. 寄生组合式继承
寄生组合式继承被认为是最理想的继承方式。它结合了寄生式继承和组合继承的优点,并避免了组合继承中多次调用父构造函数的问题。
function Parent(name) {
this.name = name;
}
Parent.prototype.greet = function() {
console.log('Hello from ' + this.name);
};
function Child(name, age) {
Parent.call(this, name); // 继承属性
this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const child = new Child('Child', 10);
child.greet(); // 输出 "Hello from Child"
console.log(child.age); // 输出 "10"
- 优点:
避免了组合继承中的缺点,并且性能优越。 - 缺点:
实现稍微复杂,但值得使用。
7.extends 关键字
在 ES6 中,extends 关键字引入了一种更加简洁和直观的方式来实现类的继承。extends 允许一个类继承另一个类的所有属性和方法,从而大大简化了 JavaScript 中的继承操作。(底层其实就是基于原型的寄生组合式继承)
- ES6 类的基本概念
在 ES6 中,class 关键字用于定义类,extends 关键字用于继承其他类。类语法是 JavaScript 构造函数和原型的语法糖,使得面向对象编程更加直观。
class Parent {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello from ${this.name}`);
}
}
在这个例子中,我们定义了一个 Parent 类,constructor 是类的构造函数,greet 是一个实例方法。
使用 extends 实现继承
通过 extends 关键字,我们可以轻松地让一个类继承另一个类。
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类的构造函数
this.age = age;
}
sayAge() {
console.log(`I am ${this.age} years old`);
}
}
const child = new Child('Child', 10);
child.greet(); // 输出 "Hello from Child"
child.sayAge(); // 输出 "I am 10 years old"
在这个例子中:
Child 类通过 extends 继承了 Parent 类。
super(name) 调用了父类 Parent 的构造函数,并将参数 name 传递给父类。这一步非常关键,因为它确保了父类的属性在子类中被正确初始化。
Child 类还定义了一个新的方法 sayAge,这是 Parent 类中没有的。
super 关键字的作用
- 在子类中,super 关键字可以用来:
调用父类的构造函数。
访问父类的实例方法或静态方法。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // 调用父类的构造函数
}
speak() {
super.speak(); // 调用父类的方法
console.log(`${this.name} barks.`);
}
}
const dog = new Dog('Rex');
dog.speak();
// 输出:
// Rex makes a noise.
// Rex barks.
- 在上面的例子中:
super(name) 调用了 Animal 类的构造函数,确保 Dog 实例具有 name 属性。
super.speak() 调用了父类的 speak 方法,并在其基础上添加了额外的功能。
静态方法的继承
extends 也可以继承父类的静态方法。静态方法是在类本身上定义的方法,而不是在实例上。
class Parent {
static greet() {
console.log('Hello from Parent');
}
}
class Child extends Parent {}
Child.greet(); // 输出 "Hello from Parent"
在这里,Child 类继承了 Parent 类的静态方法 greet。
- 为什么选择 extends?
语法简洁:extends 提供了一种更易读、更易写的继承方式,与传统的构造函数和原型继承相比,代码更为简洁直观。
功能强大:通过 super 关键字,可以轻松地在子类中调用父类的构造函数和方法,这让继承关系更为明确和灵活。
原生支持:extends 是 ES6 标准的一部分,原生支持类继承,减少了手动设置原型链的复杂性。
小结
- 原型链继承:简单直接,但共享引用类型会有问题。
- 构造函数继承:能传参,但无法继承父类原型方法。
- 组合继承:兼顾两者优点,但父构造函数被调用两次。
- 原型式继承:适合简单对象继承,但也有共享引用类型问题。
- 寄生式继承:增强版的原型式继承,灵活但性能稍差。
- 寄生组合式继承:最优继承方式,避免了组合继承的缺点。
- ES6中 extends关键字:简化了类的继承过程,还通过 super 关键字提供了对父类构造函数和方法的方便访问。
JavaScript 提供了多种对象继承方法,每种方法都有其特定的适用场景和优缺点。选择适合需求的继承方式,可以让我们的代码更加简洁、可维护,并且性能良好。在实际开发中,寄生组合式继承被广泛认为是最理想的继承方式,但根据具体场景,其他方法也可以为我们提供更简单或更直接的解决方案。