JavaScript 原型鏈
大部分面向對象的編程語言,都是以“類”( class )作為對象體系的語法基礎。 JavaScript 語言中是沒有 class 的概念的( ES6之前 ,ES6中雖然提供了 class 的寫法,但實現原理并不是傳統的“類” class 概念,僅僅是一種寫法), 但是它依舊可以實現面向對象的編程,這就是通過 JavaScript 中的“ 原型對象 ”( prototype )來實現的。
prototype 屬性
請看這樣一個例子:
|
這樣定義了一個構造函數,我們創建對象就可以使用這個構造函數作為模板來生成。不過以面向對象的思想來看,不難發現其中的一點問題: name 和 gender 屬性是每個實例都各不相同,作為一個自身的屬性沒有問題,而 sayHello 方法,每個實例對象應該都有,而且都一樣,給每個實例對象一個全新的、完全不同(雖然代碼內容一樣,但 JavaScript 中每個 sayHello 的值都在內存中單獨存在)的 sayHello 方法是沒有必要的。
|
上面代碼中展示了 zs.sayHell 和 xh.sayHello 這兩個作用相同,而且看起來代碼內容也是完全一樣的對象,實際是兩個獨立的,互不相關的對象。
面向對象思想中,是將公共的、抽象的屬性和方法提取出來,作為一個基類,子類繼承這個基類,從而繼承到這些屬性和方法。而 JavaScript 中則可以通過 prototype 屬性來實現類似的作用。以下是上面代碼的改進示例:
|
這時將 sayHello 方法定義到 Person 對象上的 prototype 屬性上,取代了在構造函數中給每個實例對象添加 sayHello 方法。可以看到,其還能實現和之前相同的作用,而且 zs.sayHell 和 xh.sayHello 是相同的內容,這樣就很貼近面向對象的思想了。那么 zs 和 xh 這兩個對象,是怎么訪問到這個 sayHello 方法的呢?
在瀏覽器控制臺中打印出 zs ,將其展開,可以看到下面的結果:
|
zs 這個對象只有兩個自身的屬性 gender 和 name ,這和其構造函數 Person 的模板相同,并且可以在 Person 對象的 __proto__ 屬性下找到 sayHello 方法。那么這個 __proto__ 是什么呢?它是 瀏覽器環境下 部署的一個對象,它指的是當前對象的原型對象,也就是構造函數的 prototype 屬性。
現在就可以明白了,我們給構造函數 Person 對象的 prototype 屬性添加了 sayHello 方法, zs 和 xh 這兩個通過 Person 構造函數產生的對象,是可訪問到 Person 對象的 prototype 屬性的,所以我們定義在 prototype 下的 sayHello 方法, Person 的實例對象都可以訪問到。
關于構造函數的 new 命令原理是這樣的:
- 創建一個空對象,作為將要返回的對象實例
- 將這個空對象的原型,指向構造函數的 prototype 屬性
- 將這個空對象賦值給函數內部的 this 關鍵字
- 開始執行構造函數內部的代碼
constructor 屬性
prototype 下有一個屬性 constructor ,默認指向此 prototype 對象所在的構造函數。
如上例中的 zs 下 __proto__ 的 constructor 值為 function Person(name, gender) 。
由于此屬性定義在 prototype 屬性上,所以它可以在所有的實例對象中獲取到。
|
將 constructor 屬性放在 prototype 屬性中的一個作用是,可以通過這個屬性來判斷這個對象是由哪個構造函數產生的,上面代碼中, zs 是由 Person 構造函數產生的,而不是 Function 或者 Object 構造函數產生。
constructor 屬性的另一個作用就是:提供了一種繼承的實現模式。
|
上面代碼中, Super 和 Sub 都是構造函數,在 Sub 內部的 this 上調用 Super ,就會形成 Sub 繼承 Super 的效果, miniui 中是這樣實現繼承的:
|
我們自己寫一個例子:
|
基本原理就是在子類中使用父類的構造函數。在 Person 和 Dog 中均沒有對 name 屬性和 introduce 方法進行操作,只是使用了父類 Animal 的構造函數,就可以將 name 屬性和 introduce 方法繼承來,請看下面例子:
|
確實實現了我們需要的效果。可是我們發現在調用 zs.sayHello() 時報錯了。為什么呢?
其實不難發現問題,我們的 Person.superclass 是 Animal 的一個實例,是有 sayHello 方法的,但是我們在 Perosn 構造函數的內部,只是使用了 Person.superclass.constructor 。而 Person.superclass.constructor 指的僅僅是 Animal 構造函數本身,并沒有包括 Animal.prototype ,所以沒有 sayHello 方法。
一種改進方法是:將自定義的 superclass 換為 prototype ,即:
|
這樣就全部繼承到了 Animal.prototype 下的方法。
但是一般不要這樣做,上面寫法中 Person.prototype = Animal.prototype; 等號兩端都是一個完整的對象,進行賦值時, Person.prototype 的原對象完全被 Animal.prototype 替換,切斷了和之前原型鏈的聯系,而且此時 Person.prototype 和 Animal.prototype 是相同的引用,給 Person.prototype 添加的屬性方法也將添加到 Animal.prototype ,反之亦然,這將引起邏輯混亂。
因此我們在原型上進行擴展是,通常是添加屬性,而不是替換為一個新對象。
|
JavaScript 原型鏈
JavaScript 的所有對象都有構造函數,而所有構造函數都有 prototype 屬性(其實是所有函數都有 prototype 屬性),所以所有對象都有自己的原型對象。
對象的屬性和方法,有可能是定義在自身,也有可能是定義在它的原型對象。由于原型本身也是對象,又有自己的原型,所以形成了一條原型鏈( prototype chain )。
zs.sayHello(); // Hello,I am zhang san . I'm a male
zs.toString(); // "[object Object]"
例如上面的 zs 對象,它的原型對象是 Person 的 prototype 屬性,而 Person 的 prototype 本身也是一個對象,它的原型對象是 Object.prototype 。
zs 本身沒有 sayHello 方法, JavaScript 通過原型鏈向上繼續尋找,在 Person.prototype 上找到了 sayHello 方法。 toString 方法在 zs 對象本身上沒有, Person.prototype 上也沒有,因此繼續沿原型鏈查找,最終可以在 Object.prototype 上找到了 toString 方法。
而 Object.prototype 的原型指向 null ,由于 null 沒有任何屬性,因此原型鏈到 Object.prototype 終止,所以 Object.prototype 是原型鏈的最頂端。
“原型鏈”的作用是,讀取對象的某個屬性時, JavaScript 引擎先尋找對象本身的屬性,如果找不到,就到它的原型去找,如果還是找不到,就到原型的原型去找。如果直到最頂層的 Object.prototype 還是找不到,則返回 undefined 。
如果對象自身和它的原型,都定義了一個同名屬性,那么優先讀取對象自身的屬性,這叫做“覆蓋”( overiding )。
JavaScript中通過原型鏈實現了類似面向對象編程語言中的繼承,我們在復制一個對象時,只用復制其自身的屬性即可,無需將整個原型鏈進行一次復制, Object.prototype 下的 hasOwnProperty 方法可以判斷一個屬性是否是該對象自身的屬性。
實例對象、 構造函數 、 prototype 之間的關系可用下圖表示:
instranceof 運算符
instanceof 運算符返回一個布爾值,表示指定對象是否為某個構造函數的實例。由于原型鏈的關系,所謂的實例并不一定是某個構造函數的直接實例,更準確的描述,應該是: 返回一個后者的原型對象是否在前者的原型鏈上
|
原型鏈相關屬性和方法
Object.prototype.hasOwnProperty()
hasOwnProperty() 方法用來判斷某個對象是否含有指定的自身屬性。這個方法可以用來檢測一個對象是否含有特定的自身屬性,和 in 運算符不同,該方法會忽略掉那些從原型鏈上繼承到的屬性。
|
Object.prototype.isPrototypeOf()
對象實例的 isPrototypeOf 方法,用來判斷一個對象是否是另一個對象的原型。
|
上面代碼表明,只要某個對象處在原型鏈上, isProtypeOf 都返回 true 。
|
看起來這個方法和 instanceof 運算符作用類似,但 實際使用是不一樣的 。
例如:
|
zs instanceof Person 可理解為判斷 Person.prototype 在不在 zs 的原型鏈上。 而 Person.isPrototypeOf(zs) 指的就是 Person 本身在不在 zs 的原型鏈上,所以返回 false ,只有 Person.prototype.isPrototypeOf(zs) 才為 true 。
Object.getPrototypeOf()
ES5 Object.getPrototypeOf 方法返回一個對象的原型。這是獲取原型對象的標準方法。
|
此方法是 ES5 方法,需要IE9+。在 ES5 中,參數只能是對象,否則將拋出異常,而在 ES6 中,此方法可正確識別原始類型。
Object.setPrototypeOf()
ES5 Object.setPrototypeOf 方法可以為現有對象設置原型,返回一個新對象。接受兩個參數,第一個是現有對象,第二個是原型對象。
|
上面代碼中, b 對象是 Object.setPrototypeOf 方法返回的一個新對象。該對象本身為空、原型為 a 對象,所以 b 對象可以拿到 a 對象的所有屬性和方法。 b 對象本身并沒有 x 屬性,但是JavaScript引擎找到它的原型對象 a ,然后讀取 a 的 x 屬性。
new 命令通過構造函數新建實例對象,實質就是將實例對象的原型,指向構造函數的 prototype 屬性,然后在實例對象上執行構造函數。
|
Object.create()
ES5 Object.create 方法用于從原型對象生成新的實例對象,它接收兩個參數:第一個為一個對象,新生成的對象完全繼承前者的屬性(即新生成的對象的原型此對象);第二個參數為一個屬性描述對象,此對象的屬性將會被添加到新對象。
上面代碼舉例:
|
可以 看出 創建的新對象 zs_clone 的原型為 zs ,從而獲得了 zs 的全部屬性和方法。但是其自身屬性為空,若需要為新對象添加自身屬性,則使用第二個參數即可。
|
參考鏈接
來自:http://blog.cdswyda.com/post/javascript/2016-11-21-javascript-prototype