[轉]Javascript原型繼承

真正意義上來講Javascript並非一門面向對象的語言,沒有提供傳統的繼承方式,可是它提供了一種原型繼承的方式,利用自身提供的原型屬性來實現繼承。Javascript原型繼承是一個被說爛掉了的話題,可是本身對於這個問題一直沒有完全理解,今天花了點時間又看了一遍《Javascript模式》中關於原型實現繼承的幾種方法,下面來一一說明下,在最後我根據本身的理解提出了一個關於繼承比較完整的實現,若是你們有不一樣意見,歡迎建議。javascript

原型與原型鏈

說原型繼承以前仍是要先說說原型和原型鏈,畢竟這是實現原型繼承的基礎。
在Javascript中,每一個函數都有一個原型屬性prototype指向自身的原型,而由這個函數建立的對象也有一個__proto__屬性指向這個原型,而函數的原型是一個對象,因此這個對象也會有一個__proto__指向本身的原型,這樣逐層深刻直到Object對象的原型,這樣就造成了原型鏈。下面這張圖很好的解釋了Javascript中的原型和原型鏈的關係。
java

每一個函數都是Function函數建立的對象,因此每一個函數也有一個__proto__屬性指向Function函數的原型。這裏須要指出的是,真正造成原型鏈的是每一個對象的__proto__屬性,而不是函數的prototype屬性,這是很重要的。app

原型繼承

基本模式

