JavaScript 常见的六种继承方式
继承是面向对象软件技术当中的一个概念,与多态、封装共为面向对象的三个基本特征。继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。
一、原型链继承 prototype
子类型的原型为父类型的一个实例对象。
父类
function Parent(name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
this.setName = function () {}
}
Parent.prototype.setAge = function () {}
子类
function Child(price) {
this.price = price
this.setScore = function () {}
}
Child.prototype = new Parent()
let child1 = new Child(15000)
let child2 = new Child(14000)
child1
Child {price: 15000, setScore: ƒ}
price: 15000
setScore: ƒ ()
__proto__: Parent
age: undefined
name: undefined
play: (3) [1, 2, 3]
setName: ƒ ()
__proto__:
setAge: ƒ ()
constructor: ƒ Parent(name, age)
__proto__: Object
child2
Child {price: 14000, setScore: ƒ}
price: 14000
setScore: ƒ ()
__proto__: Parent
age: undefined
name: undefined
play: (3) [1, 2, 3]
setName: ƒ ()
__proto__:
setAge: ƒ ()
constructor: ƒ Parent(name, age)
__proto__: Object
子类可以通过 .__proto__ 来访问到父类的方法和属性,也可以通过 .__proto__.__proto__ 来访问父类的prototype属性。
child1.__proto__.play
//[1, 2, 3]
child1.__proto__.__proto__.setAge
//ƒ () {}
操作
对child1的play属性进行push操作,play数组长度为4
child1.play.push(4)
//4
//play: (4) [1, 2, 3, 4]
这是child2的play属性
//child2的play的长度也为4
Child {price: 14000, setScore: ƒ}
price: 14000
setScore: ƒ ()
__proto__: Parent
age: undefined
name: undefined
play: (4) [1, 2, 3, 4]
setName: ƒ ()
__proto__: Object
WHY?
子类继承父类的方法和属性,是将父类的私有属性和公有方法都作为自己的公有属性和方法;修改基本数据类型的时候操作的是值,修改引用数据类型的时候操作的是地址,如果说父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2。
child1.__proto__ === child2.__proto__
//true
child1.__proto__.__proto__ === child2.__proto__.__proto__
//true
优点:
- 继承方式简单
- 父类新增方法、属性,子类都能访问到
缺点:
- 无法实现多继承
- 来自父类的所有属性被所有实例共享
- 要想为子类新增属性和方法,必须要在Child.prototype = new Parent() 之后,因为会被覆盖
- 创建子类时,不能像父类传递参数
二、构造函数继承 call
在子类型构造函数中通用call()调用父类型构造函数
父类
function Parent(name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3],
this.setName = function () {}
}
Parent.prototype.setAge = function () {}
子类
function Child(name, age, price) {
Parent.call(this, name, age) // 相当于: this.Parent(name, age)
this.price = price
}
let child1 = new Child('name1', 18, 15000)
let child2 = new Child('name2', 16, 10000)
child1
Child {name: "name1", age: 18, price: 15000, setName: ƒ}
age: 18
name: "name1"
play: (3) [1, 2, 3]
price: 15000
setName: ƒ ()
__proto__: Object
child2
Child {name: "name2", age: 16, price: 10000, setName: ƒ}
age: 16
name: "name2"
play: (3) [1, 2, 3]
price: 10000
setName: ƒ ()
__proto__: Object
child1和child2没有父类的setAge方法 只继承父类的属性和方法,不能继承父类原型上的属性和方法
child1.setAge()
//VM298324:1 Uncaught TypeError: child1.setAge is not a function
引用类型修改不会互相影响
child1.play.push(4)
//child1
//play: (4) [1, 2, 3, 4]
//child2
//play: (3) [1, 2, 3]
优点:
- 原型链继承中子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性和方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
三、原型链+构造函数的组合继承 prototype + call
调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。
父类
function Parent (name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
this.setAge = function () { }
}
Parent.prototype.setAge = function () {
console.log("This is the setAge method of Parent")
}
子类
function Child (name, age, price) {
Parent.call(this, name, age)
this.price = price
this.setScore = function () {}
}
Child.prototype = new Parent()
Child.prototype.constructor = Child//组合继承也是需要修复构造函数指向的
Child.prototype.sayHello = function () { }
let child1 = new Child('name1', 18, 15000)
let child2 = new Child('name2', 16, 10000)
child1
Child {name: "name1", age: 18, play: Array(3), price: 15000, setAge: ƒ, …}
age: 18
name: "name1"
play: (3) [1, 2, 3]
price: 15000
setAge: ƒ ()
setScore: ƒ ()
__proto__: Parent
age: undefined
constructor: ƒ Child(name, age, price)
name: undefined
play: (3) [1, 2, 3]
sayHello: ƒ ()
setAge: ƒ ()
__proto__:
setAge: ƒ ()
constructor: ƒ Parent(name, age)
__proto__: Object
child2
Child {name: "name2", age: 16, play: Array(3), price: 10000, setAge: ƒ, …}
age: 16
name: "name2"
play: (3) [1, 2, 3]
price: 10000
setAge: ƒ ()
setScore: ƒ ()
__proto__: Parent
age: undefined
constructor: ƒ Child(name, age, price)
name: undefined
play: (3) [1, 2, 3]
sayHello: ƒ ()
setAge: ƒ ()
__proto__:
setAge: ƒ ()
constructor: ƒ Parent(name, age)
__proto__: Object
对child1的play数组进行push操作
child1.play.push(4)
//4
//child1
Child {name: "name1", age: 18, play: Array(4), price: 15000, setAge: ƒ, …}
age: 18
name: "name1"
play: (4) [1, 2, 3, 4]
price: 15000
setAge: ƒ ()
setScore: ƒ ()
__proto__: Parent
age: undefined
constructor: ƒ Child(name, age, price)
name: undefined
play: (3) [1, 2, 3]
//child2
Child {name: "name2", age: 16, play: Array(3), price: 10000, setAge: ƒ, …}
age: 16
name: "name2"
play: (3) [1, 2, 3]
price: 10000
setAge: ƒ ()
setScore: ƒ ()
__proto__: Parent
age: undefined
constructor: ƒ Child(name, age, price)
name: undefined
play: (3) [1, 2, 3]
引用类型修改不会互相影响
优点:
- 可以继承实例属性/方法,也可以继承原型属性/方法
- 不存在引用属性共享问题
- 可传参
缺点:
- 调用了两次父类构造函数,生成了两份实例
四、组合继承优化1
通过父类原型和子类原型指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点。
父类
function Parent (name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
this.setName = function () {}
}
Parent.prototype.setAge = function () {
console.log('This is the setAge method of Parent')
}
子类
function Child (name, age, price) {
Parent.call(this, name, age)
this.price = price
this.setScore = function () {}
}
Child.prototype = Parent.prototype
Child.prototype.sayhello = function () {}
let child1 = new Child('name1', 18, 15000)
let child2 = new Child('name2', 16, 10000)
child1
Child {name: "name1", age: 18, play: Array(3), price: 15000, setName: ƒ, …}
age: 18
name: "name1"
play: (3) [1, 2, 3]
price: 15000
setName: ƒ ()
setScore: ƒ ()
__proto__:
sayhello: ƒ ()
setAge: ƒ ()
child2
Child {name: "name2", age: 16, play: Array(3), price: 10000, setName: ƒ, …}
age: 16
name: "name2"
play: (3) [1, 2, 3]
price: 10000
setName: ƒ ()
setScore: ƒ ()
__proto__:
sayhello: ƒ ()
setAge: ƒ ()
问题 没有办法确定是哪个实例,通过instanceof返回值都为true,调用constructor为Parent
child1 instanceof Child
//true
child1 instanceof Parent
//true
child1.constructor
//ƒ Parent (name, age) {
// this.name = name,
// this.age = age,
// this.play = [1, 2, 3]
// this.setName = function () {}
//}
优点:
- 不会调用了两次父类构造函数
缺点:
- 没办法辨别是实例是子类还是父类创造的,子类和父类的构造函数指向是同一个。
五、组合继承优化2
借助原型可以基于已有的对象来创建对象,var B = Object.create(A)以A对象为原型,生成了B对象。B继承了A的所有属性和方法。
父类
function Parent (name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
}
Parent.prototype.setAge = function () {}
子类
function Child (name, age, price) {
Parent.call(this, name, age)
this.price = price
this.setName = function () {}
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
let child1 = new Child('name1', 18, 15000)
let child2 = new Child('name2', 16, 10000)
child1
Child {name: "name1", age: 18, play: Array(3), price: 15000, setName: ƒ}
age: 18
name: "name1"
play: (3) [1, 2, 3]
price: 15000
setName: ƒ ()
__proto__: Parent
constructor: ƒ Child(name, age, price)
__proto__:
setAge: ƒ ()
child2
Child {name: "name2", age: 16, play: Array(3), price: 10000, setName: ƒ}
age: 16
name: "name2"
play: (3) [1, 2, 3]
price: 10000
setName: ƒ ()
__proto__: Parent
constructor: ƒ Child(name, age, price)
__proto__:
setAge: ƒ ()
通过instanceof返回值都为true,调用constructor为Child
child1 instanceof Child
//true
child1 instanceof Parent
//true
child1.constructor
//ƒ Child (name, age, price) {
// Parent.call(this, name, age)
// this.price = price
// this.setName = function () {}
//}
六、es6 class的继承
class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。
父类
class Parent {
constructor(name, age) {
this.name = name
this.age = age
}
setName () {
console.log('parent')
}
}
子类
let child1 = new Parent('name1', 18)
let child2 = new Parent('name2', 16)
child1
Parent {name: "name1", age: 18}
age: 18
name: "name1"
__proto__:
constructor: class Parent
setName: ƒ setName()
child2
Parent {name: "name2", age: 16}
age: 16
name: "name2"
__proto__:
constructor: class Parent
setName: ƒ setName()
子类2
class Child extends Parent {
constructor(name, age, price) {
super(name, age)
this.price = price
}
setAge () {
console.log('子类方法')
}
}
let child3 = new Child('name3', 20, 15000)
let child4 = new Child('name4', 21, 10000)
child3
Child {name: "name3", age: 20, price: 15000}
age: 20
name: "name3"
price: 15000
__proto__: Parent
constructor: class Child
setAge: ƒ setAge()
__proto__:
constructor: class Parent
setName: ƒ setName()
child4
Child {name: "name4", age: 21, price: 10000}
age: 21
name: "name4"
price: 10000
__proto__: Parent
constructor: class Child
setAge: ƒ setAge()
__proto__:
constructor: class Parent
setName: ƒ setName()
child3 instanceof Parent
//true
child3 instanceof Child
//true
child3.constructor
//class Child extends Parent {
// constructor(name, age, price) {
// super(name, age)
// this.price = price
// }
// setAge () {
// console.log('子类方法')
// }
//}
优点:
- 简单继承