瞭解問題以前,先看看它的具體實現:javascript
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運算的結果也都正確。可是從代碼組織和實現細節層面,這種方法還有如下幾個問題:html
1)代碼組織不夠優雅,繼承實現的關鍵部分的邏輯是通用的,都是以下結構:java
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() {} 的結構去寫,代碼看起來很不連續。git
解決方式:利用模塊化的方式,將通用的邏輯封裝起來,對外提供簡單的接口,只要按照約定的接口調用,就可以簡化類的構建與類的繼承。具體實現請看後面的內容介紹,暫時只能提供理論的說明。github
2)在給子類的原型設置成父類的實例時,調用的是new SuperClass(),這是對父類構造函數的無參調用,那麼就要求父類必須有無參的構造函數。但是在javascript中,函數沒法重載,因此父類不可能提供多個構造函數,在實際業務中,大部分場景下父類構造函數又不可能沒有參數,爲了在惟一的一個構造函數中模擬函數重載,只能藉助判斷arguments.length來處理。問題就是,有時候很難保證每次寫父類構造函數的時候都會添加arguments.length的判斷邏輯。這樣的話,這個處理方式就是有風險的。要是能把構造函數裏的邏輯抽離出來,讓類的構造函數所有是無參函數的話,這個問題就很好解決了。web
解決方式:把父類跟子類的構造函數所有無參化,而且在構造函數內不寫任何邏輯,把構造函數的邏輯都遷移到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方法來完成實際的構造邏輯:bootstrap
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的邏輯放在構造函數內部就行了,但是這樣的話就會違背前面說的構造函數無參無邏輯的原則。換一種方式來考慮,這個原則的目的是爲了保證在實例化父類做爲子類原型的時候,調用父類的構造函數不會出錯,那麼就能夠稍微打破一下這個原則,在類的構造函數裏添加少許的而且必定不會有問題的邏輯來解決:app
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 = Manager;
//表示結束子類的構建和類的繼承,以後調用new Employee或new Manager都會自動調用init實例方法
initializing = false;
//把構造邏輯搬到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;
};
|
調用結果仍然和前面的例子同樣。可是這個實現還有一個小問題,它引入了一個全局變量initializing,要是能把引入這個全局變量就行了,這個其實很好解決,只要咱們把關於類的構建跟繼承,封裝成一個模塊,而後把這個變量放在模塊的內部,就沒有問題了。模塊化
3)在構造子類的時候,是把子類的原型設置成了父類的一個實例,這個是不符合語義的,繼承應該發生在類與類之間,而不是類與實例之間。之因此要用父類的一個實例來做爲子類的原型:
1
|
SubClass.prototype = new SuperClass();
|
徹底是由於父類的這個實例,指向父類的原型,而子類的實例又會指向子類的原型,因此最終子類的實例就能經過原型鏈訪問到父類原型上的方法。這個作法雖然能實現實例方法的繼承,可是它不符合語義,並且它還有一個很大的問題就是會增長原型鏈的長度,致使子類在調用父類方法時,必須經過原型鏈的查找到父類的方法才行。要是繼承層次較深,會對js的執行性能有些影響。
解決方式:在解決這個問題以前,先想一想繼承能幫咱們解決什麼問題:從父類複用已有的實例屬性和實例方法。在javascript面向對象編程中,一直有一個原則就是,實例屬性都寫在構造函數或者實例方法裏面,實例方法寫在原型上面,也就是說類的原型,按照這個原則來講,就是用來寫實例方法的,並且是隻用來寫實例方法,那麼咱們徹底能夠在構建子類時,經過複製的方式將父類原型的全部方法所有添加到子類的原型上,不必定要把父類的一個實例設置成子類的原型,這樣就能將原型鏈的長度大大地縮短,藉助一個簡短的copy函數,咱們就能輕鬆對前面的代碼進行改造:
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
|
//用來複制父類原型,因爲父類原型上約定只寫實例方法,因此複製的時候沒必要擔憂引用的問題
var copy = function (source) {
var target = {};
for (var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
}
function Employee() {
this.init.apply(this, arguments);
}
Employee.prototype = {
constructor: Employee,
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() {
this.init.apply(this, arguments);
}
//將父類的原型方法複製到子類的原型上
Manager.prototype = copy(Employee.prototype);
//子類仍是須要修改constructor指向,由於從父類原型複製出來的對象的constructor仍是指向父類的構造函數
Manager.prototype.constructor = Manager;
Manager.prototype.init = function (name, salary, percentage) {
Employee.prototype.init.apply(this, [name, salary]);
this.percentage = percentage;
};
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); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false
|
這麼作了之後,當調用m.toString的時候其實調用的是Manager類自身原型上的方法,而不是Employee類的實例方法,縮短了在原型鏈上查找方法的距離。這個作法在性能上有很大的優勢,但很差的是經過原型鏈維持的繼承關係其實已經斷了,子類的原型和子類的實例都沒法再經過js原生的屬性訪問到父類的原型,因此這個調用console.log(m instanceof Employee)輸出的是false。不過跟性能比起來,這個均可以不算問題:一是instanceOf的運算,幾乎在javascript的開發裏面用不到,至少我是沒碰到過;二是經過複製方式徹底可以把父類的實例方法繼承下來,這就已經達到了繼承的最大目的。
這個方法還有一個額外的好處是,解決了第2個問題最後提到的引入initializing全局變量的問題,若是是複製的話,就不須要在構建繼承關係時,去調用父類的構造函數,那麼也就沒有必要在構造函數內先判斷initializing才能去調用init方法,上面的代碼中就已經去掉了initializing這個變量的處理。
4)在子類的構造函數和實例方法內若是想要調用父類的構造函數或者方法,顯得比較繁瑣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
function SuperClass() {}
SuperClass.prototype = {
constructor: SuperClass,
method1: function () {}
}
function SubClass() {
//調用父類構造函數
SuperClass.apply(this);
}
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.prototype.method1 = function () {
//調用父類的實例方法
SuperClass.prototype.method1.apply(this, arguments);
}
SubClass.prototype.method2 = function () {}
SubClass.prototype.method3 = function () {}
|
每次都得靠apply借用方法來處理。要是能改爲以下的調用就好用多了:
1
2
3
4
5
6
7
8
9
10
11
|
function SubClass() {
//調用父類構造函數
this.base();
}
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.prototype.method1 = function() {
//調用父類的實例方法
this.base();
}
|
解決方式:若是要在每一個實例方法裏,都能經過this.base()調用父類原型上相應的方法,那麼this.base就必定不是一個固定的方法,須要在每一個實例方法執行期間動態地將this.base指定爲父類原型的同名方法,可以作到這個實現的方式,就只有經過方法代理了,前面的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
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
72
73
74
75
76
77
78
79
80
|
//用來複制父類原型,因爲父類原型上約定只寫實例方法,因此複製的時候沒必要擔憂引用的問題
var copy = function (source) {
var target = {};
for (var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
};
function Employee() {
this.init.apply(this, arguments);
}
Employee.prototype = {
constructor: Employee,
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() {
//必須在每一個實例中添加baseProto屬性,以便實例內部能夠經過這個屬性訪問到父類的原型
//由於copy函數致使原型鏈斷裂,沒法經過原型鏈訪問到父類的原型
this.baseProto = Employee.prototype;
this.init.apply(this, arguments);
}
Manager.prototype = copy(Employee.prototype);
//子類仍是須要修改constructor指向,由於從父類原型複製出來的對象的constructor仍是指向父類的構造函數
Manager.prototype.constructor = Manager;
Manager.prototype.init = (function (name, func) {
return function () {
//記錄實例原有的this.base的值
var old = this.base;
//將實例的this.base指向父類的原型的同名方法
this.base = this.baseProto[name];
//調用子類自身定義的init方法,也就是func參數傳遞進來的函數
var ret = func.apply(this, arguments);
//還原實例原有的this.base的值
this.base = old;
return ret;
}
})('init', function (name, salary, percentage) {
//經過this.base調用父類的init方法
//這個函數真實的調用位置是var ret = func.apply(this, arguments);
//當調用Manager實例的init方法時,其實不是調用的這個函數
//而是調用上面那個匿名函數裏面return的匿名函數
//在return的匿名函數裏,先把this.base指向爲了父類原型的同名函數,而後在調用func
//func內部再經過調用this.base時,就能調用父類的原型方法。
this.base(name, salary);
this.percentage = percentage;
});
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); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false
|
經過代理的方式,就解決了在在實例方法內部經過this.base調用父類原型同名方法的問題。但是在實際狀況中,每一個實例方法都有可能須要調用父類的實例,那麼每一個實例方法都要添加一樣的代碼,顯然這會增長不少麻煩,好在這部分的邏輯也是一樣的,咱們能夠把它抽象一下,最後都放到模塊化的內部去,這樣就能簡化代理的工做。
5)未考慮靜態屬性和靜態方法。儘管靜態成員是不須要繼承的,但在有些場景下,咱們仍是須要靜態成員,因此得考慮靜態成員應該添加在哪裏。
解決方式:因爲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
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
72
73
74
75
76
77
|
var copy = function (source) {
var target = {};
for (var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
};
function Employee() {
this.init.apply(this, arguments);
}
//添加一個靜態屬性
Employee.idCounter = 1;
//添加一個靜態方法
Employee.getId = function () {
return Employee.idCounter++;
};
Employee.prototype = {
constructor: Employee,
init: function (name, salary) {
this.name = name;
this.salary = salary;
//調用靜態方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + ''s salary is ' + this.getSalary() + '.';
}
};
function Manager() {
this.baseProto = Employee.prototype;
this.init.apply(this, arguments);
}
Manager.prototype = copy(Employee.prototype);
Manager.prototype.constructor = Manager;
Manager.prototype.init = (function (name, func) {
return function () {
var old = this.base;
this.base = this.baseProto[name];
var ret = func.apply(this, arguments);
this.base = old;
return ret;
}
})('init', function (name, salary, percentage) {
this.base(name, salary);
this.percentage = percentage;
});
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); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false
console.log(m.id); //2
console.log(e.id); //1
|
最後的兩行輸出了正確的實例id,而這個id是經過Employee類的靜態方法生成的。在java的面向對象編程中,子類跟父類均可以定義靜態成員,在調用的時候還存在覆蓋的問題,在js裏面,由於受語言的限制,自定義的靜態成員不可能實現全面的面向對象功能,就像上面這種,可以給類提供一些公共的屬性和公共方法,就已經足夠了。
從第1部分的分析能夠看出,在js裏面,類的構建與繼承,有不少通用的邏輯,徹底能夠把這些邏輯封裝成一個單獨的模塊,造成一個通用的類庫,以便在工做中有須要的時候,均可以直接拿來使用。這個類庫要求能完成咱們須要的功能(類的構建與繼承和靜態成員的添加),同時在使用時要足夠簡潔方便。在利用bootstrap的modal組件自定義alert,confirm和modal對話框這篇文章裏,我曾說過一些從組件指望的調用方式,去反推組件實現的一些觀點,當你明確你須要什麼東西時,你才知道這個東西你該怎麼去創造。本文要編寫的這個繼承組件也會採起這個方法來實現,我先用前面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
36
37
38
39
40
41
42
43
44
45
|
//經過調用Class函數構造一個類
var Employee = Class({
//經過instanceMembers指定這個類的實例成員
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//調用靜態方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + ''s salary is ' + this.getSalary() + '.';
}
},
//經過staticMembers指定這個類的靜態成員
//靜態方法內部可經過this訪問其它靜態成員
//在外部可經過Employee.getId這種方式訪問到靜態成員
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var Manager = Class({
instanceMembers: {
init: function (name, salary, percentage) {
this.base(name, salary);
this.percentage = percentage;
Manager.count++;
},
getSalary: function () {
return this.salary + this.salary * this.percentage;
}
},
//經過extend指定要繼承的類
extend: Employee
});
|
從模擬的結果來看,我想要的繼承庫對外提供的名稱只有Class, instanceMembers, staticMembers和extend而已,調用方式也很簡單,只要傳遞參數給Class函數便可。接下來就按照這個目標,看看如何一步步根據第一部分羅列的那些問題和解決方式,把這個庫給寫出來。
根據API名稱和接口以及前面第1部分提出的問題,這個繼承庫要完成的功能有:
1)類的構建(關鍵:init方法)和靜態成員處理;
2)繼承關係的構建(關鍵:父類原型的複製);
3)父類方法的簡化調用(關鍵:父類原型上同名方法的代理)。
因此這個庫的實現,能夠按照這三點分紅三版來開發。
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
|
var Class = (function () {
var hasOwn = Object.prototype.hasOwnProperty;
//用來判斷是否爲Object的實例
function isObject(o) {
return typeof (o) === 'object';
}
//用來判斷是否爲Function的實例
function isFunction(f) {
return typeof (f) === 'function';
}
function ClassBuilder(options) {
if (!isObject(options)) {
throw new Error('Class options must be an valid object instance!');
}
var instanceMembers = isObject(options) & options.instanceMembers || {},
staticMembers = isObject(options) && options.staticMembers || {},
extend = isObject(options) && isFunction(options.extend) && options.extend,
prop;
//表示要構建的類的構造函數
function TargetClass() {
if (isFunction(this.init)) {
this.init.apply(this, arguments);
}
}
//添加靜態成員,這段代碼需在原型設置的前面執行,避免staticMembers中包含prototype屬性,覆蓋類的原型
for (prop in staticMembers) {
if (hasOwn.call(staticMembers, prop)) {
TargetClass[prop] = staticMembers[prop];
}
}
TargetClass.prototype = instanceMembers;
TargetClass.prototype.constructor = TargetClass;
return TargetClass;
}
return ClassBuilder
})();
|
這一版核心代碼在於類的構建和靜態成員添加的部分,其它代碼僅僅提供一些提早能夠想到的賦值函數和變量(isObject, isFunction),並作一些參數合法性校驗的處理。添加靜態成員的代碼必定要在設置原型的代碼以前,不然就有原型被覆蓋的風險。有了這個版本,就能夠直接構建帶靜態成員的Employee類了:
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
|
var Employee = Class({
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//調用靜態方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + ''s salary is ' + this.getSalary() + '.';
}
},
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var e = new Employee('jason', 5000);
console.log(e.toString()); //jason's salary is 5000.
console.log(e.id); //1
console.log(e.constructor === Employee); //true
|
在getId方法中之因此直接使用this就能訪問到構造函數Employee,是由於getId這個方法是添加到構造函數上的,因此當調用Employee.getId()時,getId方法裏面的this指向的就是Employee這個函數對象。
第二版在初版的基礎上,實現繼承關係的構建部分:
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
|
var Class = (function () {
var hasOwn = Object.prototype.hasOwnProperty;
//用來判斷是否爲Object的實例
function isObject(o) {
return typeof (o) === 'object';
}
//用來判斷是否爲Function的實例
function isFunction(f) {
return typeof (f) === 'function';
}
//簡單複製
function copy(source) {
var target = {};
for (var i in source) {
if (hasOwn.call(source, i)) {
target[i] = source[i];
}
}
return target;
}
function ClassBuilder(options) {
if (!isObject(options)) {
throw new Error('Class options must be an valid object instance!');
}
var instanceMembers = isObject(options) & options.instanceMembers || {},
staticMembers = isObject(options) && options.staticMembers || {},
extend = isObject(options) && isFunction(options.extend) && options.extend,
prop;
//表示要構建的類的構造函數
function TargetClass() {
if (extend) {
//若是有要繼承的父類
//就在每一個實例中添加baseProto屬性,以便實例內部能夠經過這個屬性訪問到父類的原型
//由於copy函數致使原型鏈斷裂,沒法經過原型鏈訪問到父類的原型
this.baseProto = extend.prototype;
}
if (isFunction(this.init)) {
this.init.apply(this, arguments);
}
}
//添加靜態成員,這段代碼需在原型設置的前面執行,避免staticMembers中包含prototype屬性,覆蓋類的原型
for (prop in staticMembers) {
if (hasOwn.call(staticMembers, prop)) {
TargetClass[prop] = staticMembers[prop];
}
}
//若是有要繼承的父類,先把父類的實例方法都複製過來
extend & (TargetClass.prototype = copy(extend.prototype));
//添加實例方法
for (prop in instanceMembers) {
if (hasOwn.call(instanceMembers, prop)) {
TargetClass.prototype[prop] = instanceMembers[prop];
}
}
TargetClass.prototype.constructor = TargetClass;
return TargetClass;
}
return ClassBuilder
})();
|
這一版關鍵的部分在於:
this.baseProto主要目的就是爲了讓子類的實例可以有一個屬性能夠訪問到父類的原型,由於後面的繼承方式是複製方式,會致使原型鏈斷裂。有了這一版以後,就能夠加入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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
var Employee = Class({
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//調用靜態方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + ''s salary is ' + this.getSalary() + '.';
}
},
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var Manager = Class({
instanceMembers: {
init: function (name, salary, percentage) {
//借用父類的init方法,實現屬性繼承(name, salary)
Employee.prototype.init.apply(this, [name, salary]);
this.percentage = percentage;
},
getSalary: function () {
return this.salary + this.salary * this.percentage;
}
},
extend: Employee
});
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(e.constructor === Employee); //true
console.log(m.constructor === Manager); //true
console.log(e.id); //1
console.log(m.id); //2
|
不過在Manager內部,調用父類的方法時仍是apply借用的方式,因此在最後一版裏面,須要把它變成咱們指望的this.base的方式,反正原理前面也已經瞭解了,無非是在方法同名的時候,對實例方法加一個代理而已,實現以下:
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
var Class = (function () {
var hasOwn = Object.prototype.hasOwnProperty;
//用來判斷是否爲Object的實例
function isObject(o) {
return typeof (o) === 'object';
}
//用來判斷是否爲Function的實例
function isFunction(f) {
return typeof (f) === 'function';
}
//簡單複製
function copy(source) {
var target = {};
for (var i in source) {
if (hasOwn.call(source, i)) {
target[i] = source[i];
}
}
return target;
}
function ClassBuilder(options) {
if (!isObject(options)) {
throw new Error('Class options must be an valid object instance!');
}
var instanceMembers = isObject(options) & options.instanceMembers || {},
staticMembers = isObject(options) && options.staticMembers || {},
extend = isObject(options) && isFunction(options.extend) && options.extend,
prop;
//表示要構建的類的構造函數
function TargetClass() {
if (extend) {
//若是有要繼承的父類
//就在每一個實例中添加baseProto屬性,以便實例內部能夠經過這個屬性訪問到父類的原型
//由於copy函數致使原型鏈斷裂,沒法經過原型鏈訪問到父類的原型
this.baseProto = extend.prototype;
}
if (isFunction(this.init)) {
this.init.apply(this, arguments);
}
}
//添加靜態成員,這段代碼需在原型設置的前面執行,避免staticMembers中包含prototype屬性,覆蓋類的原型
for (prop in staticMembers) {
if (hasOwn.call(staticMembers, prop)) {
TargetClass[prop] = staticMembers[prop];
}
}
//若是有要繼承的父類,先把父類的實例方法都複製過來
extend & (TargetClass.prototype = copy(extend.prototype));
//添加實例方法
for (prop in instanceMembers) {
if (hasOwn.call(instanceMembers, prop)) {
//若是有要繼承的父類,且在父類的原型上存在當前實例方法同名的方法
if (extend & isFunction(instanceMembers[prop]) && isFunction(extend.prototype[prop])) {
TargetClass.prototype[prop] = (function (name, func) {
return function () {
//記錄實例原有的this.base的值
var old = this.base;
//將實例的this.base指向父類的原型的同名方法
this.base = extend.prototype[name];
//調用子類自身定義的實例方法,也就是func參數傳遞進來的函數
var ret = func.apply(this, arguments);
//還原實例原有的this.base的值
this.base = old;
return ret;
}
})(prop, instanceMembers[prop]);
} else {
TargetClass.prototype[prop] = instanceMembers[prop];
}
}
}
TargetClass.prototype.constructor = TargetClass;
return TargetClass;
}
return ClassBuilder
})();
|
核心部分是:
只有當須要繼承父類,且父類原型中有方法與當前的實例方法同名時,纔會去對當前的實例方法添加代理。更詳細的原理能夠回到文章第1部分回顧相關內容。至此,咱們在Manager類內部調用父類的方法時,就很簡單了,只要經過this.base便可:
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
|
var Employee = Class({
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//調用靜態方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + ''s salary is ' + this.getSalary() + '.';
}
},
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var Manager = Class({
instanceMembers: {
init: function (name, salary, percentage) {
//經過this.base調用父類的構造方法
this.base(name, salary);
this.percentage = percentage;
},
getSalary: function () {
return this.base() + this.salary * this.percentage;
}
},
extend: Employee
});
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(e.constructor === Employee); //true
console.log(m.constructor === Manager); //true
console.log(e.id); //1
console.log(m.id); //2
|
注意這兩處調用:
以上就是本文要實現的繼承庫的所有細節,其實它所作的事就是把本文第1部分提到的那些問題的解決方式和第二部分模擬的調用場景結合起來,封裝到一個模塊內部而已,各個細節的原理只要理解了第1部分總結的那些解決方式就很掌握了。在最後一版的演示中,也能看到,本文實現的這個繼承庫,已經徹底知足了模擬場景中的需求,從此有任何須要用到繼承的場景,徹底能夠拿最後一版的實現去開發。