ES5怎么实现继承
前言
继承这个概念在面向对象编程思想里面十分重要,也是面试必考的考点之一。
javascript的继承主要是依托其原型与原型链的概念来实现的。
ECMAscript将原型链作为实现继承的主要方法。
先来看看ES6的实现
ES6提供了Class关键字来实现类的定义,Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。
咱们重点讲一下ES5的四种常用的实现方式。
ES5实现的四种方式
1. 原型链继承
原型链继承的原理很简单,直接让子类的原型对象指向父类实例,当子类实例找不到对应的属性和方法时,就会往它的原型对象,也就是父类实例上找,从而实现对父类的属性和方法的继承
function Person() {
this.name = 'Back_kk';
}
Person.prototype.getName = function() {
return this.name;
}
function Student() {}
Student.prototype = new Person();
// 根据原型链的规则,顺便绑定一下constructor, 这一步不影响继承, 只是在用到constructor时会需要
// 原型的实例等于自身
Student.prototype.constructor = Student;
const student = new Student();
console.log(student.name); // Back_kk
console.log(student.getName()); // Back_kk
缺陷
- 由于所有Student实例原型都指向同一个Person实例, 因此对某个Student实例的来自父类的引用类型变量修改会影响所有的Student实例
例如:
function Person() {
this.obj = {
name: 'Back_kk',
age: 18
};
}
function Student() {}
Student.prototype = new Person();
// 根据原型链的规则,顺便绑定一下constructor, 这一步不影响继承, 只是在用到constructor时会需要
// 原型的实例等于自身
Student.prototype.constructor = Student;
const student1 = new Student();
student1.obj.name = '佩奇';
const student2 = new Student();
console.log(student2.obj.name); // 佩奇
- 在创建子类实例时无法向父类构造传参, 即没有实现super()的功能
那么能不能实现super()功能呢?大家有兴趣可以思考下。
2. 构造函数继承
构造函数继承,即在子类的构造函数中执行父类的构造函数,并为其绑定子类的this,让父类的构造函数把成员属性和方法都挂到子类的this上去,这样既能避免实例之间共享一个原型实例,又能向父类构造方法传参。
function Person(name) {
this.name = name
}
Person.prototype.getName = function() {
return this.name;
}
function Student() {
Person.apply(this, arguments);
}
const student = new Student('Back_kk');
console.log(student.name); // Back_kk
缺陷
继承不到父类原型上的属性和方法
Students类实际上是调用Person类来生成的实例
能否交加修改让其获取到Person原型上的属性和方法呢?
function Person(name) {
this.name = name
}
Person.prototype.getName = function() {
return this.name;
}
function Student() {
// 这里偷偷用了ES6的解构,不影响大局不要在意哈
return new Person(...arguments);
}
const student = new Student('Back_kk');
console.log(student); // Back_kk
这是这样顾此失彼,student的构造方法变成了Person,这显然违背了我们的初衷。
3. 组合式继承
组合是继承结合了原型集成和构造函数继承的特点。
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('Back_kk');
console.log(student.name); // Back_kk
console.log(student.getName()); // Back_kk
缺陷
- 每次创建子类实例都执行了两次构造函数(Person.apply和new Person()),虽然这并不影响对父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法,这并不优雅。
4. 寄生式组合继承
解决构造函数被执行两次的问题, 我们将指向父类实例改为指向父类原型, 减去一次构造函数的执行。
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('Back_kk');
console.log(student.name); // Back_kk
console.log(student.getName()); // Back_kk
这是目前ES5中比较成熟的继承方式了。
总结
- 说到js继承,最开始想到的应该是是原型链继承,通过把子类实例的原型指向父类实例来继承父类的属性和方法,但原型链继承的缺陷在于对子类实例继承的引用类型的修改会影响到所有的实例对象以及无法向父类的构造方法传参。
- 构造函数继承, 通过在子类构造函数中调用父类构造函数并传入子类this来获取父类的属性和方法,但构造函数继承也存在缺陷,构造函数继承不能继承到父类原型链上的属性和方法。
- 后面有了组合式继承,但也有了新的问题,每次都会执行两次父类的构造方法,最终有了寄生式组合式继承。