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();
|
此时的关系如下:
如果此时再有一个Admin
的构造函数,它也想要用name
方法,那么这里可以直接让它的原型改为User
的原型就能拿到了。
1 2 3 4 5 6 7 8
| function Admin () {};
Admin.prototype = User.prototype;
const a = new Admin();
a.name();
|
现在就变成了这种情况:
之前的原型中我们知道,实例化后的对象a
的原型是指向Admin.prototype
的,而这里的Admin.prototype
和User.prototype
是同一个,所以,a
的原型也是指向User.prototype
的,所以这个实例化的对象a
也是可以使用name
方法的。
但是这种方式是直接改变了构造函数的原型,也就说这两个构造函数指向的是同一个对象,那么这个时候如果这两个需要的同一个方法需要进行不同的执行内容就无法实现了。
如果此时有一个新的构造函数Member
,它的原型也指向了User的原型。此时如果给Admin
和Member
添加一个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();
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();
|
关系如下:
【结论】:所以这里说的原型的继承并不是改变构造函数的原型
。
继承是原型的继承
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);
Admin.prototype.role = function () { console.log("Admin role method"); };
function Member () {};
Member.prototype.role = function () { console.log("Member role method"); };
|
正常情况下,这三个构造函数的原型关系如上,自己的prototype
的原型__proto__
都是指向Object.prototype
的。
然后如果想要让Admin
构造函数继承User
的name
方法,不应该直接改变构造函数的原型,而是继承原型。
1
| Admin.prototype.__proto__ = User.prototype;
|
这样的话原来的关系就变成了这样:
这样的话,之前写的如果需要在Admin
和Member
上添加的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();
function Admin () {}; console.log(Admin.prototype.__proto__ === Object.prototype);
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();
m.name();
const a = new Admin();
a.role();
a.name();
|
此时的关系就变成了这样:
这样之后既实现了继承User
的name方法,也实现了自己的方法不干扰到其他函数。
【结论】:实际上可以说继承
就是将一个构造函数的原型的原型改变为指向其他构造函数的原型
。
这种方式还有另一种写法:
1
| Admin.prototype = Object.create(User.prototype);
|
这样就是将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
上了。
但是在之后又改变了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);
|

正常情况下,实例化后的a对象是有constructor
属性的。
但是如果使用上面第二种方式继承后,就会丢掉这个属性,这是因为直接改变了构造函数的prototype
。
1
| Admin.prototype = Object.create(User.prototype);
|
当使用上面这种方式继承后会发现,Admin
构造函数的原型里面就不存在constructor
属性了。这个就和之前所说的的原型链里面的如果将prototype重构后就需要重新指定一下constructor
属性,将它指向自己。
1
| Admin.prototype.constructor = Admin;
|
这样的话就和之前的一样了,可以实现利用实例化后的对象的constructor
找到原来的构造函数。
但是这样还有一个问题。
先来看一下该constructor
属性的属性特征:
1
| console.log(Object.getOwnPropertyDescriptors(Admin.prototype));
|
这里可以看到添加的constructor
属性的enumerable
为true
,也就是说此时的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); };
|
再来看看修改之后的遍历结果:
1 2 3 4 5 6
| const a = new Admin();
for (const key in a) {
console.log(key); };
|
他会将原型上继承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(); };
|
使用父类构造函数初始属性
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();
|
如果这里是在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();
a.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
的方法了。
这里就能看出这种方式有个弊端,如果业务上有许多这样的场景呢,比如现在又有一个需要使用一个查看积分的对象,那只能继续再继承才能实现。关系如下:
那么在这种情况下如果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('请求后台地址'); } };
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);
Object.assign(Admin.prototype, Request, Credit, Address);
const admin = new Admin("萧兮", 18);
admin.show();
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();
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
关键字的,会报如下错误:
这是因为super
仅在方法内有效。而function
声明的是“普通”的函数,并不是方法,因为它不遵循方法语法。
方法和正常函数之间的区别:
- 方法有一个”HomeObject”,允许他们使用
super
。
- 方法不能构造,也就是说不能用
new
关键字来调用他们。
- 方法的名称不会成为方法范围内的绑定(与命名函数表达式不同)。
【参考文章】:https://www.codenong.com/39263358/
__END__