深入理解 JS 继承
一、继承分类
如图所示,JS 中继承可以按照是否使用 object 函数,将继承分成两部分。
二、继承方式
1、原型链继承
Child.prototype = new Parent()
将父类的实例作为子类的原型。
优点:父类方法可以被子类实例复用。
缺点:
- 父类的引用属性会被所有子类实例共享。
- 子类构建实例时不能向父类传递参数。
示例:
// 父类
function City() {
this.city = 'Guangzhou'
this.alias = ['Sui', 'Huacheng']
}
// 子类
function District(name) {
this.name = name
}
// 继承
District.prototype = new City()
// 使用
var district1 = new District('Haizhu')
console.log(district1) // {name: 'Haizhu'}
console.log(district1.name) // Haizhu
console.log(district1.city) // Guangzhou
var district2 = new District('Tianhe')
console.log(district2) // {name: 'Tianhe'}
console.log(district2.city) // Guangzhou
// 缺点1:父类属性如果是引用类型则该属性会被子类实例共享
district1.city = 'GZC'
console.log(district1.city) // GZC
console.log(district2.city) // Guangzhou
district1.alias.push('Yangcheng')
console.log(district1.alias) // ['Sui', 'Huacheng', 'Yangcheng']
console.log(district2.alias) // ['Sui', 'Huacheng', 'Yangcheng']
console.log(City)
// ƒ City() { this.city = 'Guangzhou', this.alias = ['Sui', 'Huacheng'] }
原型链继承的优点体现在父类方法可以被子类复用,例如:
// 父类
function City() {
this.city = 'Guangzhou'
}
// 父类函数
City.prototype.sayNum = function () { console.log('020') }
// 子类
function District(name) {
this.name = name
}
// 继承
District.prototype = new City()
// 使用
var district1 = new District('Haizhu')
console.log(district1.sayNum) // ƒ () { console.log('020') }
子类实例 district1
的打印结果:
从打印结果可以看出,父类的函数 sayNum
会被子类实例复用。
2、构造函数继承
Parent.call(this)
将父类构造函数的内容复制给子类的构造函数,这是所有继承中唯一不涉及 prototype 的继承,优劣势与原型链继承相反。
优点:
- 父类的引用属性不会被共享。
- 子类构建实例时可以向父类传递参数。
缺点:父类的方法不能被子类实例复用。
示例:
// 父类
function City() {
this.city = 'Guangzhou'
this.alias = ['Sui', 'Huacheng']
}
// 子类 & 继承
function District(name) {
this.name = name
City.call(this)
}
// 使用
var district1 = new District('Haizhu')
console.log(district1) // {name: 'Haizhu', city: 'Guangzhou', alias: ['Sui', 'Huacheng']}
console.log(district1.name) // Haizhu
console.log(district1.city) // Guangzhou
var district2 = new District('Tianhe')
console.log(district2) // {name: 'Tianhe', city: 'Guangzhou', alias: ['Sui', 'Huacheng']}
console.log(district2.city) // Guangzhou
// 优点1:父类属性在子类实例中不共享
district1.city = 'GZC'
console.log(district1.city) // GZC
console.log(district2.city) // Guangzhou
district1.alias.push('Yangcheng')
console.log(district1.alias) // ['Sui', 'Huacheng', 'Yangcheng']
console.log(district2.alias) // ['Sui', 'Huacheng']
console.log(City)
// ƒ City() { this.city = 'Guangzhou', this.alias = ['Sui', 'Huacheng'] }
构造函数继承
跟原型链继承最明显的区别在于构造函数继承
没有使用 prototype 继承,而是在子类里面执行父类的构造函数,相当于把父类的代码复制到子类里面执行一遍,这样做的另一个优点就是可以给父类传参,例如:
// 父类
function City(district) {
this.city = 'Guangzhou'
this.district = district
}
// 子类 & 继承
function District(name) {
City.call(this, name)
}
// 使用
var district1 = new District('Haizhu')
console.log(district1) // {city: 'Guangzhou', district: 'Haizhu'}
console.log(district1.name) // undefined
console.log(district1.district) // Haizhu
构造函数的缺点体现在父类的方法不能被子类复用,例如:
// 父类
function City() {
this.city = 'Guangzhou'
sayNum1 = () => { console.log('440100') }
}
// 父类函数
City.prototype.sayNum2 = function () { console.log('020') }
// 子类 & 继承
function District(name) {
this.name = name
City.call(this)
}
// 使用
var district1 = new District('Haizhu')
console.log(district1.sayNum1) // undefined
console.log(district1.sayNum2) // undefined
子类实例 district1
的打印结果:
从打印结果可以看出,父类的函数 sayNum
不会被复用。
3、组合继承
Parent.call(this)
Child.prototype = new Parent()
原型式继承和构造函数继承的组合,要独享的属性使用构造函数继承,要共享的属性使用原型链继承,兼具了二者的优点。
优点:
- 父类的方法可以被复用。
- 父类的引用属性不会被共享。
- 子类构建实例时可以向父类传递参数。
缺点:调用了两次父类的构造函数,第一次给子类的原型添加了父类的属性,第二次又给子类的构造函数添加了父类的属性,从而覆盖了子类原型中的同名参数,这种被覆盖的情况造成了性能上的浪费。
示例:
// 父类
function City(district) {
this.city = 'Guangzhou'
this.alias = ['Sui', 'Huacheng']
this.district = district
}
// 父类函数
City.prototype.sayNum = function () { console.log('020') }
// 子类 & 构造函数继承
function District(name) {
City.call(this, name)
}
// 原型链继承
District.prototype = new City()
// 使用
var district1 = new District('Haizhu')
var district2 = new District('Tianhe')
// 优点1:父类的方法可以被复用
console.log(district1.sayNum) // ƒ () { console.log('020') }
// 优点2:父类的引用属性不会被共享(实现引用属性独享)
district1.city = 'GZC'
console.log(district1.city) // GZC
console.log(district2.city) // Guangzhou
district1.alias.push('Yangcheng')
console.log(district1.alias) // ['Sui', 'Huacheng', 'Yangcheng']
console.log(district2.alias) // ['Sui', 'Huacheng']
// 优点3:子类构建实例时可以向父类传递参数
console.log(district1.name) // undefined
console.log(district1.district) // Haizhu
console.log(City)
// ƒ City(district) { this.city = 'Guangzhou' this.alias = ['Sui', 'Huacheng'] this.district = district }
组合继承的缺点体现在调用了两次父类的构造函数,例如下面子类实例 district1
的打印结果:
从打印结果可以看出,子类实例添加了父类属性,覆盖了子类实例原型种添加的父类属性,造成了性能上的浪费。
为什么组合继承会调用两次父类构造函数?
第一次调用:Child.prototype = new Parent()new 的过程中,执行了父类构造函数。
第二次调用:Parent.call(this,name,like)
call
的作用是改变函数执行时的上下文。比如:A.call(B)
。其实,最终执行的还是 A 函数,只不过是用 B 来调用而已。所以Parent.call(this,name,like)
也就是执行了父类构造函数。
上述不使用 Object.create 3 种继承方式的总结
原型链继承 | 构造函数继承 | 组合继承 | |
---|---|---|---|
父类方法是否可被子类实例复用 | 可复用 | 不可复用 | 可复用 |
子类实例间是否共享父类的引用类型属性 | 共享 | 不共享 | 不共享 |
子类实例是否能向父类传参 | 不能传参 | 能传参 | 能传参 |
优点 | 父类方法可被子类实例复用 | 子类实例间独享父类引用类型属性;子类实例能向父类传参 | 前两者优点都有 |
缺点 | 子类实例间共享父类引用类型属性;子类实例不能向父类传参 | 父类方法不可被子类实例复用 | 父类属性重复调用 |
4、原型式继承
F.prototype = proto
return new F()
创建了一个空的构造函数,然后把它的 prototype 指向参数 proto,最后返回一个实例对象完成继承。原型式继承本质上是对参数对象的一个浅复制,ES5 的 Object.create() 函数,就是基于原型式继承的。
优点:父类方法可以被子类实例复用。
缺点:
- 父类的引用属性会被所有子类实例共享。
- 子类构建实例时不能向父类传递参数。
示例:
// clone 函数
function clone(proto) {
function F() { }
F.prototype = proto
return new F()
}
// 父类
var gz = {
city: "Guangzhou",
alias: ['Sui', 'Huacheng']
};
// 子类实例
var district1 = clone(gz);
var district2 = clone(gz);
// 缺点1:父类属性如果是引用类型则该属性会被子类实例共享
district1.city = 'GZC'
console.log(district1.city) // GZC
console.log(district2.city) // Guangzhou
district1.alias.push('Yangcheng')
console.log(district1.alias) // ['Sui', 'Huacheng', 'Yangcheng']
console.log(district2.alias) // ['Sui', 'Huacheng', 'Yangcheng']
console.log(gz) // {city: 'Guangzhou', alias: ['Sui', 'Huacheng', 'Yangcheng']}
上面代码中的 clone 函数可转变为 Object.create:
var district2 = clone(gz);
// =>
var district2 = Object.create(gz);
原型式继承的优点体现在父类方法可以被子类复用,例如:
// clone 函数
function clone(proto) {
function F() { }
F.prototype = proto
return new F()
}
// 父类
var gz = {
city: "Guangzhou",
alias: ['Sui', 'Huacheng'],
sayNum: () => { console.log('020') }
};
// 父类函数
gz.sayNum = function () { console.log('020') }
// 子类实例
var district1 = clone(gz);
console.log(district1.sayNum) // ƒ () { console.log('020') }
子类实例 district1
的打印结果:
从打印结果可以看出,父类的函数 sayNum
会被子类实例复用。
5、寄生式继承
F.prototype = proto
let f = new F()
f.fn = ...
return new F()
在原型式继承的基础上,创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回构造函数。
优缺点:与原型式继承相同。
示例:
function cloneX(proto) {
function F() { }
F.prototype = proto
// 为构造函数新增属性/方法,以增强函数
let f = new F()
f.sayNum = function () {
console.log('020')
}
return f
}
// 父类
var gz = {
city: "Guangzhou",
alias: ['Sui', 'Huacheng']
};
// 子类实例
var district1 = cloneX(gz);
var district2 = cloneX(gz);
// 缺点1:父类属性如果是引用类型则该属性会被子类实例共享
district1.city = 'GZC'
console.log(district1.city) // GZC
console.log(district2.city) // Guangzhou
district1.alias.push('Yangcheng')
console.log(district1.alias) // ['Sui', 'Huacheng', 'Yangcheng']
console.log(district2.alias) // ['Sui', 'Huacheng', 'Yangcheng']
console.log(gz) // {city: 'Guangzhou', alias: ['Sui', 'Huacheng', 'Yangcheng']}
子类实例 district1
的打印结果:
6、寄生组合继承
inherit(District, City)
前面的组合继承会两次调用父类的构造函数造成浪费,而寄生组合继承就可以解决这个问题,寄生组合继承就是结合构造函数传递参数和寄生式继承来实现继承。
优点:
- 父类的方法可以被复用。
- 父类的引用属性不会被共享。
- 子类构建实例时可以向父类传递参数。
缺点:这是最成熟的方法,也是现在库实现继承的方法。
示例:
// inherit 函数
function inherit(child, father) {
var prototype = Object.create(father.prototype) // 创建对象,创建了父类原型的浅复制
prototype.constructor = child // 增强对象,弥补因重写原型而失去的默认的 constructor 属性
child.prototype = prototype // 指定对象,将新创建的对象赋值给子类的原型
}
// 父类
function City(district) {
this.city = 'Guangzhou'
this.alias = ['Sui', 'Huacheng']
this.district = district
}
// 父类函数
City.prototype.sayNum = function () { console.log('020') }
// 子类 & 构造函数继承
function District(name) {
City.call(this, name)
}
// 将父类原型指向子类
inherit(District, City)
// 使用
var district1 = new District("Haizhu")
var district2 = new District("Tianhe")
// 优点1:父类的方法可以被复用
console.log(district1.sayNum) // ƒ () { console.log('020') }
// 优点2:父类的引用属性不会被共享(实现引用属性独享)
district1.city = 'GZC'
console.log(district1.city) // GZC
console.log(district2.city) // Guangzhou
district1.alias.push('Yangcheng')
console.log(district1.alias) // ['Sui', 'Huacheng', 'Yangcheng']
console.log(district2.alias) // ['Sui', 'Huacheng']
// 优点3:子类构建实例时可以向父类传递参数
console.log(district1.name) // undefined
console.log(district1.district) // Haizhu
console.log(City)
// ƒ City(district) { this.city = 'Guangzhou' this.alias = ['Sui', 'Huacheng'] this.district = district }
子类实例 district1
的打印结果:
从打印结果可以看出,寄生组合继承解决了组合继承调用两次父类构造函数的缺点。
7、ES6 Class extends
class Child extends Parent
constructor() { super() }
ES6 Class extends 继承的结果与寄生组合继承相似,本质上,ES6 继承是一种语法糖。但寄生组合继承是先创建子类实例 this 对象,然后再对其增强;而 ES6 先将父类实例对象的属性和方法加到 this 上(所以必须先调用 super 方法),再用子类的构造函数修改 this。
super 的作用:将父类的 this 对象继承给子类。
与 ES5 继承的异同
相同点:本质上 ES6 继承是 ES5 继承的语法糖
不同点:
- ES6 继承中子类的构造函数的原型链指向父类的构造函数,ES5 中使用的是构造函数复制,没有原型链指向。
- ES6 子类实例的构建,基于父类实例,ES5 中不是。
实现原理:
class A {
}
class B {
}
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
Object.setPrototypeOf(B.prototype, A.prototype);
Object.setPrototypeOf(B, A);
示例:
// 父类
class City {
constructor(district) {
this.city = 'Guangzhou'
this.alias = ['Sui', 'Huacheng']
this.district = district
this.sayNum = () => { console.log('020') }
}
}
// 子类 & ES6 Class extends 继承
class District extends City {
constructor(name) {
super(name);
}
}
// 使用
var district1 = new District("Haizhu")
var district2 = new District("Tianhe")
// 优点1:父类的方法可以被复用
console.log(district1.sayNum) // () => { console.log('020') }
// 优点2:父类的引用属性不会被共享(实现引用属性独享)
district1.city = 'GZC'
console.log(district1.city) // GZC
console.log(district2.city) // Guangzhou
district1.alias.push('Yangcheng')
console.log(district1.alias) // ['Sui', 'Huacheng', 'Yangcheng']
console.log(district2.alias) // ['Sui', 'Huacheng']
// 优点3:子类构建实例时可以向父类传递参数
console.log(district1.name) // undefined
console.log(district1.district) // Haizhu
console.log(City)
// class City { constructor(district) { this.city = 'Guangzhou' this.alias = ['Sui', 'Huacheng'] this.district = district } }
3. 总结
原型链继承 | 构造函数继承 | 组合继承 | 原型式继承 | 寄生式继承 | 寄生组合继承 | ES6 Class extends | |
---|---|---|---|---|---|---|---|
父类方法是否可被子类实例复用 | 可复用 | 不可复用 | 可复用 | 可复用 | 可复用 | 可复用 | 可复用 |
子类实例间是否共享父类的引用类型属性 | 共享 | 不共享 | 不共享 | 共享 | 共享 | 不共享 | 不共享 |
子类实例是否能向父类传参 | 不能传参 | 能传参 | 能传参 | 不能传参 | 不能传参 | 能传参 | 能传参 |