JavaScript繼承詳解

首先讓咱們來回顧一下第一章中介紹的例子:第一章html

 

 1 function Person(name) {
 2 
 3 this.name = name;
 4 
 5 }
 6 
 7 Person.prototype = {
 8 
 9 getName: function() {
10 
11 return this.name;
12 
13 }
14 
15 }
16 
17 
18 
19 function Employee(name, employeeID) {
20 
21 this.name = name;
22 
23 this.employeeID = employeeID;
24 
25 }
26 
27 Employee.prototype = new Person();
28 
29 Employee.prototype.getEmployeeID = function() {
30 
31 return this.employeeID;
32 
33 };
34 
35 var zhang = new Employee("ZhangSan", "1234");
36 
37 console.log(zhang.getName()); // "ZhangSan" 

 

修正constructor的指向錯誤

 

從上一篇文章中關於constructor的描述,咱們知道Employee實例的constructor會有一個指向錯誤,以下所示:閉包

 var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.constructor === Employee); // false
console.log(zhang.constructor === Object); // true

咱們須要簡單的修正:app

 function Employee(name, employeeID) {
this.name = name;
this.employeeID = employeeID;
}
Employee.prototype = new Person();
Employee.prototype.constructor = Employee;
Employee.prototype.getEmployeeID = function() {
return this.employeeID;
};
var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.constructor === Employee); // true
console.log(zhang.constructor === Object); // false

 

 

建立Employee類時實例化Person是不合適的

 

但另外一方面,咱們又必須依賴於這種機制來實現繼承。 解決辦法是不在構造函數中初始化數據,而是提供一個原型方法(好比init)來初始化數據。框架

 // 空的構造函數
function Person() {
}
Person.prototype = {
init: function(name) {
this.name = name;
},
getName: function() {
return this.name;
}
}
// 空的構造函數
function Employee() {
}
// 建立類的階段不會初始化父類的數據,由於Person是一個空的構造函數
Employee.prototype = new Person();
Employee.prototype.constructor = Employee;
Employee.prototype.init = function(name, employeeID) {
this.name = name;
this.employeeID = employeeID;
};
Employee.prototype.getEmployeeID = function() {
return this.employeeID;
};

這種方式下,必須在實例化一個對象後手工調用init函數,以下:函數

 var zhang = new Employee();
zhang.init("ZhangSan", "1234");
console.log(zhang.getName()); // "ZhangSan"

 

 

如何自動調用init函數?

 

必須達到兩個效果,構造類時不要調用init函數和實例化對象時自動調用init函數。看來咱們須要在調用空的構造函數時有一個狀態標示。優化

 // 建立一個全局的狀態標示 - 當前是否處於類的構造階段
var initializing = false;
function Person() {
if (!initializing) {
this.init.apply(this, arguments);
}
}
Person.prototype = {
init: function(name) {
this.name = name;
},
getName: function() {
return this.name;
}
}
function Employee() {
if (!initializing) {
this.init.apply(this, arguments);
}
}
// 標示當前進入類的建立階段,不會調用init函數
initializing = true;
Employee.prototype = new Person();
Employee.prototype.constructor = Employee;
initializing = false;
Employee.prototype.init = function(name, employeeID) {
this.name = name;
this.employeeID = employeeID;
};
Employee.prototype.getEmployeeID = function() {
return this.employeeID;
};

// 初始化類實例時,自動調用類的原型函數init,並向init中傳遞參數
var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.getName()); // "ZhangSan"

可是這樣就必須引入全局變量,這是一個很差的信號。this

 

 

如何避免引入全局變量initializing?

 

咱們須要引入一個全局的函數來簡化類的建立過程,同時封裝內部細節避免引入全局變量。spa

 // 當前是否處於建立類的階段
var initializing = false;
function jClass(baseClass, prop) {
// 只接受一個參數的狀況 - jClass(prop)
if (typeof (baseClass) === "object") {
prop = baseClass;
baseClass = null;
}
// 本次調用所建立的類(構造函數)
function F() {
// 若是當前處於實例化類的階段,則調用init原型函數
if (!initializing) {
this.init.apply(this, arguments);
}
}
// 若是此類須要從其它類擴展
if (baseClass) {
initializing = true;
F.prototype = new baseClass();
F.prototype.constructor = F;
initializing = false;
}
// 覆蓋父類的同名函數
for (var name in prop) {
if (prop.hasOwnProperty(name)) {
F.prototype[name] = prop[name];
}
}
return F;
};

使用jClass函數來建立類和繼承類的方法:prototype

 var Person = jClass({
init: function(name) {
this.name = name;
},
getName: function() {
return this.name;
}
});
var Employee = jClass(Person, {
init: function(name, employeeID) {
this.name = name;
this.employeeID = employeeID;
},
getEmployeeID: function() {
return this.employeeID;
}
});

var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.getName()); // "ZhangSan"

OK,如今建立類和實例化類的方式看起來優雅多了。 可是這裏面還存在明顯的瑕疵,Employee的初始化函數init沒法調用父類的同名方法。code

 

 

