JS中的继承是原型的继承,而不是改变构造函数的原型

原型的继承并不是改变构造函数的原型

1
2
3
4
5
6
7
8
9
10
11
function User () {};

User.prototype.name = function () {
console.log("User name method");
}

const xx = new User();

console.log(xx);

xx.name(); // User name method

此时的关系如下:

image-20230629195310026

如果此时再有一个Admin的构造函数,它也想要用name方法,那么这里可以直接让它的原型改为User的原型就能拿到了。

1
2
3
4
5
6
7
8
function Admin () {};

// 直接改变Admin的原型为User的原型
Admin.prototype = User.prototype;

const a = new Admin();

a.name(); // User name method

现在就变成了这种情况:

image-20230629200449004

之前的原型中我们知道,实例化后的对象a的原型是指向Admin.prototype的,而这里的Admin.prototypeUser.prototype是同一个,所以,a的原型也是指向User.prototype的,所以这个实例化的对象a也是可以使用name方法的。

但是这种方式是直接改变了构造函数的原型,也就说这两个构造函数指向的是同一个对象,那么这个时候如果这两个需要的同一个方法需要进行不同的执行内容就无法实现了。

如果此时有一个新的构造函数Member,它的原型也指向了User的原型。此时如果给AdminMember添加一个role的方法。那么这个同名的方法都是添加到了User.prototype上,后面的方法会覆盖掉前面的,所以这样的话就出现了错误。

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
function User () {};

User.prototype.name = function () {
console.log("User name method");
}

const xx = new User();

console.log(xx);

xx.name(); // User name method

function Admin () {};

Admin.prototype = User.prototype;

Admin.prototype.role = function () {
console.log("Admin role method");
};

function Member () {};

Member.prototype = User.prototype;

Member.prototype.role = function () {
console.log("Member role method");
};

const a = new Admin();

a.role(); // Member role method

关系如下:

image-20230629201805506

【结论】:所以这里说的原型的继承并不是改变构造函数的原型

继承是原型的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 function User () {};

User.prototype.name = function () {
console.log("User name method");
}

function Admin () {};

console.log(Admin.prototype.__proto__ === Object.prototype); // true

Admin.prototype.role = function () {
console.log("Admin role method");
};

function Member () {};

Member.prototype.role = function () {
console.log("Member role method");
};
image-20230630152858913

正常情况下,这三个构造函数的原型关系如上,自己的prototype的原型__proto__都是指向Object.prototype的。

然后如果想要让Admin构造函数继承Username方法,不应该直接改变构造函数的原型,而是继承原型。

1
Admin.prototype.__proto__ = User.prototype;

这样的话原来的关系就变成了这样:

image-20230630154510883

这样的话,之前写的如果需要在AdminMember上添加的role方法就可以加在自己原型上了。

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
function User () {};

User.prototype.name = function () {
console.log("User name method");
}

const xx = new User();

xx.name(); // User name method

function Admin () {};
console.log(Admin.prototype.__proto__ === Object.prototype); // true

Admin.prototype.__proto__ = User.prototype;

Admin.prototype.role = function () {
console.log("Admin role method");
};

function Member () {};

Member.prototype.__proto__ = User.prototype;

Member.prototype.role = function () {
console.log("Member role method");
};

const m = new Member();

m.role(); // Member role method

m.name(); // User name method

const a = new Admin();

a.role(); // Admin role method

a.name(); // User name method

此时的关系就变成了这样:

image-20230630155028985

这样之后既实现了继承User的name方法,也实现了自己的方法不干扰到其他函数。

【结论】:实际上可以说继承就是将一个构造函数的原型的原型改变为指向其他构造函数的原型

这种方式还有另一种写法:

1
Admin.prototype = Object.create(User.prototype);
image-20230630161007102

这样就是将Admin的原型指向了一个原型为User.prototype的对象上。其实这个对象和__proto__有同样的意思。

不过这里有一个需要注意的一点,在将role方法声明的位置换一下之后,如下:

1
2
3
4
5
Admin.prototype.role = function () {
console.log("Admin role method");
};

Admin.prototype = Object.create(User.prototype);

如果将方法的声明放在前面之后,这样的话在进行实例化调用role方法就会报错,之前的那种写法是不会报错的。

那么这是为什么呢?

这是因为在将方法声明后,这个时候的方法已经挂在 Admin.prototype上了。

