JS中的常见继承方法

本文最后更新于:2023年3月19日 晚上

本文部分转自:https://juejin.im/post/6844903798624747528

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
function Person(name) {
this.name = name
}

Person.prototype.getName = function(){
console.log('person fun!')
return this.name
}

// 1.构造函数继承,无法获得父类原型链上的参数
let Student = function(name,age) {
Person.call(this,name)
this.age = age
}
let stu = new Student('zs',12)
console.log(stu.name) //zs 12
console.log(stu.getName()) // stu.getName is not a cm-functionOps

// 2.原型链继承,无法向父类传递参数,可以调用父类原型链上的方法,只可以继承一个父类
let Student = function(){}
Student.prototype = new Person()
Student.prototype.constructor = Student
Student.prototype.stuFun = function(){
console.log('student fun!')
}
let stu = new Student('123')

console.log(stu.name) // undefined 无法向父类传递参数
console.log(stu.getName()) // person fun! return undefined
console.log(stu.stuFun()) // student fun!

3.组合继承,ES6之前常用方案,重复调用两次父类性能损耗
let Student = function(name,age){
Person.call(this,name)
this.age = age
}
Student.prototype = new Person()
Student.prototype.constructor = Student
Student.prototype.stuFun = function(){
console.log('student fun!')
}
let stu = new Student('hhh',123)
console.log(stu.name) // undefined 无法向父类传递参数
console.log(stu.getName()) // person fun! return hhh
console.log(stu.stuFun()) // student fun!

// 4.原型式继承,使用ES5 Object.create 使用传入的对象作为__proto__,返回一个新对象。本质上是一个浅拷贝,所以引用类型是父子都可以更改的
let parent = {
name: 'parent',
arr: [1,2,3,4],
getName: function(){
return this.name
}
}

let child = Object.create(parent)
child.name = 'child'
console.log(child.name) // child
console.log(child.arr.pop()) // [4]
console.log(parent.arr) // [1,2,3]
console.log(parent.name) // parent
console.log(child.getName()) // child
console.log(parent.getName()) // parent

// 5.寄生式继承,原型式继承的基础上添加属性或方法,再封装起来
let parent = {
name: 'parent',
arr: [1,2,3,4],
getName: function(){
return this.name
}
}

function object(obj){
let child = Object.create(parent)
child.childFn = function(){
console.log('add child function!')
}
return child
}

let child = object(parent)
child.name = 'child'
console.log(child.name) // child
console.log(child.arr.pop()) // [4]
console.log(parent.arr) // [1,2,3]
console.log(parent.name) // parent
console.log(child.getName()) // child
console.log(parent.getName()) // parent
console.log(child.childFn()); // add child function!

// 6.寄生组合式继承,使用中继函数,免去两次调用父类构造函数
function Student(name,age){
Person.call(this,name)
this.age = age
}

// 其实是一种原型链继承方式
Object.setPrototypeOf(Student.prototype,Person.prototype)

let stu = new Student('yyy',12)
console.log(stu.name,stu.age) // yyy 12
console.log(stu.getName()) // person fun! yyy

// 7.ES6 class继承

class Parent{
constructor(name){
this.name = name;
}
getName(){
return this.name
}
}

class Child extends Parent{
constructor(name,age){
super(name)
this.age = age
}
getAge(){
return this.age
}
}

let child = new Child('hsw',20)
console.log(child.name,child.age) // hsw 20
console.log(child.getName(),child.getAge()) // hsw 20

构造函数继承

构造函数继承没有用到 prototype 这种方式比较常见,定义和使用也较为简单,下面是一个例子 🌰:

  • 👍 可以定义私有属性方法
  • 👍 子类可以传递参数给父类
  • ❌ 不能定义共享属性方法/或写在外面失去了封装性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Parent(name, friends) {
this.name = name
this.friends = friends // 👍 可以定义私有 引用类型不会被共享
this.share = share // ❌ 可以定义公有 但需要放在外部
this.log = log // ❌ 避免重复声明,为了复用需要放在外面
}
// ❌ 公有属性和方法定义在外面失去了封装性
let share = [1, 2, 3]
function log() {
return this.name
}
function Child(name, friends, gender) {
Parent.call(this, name, friends) // 👍 可以在子类传递参数给父类
this.gender = gender
}
复制代码

原型链继承

原型链模式需要手动重新绑定 constructor 而且不能定义私有变量

  • 👍 可以定义公有属性方法
  • ❌ 无论是定义还是继承都需要手动修改 constructor
  • ❌ 封装性一般
  • ❌ 不能定义私有属性方法
  • ❌ 没办法向父类传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent() {}
Parent.prototype = {
constructor: Parent, // ❌ 需要手动绑定 constructor
name: 'oli', // ❌ 不能定义私有属性,全部都是公有
friends: ['alice', 'troy'], // 👍 可以定义公有属性 所有实例都引用这个
log: function() { // 👍 方法被共享了
return this.name
}
}
// 也可以写成多个 Parent.prototype.func1 = function(){} 封装性更差 但不用修改 constructor
// ❌ 封装性一般
function Child() {} // ❌ 没办法向父类传递参数
Child.prototype = new Parent() // 使用 new 操作符创建并重写 prototype
Child.prototype.constructor = Child // ❌ 每次继承都需要手动修改 constructor 谁叫你是覆盖 prototype 属性呢
复制代码

组合继承

