详解Javascript的继承实现
Posted moon3
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解Javascript的继承实现相关的知识,希望对你有一定的参考价值。
我最早掌握的在js中实现继承的方法是在w3school学到的混合原型链和对象冒充的方法,在工作中,只要用到继承的时候,我都是用这个方法实现。它的实现简单,思路清晰:用对象冒充继承父类构造函数的属性,用原型链继承父类prototype 对象的方法,满足我遇到过的所有继承的场景。正因如此,我从没想过下次写继承的时候,我要换一种方式来写,才发现在js里面,继承机制也可以写的如此贴近java这种后端语言的实现,确实很妙!所以我想在充分理解他博客的思路下,实现一个自己今后用得到的一个继承库。
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
40
41
42
43
44
45
46
47
48
49
50
51
|
//父类构造函数
function Employee(name, salary) {
//实例属性:姓名
this.name = name;
//实例属性:薪资
this.salary = salary;
}
//通过字面量对象设置父类的原型,给父类添加实例方法
Employee.prototype = {
//由于此处添加实例方法时也是通过修改父类原型处理的,
//所以必须修改父类原型的constructor指向,避免父类实例的constructor属性指向Object函数
constructor: Employee,
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + ‘‘s salary is ‘ + this.getSalary() + ‘.‘;
}
}
//子类构造函数
function Manager(name, salary, percentage) {
//对象冒充,实现属性继承(name, salary)
Employee.apply(this, [name, salary]);
//实例属性:提成
this.percentage = percentage;
}
//将父类的一个实例设置为子类的原型,实现方法继承
Manager.prototype = new Employee();
//修改子类原型的constructor指向,避免子类实例的constructor属性指向父类的构造函数
Manager.prototype.constructor = Manager;
//给子类添加新的实例方法
Manager.prototype.getSalary = function () {
return this.salary + this.salary * this.percentage;
}
var e = new Employee(‘jason‘, 5000);
var m = new Manager(‘tom‘, 8000, 0.15);
console.log(e.toString()); //jason‘s salary is 5000.
console.log(m.toString()); //tom‘s salary is 9200.
console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //true
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false
|
从结果上来说,这种继承实现方式没有问题,Manager的实例同时继承到了Employee类的实例属性和实例方法,并且通过instanceOf运算的结果也都正确。但是从代码组织和实现细节层面,这种方法还有以下几个问题:
1)代码组织不够优雅,继承实现的关键部分的逻辑是通用的,都是如下结构:
1
2
3
4
5
6
7
8
9
10
11
|
//将父类的一个实例设置为子类的原型,实现方法继承
SubClass.prototype = new SuperClass();
//修改子类原型的constructor指向,避免子类实例的constructor属性指向父类的构造函数
SubClass.prototype.constructor = SubClass;
//给子类添加新的实例方法
SubClass.prototype.method1 = function() {
}
SubClass.prototype.method2 = function() {
}
SubClass.prototype.method3 = function() {
}
|
这段代码缺乏封装。另外在添加子类的实例方法时,不能通过SubClass.prototype = { method1: function() {} }这种方式去设置,否则就把子类的原型整个又修改了,继承就无法实现了,这样每次都得按SubClass.prototype.method1 = function() {} 的结构去写,代码看起来很不连续。
解决方式:利用模块化的方式,将通用的逻辑封装起来,对外提供简单的接口,只要按照约定的接口调用,就能够简化类的构建与类的继承。具体实现请看后面的内容介绍,暂时只能提供理论的说明。
2)在给子类的原型设置成父类的实例时,调用的是new SuperClass(),这是对父类构造函数的无参调用,那么就要求父类必须有无参的构造函数。可是在javascript中,函数无法重载,所以父类不可能提供多个构造函数,在实际业务中,大部分场景下父类构造函数又不可能没有参数,为了在唯一的一个构造函数中模拟函数重载,只能借助判断arguments.length来处理。问题就是,有时候很难保证每次写父类构造函数的时候都会添加arguments.length的判断逻辑。这样的话,这个处理方式就是有风险的。要是能把构造函数里的逻辑抽离出来,让类的构造函数全部是无参函数的话,这个问题就很好解决了。
解决方式:把父类跟子类的构造函数全部无参化,并且在构造函数内不写任何逻辑,把构造函数的逻辑都迁移到init这个实例方法,比如前面给出的Employee和Manager的例子就能改造成下面这个样子:
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
|
//无参无逻辑的父类构造函数
function Employee() {}
Employee.prototype = {
constructor: Employee,
//把构造逻辑搬到init方法中来
init: function (name, salary) {
this.name = name;
this.salary = salary;
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + ‘‘s salary is ‘ + this.getSalary() + ‘.‘;
}
};
//无参无逻辑的子类构造函数
function Manager() {}
Manager.prototype = new Employee();
Manager.prototype.constructor = Manager;
//把构造逻辑搬到init方法中来
Manager.prototype.init = function (name, salary, percentage) {
//借用父类的init方法,实现属性继承(name, salary)
Employee.prototype.init.apply(this, [name, salary]);
this.percentage = percentage;
};
Manager.prototype.getSalary = function () {
return this.salary + this.salary * this.percentage;
};
|
用init方法来完成构造功能,就可以保证在设置子类原型时(Manager.prototype = new Employee()),父类的实例化操作一定不会出错,唯一不好的是在调用类的构造函数来初始化实例的时候,必须在调用构造函数后手动调用init方法来完成实际的构造逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
|
var e = new Employee();
e.init(‘jason‘, 5000);
var m = new Manager();
m.init(‘tom‘, 8000, 0.15);
console.log(e.toString()); //jason‘s salary is 5000.
console.log(m.toString()); //tom‘s salary is 9200.
console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //true
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false
|
要是能把这个init的逻辑放在构造函数内部就好了,可是这样的话就会违背前面说的构造函数无参无逻辑的原则。换一种方式来考虑,这个原则的目的是为了保证在实例化父类作为子类原型的时候用网盘资源www.sosuopan.com,调用父类的构造函数不会出错,那么就可以稍微打破一下这个原则,在类的构造函数里添加少量的并且一定不会有问题的逻辑来解决:
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
|
//添加一个全局标识initializing,表示是否正在进行子类的构建和类的继承
var initializing = false;
//可自动调用init方法的父类构造函数
function Employee() {
if (!initializing) {
this.init.apply(this, arguments);
}
}
Employee.prototype = {
constructor: Employee,
//把构造逻辑搬到init方法中来
init: function (name, salary) {
this.name = name;
this.salary = salary;
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + ‘‘s salary is ‘ + this.getSalary() + ‘.‘;
}
};
//可自动调用init方法的子类构造函数
function Manager() {
if (!initializing) {
this.init.apply(this, arguments);
}
}
//表示开始子类的构建和类的继承
initializing = true;
//此时调用new Emplyee(),并不会调用Employee.prototype.init方法
Manager.prototype = new Employee();
Manager.prototype.constructor |