JavaScript 常见的六种继承方式

248 阅读7分钟

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
优点:
  1. 继承方式简单
  2. 父类新增方法、属性,子类都能访问到
缺点:
  1. 无法实现多继承
  2. 来自父类的所有属性被所有实例共享
  3. 要想为子类新增属性和方法,必须要在Child.prototype = new Parent() 之后,因为会被覆盖
  4. 创建子类时,不能像父类传递参数

二、构造函数继承 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]
优点:
  1. 原型链继承中子类实例共享父类引用属性的问题
  2. 创建子类实例时,可以向父类传递参数
  3. 可以实现多继承(call多个父类对象)
缺点:
  1. 实例并不是父类的实例,只是子类的实例
  2. 只能继承父类的实例属性和方法,不能继承原型属性和方法
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

三、原型链+构造函数的组合继承 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. 可以继承实例属性/方法,也可以继承原型属性/方法
  2. 不存在引用属性共享问题
  3. 可传参
缺点:
  1. 调用了两次父类构造函数,生成了两份实例

四、组合继承优化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 () {}
//}
优点:
  1. 不会调用了两次父类构造函数
缺点:
  1. 没办法辨别是实例是子类还是父类创造的,子类和父类的构造函数指向是同一个。

五、组合继承优化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('子类方法')
//	}
//}
优点:
  1. 简单继承