继承
# 原型链继承
重点:让新实例的原型等于父类的实例。
特点:实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!)
缺点:
- 新实例无法向父类构造函数传参。
- 继承单一。
- 所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
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
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、3。
- 可以继承多个构造函数属性(call多个)。
- 在子实例中可向父实例传参。
缺点:
- 只能继承父类构造函数的属性。
- 无法实现构造函数的复用。(每次用每次都要重新调用)
- 每个新实例都有父类构造函数的副本,臃肿。
// 父类
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 -> 不在原型链上
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,可以看到它们与原型链继承的区别
# 组合继承
重点:结合了两种模式的优点,传参和复用。
特点:
- 可以继承父类原型上的属性,可以传参,可复用。
- 每个新实例引入的构造函数属性是私有的。
缺点:
- 调用了两次父类构造函数(耗内存),子类的构造函数不重新指向的话会指向父类的构造函数。
- 父类原型属性的修改依然会影响到所有实例的属性
// 父类
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 原型上的属性
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
打印一下实例 m1 和 m2,可以看到结合了前面两种继承方式
# 原型式继承
重点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。
特点:类似于复制一个对象,用函数来包装。
缺点:
- 父类原型属性的修改依然会影响到所有实例的属性
- 每次执行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 属性不会改变
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, '上海');
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
可以看到寄生式继承跟原型式继承没有什么区别,只是多加了个函数,可以接受参数设置私有属性。
# 寄生组合式继承
寄生:在函数内返回对象然后调用。
组合:
- 函数的原型等于另一个实例。
- 在函数中用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 -> 依然可以继承父类原型上的属性
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 关键字,可以用来实现继承。
重点:关键字 class
、extends
、super
,需要了解它们的使用以及它们之间的关系。
优点:实现了面向对象的编程方式,不需要手动写继承方法。
本质: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' -> 不会影响其他实例
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();
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