JavaScript的原型繼承詳解_javascript技巧
JavaScript是一門物件導向的語言。在JavaScript中有一句很經典的話,萬物皆物件。既然是物件導向的,那就有物件導向的三大特徵:封裝、繼承、多型。這裡講的是JavaScript的繼承,其他兩個容後再說。
JavaScript的繼承和C 的繼承不大一樣,C 的繼承是基於類別的,而JavaScript的繼承是基於原型的。
現在問題來了。
原型是什麼?原型我們可以參考C 裡的類,同樣的保存了物件的屬性和方法。例如我們寫一個簡單的物件
function Animal(name) {
this.name = name;
}
Animal.prototype.setName = function(name) {
this.name = name;
}
var animal = new Animal("wangwang");
我們可以看到,這就是一個物件Animal,該物件有個屬性name,有個方法setName。要注意,一旦修改prototype,例如增加某個方法,則該物件所有實例將同享這個方法。例如
function Animal(name) {
this.name = name;
}
var animal = new Animal("wangwang");
這時animal只有name屬性。如果我們加上一句,
Animal.prototype.setName = function(name) {
this.name = name;
}
這時animal也會有setName方法。
繼承本複製-從空的物件開始我們知道,JS的基本型別中,有一種叫做object,而它最基本的實例就是空的對象,也就是直接呼叫new Object()產生的實例,或者是用字面量{ }來宣告。空的對像是“乾淨的對象”,只有預先定義的屬性和方法,而其他所有對像都是繼承自空對象,因此所有的對像都擁有這些預定義的 屬性與方法。原型其實也是一個物件實例。原型的意義是指:如果建構器有一個原型物件A,則由該建構器所建立的實例都必然複製自A。由於實例複製自物件A,因此實例必然繼承了A的所有屬性、方法和其他性質。那麼,複製又是怎麼實現的呢?方法一:建構複製每構造一個實例,都從原型複製出一個實例來,新的實例與原型佔用了相同的記憶體空間。這雖然使得obj1、obj2與它們的原型“完全一致”,但也非常不經濟——內存空間的消耗會急速增加。如圖:
方法二:寫時複製這種策略來自於一致欺騙系統的技術:寫時複製。這種欺騙的典型範例就是作業系統中的動態連結庫(DDL),它的記憶體區總是寫時複製的。如圖:
我們只要在系統中指明obj1和obj2等同於它們的原型,這樣在讀取的時候,只需要順著指示去讀原型即可。當需要寫入物件(例如obj2)的屬性時,我們就會複製一個原型的映像出來,並使以後的操作指向該映像即可。如圖:
這種方式的優點是我們在創建實例和讀取屬性的時候不需要大量內存開銷,只在第一次寫的時候會用一些代碼來分配內存,並帶來一些代碼和內存上的開銷。但此後就不再有這種開銷了,因為存取映像和存取原型的效率是一致的。不過,對於經常進行寫入操作的系統來說,這種方法並不比上一種方法更經濟。方法三:讀遍歷這種方法把複製的粒度從原型變成了成員。這種方法的特點是:僅當寫某個實例的成員,將成員的資訊複製到實例映像中。當寫入物件屬性時,例如(obj2.value=10)時,會產生一個名為value的屬性值,放在obj2物件的成員清單中。看圖:
可以發現,obj2仍然是一個指向原型的引用,在操作過程中也沒有與原型相同大小的物件實例創建出來。這樣,寫入操作並不導致大量的記憶體分配,因此記憶體的使用就顯得經濟了。不同的是,obj2(以及所有的物件實例)需要維護一張成員清單。這個成員清單遵循兩條規則:保證在讀取時首先被存取到如果在物件中沒有指定屬性,則嘗試遍歷物件的整個原型鏈,直到原型為空或或找到該屬性。原型鏈後面會講。顯然,三種方法中,讀遍歷是效能最優的。所以,JavaScript的原型繼承是讀遍歷的。 constructor熟悉C 的人看完最上面的物件的程式碼,一定會懷疑。沒有class關鍵字還好理解,畢竟有function關鍵字,關鍵字不一樣而已。但是,構造函數呢?實際上,JavaScript也是有類似的建構函數的,只不過叫做構造器。在使用new運算子的時候,其實已經呼叫了建構器,並將this綁定為物件。例如,我們用以下的程式碼
var animal = Animal("wangwang");
animal將是undefined。有人會說,沒有回傳值當然是undefined。那如果將Animal的物件定義改一下:
function Animal(name) {
this.name = name;
return this;
}
猜猜現在animal是什麼?
此時的animal變成window了,不同之處在於擴展了window,使得window有了name屬性。這是因為this在沒有指定的情況下,預設指向window,也也就是最頂層變數。只有呼叫new關鍵字,才能正確呼叫建構器。那麼,該如何避免用的人漏掉new關鍵字呢?我們可以做點小修改:
function Animal(name) {
if(!(this instanceof Animal)) {
return new Animal(name);
}
this.name = name;
}
這樣就萬無一失了。建構器還有一個用處,標示實例是屬於哪個物件的。我們可以用instanceof來判斷,但instanceof在繼承的時候對祖先物件跟真正物件都會回傳true,所以不太適合。 constructor在new呼叫時,預設指向目前物件。
console.log(Animal.prototype.constructor === Animal); // true
我們可以換種思維:prototype在函數初始時根本是無值的,實作上可能是下面的邏輯
// 設定__proto__是函數內建的成員,get_prototyoe()是它的方法
var __proto__ = null;
function get_prototype() {
if(!__proto__) {
__proto__ = new Object();
__proto__.constructor = this;
}
return __proto__;
}
這樣的好處是避免了每聲明一個函數都建立一個物件實例,節省了開銷。 constructor是可以修改的,後面會講到。基於原型的繼承繼承是什麼相信大家都差不多知道,就不秀智商下限了。
JS的繼承有好幾種,這裡講兩種
1. 方法一這種方法最常用,安全性也比較好。我們先定義兩個物件
function Animal(name) {
this.name = name;
}
function Dog(age) {
this.age = age;
}
var dog = new Dog(2);
要建構繼承很簡單,將子物件的原型指向父物件的實例(注意是實例,不是物件)
Dog.prototype = new Animal("wangwang");
這時,dog就將有兩個屬性,name和age。而如果對dog使用instanceof操作符
console.log(dog instanceof Animal); // true
console.log(dog instanceof Dog); // false
這樣就實現了繼承,但是有個小問題
console.log(Dog.prototype.constructor === Animal); // true
console.log(Dog.prototype.constructor === Dog); // false
可以看到建構器指向的物件改變了,這樣就不符合我們的目的了,我們無法判斷我們new出來的實例屬於誰。因此,我們可以加一句話:
Dog.prototype.constructor = Dog;
再來看一下:
console.log(dog instanceof Animal); // false
console.log(dog instanceof Dog); // true
done。這種方法是屬於原型鏈的維護中的一環,下文將詳細闡述。 2. 方法二這種方法有它的好處,也有它的弊端,但弊大於利。先看程式碼
function Animal(name) {<br> this.name = name;<br> }<br> Animal.prototype.setName = function(name) {<br> this.name = name;<br> }<br> function Dog(age) {<br> this.age = age;<br> }<br> Dog.prototype = Animal.prototype;<br>
這樣就實作了prototype的拷貝。
這種方法的好處就是不需要實例化物件(和方法一相比),節省了資源。弊端也是明顯,除了和上文一樣的問題,即constructor指向了父對象,還只能複製父對像用prototype宣告的屬性和方法。也即是說,在上述程式碼中,Animal物件的name屬性得不到複製,但能複製setName方法。最致命的是,子物件的prototype的任何修改,都會影響父物件的prototype,也就是兩個物件宣告出來的實例都會受到影響。所以,不推薦這種方法。
原型鏈
寫過繼承的人都知道,繼承可以多層繼承。而在JS中,這種就構成了原型鏈。上文也多次提到了原型鏈,那麼,原型鍊是什麼?一個實例,至少應該擁有指向原型的proto屬性,這是JavaScript中的物件系統的基礎。不過這個屬性是不可見的,我們稱之為“內部原型鏈”,以便和構造器的prototype所組成的“構造器原型鏈”(也即我們通常所說的“原型鏈”)區分開。我們先以上述程式碼建構一個簡單的繼承關係:
function Animal(name) {
this.name = name;
}
function Dog(age) {
this.age = age;
}
var animal = new Animal("wangwang");
Dog.prototype = animal;
var dog = new Dog(2);
提醒一下,前文說過,所有物件都是繼承空的物件的。所以,我們就建構了一個原型鏈:
我們可以看到,子物件的prototype指向父物件的實例,構成了建構器原型鏈。子實例的內部proto物件也是指向父物件的實例,構成了內部原型鏈。當我們需要尋找某個屬性的時候,程式碼類似
function getAttrFromObj(attr, obj) {
if(typeof(obj) === "object") {
var proto = obj;
while(proto) {
if(proto.hasOwnProperty(attr)) {
return proto[attr];
}
proto = proto.__proto__;
}
}
return undefined;
}
在這個例子中,我們如果在dog中查找name屬性,它將在dog中的成員列表中尋找,當然,會找不到,因為現在dog的成員列表只有age這一項。接著它會順著原型鏈,也就是.proto指向的實例繼續尋找,即animal中,找到了name屬性,並將之傳回。假如尋找的是一個不存在的屬性,在animal中尋找不到時,它會繼續順著.proto尋找,找到了空的對象,找不到之後繼續順著.proto尋找,而空的對象的. proto指向null,尋找退出。
原型鏈的維護我們在剛才講原型繼承的時候提出了一個問題,使用方法一構造繼承時,子物件實例的constructor指向的是父物件。這樣的好處是我們可以透過constructor屬性來存取原型鏈,壞處也是顯而易見的。一個對象,它所產生的實例應該指向它本身,也即是
(new obj()).prototype.constructor === obj;
然後,當我們重寫了原型屬性之後,子物件產生的實例的constructor不是指向本身!這樣就和構造器的初衷背道而馳了。我們在上面提到了一個解決方案:
Dog.prototype = new Animal("wangwang");
Dog.prototype.constructor = Dog;
看起來沒有什麼問題了。但實際上,這又帶來了一個新的問題,因為我們會發現,我們沒辦法回溯原型鏈了,因為我們沒法尋找到父對象,而內部原型鏈的.proto屬性是無法訪問的。於是,SpiderMonkey提供了一個改良方案:在任何已建立的物件上新增了一個名為__proto__的屬性,該屬性總是指向建構器所使用的原型。這樣,對任何constructor的修改,都不會影響__proto__的值,就方便維護constructor了。
但是,這樣又兩個問題:
__proto__是可以重寫的,這意味著使用它時仍然有風險
__proto__是spiderMonkey的特殊處理,在別的引擎(例如JScript)中是無法使用的。
我們還有一個辦法,那就是保持原型的建構器屬性,而在子類別建構子函式內初始化實例的建構器屬性。
程式碼如下:改寫子物件
function Dog(age) {
this.constructor = arguments.callee;
this.age = age;
}
Dog.prototype = new Animal("wangwang");
這樣,所有子對象的實例的constructor都正確的指向該對象,而原型的constructor則指向父對象。雖然這種方法的效率比較低,因為每次構造實例都要重寫constructor屬性,但毫無疑問地這種方法能有效解決之前的矛盾。 ES5考慮到了這種情況,徹底的解決了這個問題:可以在任意時候使用Object.getPrototypeOf() 來獲得一個物件的真實原型,而無須存取構造器或維護外部的原型鏈。因此,像上一節所說的尋找物件屬性,我們可以如下改寫:
function getAttrFromObj(attr, obj) {
if(typeof(obj) === "object") {
do {
var proto = Object.getPrototypeOf(dog);
if(proto[attr]) {
return proto[attr];
}
}
while(proto);
}
return undefined;
}
當然,這種方法只能在支援ES5的瀏覽器中使用。為了向後相容,我們還是需要考慮上一種方法的。更適合的方法是將這兩種方法整合封裝起來,這個相信讀者們都非常擅長,這裡就不獻醜了。

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

