javascript中類式繼承和原型式繼承的實現方法和區別

在全部面向對象的編程中,繼承是一個重要的話題。通常說來,在設計類的時候,咱們但願能減小重複性的代碼,而且儘可能弱化對象間的耦合(讓一個類繼承另外一個類可能會致使兩者產生強耦合)。關於「解耦」是程序設計中另外一個重要的話題,本篇重點來看看在javascript如何實現繼承。javascript

其它的面向對象程序設計語言都是經過關鍵字來解決繼承的問題(好比extend或inherit等方式)。可是javascript中並無定義這種實現的機制,若是一個類須要繼承另外一個類,這個繼承過程須要程序員本身經過編碼來實現。java

1、類式繼承的實現

一、建立一個類的方式:程序員

 1 //定義類的構造函數
 2 function Person(name) {
 3     this.name = name || '默認姓名';
 4 }
 5 //定義該類全部實例的公共方法
 6 Person.prototype.getName = function() {
 7     return this.name;
 8 }
 9 
10 var smith = new Person('Smith');
11 var jacky = new Person('Jacky');
12 
13 console.log( smith.getName(), jacky.getName() ); //Smith Jacky

二、繼承這個類:這須要分兩個步驟來實現,第1步是繼承父類構造函數中定義的屬性,第2步是繼承父類的prototype屬性編程

//定義類的構造函數
function Person(name) {
    this.name = name || '默認姓名';
}
//定義該類全部實例的公共方法
Person.prototype.getName = function() {
    return this.name;
}
function Author(name, books) {
    //繼承父類構造函數中定義的屬性
    //經過改變父類構造函數的執行上下文來繼承
    Person.call(this, name);
    this.books = books;
}

//繼承父類對應的方法
Author.prototype = new Person(); //Author.prototype.constructor === Person
Author.prototype.constructor = Author; //修正修改原型鏈時形成的constructor丟失
Author.prototype.getBooks = function() {
    return this.books;
};

//測試
var smith = new Person('Smith');
var jacky = new Author('Jacky', ['BookA', 'BookB']);

console.log(smith.getName()); //Smith
console.log(jacky.getName()); //Jacky
console.log(jacky.getBooks().join(', ')); //BookA, BookB
console.log(smith.getBooks().join(', ')); //Uncaught TypeError: smith.getBooks is not a function

從測試的結果中能夠看出,Author正確繼承了Person,並且修改Author的原型時,並不會對Person產生影響。這其中的關鍵一句就是 Author.prototype = new Person(),要與Author.prototype = Person.prototype區分開來。前者產生了一個實例,這個實例有Person.prototype的副本(這裏先這麼理解,後面有更詳細的解析)。後者是指將二者的prototype指向同一個原型對象。函數

那麼,這也意味着每次繼承都將產生一個父類的副本,確定對內存產生消耗,但爲了類式繼承這個內存開銷必須得支付,但還能夠作得更節省一點:Author.prototype = new Person()這一句其實多執行了構造函數一次(而這一次其實只需在子類構造函數中執行便可),尤爲是在父類的構造函數很龐大時很耗時和內存。修改一下繼承的方式,以下:測試

Author.prototype = (function() {
    function F() {}
    F.prototype = Person.prototype;
    return new F();
})();

如上所示的代碼,new時,去掉了對父類的構造函數的調用,節省了一次調用的開銷。this

三、類式繼承顯著的特色每一次實例化對象時,子類都將執行一次父類的構造函數。若是E繼承了D,D繼承了C,C繼承了B,B繼承了A,在實例化一個E時,一共要通過幾回構造函數的調用呢?編碼

/*繼承方法的函數*/
function extend(son, father) {
    function F() {}
    F.prototype = father.prototype;
    son.prototype = new F();
    son.prototype.constructor = son;
}
//A類
function A() {
    console.log('A()');
}
A.prototype.hello = function() {
    console.log('Hello, world.');
}
//B類
function B() {
    A.call(this);
    console.log('B()');
}
extend(B, A);
//C類
function C() {
    B.call(this);
    console.log('C()');
}
extend(C, B);
//D類
function D() {
    C.call(this);
    console.log('D()');
}
extend(D, C);
//E類
function E() {
    D.call(this);
    console.log('E()');
}
extend(E, D);

//建立一個E的實例
var e = new E(); //A() B() C() D() E()
e.hello(); //hello, world.

5次,這還只是實例化一個E時調用的次數。因此,咱們應該儘量的減小繼承的級別。但這並非說不要使用這種類式繼承,而是應該根據本身的應用場合決定採用什麼方法。spa

2、原型式繼承