上面两者结合即成为组合继承模式,这个是结合了两者的优势,在 ES6 的 class 出现之前的常用方法,🦐🍜 看看例子:

  • 👍 公有的写在原型
  • 👍 私有的写在构造函数
  • 👍 可以向父类传递参数
  • ❌ 需要手动绑定 constructor
  • ❌ 封装性一般
  • ⚡ 重复调用父类性能损耗
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Parent(name, friends) {
// 😀 私有的写这里
this.name = name // 👍 可以定义私有属性
this.friends = friends // 👍 可以定义公有引用属性不会被共享
}
Parent.prototype = {
// 😀 公有的写这里
constructor: Parent, // ❌ 需要手动绑定 constructor
share: [1, 2, 3], // 👍 这里定义的公有属性会被共享
log: function() { // 👍 方法被共享了
return this.name
}
}
// ❌ 封装性一般
function Child(name, friends, gender) {
Parent.call(this, name, friends) // 👍 可以向父类传递参数 ⚡ 这里又调用了一次 Parent
this.gender = gender
}
Child.prototype = new Parent() // 使用 new 操作符创建并重写 prototype ⚡ 这里调用了一次 Parent
// 有方法避免多次调用直接去掉 new 操作符 转而写成 Child.prototype = Parent.prototype 这样并不好,虽然避免出现重复调用但导致修改子类 constructor 的时候父类也被修改了
Child.prototype.constructor = Child // ❌ 每次继承都需要手动修改 constructor 谁叫你是覆盖 prototype 属性呢
// 如果使用 Child.prototype = Parent.prototype 那么 constructor 子类父类是同一个
复制代码

image.svg

原型式继承

原型式继承直接使用 ES5 Object.create 方法,该方法的原理是创建一个构造函数,构造函数的原型指向对象,然后调用 new 操作符创建实例,并返回这个实例,本质是一个浅拷贝

  • 👍 父类方法可以复用
  • ❌ 父类引用属性全部被共享
  • ❌ 子类不可传递参数给父类
1
2
3
4
5
6
7
8
9
let parent = {
name: 'parent',
share: [1, 2, 3], // ❌ 父类的引用属性全部被子类所共享
log: function() { // 👍 父类方法可以复用
return this.name
}
}
let child = Object.create(parent) // ❌ 子类不能向父类传递参数
复制代码

image.svg

寄生式继承

原型式继承的基础上为子类增加属性和方法

  • 👍 父类方法可以复用
  • 👍 增加了别的属性和方法
  • ❌ 父类引用属性全部被共享
  • ❌ 子类不可传递参数给父类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let parent = {
name: 'parent',
share: [1, 2, 3],
log: function() {
return this.name
}
}
function create(obj) {
let clone = Object.create(obj) // 本质上还是 Object.create
clone.print = function() { // 增加一些属性或方法
console.log(this.name)
}
return clone
}
let child = create(parent)
复制代码

寄生组合式继承

杂糅了原型链式、构造函数式、组合式、原型式、寄生式而形成的一种方式:
组合继承的方法会调用两次 Parent,一次是在 Child.prototype = new Parent() ,一次是在 Parent.call()。这个是组合继承的唯一缺点,寄生组合式解决了这个问题:

  • 👍 公有的写在原型
  • 👍 私有的写在构造函数
  • 👍 可以向父类传递参数
  • 👍 不会重复调用父类
  • ❌ 需要手动绑定 constructor (如果重写 prototype)
  • ❌ 需要调用额外的方法封装性一般
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Parent(name, friends) {
this.name = name
this.friends = friends
}
Parent.prototype = {
constructor: Parent, // ❌ 需要手动绑定 constructor
share: [1, 2, 3],
log: function() {
return this.name
}
}
function Child(name, friends, gender) {
Parent.call(this, name, friends) // ⚡ 这里只需要调用一次 Parent
this.gender = gender
}
// 上半部分和组合继承一样
let F = function() {} // 创建一个中介函数
F.prototype = Parent.prototype // 这个中介的原型指向 Parent 的原型
Child.prototype = new F() // 注意这里没有使用 new 操作符调用 Parent
Child.prototype.constructor = Child
复制代码

对上述方法进行一个封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Parent(name, friends) {
this.name = name // 👍 可以定义私有属性
this.friends = friends // 👍 可以定义公有引用属性不会被共享
}
Parent.prototype = {
constructor: Parent, // ❌ 需要手动绑定 constructor
share: [1, 2, 3], // 👍 这里定义的公有属性会被共享
log: function() { // 👍 方法被共享了
return this.name
}
}
function Child(name, friends, gender) {
Parent.call(this, name, friends) // 👍 可以向父类传递参数 ⚡ 这里又调用了一次 Parent
this.gender = gender
}
function proto(child, parent) {
let clonePrototype = Object.create(parent.prototype)
child.prototype = clonePrototype
child.prototype.constructor = child
}
proto(Child, Parent)
复制代码

ES6 class

class 的语法,就比较清晰了,能用 class 就用 class 吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Parent {
constructor(name, friends) { // 该属性在构造函数上,不共享
this.name = name
this.friends = friends
}
log() { // 该方法在原型上,共享
return this
}
}
Parent.prototype.share = [1, 2, 3] // 原型上的属性,共享
class Child extends Parent {
constructor(name, friends, gender) {
super(name, friends)
this.gender = gender
}
}
复制代码

另外可以使用 get set 方法将 share 属性写入到原型中去

另外,class 是一种语法糖使用 babel 将其转化一下看看:

小结

最后上个图作为总结:


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!