继承


2020-04-23 上次更新时间:4/29/2022, 9:34:08 AM 0

# 原型链继承

重点:让新实例的原型等于父类的实例。

特点:实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!)

缺点

  1. 新实例无法向父类构造函数传参。
  2. 继承单一。
  3. 所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
function Human() {
    this.level = 'human';
}
Human.prototype.live = 'earth';

function Person(name) {
    this.type = 'person';
}
Person.prototype = new Human(); // 重要!
Person.prototype.constructor = Person; // 将 constructor 指回本身
Person.prototype.color = 'yellow';

function Man() {
    this.sex = 'man';
}
Man.prototype = new Person();  // 重要!
Man.prototype.constructor = Man; // 将constructor指回本身
Man.prototype.like = 'car';

var asian = new Man();
var europe = new Man();

asian instanceof Human; // true -> 处于原型链上
urope instanceof Human; // true -> 处于原型链上

asian.live; // earth -> 属性共享 - 拿到最顶层 Human 的原型的属性
europe.live; // earth -> 属性共享 - 拿到最顶层 Human 的原型的属性

Human.prototype.live = 'china'; // 修改原型的属性

// 所有的实例属性都会修改
asian.live; // china
europe.live; // china
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

打印一下实例 asian,可以看到通过__proto__,可以访问到链路上的任意数据

用流程图能更清晰的看到整个链路关系

简单的回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。 ——摘自《javascript高级程序设计》

# 构造函数继承

重点:用.call()和.apply()将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))

特点

  1. 只继承了父类构造函数的属性,没有继承父类原型的属性。
  2. 解决了原型链继承缺点1、2、3。
  3. 可以继承多个构造函数属性(call多个)。
  4. 在子实例中可向父实例传参。

缺点

  1. 只能继承父类构造函数的属性。
  2. 无法实现构造函数的复用。(每次用每次都要重新调用)
  3. 每个新实例都有父类构造函数的副本,臃肿。
// 父类
function Person(sex) {
    this.sex = sex
    this.age = 18
}
Person.prototype.address = 'earth';

function Man(sex) {
    // 可传参
    Person.call(this, sex)  // 重要!
    // 可以 call 多个
    // Human.call(this, argument)
}

function Woman(sex) {
    Person.call(this, sex)
}

var m1 = new Man('man');
var w1 = new Woman('woman');

console.log(m1); // Man { sex: 'man', age: 18}
console.log(m2); // Woman { sex: 'woman', age: 18}

m1.age = 33; // -> 修改
m2.age; // 18 -> 不会影响到其他实例

m1.address; // undefined -> 父类原型上的属性无法获取
m1 instanceof Person; // false -> 不在原型链上
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

打印一下实例 m1 和 w1,可以看到它们与原型链继承的区别

# 组合继承

重点:结合了两种模式的优点,传参和复用。

特点

  1. 可以继承父类原型上的属性,可以传参,可复用。
  2. 每个新实例引入的构造函数属性是私有的。

缺点

  1. 调用了两次父类构造函数(耗内存),子类的构造函数不重新指向的话会指向父类的构造函数。
  2. 父类原型属性的修改依然会影响到所有实例的属性
// 父类
function Person(sex) {
    this.sex = sex
    this.age = 18
}
Person.prototype.address = 'earth';

function Man(sex) {
    // 可传参
    Person.call(this, sex)  // 重要!借用构造函数
    // 可以 call 多个
    // Human.call(this, argument)
}

Man.prototype = new Person('one more') // 重要!原型链继承

var m1 = new Man('man');
var m2 = new Man('old man');

m1.sex; // man -> 继承了构造函数 Man 的属性
m1.address; // earth -> 继承了 Person 原型上的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

打印一下实例 m1 和 m2,可以看到结合了前面两种继承方式

# 原型式继承

重点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。

特点:类似于复制一个对象,用函数来包装。

缺点

  1. 父类原型属性的修改依然会影响到所有实例的属性
  2. 每次执行create()都生成了一个新相同的对象
// 父类
function Person(sex) {
    this.sex = sex
    this.age = 18
}
Person.prototype.address = 'earth';

function create(instance) {
    function F(){
        // 重要!添加共用实例属性
        this.like = 'money'
    }
    F.prototype = instance; // 重要!原型指向传入的实例
    return new F(); // 重要!返回一个新实例
}

var man = new Person('man'); // 重要!拿到父类的实例
var p1 = create(man);
var p2 = create(man);

p1.address; // earth -> 可以访问父类的原型
p1.like = 'study'; // 改变 p1 的 like 属性
p2.like; // money -> 其他实例的 like 属性不会改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

打印一下实例 p1 和 p2

# 寄生式继承

重点:就是给原型式继承外面套了个壳。

优点:没有创建自定义类型,因为只是套了个壳返回新的对象。

缺点:没用到原型,无法复用。

// 父类
function Person(sex) {
    this.sex = sex
    this.age = 18
}
Person.prototype.address = 'earth';

function create(instance) {
    function F(){
        // 重要!添加共用实例属性
        this.like = 'money'
    }
    F.prototype = instance; // 重要!原型指向传入的实例
    return new F(); // 重要!返回一个新实例
}

