萬盛學電腦網

 萬盛學電腦網 >> 腳本專題 >> javascript >> 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與它們的原型“完全一致”,但也非常不經濟——內存空間的消耗會急速增加。如圖:

JavaScript的原型繼承詳解  三聯

  方法二:寫時復制這種策略來自於一致欺騙系統的技術:寫時復制。這種欺騙的典型示例就是操作系統中的動態鏈接庫(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) {

  this.name = name;

  }

  Animal.prototype.setName = function(name) {

  this.name = name;

  }

  function Dog(age) {

  this.age = age;

  }

  Dog.prototype = Animal.prototype;

  這樣就實現了prototype的拷貝。

  這種方法的好處就是不需要實例化對象(和方法一相比),節省了資源。弊端也是明顯,除了和上文一樣的問題,即constructor指向了父對象,還只能復制父對象用prototype聲明的屬性和方法。也即是說,上述代碼中,Animal對象的name屬性得不到復制,但能復制setName方法。最最致命的是,對子對象的prototype的任何修改,都會影響父對象的prototype,也就是兩個

copyright © 萬盛學電腦網 all rights reserved