var Parent = function(){ this.name = 'parent' ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(){ this.name = 'child' ; } ; Child.prototype = new Parent() ; var parent = new Parent() ; var child = new Child() ; console.log(parent.getName()) ; //parent console.log(child.getName()) ; //child

這種是最簡單實現原型繼承的方法,直接把父類的對象賦值給子類構造函數的原型,這樣子類的對象就能夠訪問到父類以及父類構造函數的prototype中的屬性。 這種方法的原型繼承圖以下:
函數

這種方法的優勢很明顯,實現十分簡單,不須要任何特殊的操做;同時缺點也很明顯,若是子類須要作跟父類構造函數中相同的初始化動做,那麼就得在子類構造函數中再重複一遍父類中的操做:ui

var Parent = function(name){ this.name = name || 'parent' ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ this.name = name || 'child' ; } ; Child.prototype = new Parent() ; var parent = new Parent('myParent') ; var child = new Child('myChild') ; console.log(parent.getName()) ; //myParent console.log(child.getName()) ; //myChild 

上面這種狀況還只是須要初始化name屬性,若是初始化工做不斷增長,這種方式是很不方便的。所以就有了下面一種改進的方式。this

借用構造函數

var Parent = function(name){ this.name = name || 'parent' ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; Child.prototype = new Parent() ; var parent = new Parent('myParent') ; var child = new Child('myChild') ; console.log(parent.getName()) ; //myParent console.log(child.getName()) ; //myChild

上面這種方法在子類構造函數中經過apply調用父類的構造函數來進行相同的初始化工做,這樣無論父類中作了多少初始化工做,子類也能夠執行一樣的初始化工做。可是上面這種實現還存在一個問題,父類構造函數被執行了兩次,一次是在子類構造函數中,一次在賦值子類原型時,這是不少餘的,因此咱們還須要作一個改進:spa

var Parent = function(name){ this.name = name || 'parent' ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; Child.prototype = Parent.prototype ; var parent = new Parent('myParent') ; var child = new Child('myChild') ; console.log(parent.getName()) ; //myParent console.log(child.getName()) ; //myChild

這樣咱們就只須要在子類構造函數中執行一次父類的構造函數,同時又能夠繼承父類原型中的屬性,這也比較符合原型的初衷,就是把須要複用的內容放在原型中,咱們也只是繼承了原型中可複用的內容。上面這種方式的原型圖以下:
prototype

臨時構造函數模式(聖盃模式)

上面借用構造函數模式最後改進的版本仍是存在問題,它把父類的原型直接賦值給子類的原型,這就會形成一個問題,就是若是對子類的原型作了修改,那麼這個修改同時也會影響到父類的原型,進而影響父類對象,這個確定不是你們所但願看到的。爲了解決這個問題就有了臨時構造函數模式。3d

var Parent = function(name){ this.name = name || 'parent' ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; var F = function(){} ; F.prototype = Parent.prototype ; Child.prototype = new F() ; var parent = new Parent('myParent') ; var child = new Child('myChild') ; console.log(parent.getName()) ; //myParent console.log(child.getName()) ; //myChild

該方法的原型繼承圖以下:
很容易能夠看出,經過在父類原型和子類原型之間加入一個臨時的構造函數F,切斷了子類原型和父類原型之間的聯繫,這樣當子類原型作修改時就不會影響到父類原型。code

個人方法

《Javascript模式》中到聖盃模式就結束了,但是無論上面哪種方法都有一個不容易被發現的問題。你們能夠看到我在'Parent'的prototype屬性中加入了一個obj對象字面量屬性,可是一直都沒有用。咱們在聖盃模式的基礎上來看看下面這種狀況:

var Parent = function(name){ this.name = name || 'parent' ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; var F = function(){} ; F.prototype = Parent.prototype ; Child.prototype = new F() ; var parent = new Parent('myParent') ; var child = new Child('myChild') ; console.log(child.obj.a) ; //1 console.log(parent.obj.a) ; //1 child.obj.a = 2 ; console.log(child.obj.a) ; //2 console.log(parent.obj.a) ; //2

在上面這種狀況中,當我修改child對象obj.a的時候,同時父類的原型中的obj.a也會被修改,這就發生了和共享原型一樣的問題。出現這個狀況是由於當訪問child.obj.a的時候,咱們會沿着原型鏈一直找到父類的prototype中,而後找到了obj屬性,而後對obj.a進行修改。再看看下面這種狀況:

var Parent = function(name){ this.name = name || 'parent' ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; var F = function(){} ; F.prototype = Parent.prototype ; Child.prototype = new F() ; var parent = new Parent('myParent') ; var child = new Child('myChild') ; console.log(child.obj.a) ; //1 console.log(parent.obj.a) ; //1 child.obj.a = 2 ; console.log(child.obj.a) ; //2 console.log(parent.obj.a) ; //2

這裏有一個關鍵的問題,當對象訪問原型中的屬性時,原型中的屬性對於對象來講是隻讀的,也就是說child對象能夠讀取obj對象,可是沒法修改原型中obj對象引用,因此當child修改obj的時候並不會對原型中的obj產生影響,它只是在自身對象添加了一個obj屬性,覆蓋了父類原型中的obj屬性。而當child對象修改obj.a時,它先讀取了原型中obj的引用,這時候child.objParent.prototype.obj是指向同一個對象的,因此childobj.a的修改會影響到Parent.prototype.obj.a的值,進而影響父類的對象。AngularJS中關於$scope嵌套的繼承方式就是模範Javasript中的原型繼承來實現的。
根據上面的描述,只要子類對象中訪問到的原型跟父類原型是同一個對象,那麼就會出現上面這種狀況,因此咱們能夠對父類原型進行拷貝而後再賦值給子類原型,這樣當子類修改原型中的屬性時就只是修改父類原型的一個拷貝,並不會影響到父類原型。具體實現以下:

var deepClone = function(source,target){ source = source || {} ; target = target || {}; var toStr = Object.prototype.toString , arrStr = '[object array]' ; for(var i in source){ if(source.hasOwnProperty(i)){ var item = source[i] ; if(typeof item === 'object'){ target[i] = (toStr.apply(item).toLowerCase() === arrStr) ? [] : {} ; deepClone(item,target[i]) ; }else{ target[i] = item; } } } return target ; } ; var Parent = function(name){ this.name = name || 'parent' ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : '1'} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; Child.prototype = deepClone(Parent.prototype) ; var child = new Child('child') ; var parent = new Parent('parent') ; console.log(child.obj.a) ; //1 console.log(parent.obj.a) ; //1 child.obj.a = '2' ; console.log(child.obj.a) ; //2 console.log(parent.obj.a) ; //1

綜合上面全部的考慮,Javascript繼承的具體實現以下,這裏只考慮了Child和Parent都是函數的狀況下:

var deepClone = function(source,target){ source = source || {} ; target = target || {}; var toStr = Object.prototype.toString , arrStr = '[object array]' ; for(var i in source){ if(source.hasOwnProperty(i)){ var item = source[i] ; if(typeof item === 'object'){ target[i] = (toStr.apply(item).toLowerCase() === arrStr) ? [] : {} ; deepClone(item,target[i]) ; }else{ target[i] = item; } } } return target ; } ; var extend = function(Parent,Child){ Child = Child || function(){} ; if(Parent === undefined) return Child ; //借用父類構造函數 Child = function(){ Parent.apply(this,argument) ; } ; //經過深拷貝繼承父類原型 Child.prototype = deepClone(Parent.prototype) ; //重置constructor屬性 Child.prototype.constructor = Child ; } ; 

總結

說了這麼多,其實Javascript中實現繼承是十分靈活多樣的,並無一種最好的方法,須要根據不一樣的需求實現不一樣方式的繼承,最重要的是要理解Javascript中實現繼承的原理,也就是原型和原型鏈的問題,只要理解了這些,本身實現繼承就能夠遊刃有餘。

相關文章
相關標籤/搜索