js实现继承常见的方式
- 原型链继承
- 借用构造函数继承
- 组合式继承(原型链与借用构造函数继承的组合)
- 寄生组合式继承
- es6的类继承
接下来分别介绍这6种继承方式各自的优缺点
1、原型链继承
function Parent() {
this.test = [1];
}
function Son() {
}
Son.prototype = new Parent()
let son1 = new Son();
let son2 = new Son();
son1.test.push(2);
console.log(son1.test); // [ 1, 2 ]
console.log(son2.test); // [ 1, 2 ]
- 优点:
- 可继承的属性有: 实例构造函数的属性,父类构造函数的属性,父类原型的属性
- 缺点:
- 从以上可以看出所有实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例改变原型上的属性,另一个实例获取对应的属性也会跟着变)
- 不能像父类构造函数传参。
2、借用构造函数
function Parent(name) {
this.name = '张三';
this.test = [1];
}
Parent.prototype.demo = function () {
console.log('demo');
}
function Son(name, age) {
Parent.call(this, name); // 可向父类构造函数传参
this.age = age;
}
let son1 = new Son();
let son2 = new Son();
son1.test.push(2);
son1.demo(); // 报错
console.log(son1.test); // [ 1, 2 ]
console.log(son2.test); // [ 1 ]
- 优点:
- 可以继承多个构造函数属性(call多个)
- 实例不共享父类构造函数属性,是私有的
- 可向父类构造函数传参
- 缺点:
- 只能继承父类构造函数的属性,不能继承其原型上的属性
- 每个新实例都有父类构造函数的副本,臃肿
3、组合式继承(组合原型链继承和借用构造函数继承)(常用)
function Parent(name) {
this.name = name;
this.test = [1];
}
Parent.prototype.demo = function () {
console.log('demo');
}
function Son(name, age) {
Parent.call(this, name); // 第二次调用父级构造函数
this.age = age;
}
Son.prototype = new Parent(); // 创建父类的实例并赋值给子类的原型 (第一次调用父级构造函数)
let son1 = new Son();
let son2 = new Son();
son1.test.push(2);
son1.demo(); // 可访问原型上的属性
console.log(son1.test); // [ 1, 2 ]
console.log(son2.test); // [ 1 ]
- 优点:
- 组合了原型链和借用构造函数继承的优点
- 缺点:
- 调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
4、寄生组合式继承(通过Object.create方法,解决组合继承调用两次父级构造函数的问题)
// parent:父类构造函数 son: 子类构造函数
function inherit(parent, son) {
// 创建父类原型副本
const pro = Object.create(parent.prototype);
// 防止重写原型造成失去默认的constructor
pro.constructor = son;
// 将之前创建的副本赋值给子类原型
son.prototype = pro;
}
function Parent(name) {
this.name = name;
this.test = [1];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Son(name, age) {
Parent.call(this, name);
this.age = age;
}
inherit(Parent, Son)
// 给子类原型增加方法,这一步得放在 inherit 函数调用之后,不然会丢失,因为inherit方法内给Son的原型重新赋值
Son.prototype.getAge = function () {
console.log(this.age);
}
let son1 = new Son('张三', 23);
let son2 = new Son();
son1.test.push(2);
son1.getName(); // 继承父累原型属性
son1.getAge();
console.log(son1.test); // [ 1, 2 ]
console.log(son2.test); // [ 1 ]
- 优点:
- 组合寄生式继承,只调用了一次父类的构造函数,避免了在子类型原型对象prototype上创建不必要的、多余的属性,同时保持原型链不变,是引用类型最理想的继承范式。
5、es6类继承(使用extends关键字来实现)
class Parent{
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Son extends Parent{
constructor(name, age) {
super(name);
this.age = age;
}
}
const son = new Son('张三', 22);
const son1 = new Son('李四', 23);
console.log(son.getName()) // 张三
console.log(son1.getName()) // 李四
es6的class类继承,子类的constructor方法必须要执行super方法,具体原因请跳转查看es6的constructor方法和super关键字
总结:ES5与ES6继承的区别
- es5实际上是先创造子类的实例对象,然后再往是对象上增加父类的属性和方法(即实例在前,继承在后),如Parent.call(this,name)。
- es6的class类继承实际上是必须先实例化父类的实例,然后子类去继承父类的实例并对其进行加工改造(即继承在前,实例在后)。所以说在子类的constructor中必须要先调用super方法之后才能使用this,如上代码,如果把
this.age = age
提升到super方法前面是会报错的,应为这是还没有调用super方法,也就不存在this。 - es5中,每一个实例对象都有一个__proto__属性指向其对应的构造函数的prototype。
- es6的class类作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
- 子类的__proto__属性,表示构造函数的继承,总是指向父类。
- 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。