如何使用WebSocket和JavaScript實現線上語音辨識系統引言:隨著科技的不斷發展,語音辨識技術已成為了人工智慧領域的重要組成部分。而基於WebSocket和JavaScript實現的線上語音辨識系統,具備了低延遲、即時性和跨平台的特點,成為了廣泛應用的解決方案。本文將介紹如何使用WebSocket和JavaScript來實現線上語音辨識系

WebSocket與JavaScript:實現即時監控系統的關鍵技術引言:隨著互聯網技術的快速發展,即時監控系統在各個領域中得到了廣泛的應用。而實現即時監控的關鍵技術之一就是WebSocket與JavaScript的結合使用。本文將介紹WebSocket與JavaScript在即時監控系統中的應用,並給出程式碼範例,詳細解釋其實作原理。一、WebSocket技

如何利用JavaScript和WebSocket實現即時線上點餐系統介紹:隨著網路的普及和技術的進步,越來越多的餐廳開始提供線上點餐服務。為了實現即時線上點餐系統,我們可以利用JavaScript和WebSocket技術。 WebSocket是一種基於TCP協定的全雙工通訊協議,可實現客戶端與伺服器的即時雙向通訊。在即時線上點餐系統中,當使用者選擇菜餚並下訂單

如何使用WebSocket和JavaScript實現線上預約系統在當今數位化的時代,越來越多的業務和服務都需要提供線上預約功能。而實現一個高效、即時的線上預約系統是至關重要的。本文將介紹如何使用WebSocket和JavaScript來實作一個線上預約系統,並提供具體的程式碼範例。一、什麼是WebSocketWebSocket是一種在單一TCP連線上進行全雙工