如何調用父類的同名方法?

 

咱們能夠經過爲實例化對象提供一個base的屬性,來指向父類(構造函數)的原型,以下:

 // 當前是否處於建立類的階段
var initializing = false;
function jClass(baseClass, prop) {
// 只接受一個參數的狀況 - jClass(prop)
if (typeof (baseClass) === "object") {
prop = baseClass;
baseClass = null;
}
// 本次調用所建立的類(構造函數)
function F() {
// 若是當前處於實例化類的階段,則調用init原型函數
if (!initializing) {
// 若是父類存在,則實例對象的base指向父類的原型
// 這就提供了在實例對象中調用父類方法的途徑
if (baseClass) {
this.base = baseClass.prototype;
}
this.init.apply(this, arguments);
}
}
// 若是此類須要從其它類擴展
if (baseClass) {
initializing = true;
F.prototype = new baseClass();
F.prototype.constructor = F;
initializing = false;
}
// 覆蓋父類的同名函數
for (var name in prop) {
if (prop.hasOwnProperty(name)) {
F.prototype[name] = prop[name];
}
}
return F;
};

調用方式:

 var Person = jClass({
init: function(name) {
this.name = name;
},
getName: function() {
return this.name;
}
});
var Employee = jClass(Person, {
init: function(name, employeeID) {
// 調用父類的原型函數init,注意使用apply函數修改init的this指向
this.base.init.apply(this, [name]);
this.employeeID = employeeID;
},
getEmployeeID: function() {
return this.employeeID;
},
getName: function() {
// 調用父類的原型函數getName
return "Employee name: " + this.base.getName.apply(this);
}
});

var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.getName()); // "Employee name: ZhangSan"

 

目前爲止,咱們已經修正了在第一章手工實現繼承的種種弊端。 經過咱們自定義的jClass函數來建立類和子類,經過原型方法init初始化數據, 經過實例屬性base來調用父類的原型函數。

惟一的缺憾是調用父類的代碼太長,而且很差理解, 若是可以按照以下的方式調用豈不是更妙:

 var Employee = jClass(Person, {
init: function(name, employeeID) {
// 若是可以這樣調用,就再好不過了
this.base(name);
this.employeeID = employeeID;
}
});

 

優化jClass函數

 

 // 當前是否處於建立類的階段
var initializing = false;
function jClass(baseClass, prop) {
// 只接受一個參數的狀況 - jClass(prop)
if (typeof (baseClass) === "object") {
prop = baseClass;
baseClass = null;
}
// 本次調用所建立的類(構造函數)
function F() {
// 若是當前處於實例化類的階段,則調用init原型函數
if (!initializing) {
// 若是父類存在,則實例對象的baseprototype指向父類的原型
// 這就提供了在實例對象中調用父類方法的途徑
if (baseClass) {
this.baseprototype = baseClass.prototype;
}
this.init.apply(this, arguments);
}
}
// 若是此類須要從其它類擴展
if (baseClass) {
initializing = true;
F.prototype = new baseClass();
F.prototype.constructor = F;
initializing = false;
}
// 覆蓋父類的同名函數
for (var name in prop) {
if (prop.hasOwnProperty(name)) {
// 若是此類繼承自父類baseClass而且父類原型中存在同名函數name
if (baseClass &&
typeof (prop[name]) === "function" &&
typeof (F.prototype[name]) === "function") {

// 重定義函數name -
// 首先在函數上下文設置this.base指向父類原型中的同名函數
// 而後調用函數prop[name],返回函數結果

// 注意:這裏的自執行函數建立了一個上下文,這個上下文返回另外一個函數,
// 此函數中能夠應用此上下文中的變量,這就是閉包(Closure)。
// 這是JavaScript框架開發中經常使用的技巧。
F.prototype[name] = (function(name, fn) {
return function() {
this.base = baseClass.prototype[name];
return fn.apply(this, arguments);
};
})(name, prop[name]);

} else {
F.prototype[name] = prop[name];
}
}
}
return F;
};

此時,建立類與子類以及調用方式都顯得很是優雅,請看:

 var Person = jClass({
init: function(name) {
this.name = name;
},
getName: function() {
return this.name;
}
});
var Employee = jClass(Person, {
init: function(name, employeeID) {
this.base(name);
this.employeeID = employeeID;
},
getEmployeeID: function() {
return this.employeeID;
},
getName: function() {
return "Employee name: " + this.base();
}
});

var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.getName()); // "Employee name: ZhangSan"

 

至此,咱們已經建立了一個完善的函數jClass, 幫助咱們在JavaScript中以比較優雅的方式實現類和繼承。

在之後的章節中,咱們會陸續分析網上一些比較流行的JavaScript類和繼承的實現。 不過萬變不離其宗,那些實現也無非把咱們這章中提到的概念顛來簸去的「炒做」, 爲的就是一種更優雅的調用方式。

 

本文轉載自:http://www.cnblogs.com/sanshi/archive/2009/07/09/1519890.html

相關文章
相關標籤/搜索