JS继承

预备知识

构造函数的属性

1
2
3
4
5
6
7
function A(name){
this.name = name;
this.arr = [1];
this.say = function(){
console.log('hello');
}
};

原型对象

每个函数都有prototype属性,它就是原型对象,它存储了实例共享的方法和属性,同时有一个构造函数指向函数本身

通过函数实例化出来的对象有–proto–属性指向原型对象

1
2
3
4
5
6
7
let a  = new A(); //a是一个实例
a.__proto__===A.prototype; //true
//prototype的结构
/*
constructor:A
其他属性和方法
*/

原型对象的作用

原型对象的用途是为每个实例对象存储共享的方法和属性,它仅仅是一个普通对象而已,并且所有的实例共享同一个原型对象,因此有别于实例方法或属性,原型对象仅有一份,而实例有很多份,且实例属性和方法是独立的。

在构造函数中,为了属性(实例基本属性)的私有性,以及方法(实例引用属性)的复用、共享,我们提倡:

  • 将属性封装在构造函数中
  • 将方法定义在原型对象上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function A(name){//定义相应的属性
this.name = name;
this.arr = [1];
};
A.prototype.say = function(){
console.log('hello');
}
//不推荐的写法
/*
A.prototype = {
say:function(){
console.log('hello');
}
}
原因:这样会重写原型对象,而原型对象中还有构造函数,如此那个构造函数就消失了
*/

五种JS继承方式

原型链继承

  • 缺点:
    • 不能向父类构造函数传参
    • 共享父类的引用属性
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
function Parent(name) {
this.name = name||'父亲';
this.arr = [1];
}
Parent.prototype.say = function () {
console.log('hello');
}

function Child(like){
this.like = like;
}
Child.prototype = new Parent; //核心,但此时Child.prototype.constructor ==Parent
Child.prototype.constructor = Child; //修正constructor指向

let boy1 = new Child();
let boy2 = new Child();

//优点:共享了父类构造函数的say方法
console.log(boy1.say===boy2.say);//true
//缺点:不能向父类构造函数传参
console.log(boy1.name,boy2.name,boy1.name===boy2.name)//父亲,父亲,true
//缺点2:子类实例共享了父类构造函数的引用属性,比如arr属性
//但是修改boy1的name,boy2的name不会改变,因为这相当于在boy1
boy1.arr.push(2);
console.log(boy2.arr); //[1,2]

借用构造函数

  • 核心:借用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类

  • 优点:实例之间独立。

    • 创建子类实例,可以向父类构造函数传参
    • 子类实例不共享父类构造函数的引用属性,如arr属性
    • 可实现多继承(通过多个call或者apply继承多个父类)
  • 缺点:

    • 父类的方法不能复用

      由于方法在父构造函数中定义,导致方法不能复用(因为每次创建子类实例都要创建一遍方法,比如say方法)(方法应该要复用、共享)

    • 子类实例,继承不了父类原型上的属性(因为没有用到原型)

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
function Parent(name) {
this.name = name||'父亲';
this.arr = [1];
this.say = function(){
console.log('hello');
}
}

function Child(name,like){
Parent.call(this,name); //核心,拷贝了父类的实例属性和方法
this.like = like;
}

let boy1 = new Child('小红','apple');
let boy2 = new Child('小明','orange');
//优点1:可向父类构造函数传参
console.log(boy1.name,boy2.name); //小红,小明
boy1.arr.push(2);
//优点2:不共享父类的引用属性
console.log(boy2.arr);//[1,2]
console.log(boy1.arr);//[1]

//缺点1:方法不能复用
console.log(boy1.say===boy2.say)//false

//缺点2:不能继承父类原型上的方法
Parent.prototype.walk= function(){
console.log('我会走路');
}
console.log(boy1.walk) //undefined