var man = new Person('man'); // 重要!拿到父类的实例
// 以上是原型式继承


// 加多个函数包装,传递参数
var chinese = function(obj, native) {
    var people = create(obj);
    // 添加公共实例属性
    people.native = native;
    return people;

}

var p1 = chinese(man, '北京');
var p2 = chinese(man, '上海');
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

可以看到寄生式继承跟原型式继承没有什么区别,只是多加了个函数,可以接受参数设置私有属性。

# 寄生组合式继承

寄生:在函数内返回对象然后调用。

组合

  1. 函数的原型等于另一个实例。
  2. 在函数中用apply或者call引入另一个构造函数,可传参。
// 父类
function Person(sex) {
    // 每个实例都继承了这些属性,并且成了私有属性
    this.sex = sex
    this.age = 18
}
Person.prototype.address = 'earth';

// 寄生
function create(instance) {
    function F(){
        // 这里的属性变成了原型上的属性
        this.like = 'money'
    }
    F.prototype = instance;
    var f1 = new F();
    return f1;
}

var p = create(Person.prototype);
p.constructor = Man; // 修复下实例,指回本身

// 组合
function Man(sex) {
    Person.call(this, sex)
}

// 重点!
Man.prototype = p;

var m1 = new Man('man');
var m2 = new Man('old man');

m1.age = 100; // 修改父类继承下来的属性
m2.age; // 18 -> 其他实例没被影响

m1.address; // address -> 依然可以继承父类原型上的属性
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

打印一下实例 m1、m2,可以看到跟之前继承方式的区别

用流程图能更清晰的看到整个链路关系

# class继承

ES6 新增了 class 关键字,可以用来实现继承。

重点:关键字 classextendssuper,需要了解它们的使用以及它们之间的关系。

优点:实现了面向对象的编程方式,不需要手动写继承方法。

本质:class 继承其实是原型链继承的语法糖。

class Human {
    // constructor 构造函数内,被后代继承并且成为实例属性
    constructor () {
        this.level = 'human';
        this.sayHi = () => {
            console.log('human hi')
        }
    }

    // 变量被继承并且成为实例属性
    live = 'earth'

    // 函数挂载在原型上,但是 this 指向实例的构造函数 Man
    changeLive(live) {
        this.live = live
    }
}

class Person extends Human {
    // 父类可接受参数
    constructor (age) {
        super(); // 在使用 this 之前必须调用一次 super()
        this.age = age;
    }

    changeAge(age) {
        this.age = age;
    }
}

class Man extends Person {
    sex = 'man'
    constructor (sex) {
        // 可给父类传参
        super(18);
        this.sex = sex || this.sex;
    }

    changeSex(sex) {
        this.sex = sex
    }
}

var m1 = new Man();
var m2 = new Man()

m1.changeLive('china'); // 调用父类 Human 的方法修改属性
m2.live; // 'earth' -> 不会影响其他实例

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

打印一下实例,可以看到 class 继承方式和寄生组合继承非常像。父类的属性都被实例继承,并且能通过原型找到父类的方法。

尝试把上述例子转换为 es5,就可以知道为什么说 class 继承是原型链继承的语法糖了。下面注释以 Man 和 Person 继承为主。

小提示:上面的例子是使用Typescript的在线编译,编译的 。

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        // 设置原型 Object.setPrototypeOf(要设置其原型的对象, 该对象的新原型)
        extendStatics = Object.setPrototypeOf ||
            (
                                        
                { __proto__: [] } instanceof Array && 
                // 设置 Man 的 __proto__ 指向 Person
                function (d, b) { d.__proto__ = b; }
            ) ||
            function (d, b) { 
                // 循环 Person 属性,赋值给 Man
                for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 
            };
        return extendStatics(d, b);
    }
    return function (d, b) {
        extendStatics(d, b);
        // 修复 constructor 指向
        function __() { this.constructor = d; }
        // 修改原型指向
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var Human = /** @class */ (function () {
    function Human() {
        this.live = 'earth';
        this.level = 'human';
        this.sayHi = function () {
            console.log('human hi');
        };
    }
    Human.prototype.changeLive = function (live) {
        this.live = live;
    };
    return Human;
}());
var Person = /** @class */ (function (_super) {
    __extends(Person, _super);
    function Person(age) {
        
        var _this = _super.call(this) || this;
        _this.age = age;
        return _this;
    }
     // 把方法挂载到原型上
    Person.prototype.changeAge = function (age) {
        this.age = age;
    };
    // 返回一个函数
    return Person;
}(Human));
var Man = /** @class */ (function (_super) {
    // _super 就是传入的 Person

    // 把 Man 和 Person 进行关联
    __extends(Man, _super);
    function Man(sex) {
        // 通过 call,改变 Person 内的 this 指向
        var _this = _super.call(this, 18) || this;
        _this.sex = 'man';
        _this.sex = sex || _this.sex;
        return _this;
    }
    // 把方法挂载到原型上
    Man.prototype.changeSex = function (sex) {
        this.sex = sex;
    };
    return Man;
}(Person));
var m1 = new Man();
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
上次更新时间: 4/29/2022, 9:34:08 AM