概述 在之前的文章中,我們藉助構造函數實現了“類”,然後結合原型對象實現了“繼承”,並瞭解了JavaScript中原型鏈的概念。 理解這些內容,有助於我們更深入地進行JavaScript面向對象編程。 由於JavaScript是一門基於對象和原型的弱語言,靈活度非常高,這使得JavaScript有各... ...
概述
在之前的文章中,我們藉助構造函數實現了“類”,然後結合原型對象實現了“繼承”,並瞭解了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','[email protected]');
實現要點
- 基於構造函數和原型鏈
- 子類構造函數的原型指向父類構造函數的一個對象
- 重寫子類構造函數原型對象的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', '[email protected]');
註意:當未指定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', '[email protected]');
實現要點
- 在子構造函數中使用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','[email protected]'); var dev = new Developer('Jack','[email protected]',['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','[email protected]'); var dev = new Developer('Jack','[email protected]',['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','[email protected]'); var dev = new Developer('Jack','[email protected]',['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','[email protected]'); var dev = new Developer('Jack','[email protected]',['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', '[email protected]'); var emp2 = new Employee('Rose','[email protected]'); 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','[email protected]');
這時,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 = '[email protected]'; // 在重寫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: '[email protected]', 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: '[email protected]' }; 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','[email protected]');
註意:這段代碼中的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》