一、先來看一段代碼:咱們先將以前類式繼承中的繼承prototype那一段改爲另外一個函數clone,而後經過字面量建立一個Person,最後讓Author變成Person的克隆體。prototype

//這個函數能夠理解爲克隆一個對象
function clone(object) {
    function F() {}
    F.prototype = object;
    return new F();
}

var Person = {
    name: 'Default Name';
    getName: function() {
        return this.name;
    }
}

//接下來讓Author變爲Person的克隆體
var Author = clone(Person);

問一個問題:clone函數裏的new F()爲這個實例開闢內存空間來存儲object的副本了嗎?

按我以前的理解,回答是確定的。可是,當我繼續將代碼寫下去的時候,奇怪的事情發生了,代碼以下:

//接下來讓Author變爲Person的克隆體
var Author = clone(Person);
Author.books = [];
Author.getBooks = function() {
    return this.books.join(', ');
}

//增長一個做者Smith
var Smith = clone(Author);
console.log(Smith.getName(), Smith.getBooks()); //Default Name
Smith.name = 'Smith';
Smith.books.push('<<Book A>>', '<<Book B>>'); //做者寫了兩本書
console.log(Smith.getName(), Smith.getBooks()); //Smith <<Book A>>, <<Book B>>

//再增長一個做者Jacky
var Jacky = clone(Author);
console.log(Jacky.getName(), Jacky.getBooks()); // Default Name <<Book A>>, <<Book B>>

當咱們繼續增長做者Jacky時,奇怪的現象發生了!!Jacky的名字依然是Default Name,可是他竟然也寫兩本與Smith同樣的書?Jacky的書都還沒push呢。到了這裏,我想到了引用對象的狀況(引用一個對象時,引用的是該對象的內存地址),發生這樣的現象,問題確定出在clone()函數中的new F()這裏。

事實上,這個clone中的new F()確實返回了一個新的對象,該對象擁有被克隆對象的全部屬性。但這些屬性保留的是對被克隆對象中相應屬性的引用,而非一個徹底獨立的屬性副本。換句話說,新對象的屬性被克隆的對象的屬性指向同一個內存地址(學過C語言的同窗應該明白指針類型,這裏意義差很少)。

那麼爲何上面的代碼中,Jacky的書與Smith的書相同了,爲何Jacky的名字卻不是Smith而是Default Name呢?這就是Javascript中繼承的機制所在,當Smith剛剛繼承自Author時,他的屬性保留了對Author的屬性的引用,一旦咱們顯示的對Smith的屬性從新賦值時,Javascritp引擎就會給Smith的該屬性從新劃份內存空間來存儲相應的值,因爲從新劃分了內址地址,那麼對Smith.name的改寫就不會影響到Author.name去了。這就很好的解釋了前面的那個問題——爲何Jacky的名字卻不是Smith而是Default Name。

二、基於原型繼承

經過前面的狀況分析,能夠看出基於原型繼承的方式更能節約內存(只有在須要時候纔開闢新的內存空間)。但要注意:基於原型繼承時,對象的屬性必定要從新賦值後(從新劃份內存)再去引用該屬性。對於對象的方法,若是有不一樣的處理方式,咱們只需從新定義便可。

下面將前一段代碼作一個完整、正確的範例出來,以說明原型繼承的特色和使用方式:

//這個函數能夠理解爲克隆一個對象
function clone(object) {
    function F() {}
    F.prototype = object;
    return new F();
}

var Person = {
    name: 'Default Name',
    getName: function() {
        return this.name;
    }
}

//接下來讓Author變爲Person的克隆體
var Author = clone(Person);
Author.books = [];
Author.getBooks = function() {
    return this.books.join(', ');
}

//增長一個做者Smith
var Smith = clone(Author);
Smith.name = 'Smith';
Smith.books = [];
Smith.books.push('<<Book A>>', '<<Book B>>'); //做者寫了兩本書
console.log(Smith.getName(), Smith.getBooks()); //Smith <<Book A>>, <<Book B>>

//再增長一個做者Jacky
var Jacky = clone(Author);
Jacky.name = 'Jacky';
Jacky.books = [];
Jacky.books.push('<<Book C>>', '<<Book D>>');
console.log(Jacky.getName(), Jacky.getBooks()); // Jacky <<Book C>>, <<Book D>>

3、類式繼承與原型式繼承的區別與相式之處

一、類式繼承中:使用構造函數初始化對象的屬性,經過調用父類的構造函數來繼承這些屬性。經過new 父類的prototype來繼承方法。

二、原型式繼承中:去掉了構造函數,但須要將對象的屬性和方法寫一個{}裏申明。準確的說,原型式繼承就是類式繼承中繼承父類的prototype方法。

相關文章
相關標籤/搜索