js中的6种继承方式

js中es6之前没有类和继承,但是可以通过各种巧妙的方式来实现继承

继承应该达到的状态:

1.子类可以使用父类中的属性和方法

2.子类不同的实例之间不会互相影响

3.子类实例能够向父类传参

4.能实现多继承(一个子类可继承多个父类)

5.父类的方法能被复用(不会过多的消耗内存),而不是每创建一个子类实例都生成一份父类方法

一.原型链继承

1.具体实现:父类的实例作为子类的原型对象,核心实现代码标注如下:

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
js复制代码         // 父类(我就不写成父构造函数了,这样比较简洁)
function Father(name, age) {
(this.name = name), (this.age = age), (this.arr = [1, 2, 3]), (this.value = 33)
}
Father.prototype = {
say() {
alert('hellow')
},
}
// 子类
function Child(name, age) {
;
(this.name = name), (this.age = age)
}

Child.prototype = new Father() // 核心实现代码--->父类的实例作为子类的原型对象

// 创建实例并让constructor重新指回Child
let newChild = new Child('zkp')
newChild.constructor =Child
let newChild2 = new Child('zhy')
newChild.constructor =Child

// 两个不同的子类实例上都有父类里的属性和方法
console.log(newChild.arr) // [1, 2, 3]
console.log(newChild2.arr) // [1, 2, 3]
console.log(newChild.value) // 33
console.log(newChild2.value) // 33

// 在一个子类实例身上修改继承的父类的基础属性值,不会影响到其他子类实例
newChild.value = 55
console.log(newChild.value) // 55
console.log(newChild2.value) // 33

// 致命缺陷: 在一个子类实例身上修改继承父类的引用属性值,其他子类实例其他子类实例的值也会跟着改变
newChild.arr.push(100)
console.log(newChild.arr) // [1, 2, 3 , 100]
console.log(newChild2.arr) // [1, 2, 3 , 100]

2.优点:易于实现,一行代码就能实现

3.缺点:

1—>创建子类实例的时候不能向父类传参

2—>父类的引用数据类型属性被子类实例修改后,所有的子类实例上的该属性值也会跟着被修改

二.借用构造函数继承

1.具体实现:借父类的构造函数来增强子类实例(call,apply),相当于把父类的实例属性复制一份到子类实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码         // 父类
function Father(name, age) {
this.name = name,
this.age =age,
this.arr= [2, 3, 4, 5, 6]
}
Father.prototype = {
say() {
alert('hellow');
},
}
// 子类
function Child(name, age) {
Father.call(this, name, age) // 核心代码--->在子类中利用call()调用父类并向父类传参
}

let newChild = new Child('zkp', 11)
let newChild2 = new Child('zhy', 6)
// 子类实例修改父类引用属性 其他子类实例上该属性不会改变
newChild.arr.push(100)
console.log(newChild.arr); // [2, 3, 4, 5, 6, 100]
console.log(newChild2.arr); // [2, 3, 4, 5, 6]

// newChild.say() // newChild.say is not a function 报错 只是继承了父类的构造函数 无法使用父类的原型方法

2.优点:

1.解决了子类实例共享父类引用属性的问题

2.创建子类实例时,可以向父类构造函数传参

3.可以实现多继承(call多个)

3.缺点:无法继承父类原型中的方法,除非父类的方法写入构造函数中,但这样就实现不了函数的复用

三.组合式继承(伪经典继承)

1.具体实现:

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
js复制代码        // 父类
function Father(name, age) {
this.name = 'hahaha',
this.age = 11,
this.arr = [2, 3, 4, 5, 6]
}
Father.prototype = {
say() {
alert('hellow');
},
}
// 子类继承步骤:
function Child(name, age) {
Father.call(this, name, age) // 1.call方法调用父类的构造函数 并改变this的指向为子类函数(实现传参)
}
// 2.子构造函数的原型对象指向父构造函数的实例
Child.prototype = new Father()

// 3.创建实例并让constructor重新指回Child
let newChild = new Child('zkp',12)
newChild.constructor =Child
let newChild2 = new Child('zhy',6)
newChild2.constructor =Child

newChild.say() // alert ('hellow') 页面弹出hellow 子函数的实例继承了父函数的原型中的方法
console.log(newChild.name); // hahaha 子类的实例可以向父类传参

// 一个子类实例修改引用类型属性 其他实例该引用类型不会改变
newChild.arr.push(100)
console.log(newChild.arr,newChild2.arr); // [2, 3, 4, 5, 6, 100] ,[2, 3, 4, 5, 6]
// 缺点:父类的构造函数会被调用两次 call方法调用一次 new Father 一次

2.核心

**把实例方法都放在原型对象上,以实现函数复用。同时还要保留借用构造函数方式的优点,通过call(this)继承父类的基本属性和引用属性并保留能传参的优点.

通过Child.prototype = new Father()继承父类函数,实现函数复用**

优点:

  1. 不存在引用属性共享问题(不同子类实例之间不会互相影响)
  2. 可传参
  3. 函数可复用

4.缺点:也有一点小缺点

子类原型上有一份多余的父类实例属性,因为父类构造函数被调用了两次,生成了两份,而子类实例上的那一份屏蔽了子类原型上的,造成内存浪费

四.原型式继承(对象中的继承,更像是拷贝而不是继承)

1.具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
js复制代码        // 创建对象obj
let obj = {a:1 , b:2 , c:[1,2,3]}

