玩轉JavaScript OOP[4]——實現繼承的12種套路

来源:http://www.cnblogs.com/keepfool/archive/2016/06/16/5592256.html
-Advertisement-
Play Games

概述 在之前的文章中,我們藉助構造函數實現了“類”,然後結合原型對象實現了“繼承”,並瞭解了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方法。

image

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"。

image 

如果希望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()方法:

image

有些人可能不是明白這個過程,也可能會對下麵這幾行代碼產生好奇:

Person.prototype.toString = function(){
    return this.constructor.uber ? this.constructor.uber.toString() + ',' + this.type : this.type;
}

我們以emp對象為例來解釋為什麼emp.toString()會輸出"Person,Employee"。

  1. 由於Employee.prototype.constructor === emp.constructor === Employee,所以Employee.uber === emp.constructor.uber。
  2. this.constructor.uber中的this是emp對象, 我們將它看作emp.constructor.uber,也就是Employee.uber,
  3. 而Employee.uber指向Person()的原型對象,執行this.constructor.uber.toString()相當於執行Person.prototype.toString()方法,這個方法輸出為'Person'。
  4. type屬性是定義在原型對象上的,this.type就是emp.type,由於Employee.prototype.type = 'Employee',所以this.type的值為'Employee'。
  5. 因此,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__)。
image

如果使用extend2()函數構建繼承關係,type屬性會成為Employee.prototype的屬性。

image

需要註意的是,toString()方法由於是複雜類型,extend2()方法只拷貝了toString()方法的引用。
也就是說Employee.prototype.toString和Person.prototype.toString是同一個引用。

image

extend()和extend2()效率的比較

通過以上示例,可以看到extend2()函數的效率是不如extend()函數的,因為extend2()在拷貝屬性時,每一個定義於Parent.prototype中的屬性,都要在Child.prototype中重建。

但這並不是很糟糕,因為extend2()方法只重建了基礎類型,複雜類型則是拷貝了引用,重建基礎類型屬性造成的性能的損失是可以接受的。

extend2()方法有兩個好處:

  1. 可以減少在原型鏈的遍歷和查找,因為基礎類型的屬性通過第一層的原型鏈就能找到,因為這些屬性已經拷貝到Child.prototype上了。
  2. 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']

image

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屬性訪問父對象。

image

這種方式是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屬性來訪問父對象。

image

實現要點

  • 基於對象工作
  • 遍歷父對象的所有屬性,基礎類型的屬性完全拷貝,複雜類型的屬性則只拷貝引用
  • 子對象的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是兩個完全獨立的個體,它們互不依賴。

image

實現要點

  • 基於對象工作
  • 遍歷父對象的所有屬性,基礎類型的屬性和複雜類型的屬性都會重建
  • 複雜類型屬性需要遞歸調用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);

image

實現要點

  • 基於對象和原型鏈工作
  • 使用了臨時構造函數

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;
	}
});

image

這種方式使得我們一次性完成了對象的繼承和擴展。

實現要點

  • 基於對象和原型鏈工作
  • 遍歷父對象的所有屬性,基礎類型的屬性完全拷貝,複雜類型的屬性則只拷貝引用

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']
});

image

註意:如果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不是一個關鍵字,它只是一個普通的對象。

image

實現要點

  • 基於對象和原型鏈工作
  • 使用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》


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在scala里,類繼承有兩點限制: 重寫方法需要使用override關鍵字; 只有主構造函數才能往父類構造函數中傳參數。 在java1.5中引入了override註解,但不強制使用。不過在scala中要想重寫方法必須使用override關鍵字。如果確實重寫了父類的方法又不使用override關鍵字的... ...
  • 最早見過手寫的,類似於下麵這種: 輸出如下: 另外一種方法是使用timeit模塊,使用方法如下: 還可以在命令行上使用這種timeit模塊,如下: 註意:timeit模塊會多次運行程式以獲得更精確的時間,所以需要避免重覆執行帶來的影響。比方說x.sort()這種操作,因為第一次執行之後,後邊已經是排 ...
  • 我想學ruby以後開髮網站,但ruby是高級語言,隱藏了許多底層的東西,因此先熟悉c語言 首先c程式的文件名是以.c結尾的 c程式的格式: 第一行#include<stdio.h> #是一個預處理標準,用來對文本進行預處理操作,表示該行代碼要最先進行處理,在編譯代碼之前運行 include是一個指令 ...
  • 以下內容轉自博客:http://blog.chinaunix.net/uid-22670933-id-1771613.html。 一、字元編碼是怎麼回事 0. 概念 位元組是電腦的最基本存儲單位,一個位元組包括8個位. 字元是一種文字的基本單位,比如'A' 是一個字元,'漢' 也是一個字元. 1. 計 ...
  • zookeeper 單機安裝配置 1、安裝前準備 linux系統(此文環境為Centos6.5) Zookeeper安裝包,官網https://zookeeper.apache.org/,演示版本zookeeper-3.4.7.tar.gz linux系統(此文環境為Centos6.5) Zooke ...
  • 問題1:Could not calculate build plan: Plugin org.apache... 不能成功創建maven項目 解決方法1: 問題2: 轉Maven project是生成的pom.xml文件錯誤——Unknown packaging:apk以及Failed to col ...
  • Cycle.js 是一個極簡的JavaScript框架(核心部分加上註釋125行),提供了一種函數式,響應式的人機交互介面。在這個交互模型中,人機之間的信息流互為輸出輸出,構成一個迴圈,也即 Cycle這一命名所指,框架的Logo更是以莫比烏斯環貼切的描述了這個迴圈。 ...
  • clientWidth:元素可見區域的寬度 clientWidth=padding+width-滾動條 scrollWidth:元素實際內容的寬度 scrollWidth=padding+width(元素實際的內容) offsetWidth:元素可見區域的高度+邊框 offsetWidth=bord ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...