继承
2022年7月13日大约 5 分钟
原型链继承
function Parent() {
this.parentValue = 'parentValue'
this.info = {
name: 'parent',
age: 23,
}
this.parentMethod = function () {
console.log(this.parentValue)
}
}
Parent.prototype.protoValue = 'protoValue'
Parent.prototype.protoMethod = function () {
console.log(this.protoValue)
}
function Child() {
this.childValue = 'childValue'
}
// Child.prototype.__proto__ === Parent.prototype
Child.prototype = new Parent()
// 缺点1:不能给父类构造函数传参
let child1 = new Child()
let child2 = new Child()
// 缺点2:子类实例继承到父类构造函数、原型链的属性和方法,但不可枚举
console.log(child1) // Child {childValue: 'childValue'}
console.log(child2) // Child {childValue: 'childValue'}
// 缺点3:父类的引用属性会被所有子类实例共享
child1.info.address = 'Earth'
// 可以访问父类构造函数、原型链的属性和方法
child1.protoMethod() // protoValue
child1.parentMethod() // parentValue
child1.protoValue // 'protoValue'
优点:父类方法可以复用
缺点:
- 不能给父类构造函数传参
- 子类实例可以继承父类构造函数、原型链上的属性和方法,但不可枚举
- 父类的引用属性会被所有子类实例共享,修改某个子类实例继承的的引用类型属性,会影响到所有的子类实例
借用构造函数继承
function Parent(name, age) {
this.parentValue = 'parentValue'
this.info = { name, age }
this.parentMethod = function () {
console.log(this.parentValue)
}
}
Parent.prototype.protoValue = 'protoValue'
Parent.prototype.protoMethod = function () {
console.log(this.protoValue)
}
function Child(name, age) {
Parent.call(this, name, age)
this.childValue = 'childValue'
}
let child1 = new Child('child1', 23)
let child2 = new Child('child2', 233)
// 可以继承父类构造函数的属性和方法,且可枚举
console.log(child1) // Child {parentValue: 'parentValue', info: {…}, childValue: 'childValue', parentMethod: ƒ}
console.log(child2) // Child {parentValue: 'parentValue', info: {…}, childValue: 'childValue', parentMethod: ƒ}
// 修改某个子类实例继承的的引用类型属性,不会影响到所有的子类实例
child1.info.address = 'Earth'
console.log(child1.info) // {name: 'child1', age: 23, address: 'Earth'}
console.log(child2.info) // {name: 'child2', age: 233}
// 缺点:无法继承到父类原型上的属性和方法
// child1.protoMethod() // 报错
// console.log(child1.protoValue) // undefined
优点:
- 避免父类引用类型属性被所有实例共享
- 子类可以向父类构造函数传参
缺点:
- 只能继承父类构造函数的属性和方法,不能继承原型链上的属性和方法
- 方法定义在构造函数中,每个子类实例都含有父类函数副本,无法实现父类函数复用
组合继承
将原型链继承与构造函数继承组合使用
function Parent(name, age) {
this.parentValue = 'parentValue'
this.info = { name, age }
this.parentMethod = function () {
console.log(this.parentValue)
}
}
Parent.prototype.protoValue = 'protoValue'
Parent.prototype.protoMethod = function () {
console.log(this.protoValue)
}
function Child(name, age) {
// 继承父类构造函数的属性和方法
Parent.call(this, name, age)
this.childValue = 'childValue'
}
// 继承原型链上的属性和方法
Child.prototype = new Parent()
let child1 = new Child('child1', 23)
let child2 = new Child('child2', 233)
// 可以继承父类构造函数的属性和方法,且可枚举
console.log(child1) // Child {parentValue: 'parentValue', info: {…}, childValue: 'childValue', parentMethod: ƒ}
console.log(child2) // Child {parentValue: 'parentValue', info: {…}, childValue: 'childValue', parentMethod: ƒ}
// 避免父类的引用类型属性被所有子类实例共享
child1.info.address = 'Earth'
console.log(child1.info) // {name: 'child1', age: 23, address: 'Earth'}
console.log(child2.info) // {name: 'child2', age: 233}
// 可以继承到父类原型链上的属性和方法,但不可枚举
child1.protoMethod() // protoValue
console.log(child1.protoValue) // 'protoValue'
优点:可以继承父类构造函数和原型链上的属性、方法
缺点:组合继承会调用两次父类构造函数,实例中会存在两份属性、方法
原型式继承
对对象的浅拷贝,有三种等价方法,原型式继承的缺点与原型链继承是相同的
let parent = {
parentValue: 'parentValue',
info: {
name: 'parent',
age: 23,
},
parentMethod: function () {
console.log(this.parentValue)
},
}
// 1.临时构造函数
function copyObject1(obj) {
function Fn() {}
Fn.prototype = obj
return new Fn()
}
// 缺点1:不能向父类传参
let child1 = copyObject1(parent)
// 2.Object.setPrototypeOf
function copyObject2(obj) {
let newObj = {}
Object.setPrototypeOf(newObj, obj)
return newObj
}
let child2 = copyObject2(parent)
// 3.Object.create()
let child3 = Object.create(parent)
// 可以继承到父类的属性和方法,但不可枚举
console.log(child1) // Fn {}
console.log(child2) // {}
console.log(child3) // {}
console.log(child1.info) // { name: 'parent', age: 23 }
child1.parentMethod() // parentValue
// 缺点2:父类引用类型属性被所有子类共享
child1.info.address = 'Earth'
console.log(child2.info) // {name: 'parent', age: 23, address: 'Earth'}
优点:父类方法可复用
缺点:
- 不能给父类构造函数传参
- 父类的引用属性会被所有子类实例共享
寄生式继承
结合原型类继承和工厂模式:创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象
function copyObject(obj) {
function Fn() {}
Fn.prototype = obj
return new Fn()
}
function createChild(original) {
let clone = copyObject(original)
clone.parentMethod = function () {
console.log(this.parentValue)
}
return clone
}
let parent = {
parentValue: 'parentValue',
info: {
name: 'parent',
age: 23,
},
}
let child1 = createChild(parent)
let child2 = createChild(parent)
console.log(child1) // Fn {parentMethod: ƒ}
child1.parentMethod() // parentValue
console.log(child1.info) // {name: 'parent', age: 23}
// 父类的引用属性会被所有子类实例共享
child1.info.address = 'Earth'
console.log(child1.info) // {name: 'child1', age: 23, address: 'Earth'}
console.log(child2.info) // {name: 'child1', age: 23, address: 'Earth'}
缺点:
- 无法实现父类函数复用
- 父类的引用属性会被所有子类实例共享
寄生组合式继承
组合继承最大的缺点在于执行两次父类构造函数,寄生组合式继承解决了这个缺点
function inheritPrototype(child, parent) {
let prototype = Object.create(parent.prototype) // 创建对象
prototype.constructor = child // 增强对象
child.prototype = prototype // 赋值对象
}
function Parent(name, age) {
this.parentValue = 'parentValue'
this.info = { name, age }
this.parentMethod = function () {
console.log(this.parentValue)
}
}
Parent.prototype.protoValue = 'protoValue'
Parent.prototype.protoMethod = function () {
console.log(this.protoValue)
}
function Child(name, age) {
// 继承父类构造函数的属性和方法
Parent.call(this, name, age)
this.childValue = 'childValue'
}
inheritPrototype(Child, Parent)
Child.prototype.childProtoMethod = function () {
console.log(this.info)
}
let child1 = new Child('child1', 23)
let child2 = new Child('child2', 233)
console.log(child1) // Child {parentValue: 'parentValue', info: {…}, childValue: 'childValue', parentMethod: ƒ}
child1.parentMethod() // parentValue
child1.protoMethod() // protoValue
child1.childProtoMethod() // {name: 'child1', age: 23}
// 避免父类的引用类型属性被所有子类实例共享
child1.info.address = 'Earth'
console.log(child1.info) // {name: 'child1', age: 23, address: 'Earth'}
console.log(child2.info) // {name: 'child2', age: 233}
// 子类实例的构造函数是它本身
console.log(child1.constructor.name) // Child
优点:
- 只调用一次父类构造函数
- 可以向父类型的构造函数中传递参数
- 父类方法可以复用
- 避免父类引用类型属性被所有子类实例共享
ES6 继承
class + extends
是实现继承的语法糖,基于寄生组合式继承实现
class Parent {
constructor(name, age) {
this.parentValue = 'parentValue'
this.info = { name, age }
}
parentMethod() {
console.log(this.parentValue)
}
}
class Child extends Parent {
constructor(name, age, id) {
super(name, age) // 通过 super() 调用父类构造函数
this.id = id
}
}
let child1 = new Child('child1', 23, '001')
let child2 = new Child('child2', 233, '002')
// 只有父类方法不可枚举
console.log(child1) // Child {parentValue: 'parentValue', info: {…}, id: '001'}
child1.parentMethod() // parentValue
console.log(child1.info) // {name: 'child1', age: 23}
// 避免父类的引用属性被所有子类实例共享
child1.info.address = 'Earth'
console.log(child1.info) // {name: 'child1', age: 23, address: 'Earth'}
console.log(child2.info) // {name: 'child2', age: 233}
// 子类实例的构造函数是它本身
console.log(child1.constructor.name) // Child
参考资料
- JavaScript进阶之继承
- JS继承
- JavaScript高级程序设计(第4版)