JavaScript和WebSocket:打造高效的即時天氣預報系統引言:如今,天氣預報的準確性對於日常生活以及決策制定具有重要意義。隨著技術的發展,我們可以透過即時獲取天氣數據來提供更準確可靠的天氣預報。在本文中,我們將學習如何使用JavaScript和WebSocket技術,來建立一個高效的即時天氣預報系統。本文將透過具體的程式碼範例來展示實現的過程。 We

JavaScript教學:如何取得HTTP狀態碼,需要具體程式碼範例前言:在Web開發中,經常會涉及到與伺服器進行資料互動的場景。在與伺服器進行通訊時,我們經常需要取得傳回的HTTP狀態碼來判斷操作是否成功,並根據不同的狀態碼來進行對應的處理。本篇文章將教你如何使用JavaScript來取得HTTP狀態碼,並提供一些實用的程式碼範例。使用XMLHttpRequest

用法:在JavaScript中,insertBefore()方法用於在DOM樹中插入一個新的節點。這個方法需要兩個參數:要插入的新節點和參考節點(即新節點將要插入的位置的節點)。

JavaScript是一種廣泛應用於Web開發的程式語言,而WebSocket則是一種用於即時通訊的網路協定。結合二者的強大功能,我們可以打造一個高效率的即時影像處理系統。本文將介紹如何利用JavaScript和WebSocket來實作這個系統,並提供具體的程式碼範例。首先,我們需要明確指出即時影像處理系統的需求和目標。假設我們有一個攝影機設備,可以擷取即時的影像數