// 在函数中把一个对象作为一个空构造函数的原型对象 并返回该构造函数的调用
function CreateObj(o) {
function F() {}
F.prototype = o;
return new F();
}
let newObj = CreateObj(obj);
let newObj2=CreateObj(obj)

//新创建的对象拥有obj的属性和方法
console.log(newObj.a , newObj.b , newObj.c); // 1 , 2 , [1, 2, 3]
console.log(newObj2.a , newObj2.b , newObj2.c); // 1 , 2 , [1, 2, 3]

// 通过一个实例修改引用类型值 其他实例也会被改变
newObj.c.push(100)
console.log(newObj2.a , newObj2.b , newObj2.c); // 1 , 2 ,  [1, 2, 3, 100]
console.log(newObj.a , newObj.b , newObj.c); // 1 , 2 ,  [1, 2, 3, 100]

3.核心:通过空的构造函数作为跳板 ,返回该构造函数的调用(类似于复制一个对象,用函数来包装)

3.优缺点

优点:

  1. 从已有对象衍生新对象,不需要创建自定义类型(一种新的创建对象方式) es5中内置方法Object.create()用到了这种方式

缺点:

  1. 父类引用属性会被所有实例共享,因为是用整个父类对象来充当了子类原型对象,所以这个缺陷无可避免
  2. 无法实现代码复用(新对象是现取的,每次new F()返回的都是一个新的对象,也没用到原型,无法复用)

五.寄生式继承

1.具体实现:

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
js复制代码 let obj = {
a: 1,
b: [1, 2, 3],
c() {
alert('hellow')
},
}

function CreateObj(o) {
function F() {}
F.prototype = o
return new F()
}
// 寄生继承就是在原型继承的基础上再封装 给队象增加方法和属性(对象增强) 实际上跟原型式继承是一样的
function CreateChild(o) {
let newObj = CreateObj(o) // 创建对象 或者用 var newObj = Object.create(o)
// 增强对象
newObj.x = function () {
alert('这是给新对象增强的方法')
}),
newObj.y = 'biubiu~'
......
return newObj
}

let p = CreateChild(obj)
p.x() //弹出信息
console.log(p.y) // biubiu~
console.log(p.a, p.b) // 1 , [1 , 2 , 3]

缺点:

函数还是不能复用,一个实例修改原型上的引用属性,其他实例依然会跟着改变(就是给原型式继承套上一层函数而已,让原型式看起开更像继承,并没有解决根本问题)

到这里的几个分析:

1.以上的方式多少都会有点缺陷,要达到完美继承就需要在组合式继承(伪经典继承)身上进行改造,但是要达到能传参,并且还要实现多继承的目的,那么在子类内部调用父类构造函数Father.call()这一步就不能动,

所以只能考虑Child.prototype = new Father 这一步,

2.最终需要达到目的:Child.Prototype.__ proto __ = Father.prototype(子类的原型指向父类的原型, 解决父类调用两次的缺陷) —–>(也就是一个对象继承另一个对象,上面的寄生式和原型式继承方式),

另外最后还是需要将子类实例的constructor指回子类

六.寄生式组合式继承(完美继承)

1.具体实现:

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
js复制代码            //1. 父类 实例属性放在构造函数中
function Father(name, age) {
this.name = name
this.age = age
this.hobby = ['敲代码', '解Bug', '睡觉']
}
// 父类方法放在原型上实现复用
Father.prototype.sayName = function () {
console.log(this.name, this.age, 666)
}

//2. 子类
function Child(name, age) {
Father.call(this, name, age) // 调用父类的构造函数 (继承父类的属性)
this.a = 1
}

// 3. 利用跳板创建对象
function CreateObj(o) {
function F() {}
F.prototype = o
return new F()
}

// 4.子类的原型对象用CreateObj(Father)创建
Child.prototype = CreateObj(Father.prototype)
console.log(Child.prototype.__proto__ === Father.prototype) // true 实现对Child.prototype = new Father的改造

/* 或者直接把3,4写成es5中的Object.create() 一行代码实现
Child.prototype = Object.create(Father.prototype) */

// 5.创建子类实例 constructor属性指回子类
let zkp = new Child('zkp', 12)
zkp.constructor = Child
let zhy = new Child('zhy', 6)
zhy.constructor = Child

// 验证:
console.log(zkp.a , zhy.a) // 1 , 1 子类自己的属性
// 一个子类实例修改继承的引用类型属性 其他实例不会被改变
zkp.hobby.push('吃饭')
console.log(zkp.name, zkp.age, zkp.hobby) // zkp , 12 , ['敲代码', '解Bug', '睡觉', '吃饭']
console.log(zhy.name, zhy.age, zhy.hobby) // zhy , 6 , ['敲代码', '解Bug', '睡觉']
// 子类调用父类的原型方法
zkp.sayName() // zkp 666
zhy.sayName() // zhy 666

优点:

完美实现了函数复用,传参,实例之间不会相互影响,多继承

寄生组合式优化(Object.create()):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码           //1. 父类 实例属性放在构造函数中
function Father(name, age) {
this.name = name
this.age = age
this.hobby = ['敲代码', '解Bug', '睡觉']
}
// 父类方法放在原型上实现复用
Father.prototype.sayName = function () {
console.log(this.name, 666)
}
Father.prototype.x = 1
//2. 子类
function Child(name, age) {
Father.call(this, name, age) // 调用父类的构造函数 (继承父类的属性)
this.a = 1
}
Child.prototype = Object.create(Father.prototype)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%