组合继承

  • 核心:通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过将父类实例作为子类原型,实现函数复用

  • 优点:

    • 保留构造函数的优点:创建子类实例,可以向父类构造函数传参数
    • 保留原型链的优点:父类的方法定义在父类的原型对象上,可以实现方法复用
    • 不共享父类的引用属性:比如arr属性
  • 缺点:

    • 由于调用了2次父类的构造方法,会存在一份多余的父类实例属性
  • 注意:组合继承这种方式,要记得修复Child.prototype.constructor指向

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
function Parent(name) {
this.name = name||'父亲';
this.arr = [1];
}
Parent.prototype.say = function(){
console.log('hello');
}

function Child(name,like){
Parent.call(this,name); //核心一
this.like = like;
}

Child.prototype = new Parent() //核心二,解决构造函数无法继承父类原型上属性的问题

Child.prototype.constructor = Child //修改constructor

let boy1 = new Child('小红','apple');

let boy2 = new Child('小明','orange');
//优点1:能传参
console.log(boy1.name,boy1.like);
console.log(boy2.name,boy2.like);
boy1.arr.push(2);
//优点2:不共享父类的引用属性

boy1.arr.push(2);
console.log(boy1.arr);
console.log(boy2.arr);

//优点3:方法可以复用
console.log(boy1.say===boy2.say) //true

//优点4:可以继承父类原型上的方法
Parent.prototype.walk= function(){
console.log('我会走路');
}
boy1.walk();//我会走路
//缺点:new和call创建了两份父类实例

组合继承优化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
function Parent(name) {
this.name = name||'父亲';
this.arr = [1];
}
Parent.prototype.say = function(){
console.log('hello');
}

function Child(name,like){
Parent.call(this,name);
this.like = like;
}

Child.prototype = Parent.prototype //核心,子类原型和父类原型,实质上是同一个

Child.prototype.constructor = Child //缺点,修改子类构造函数的指向后,父类实例的构造函数指向也会跟着改变

let boy1 = new Child('小红','apple');

let boy2 = new Child('小明','orange');
//优点1:能传参
console.log(boy1.name,boy1.like);
console.log(boy2.name,boy2.like);
boy1.arr.push(2);
//优点2:不共享父类的引用属性

boy1.arr.push(2);
console.log(boy1.arr);
console.log(boy2.arr);

//优点3:方法可以复用
console.log(boy1.say===boy2.say)

//优点4:可以继承父类原型上的方法
Parent.prototype.walk= function(){
console.log('我会走路');
}
boy1.walk();//我会走路
console.log(Parent.prototype.constructor); //指向子类的构造函数

组合继承优化2(寄生组合继承)

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
function Parent(name) {
this.name = name||'父亲';
this.arr = [1];
}
Parent.prototype.say = function(){
console.log('hello');
}

function Child(name,like){
Parent.call(this,name);
this.like = like;
}

Child.prototype = Object.create(Parent.prototype) //核心,不改变父类的原型而是新创建一个

Child.prototype.constructor = Child

let boy1 = new Child('小红','apple');

let boy2 = new Child('小明','orange');

let p1 = new Parent('father');
//优点1:能传参
console.log(boy1.name,boy1.like);
console.log(boy2.name,boy2.like);
boy1.arr.push(2);
//优点2:不共享父类的引用属性

boy1.arr.push(2);
console.log(boy1.arr);
console.log(boy2.arr);

//优点3:方法可以复用
console.log(boy1.say===boy2.say)

//优点4:可以继承父类原型上的方法
Parent.prototype.walk= function(){
console.log('我会走路');
}
boy1.walk();//我会走路
//优点5:不改变父类构造函数的指向
console.log(p1.constructor); //指向父类的构造函数
console.log(boy1.constructor) //指向子类的构造函数

总结

仅仅通过原型链继承的话,子类会共享父类的引用属性,而且不能向父类的构造函数传参

而仅仅通过构造函数继承的话,实例不能共享父类相同的方法、不能继承父类原型上的属性

通过组合继承可以规避上面的缺点,但是如果把实例赋值给原型,会创建两份父类实例

而如果把父类原型直接赋给子类原型,会改变父类构造函数的指向

于是想到用Object.create创建一个新的原型再赋值,就避免了两份实例和改变指向的问题


JS继承
https://blog-theta-ten.vercel.app/2021/10/13/JS继承/
作者
Chen
发布于
2021年10月13日
许可协议