image-20230630161746143

但是在之后又改变了Admin的指向为另一个对象,这个时候role方法就不在了。

但是第一种写法是将原型的原型改变了,并不会影响构造函数的原型,所以第一种方式没有这种问题。

继承对constructor的影响

1
2
3
4
5
6
7
8
9
10
11
12
function User () {};

User.prototype.name = function () {

console.log("User name method");
};

function Admin () {};

const a = new Admin();

console.log(a);

image-20230630172514057

正常情况下,实例化后的a对象是有constructor属性的。

但是如果使用上面第二种方式继承后,就会丢掉这个属性,这是因为直接改变了构造函数的prototype

1
Admin.prototype = Object.create(User.prototype);
image-20230630172336289

当使用上面这种方式继承后会发现,Admin构造函数的原型里面就不存在constructor属性了。这个就和之前所说的的原型链里面的如果将prototype重构后就需要重新指定一下constructor属性,将它指向自己。

1
Admin.prototype.constructor = Admin;
image-20230630172838215

这样的话就和之前的一样了,可以实现利用实例化后的对象的constructor找到原来的构造函数。

但是这样还有一个问题。

先来看一下该constructor属性的属性特征:

1
console.log(Object.getOwnPropertyDescriptors(Admin.prototype));
image-20230630173242222

这里可以看到添加的constructor属性的enumerabletrue,也就是说此时的constructor属性是允许遍历的。我们这里使用for...in遍历一下对象看看。

for...in是可以遍历对象原型的。MDN上是如此描述的:

for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。

先来看一下正常对象的遍历:

1
2
3
4
5
6
7
8
function User () {};

const user = new User();

for (const key in user) {

console.log(key); // name
};

再来看看修改之后的遍历结果:

1
2
3
4
5
6
const a = new Admin();

for (const key in a) {

console.log(key); // constructor role name
};

他会将原型上继承User的name方法和自身的role方法都会遍历出来,并且还会遍历到constructor。这是有问题的。

知道问题后那么解决起来也就简单了。可以在添加constructor属性的时候直接定义属性。

1
2
3
4
Object.defineProperty(Admin.prototype, 'constructor', {
value: Admin,
enumerable: false
});

这样就可以了。

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
26
27
function User () {};

User.prototype.show = function () {
console.log(this);
console.log(this.description());
};

function Admin () {};

Admin.prototype = Object.create(User.prototype);

Admin.prototype.description = function () {
return "Admin description";
};

function Member () {};

Member.prototype = Object.create(User.prototype);

Member.prototype.description = function () {
return "Member description";
};

for (const obj of [new Admin(), new Member()]) {

obj.show(); // Admin description Member description
};

使用父类构造函数初始属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function User (name, age) {

this.name = name;

this.age = age;
};

User.prototype.show = function () {

console.log(this.name, this.age);
};

function Admin(...args) {

User.apply(this, args);
};

Admin.prototype = Object.create(User.prototype);

const admin = new Admin("萧兮", 18);

admin.show(); // 萧兮 18

如果这里是在Admin函数里面进行赋值的话,那么以后每加一个构造函数都必须重复添加一下,这就没有利用到继承的优点。所以这里可以直接在父级构造函数中初始化属性。

原型工厂封装继承

由于如果一直按照上面那种方式来写继承的话特别麻烦,而且代码量大,不优雅。所以需要进行封装。

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
// 封装继承通用
function extend (sub, sup) {

sub.prototype = Object.create(sup.prototype);

Object.defineProperty(sub.prototype, "constructor", {
value: sub,
enumerable: false
});
}

function User (name, age) {

this.name = name;

this.age = age;
};

User.prototype.show = function () {

console.log(this.name, this.age);
};

function Admin(...args) {

User.apply(this, args);
};

extend(Admin, User);

const admin = new Admin("萧兮", 18);

admin.show();

封装后调用继承就特别方便了。

extend(目标对象, 继承的对象)

对象工厂派生对象实现继承

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
function User (name, age) {

this.name = name;

this.age = age;
};

User.prototype.show = function () {

console.log(this.name, this.age);
};

function admin (name, age) {

const instance = Object.create(User.prototype);

User.call(instance, name, age);

instance.role = function () {

console.log('role');
};

return instance;
};

const a = new admin("萧兮", 18);

a.show(); // 萧兮 18

a.role(); // role

