目录
引入
原型链继承
构造函数继承
组合式继承
寄生式组合继承
Class继承
JavaScript中的继承问题是一个很重要的知识点,很多面试都会问到。本文主要来详细地讲解JavaScript实现继承的不同方法。其中包括原型链继承、构造函数基础、组合式继承以及寄生组合式继承等ES5的实现方式,也会介绍ES6新出现的class继承。
原型链继承简单地将子类的原型对象指向父类实例,这样子类实例在无法找到对应的属性或方法时会继续向其原型对象,即父类实例上查找,从而实现对父类属性和方法的继承。
function Person() {
this.name = 'N-A';
}
Person.prototype.getName = function() {
return this.name;
}
function Student() {}
Student.prototype = new Person();
// 原型的实例等于自身
Student.prototype.constructor = Student;
const student = new Student();
console.log(student.name); // N-A
console.log(student.getName()); // N-A
但这种方法存在一些缺陷:
1.引用类型共享:由于所有子类实例的原型都指向同一个父类实例,当子类实例修改继承的引用类型属性时,会影响所有子类实例,因为它们共享相同的原型对象。
function Person() {
this.obj = {
name: 'N-A',
age: 5
};
}
function Student() {}
Student.prototype = new Person();
// 原型的实例等于自身
Student.prototype.constructor = Student;
const student1 = new Student();
student1.obj.name = 'CSDN';
const student2 = new Student();
console.log(student2.obj.name); // CSDN
2.无法传参:在创建子类实例时无法向父类构造函数传递参数,因此无法像使用 super()
一样实现在子类中调用父类构造函数的功能,限制了灵活性和定制性。
构造函数继承通过在子类构造函数中调用父类构造函数并使用子类的 this
,将父类的成员属性和方法直接挂载到子类实例上。这种方式避免了子类实例共享一个原型实例,同时也能够向父类构造函数传递参数。
function Person(name) {
this.name = name
}
Person.prototype.getName = function() {
return this.name;
}
function Student() {
Person.apply(this, arguments);
}
const student = new Student('N-A');
console.log(student.name); // N-A
然而,这种继承方式同样也存在缺陷:无法继承父类原型上的属性和方法,子类只能继承到父类构造函数中定义的属性和方法,而无法直接访问和继承父类原型上的属性和方法。这导致子类无法复用父类原型链上的方法,降低了代码的复用性和灵活性。
虽然构造函数继承解决了一些原型链继承的问题,但它的局限性在于无法继承父类原型上的属性和方法,限制了子类的功能扩展和代码复用。
组合继承结合了原型链继承和构造函数继承的优点,通过在子类构造函数中调用父类构造函数来实现对父类属性的继承,同时利用原型链继承来继承父类原型上的方法和属性。
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
function Student() {
// 构造函数继承
Person.apply(this, arguments)
}
// 原型式继承
Student.prototype = new Person();
// 原型的实例等于自身
Student.prototype.constructor = Student;
const student = new Student('N-A');
console.log(student.name); // N-A
console.log(student.getName()); // N-A
但是,这种继承方式也存在一些缺陷:重复调用构造函数:每次创建子类实例时,都会执行两次构造函数。一次是通过 Person.apply
将父类的属性赋予子类实例,另一次是使用 new Person()
创建父类实例,并将其作为子类原型的一部分。这不影响对父类属性的继承,但会导致子类原型中存在两份相同的属性和方法,造成资源浪费和内存占用。
尽管组合继承解决了原型链继承和构造函数继承各自的缺陷,但重复调用构造函数会导致资源浪费,使得子类原型上存在冗余的属性和方法,不够优雅。
寄生式组合继承是一种通过在子类构造函数中使用 Object.create
方法来优化组合继承的方式,解决了重复调用构造函数的问题。
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
function Student() {
// 构造函数继承
Person.apply(this, arguments)
}
// 原型式继承
// Student.prototype = new Person();
Student.prototype = Object.create(Person.prototype);
// 原型的实例等于自身
Student.prototype.constructor = Student;
const student = new Student('N-A');
console.log(student.name); // N-A
console.log(student.getName()); // N-A
它的基本原理是:
1. 使用 Object.create
创建一个临时的中间对象,这个对象的原型指向了父类的原型对象,但不直接执行父类的构造函数。
2. 将这个临时创建的对象赋值给子类的原型,这样子类的原型就可以继承自父类的原型,但避免了多余的构造函数调用。
这种继承方式在 ES5 中被认为是相对成熟且高效的继承方式,解决了组合继承中构造函数被执行两次的问题,保留了原型链继承和构造函数继承的优点,同时避免了它们的缺点。
ES6 引入了类的概念,可以使用 class
和 extends
关键字来实现类的继承。
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, my name is ${this.name}.`;
}
}
class Student extends Person {
constructor(name, level) {
super(name); // 调用父类构造函数来初始化父类的属性
this.level = level;
}
study() {
return `${this.name} studies at level ${this.level}.`;
}
}
// 创建一个 Student 实例
const student = new Student('N-A', 'Senior');
console.log(student.greet()); // 输出:Hello, my name is N-A.
console.log(student.study()); // 输出:N-A studies at level Senior.
在这个示例中:
Person
类有一个 name
属性和一个 greet
方法。Student
类通过 extends
关键字继承了 Person
类,并在构造函数中使用 super()
调用父类的构造函数初始化父类的属性。
Student
类还有一个 study
方法。创建 Student
类的实例,并调用继承自父类和自身定义的方法。
类继承使得 JavaScript 中的继承更加直观和易用,同时也更符合传统面向对象编程的习惯。通过 extends
关键字,子类可以轻松地继承父类的属性和方法,并且可以在子类中添加自己的属性和方法。使用 super()
可以方便地调用父类构造函数,初始化父类的属性。
好啦,本文就到这里啦!