概述
在之前的文章中,我們借助構造函數實現了“類”,然后結合原型對象實現了“繼承”,並了解了JavaScript中原型鏈的概念。
理解這些內容,有助於我們更深入地進行JavaScript面向對象編程。
由於JavaScript是一門基於對象和原型的弱語言,靈活度非常高,這使得JavaScript有各種套路去實現繼承。本篇文章將逐一介紹實現繼承的12種套路,它們可以適用於不同的場景,總一種套路適合你。
(親:文章有點長,請點擊右側的「顯示文章目錄」按鈕,以便導航和閱讀哦。)
01.原型鏈(經典模式)
這是實現繼承的經典方式,這種方式我就不再多做介紹了,具體請參考上一篇文章。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
return 'Hello, I am ' + this.name +'!';
}
function Employee(name,email) {
this.name = name;
this.email = email;
}
Employee.prototype = new Person();
Employee.prototype.constructor = Employee;
var emp = new Employee('keepfool','keepfool@xxx.com');
實現要點
- 基於構造函數和原型鏈
- 子類構造函數的原型指向父類構造函數的一個對象
- 重寫子類構造函數原型對象的constructor
02.僅繼承父構造函數的原型
以上的sayHello()方法是定義在Person.prototype上的,name屬性在Employee()構造函數也定義了,所以我們可以只用繼承副高早函數的原型。
/*
* 繼承方式02:共用父構造函數的原型對象
*/
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
return 'Hello, I am ' + this.name +'!';
}
function Employee(name,email) {
this.name = name;
this.email = email;
}
Employee.prototype = Person.prototype;
Employee.prototype.constructor = Employee;
和01方式唯一的區別在於Employee.prototype = Person.prototype這行代碼。
實現要點
- 基於構造函數,但不基於原型鏈
- 子構造函數和父構造函數共用一個原型對象
- 重寫子類構造函數原型對象的constructor
優點
- 由於共用一個原型對象,訪問對象的屬性和方法時無需遍歷原型鏈,使得訪問效率得以提升
- 實現繼承關系時,不需要新建父構造函數的實例
缺點
- 由於共用一個原型對象,所以當子對象修改復雜類型的屬性時,會同時影響父對象。
Employee.prototype.sayHello = function(){
return 'Hello, I am ' + this.name + ', my email is ' + this.email;
}
var person = new Person('Jack');
這段代碼修改了Employee.prototype.sayHello方法,同時也影響了Person.prototype.sayHello方法。
03. 借用構造函數
在01和02兩種方式中,Person()構造函數和Employee()構造函數都定義了name屬性。
如果Person()和Employee()構造函數相同的屬性很多,在Employee()構造函數中將會出現大量重復的this.xxx = xxx賦值操作。
使用apply()方法借用Person()構造函數,可以減少這些重復的賦值操作。
初版
function Person(name) {
this.name = name;
//調用Person.apply(this,[name])后,emp對象也會擁有favorites屬性
this.favorits = ['orange','apple'];
}
Person.prototype.sayHello = function() {
return 'Hello, I am ' + this.name +'!';
}
function Employee(name,email) {
Person.apply(this,[name]);
this.email = email;
}
var emp = new Employee('keepfool', 'keepfool@xxx.com');
注意:當未指定Employee.prototype = new Person()時,emp對象是可以訪問favorites屬性的。因為Person.apply(this,[name,age]);中的this是Employee()構造函數的實例,調用apply方法時,Person()構造函數中的屬性和方法都會被分配給this對象,所以emp對象是可以訪問favorites屬性的。
完整版
/*
* 繼承方式03:借用構造函數
*/
function Person(name) {
this.name = name;
//調用Person.apply(this,[name])后,emp對象也會擁有favorites屬性
this.favorits = ['orange','apple'];
}
Person.prototype.sayHello = function() {
return 'Hello, I am ' + this.name +'!';
}
function Employee(name,email) {
// 第2次調用Person()構造函數
Person.apply(this,[name]);
this.email = email;
}
// 第1次調用Person()構造函數
Employee.prototype = new Person();
Employee.prototype.constructor = Employee;
var emp = new Employee('keepfool', 'keepfool@xxx.com');
實現要點
- 在子構造函數中使用apply()方法,借用父構造函初始化子構造函數的屬性和方法
- 基於構造函數和原型鏈,子類構造函數的原型指向父類構造函數的一個對象
- 重寫子構造函數原型對象的constructor
缺點
- 父構造函數會被調用2次:第1次是
Employee.prototype = new Person();,第2次是調用Person.apply()方法。
04.臨時構造函數
/*
* 繼承方式04:使用臨時構造函數
*/
// Person
function Person(name) {
this.name = name;
this.favorites = ['orange','apple'];
}
Person.prototype.sayHello = function() {
return 'Hello, I am ' + this.name +'!';
}
// Employee
function Employee(name,email) {
this.name = name;
this.email = email;
}
// Developer
function Developer(name, email, skills){
this.name = name;
this.email = email;
this.skills = skills;
}
/* 1.借助臨時構造函數實現Employee()繼承Person() */
var F = function() {};
F.prototype = Person.prototype;
Employee.prototype = new F();
Employee.prototype.constructor = Employee;
/* 2.借助臨時構造函數實現Developer()繼承Employee() */
var F = function(){};
F.prototype = Employee.prototype;
Developer.prototype = new F();
Developer.prototype.constructor = Developer;
var emp = new Employee('keepfool','keepfool@xxx.com');
var dev = new Developer('Jack','Jack@xxx.com',['C#','JavaScript','HTML5'])
注意:子構造函數只繼承定義在父構造函數原型對象上的屬性和方法,例如:Employee()只繼承定義在Person.prototype上的屬性和方法,Person()構造函數中定義的favorites屬性不會被Employee()繼承。
實現要點
- 基於構造函數和原型鏈,臨時構造函數的原型指向父構造函數的原型對象
- 子構造函數的原型指向臨時構造函數的一個實例
- 重寫子構造函數原型對象的constructor
缺點
- 每次實現繼承時都需要創建臨時構造函數
uber——讓子對象訪問父對象
在介紹uber前,我們先看下面一則代碼:
// Person
function Person(name) {
this.name = name;
}
Person.prototype.type = 'Person';
Person.prototype.toString = function(){
return this.type;
}
// Employee
function Employee(name,email) {
this.name = name;
this.email = email;
}
// Developer
function Developer(name, email, skills){
this.name = name;
this.email = email;
this.skills = skills;
}
var F = function() {};
F.prototype = Person.prototype;
Employee.prototype = new F();
Employee.prototype.constructor = Employee;
Employee.prototype.type = 'Employee';
var F = function(){};
F.prototype = Employee.prototype;
Developer.prototype = new F();
Developer.prototype.constructor = Developer;
Developer.prototype.type = 'Developer';
var emp = new Employee('keepfool','keepfool@xxx.com');
var dev = new Developer('Jack','Jack@xxx.com',['C#','JavaScript','HTML5'])
這則代碼通過臨時構造函數構建了Developer → Employee → Person繼承關系。
調用emp.toString(),會輸出"Employee"; 調用dev.toString(),則會輸出"Developer"。
如果希望emp.toString()輸出"Person,Employee",dev.toString()輸出"Person,Employee,Developer",我們該如何做呢?
這意味着我們需要遍歷原型鏈。
在構建繼承關系時,可以為子構造函數引入uber屬性,並將它指向父構造函數的原型對象(因為toString()方法是定義在Person.prototype上的)。
uber這個詞表示“超級的”(不是優步哦),意指引用父類。為什么不用super呢?因為super是JavaScript的一個保留關鍵字。
// Person
function Person(name) {
this.name = name;
}
Person.prototype.type = 'Person';
Person.prototype.toString = function(){
return this.constructor.uber ? this.constructor.uber.toString() + ',' + this.type : this.type;
}
// Employee
function Employee(name,email) {
this.name = name;
this.email = email;
}
// Developer
function Developer(name, email, skills){
this.name = name;
this.email = email;
this.skills = skills;
}
var F = function() {};
F.prototype = Person.prototype;
Employee.prototype = new F();
Employee.prototype.constructor = Employee;
/* 1.引入uber屬性,使它指向Person()構造函數的原型對象 */
Employee.uber = Person.prototype;
Employee.prototype.type = 'Employee';
var F = function(){};
F.prototype = Employee.prototype;
Developer.prototype = new F();
Developer.prototype.constructor = Developer;
/* 2.引入uber屬性,使它指向Person()構造函數的原型對象 */
Developer.uber = Employee.prototype;
Developer.prototype.type = 'Developer';
var emp = new Employee('keepfool','keepfool@xxx.com');
var dev = new Developer('Jack','Jack@xxx.com',['C#','JavaScript','HTML5'])
再次調用emp.toString()和dev.toString()方法:
有些人可能不是明白這個過程,也可能會對下面這幾行代碼產生好奇:
Person.prototype.toString = function(){
return this.constructor.uber ? this.constructor.uber.toString() + ',' + this.type : this.type;
}
我們以emp對象為例來解釋為什么emp.toString()會輸出"Person,Employee"。
- 由於Employee.prototype.constructor === emp.constructor === Employee,所以Employee.uber === emp.constructor.uber。
- this.constructor.uber中的this是emp對象, 我們將它看作emp.constructor.uber,也就是Employee.uber,
- 而Employee.uber指向Person()的原型對象,執行this.constructor.uber.toString()相當於執行Person.prototype.toString()方法,這個方法輸出為'Person'。
- type屬性是定義在原型對象上的,this.type就是emp.type,由於Employee.prototype.type = 'Employee',所以this.type的值為'Employee'。
- 因此,emp.toString()方法的輸出結果最終是'Person, Employee'。
將繼承封裝為函數
在構建Devloper → Employee → Person的繼承關系時,我們使用了2次臨時構造函數,這些代碼是重復的,我們可以把實現繼承關系的代碼提煉為一個函數。
/*
* 繼承方式04:使用臨時構造函數-精簡版
*/
// Person
function Person(name) {
this.name = name;
}
Person.prototype.type = 'Person';
Person.prototype.toString = function(){
return this.constructor.uber ? this.constructor.uber.toString() + ',' + this.type : this.type;
}
// Employee
function Employee(name,email) {
this.name = name;
this.email = email;
}
// Developer
function Developer(name, email, skills){
this.name = name;
this.email = email;
this.skills = skills;
}
function extend(Child,Parent){
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
extend(Employee,Person);
Employee.prototype.type = 'Employee';
extend(Developer,Employee);
Developer.prototype.type = 'Developer';
var emp = new Employee('keepfool','keepfool@xxx.com');
var dev = new Developer('Jack','Jack@xxx.com',['C#','JavaScript','HTML5'])
這段代碼引入了一個extend()函數,指定了2個參數,分別表示Child和Parent,並將臨時構造函數封裝在exntend()中。
當我們需要構建繼承關系時,首先定義好構造函數,然后將構造函數傳入extend()方法就可以實現繼承了。 使用extend()方法,既能夠讓我們的代碼保持整潔,又能夠達到重用的目的。
05.拷貝父構造函數原型對象的屬性
由於繼承是為了使代碼能夠重用,難道我們就不能簡單地將一個對象的屬性拷貝給另外一個嗎?
這當然是可以的,沿用上面的代碼,我們將extend()方法替換為extend2()方法:
function extend2(Child,Parent){
var p = Parent.prototype;
var c = Child.prototype;
for(var i in p){
c[i] = p[i];
}
c.uber = p;
}
通過for循環,我們將Parent.prototype中的屬性拷貝到了Child.prototype。
extend2()和extend()的比較
extend2()方法和extend()方法有兩個不同之處。
extend2()不需要重寫Child.prototype.constructor
在extend()方法中,由於Child.prototype = new F()這行代碼使得Child.prototype被覆蓋了,所以extend()方法需要重寫Child.prototype.constructor。
而在extend2()方法中,Child.prototype沒有被覆蓋,所以無需重寫Child.prototype.constructor,Child.prototype.constructor本來就是指向Child的。
extend2()拷貝原型屬性
下面這段代碼,定義了一個Person()和Employee()構造函數,Person.prototype提供了一個基礎類型的type屬性,以及一個復雜類型的toString()方法。
// Person
function Person(name) {
this.name = name;
}
Person.prototype.type = 'Person';
Person.prototype.toString = function(){
return this.constructor.uber ? this.constructor.uber.toString() + ',' + this.type : this.type;
}
// Employee
function Employee(name,email) {
this.name = name;
this.email = email;
}
如果使用extend()函數構建繼承關系,則type屬性既不是emp對象的屬性,也不是Employee.prototype的屬性(提示:Employee.prototype === emp.__proto__)。
如果使用extend2()函數構建繼承關系,type屬性會成為Employee.prototype的屬性。
需要注意的是,toString()方法由於是復雜類型,extend2()方法只拷貝了toString()方法的引用。
也就是說Employee.prototype.toString和Person.prototype.toString是同一個引用。
extend()和extend2()效率的比較
通過以上示例,可以看到extend2()函數的效率是不如extend()函數的,因為extend2()在拷貝屬性時,每一個定義於Parent.prototype中的屬性,都要在Child.prototype中重建。
但這並不是很糟糕,因為extend2()方法只重建了基礎類型,復雜類型則是拷貝了引用,重建基礎類型屬性造成的性能的損失是可以接受的。
extend2()方法有兩個好處:
- 可以減少在原型鏈的遍歷和查找,因為基礎類型的屬性通過第一層的原型鏈就能找到,因為這些屬性已經拷貝到Child.prototype上了。
- Parent.prototype上定義的方法得以重用,且無須在Child.prototype中重建,例如上述的toString()方法。
引用類型拷貝的隱患
實際上,復雜類型(引用類型)都是通過拷貝引用來完成的,共用一個引用可能會導致一些預期之外的結果。
例如:在Person.prototype上定義一個favorites屬性,它是數組類型的,當然也是引用類型。
Person.prototype.favorites = ['orange','apple'];
然后我們使用Employee()構造函數創建兩個對象。
var emp1 = new Employee('Jack', 'jack@xxx.com');
var emp2 = new Employee('Rose','rose@xxx.com');
emp2.favorites; // ['orange','apple']
emp1.favorites.push('banana');
emp2.favorites; // ['orange','apple','nanana']
emp2.favorites最開始是['orange','apple'],然后通過數組的push方法更改了emp1.favorites,由於emp2.favorites和emp1.favorites指向同一個引用,所以emp2.favorites也變成了 ['orange','apple','nanana']。
也就是說,emp1的改變影響了emp2,這不是我們預期的結果。
所以當使用extend2()實現繼承時,對待引用類型應該謹慎,應該盡量避免對象修改類型為引用類型的屬性。
06.借用構造函數並拷貝原型
借用構造函數會帶來重復執行兩次構造函數的問題,我們可以結合apply()和extend2()函數來修復這個問題。
使用apply()方法調用父構造函數,獲取父構造函數自身的屬性,然后使用extend2()函數繼承父構造函數的原型屬性。
/*
* 繼承方式06.借用構造函數並拷貝原型
*/
function Person(name) {
this.name = name;
this.favorites = ['orange','apple'];
}
Person.prototype.type = 'Person';
Person.prototype.sayHello = function() {
return 'Hello, I am ' + this.name +'!';
}
function Employee(name,email) {
// 調用Person()構造函數
Person.apply(this,[name]);
this.email = email;
}
function extend2(Child,Parent){
var p = Parent.prototype;
var c = Child.prototype;
for(var i in p){
c[i] = p[i];
}
c.uber = p;
}
// 繼承Person()構造函數的原型對象的屬性
extend2(Employee,Person);
Employee.prototype.type = 'Employee';
var emp = new Employee('keepfool','keepfool@xxx.com');
這時,emp對象不僅繼承了Person()構造函數中的屬性,也繼承了Person()構造函數原型對象上的屬性。
另外,emp對象還可以通過uber屬性訪問父對象。
這種方式是03和05的結合,它使得我們可以在不重復調用父構造函數的情況下,同時繼承父構造函數的自身屬性和原型屬性。
實現要點
- 基於構造函數和原型鏈工作
- 拷貝父構造函數的原型屬性
07.對象的淺拷貝
在這之前,我們使用構造函數來創建“類”,並使用new構造函數創建對象。 然后,我們通過Child.prototype = new Parent()來構建繼承關系,這里的new Parent()也是調用了構造函數。
在實現“類”、“繼承”這些概念的過程中,構造函數充當了一個中間人的作用,繼承的目的是對象的屬性和方法可以被其他對象重用。
如果不使用構造函數,直接進行對象之間的拷貝難道不可行嗎?
這當然是可行的,我們首先介紹一種方式——對象的淺拷貝。
/*
* 繼承方式07:對象的淺拷貝
*/
function shallowCopy(p){
var c = {};
for(var i in p){
c[i] = p[i];
}
c.uber = p;
return c;
}
var person = {
type : 'Person',
toString : function(){
return this.type;
}
}
var emp = shallowCopy(person);
emp.type = 'Employee';
emp.name = 'keepfool';
emp.email = 'keepfool@xxx.com';
// 在重寫emp的toString()方法前,emp.toString === person.toString為true
emp.toString = function(){
return this.uber.toString() + ', ' + this.type;
}
注意:
1. c.uber = p這行代碼,表示子對象的uber屬性指向父對象。
2. toString()方法是引用類型,拷貝時只拷貝引用。然后emp.toString = function() { ... }將重寫了該方法,重寫不會影響person.toString。
該方式不僅繼承了父對象的屬性,還可以通過uber屬性來訪問父對象。
實現要點
- 基於對象工作
- 遍歷父對象的所有屬性,基礎類型的屬性完全拷貝,復雜類型的屬性則只拷貝引用
- 子對象的uber屬性引用父對象
優點
- 不用定義構造函數
- 不用遍歷原型鏈
缺點
- 由於沒有構造函數,子對象的屬性每次都要手動聲明,例如emp.name,emp.email。
- 由於是淺拷貝,所以存在引用類型的拷貝隱患。
08.對象的深拷貝
在拷貝屬性時,如果是引用類型的拷貝,由於共用一個對象,則可能存在一些隱患,深拷貝有助於解決這個問題。
淺拷貝和深拷貝最大的區別是:如果屬性為復雜類型,淺拷貝是拷貝其引用,而深拷貝則會創建一個新的復雜類型。
/*
* 繼承方式08:對象的深拷貝
*/
function deepCopy(p,c){
c = c || {};
for(var i in p){
// 屬性i是否為p對象的自有屬性
if(p.hasOwnProperty(i)){
// 屬性i是否為復雜類型
if(typeof p[i] === 'object'){
// 如果p[i]是數組,則創建一個新數組
// 如果p[i]是普通對象,則創建一個新對象
c[i] = Array.isArray(p[i]) ? [] : {};
// 遞歸拷貝復雜類型的屬性
deepCopy(p[i],c[i]);
}else{
// 屬性是基礎類型時,直接拷貝
c[i] = p[i];
}
}
}
return c;
}
深拷貝的實現邏輯,已經很清晰地在注釋中描述了,請注意這段代碼是如何處理數組類型和對象類型的。
下面這段代碼使用deepCopy()方法創建了一個child對象。
var parent = {
name : 'keepfool',
age : 28,
favorites : ['orange','apple'],
experience : {
limit : 7,
skills : ['C#','JavaScript','HTML5']
}
};
// 修改child對象的屬性,不會影響parent對象
var child = deepCopy(parent);
如果修改child對象的復雜類型屬性,不會對parent對象造成影響,因為child和parent是兩個完全獨立的個體,它們互不依賴。
實現要點
- 基於對象工作
- 遍歷父對象的所有屬性,基礎類型的屬性和復雜類型的屬性都會重建
- 復雜類型屬性需要遞歸調用deepCopy()方法
09.使用object()函數
基於對象繼承對象的理念,Douglas Crockford提出了一個建議,使用object()函數,接收父對象,然后返回父對象的原型。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
如果要訪問父對象,則為子對象添加uber屬性,並指向父對象。
/*
* 繼承方式09:使用object()函數
*/
function object(o) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o;
return n;
}
使用object()函數和使用shallowCopy()函數是一樣的,復雜類型的拷貝仍然是引用拷貝。
var person = {
type: 'Person',
favorites : ['orange','apple'],
toString: function() {
return this.type;
}
}
var emp = object(person);
實現要點
- 基於對象和原型鏈工作
- 使用了臨時構造函數
10. 原型繼承與屬性拷貝的混合模式
繼承的目的之一在於重用已有對象的屬性,然后在子對象上擴展一些額外的屬性。
既然如此,我們可以將原型繼承和屬性拷貝混合起來使用。
通俗地講,就是我們不僅使用現有對象(使用原型的屬性),還要基於現有的對象擴展一些屬性。
/*
* 繼承方式10:原型繼承與屬性拷貝的混合模式
*/
function objectPlus(o, stuff) {
var n;
// 1. 從對象o繼承原型
function F() {}
F.prototype = o;
n = new F();
n.uber = o;
// 2. 從對象stuff拷貝屬性
for (var i in stuff) {
n[i] = stuff[i];
}
return n;
}
objectPlus()方法有兩個參數,第1個參數用於繼承原型,第2個參數用於拷貝屬性。
下面這段代碼:person對象是一個參數,用於繼承原型;{}匿名對象中定義了一些屬性,用於擴展子對象的屬性。
var person = {
type: 'Person',
favorites : ['orange','apple'],
toString: function() {
return this.type;
}
}
var emp = objectPlus(person, {
type : 'Employee',
name: 'keepfool',
email: 'keepfool@xxx.com',
toString : function(){
return this.uber + ',' + this.type;
}
});
這種方式使得我們一次性完成了對象的繼承和擴展。
實現要點
- 基於對象和原型鏈工作
- 遍歷父對象的所有屬性,基礎類型的屬性完全拷貝,復雜類型的屬性則只拷貝引用
11. 多重繼承
像C#,Java這樣的面向對象編程語言,是不支持多重繼承的(但是支持多重接口實現)。
但對於JavaScript這樣的動態語言,實現多重繼承就比較簡單了。
下面這段代碼定義了一個multi()函數,它沒有顯式地定義參數。但通過arguments可以獲取調用函數時的參數,所以我們只需遍歷arguments參數,然后拷貝每個參數對象的屬性即可。
/*
* 繼承方式11:多重繼承
*/
function multi() {
var n = {},
stuff, j = 0,
len = arguments.length;
for (j = 0; j < len; j++) {
stuff = arguments[j];
for (var i in stuff) {
n[i] = stuff[i];
}
}
return n;
}
使用multi()函數:
var person = {
type: 'Person',
toString: function() {
return this.type;
}
};
var emp = {
type: 'Employee',
name: 'keepfool',
email: 'keepfool@xxx.com'
};
var dev = multi(person, emp, {
type: 'Developer',
age: 28,
skills: ['C#', 'JavaScript']
});
注意:如果arguments數組中的對象存在相同的屬性,則后遍歷的對象屬性會覆蓋先遍歷的對象屬性。
實現要點
- 遍歷argument數組的每一個元素(每一個元素都是對象)
- 遍歷對象的所有屬性,基礎類型的屬性完全拷貝,復雜類型的屬性則只拷貝引用
12.寄生繼承
寄生繼承是指:在創建對象的函數中,創建要返回的對象時,首先直接吸收其他對象的屬性,然后再擴展自己的屬性。
這個過程就好似一個對象的創建是寄生在另外一個對象上完成的。
下面這段代碼,employee()是寄生繼承函數,實現寄生函數時借用了09條的object()函數。
/*
* 繼承方式12:寄生繼承
*/
function object(o) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o;
return n;
}
var person = {
type: 'Person',
toString: function() {
return this.type;
}
};
// employee()是寄生函數
function employee(name, email) {
// 寄生peson對象
var that = object(person);
// 然后擴展自己的屬性
that.type = 'Employee';
that.toString = function(){
return this.uber.type + ',' + this.type;
}
return that;
}
var emp = employee('keepfool','keepfoo@xxx.com');
注意:這段代碼中的that不是一個關鍵字,它只是一個普通的對象。
實現要點
- 基於對象和原型鏈工作
- 使用object()函數
總結
由於這篇文章的篇幅較長,讀到這兒的人,可能已經忘了我這篇文章開頭講的是什么內容了。但這並不要緊,咱們來個全篇的總結,能讓你馬上回憶起來。
| 編號 | 原型鏈 | 示例 |
|---|---|---|
| 01 | 原型鏈(經典模式) | Child.prototype = new Parent(); |
| 02 | 僅繼承父構造函數的原型 | Child.prototype = Parent.prototype; |
| 03 | 借用構造函數 | function Child() {
Parent.apply(this, arguments);
}
|
| 04 | 臨時構造函數 | function extend(Child,Parent) {
var F = function() {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
|
| 05 | 復制父構造函數的原型屬性 | function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (vari in p) {
c[i] = p[i];
}
c.uber = p;
}
|
| 06 | 借用構造函數並拷貝原型 | function Child() {
Parent.apply(this, arguments);
}
extend2(Child,Parent);
|
| 07 | 基於對象的淺拷貝 | function shallowCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
|
| 08 | 基於對象的深拷貝 | function shallowCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
|
| 09 | 原型繼承 | function object(o) {
function F() {}
F.prototype = o;
return new F();
}
|
| 10 | 原型繼承與屬性拷貝的混合模式 | function objectPlus(o, stuff) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o;
for (var i in stuff) {
n[i] = stuff[i];
}
return n;
}
|
| 11 | 多重繼承 | function multi() {
var n = {},stuff, j = 0,len = arguments.length;
for (j = 0; j < len; j++) {
stuff = arguments[j];
for (var i in stuff) {
n[i] = stuff[i];
}
}
return n;
}
|
| 12 | 寄生繼承 | function parasite(victim) {
var that = object(victim);
that.more = 1;
return that;
}
|
面對這么多的方法,你該如何選擇呢?這取決於性能的需求、任務的目標以及設計風格等等。
如果你已經習慣了從“類”的角度去理解和分析問題,那么基於構造函數的繼承實現比較適合你,01~06方式都是基於構造函數的。
如果你只是處理某些具體對象或實例,那么基於對象的繼承實現比較適合你,07~12方式都是基於對象的。
參考
《Object-Oriented JavaScript 2nd Edition》