这里的admin不再是构造函数了,而是利用工厂方法admin来生成一个对象instance来实现继承。

多继承

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
26
27
28
29
30
31
32
33
function extend (sub, sup) {

sub.prototype = Object.create(sup.prototype);

Object.defineProperty(sub.prototype, "constructor", {
value: sub,
enumerable: false
});
}

function User (name, age) {

this.name = name;

this.age = age;
};

User.prototype.show = function () {

console.log(this.name, this.age);
};

function Request () {};

Request.prototype.ajax = function () {

console.log('请求后台地址');
};

function Admin(...args) {

User.apply(this, args);
};

如果此时Admin构造函数想要继承User的同时也想要继承Request使用ajax方法是不能实现的,那么我们怎么解决呢,我们可以将User继承Request来实现达到这个目的。在User的声明下面添加一个继承extend(User, Request);。这样就可以使用Request的方法了。

这里就能看出这种方式有个弊端,如果业务上有许多这样的场景呢,比如现在又有一个需要使用一个查看积分的对象,那只能继续再继承才能实现。关系如下:

image-20230701171858307

那么在这种情况下如果Admin不想使用其中一个但是其他的又需要使用的话就会出问题了,因为每次继承都会影响所有的构造函数。

那么怎么解决呢?使用mixin混合功能。

就是说写好一些类,这些类给其他类提供服务,但是并不是使用继承。

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
function extend (sub, sup) {

sub.prototype = Object.create(sup.prototype);

Object.defineProperty(sub.prototype, "constructor", {
value: sub,
enumerable: false
});
};

// 将之前需要使用的方法转换成对象,然后方法转换为对象中的属性
const Address = {
getAddress : function () {
console.log('获取地址');
}
};

const Credit = {
total: function () {
console.log('积分查看');
}
};

const Request = {
ajax: function() {
console.log('请求后台地址');
}
};

// 剩下的就和之前的一样了,Admin只继承了User
function User (name, age) {

this.name = name;

this.age = age;
};

User.prototype.show = function () {

console.log(this.name, this.age);
};

function Admin(...args) {

User.apply(this, args);
};

extend(Admin, User);

// 现在就可以根据需要自己引入自己想要使用的方法了 : Admin.prototype.ajax = Request.ajax;
// 但是这种方式有个问题,当存在很多方法时这种写法就不太现实了
// js提供了一个属性合并的: Object.assiign()
Object.assign(Admin.prototype, Request, Credit, Address);

// 这个时候就可以正常使用了
const admin = new Admin("萧兮", 18);

admin.show(); // 萧兮 18

admin.ajax(); // 请求后台地址

admin.total(); // 积分查看

admin.getAddress(); // 获取地址

如果这个时候新加了一个会员对象,不能使用获取地址的功能的话,就可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Member(...args) {

User.apply(this, args);
};

extend(Member, User);

Object.assign(Member.prototype, Request, Credit);

const member = new Member("李四", 43);

member.show(); // 李四 43

member.ajax(); // 请求后台地址

member.total(); // 积分查看

根据自己的需要来添加不同的方法属性。

【结论】:其实利用的是对象里面可以压入属性的原理。

对象的原型其实就是一个对象,那么它就可以压入属性方法。

mixin的内部继承以及super关键字

1
2
3
4
5
6
7
8
const Credit = {

__proto__: Request,

total() {
console.log(this.__proto__.ajax() + '积分查看');
}
};

就是给对象内部添加一个__proto__的指定,然后里面就可以使用该指定对象的方法了。

上面的代码可以换一个写法:

1
2
3
4
5
6
7
8
const Credit = {

__proto__: Request,

total() {
console.log(super.ajax() + '积分查看');
}
};

可以使用super关键字,其实super就相当于this.__proto__。指的是当前对象的原型。

【注】:如果这里的total声明方式使用的是原始的函数声明方式,也就是说使用的是function来声明的话是不能使用super关键字的,会报如下错误:

image-20230703153952460

这是因为super仅在方法内有效。而function声明的是“普通”的函数,并不是方法,因为它不遵循方法语法。

方法和正常函数之间的区别:

  • 方法有一个”HomeObject”,允许他们使用super
  • 方法不能构造,也就是说不能用new关键字来调用他们。
  • 方法的名称不会成为方法范围内的绑定(与命名函数表达式不同)。

【参考文章】:https://www.codenong.com/39263358/

__END__