首頁  >  文章  >  web前端  >  JavaScript繼承體系的深入淺出

JavaScript繼承體系的深入淺出

黄舟
黄舟原創
2017-10-23 09:39:061220瀏覽

一、建構器的原型屬性與原型物件

#  剛接觸js時通常依樣畫瓢,用函數new一個實例,也不知道原因,只聽說js中函數即物件。原來js中沒有採用Java等語言中的類別繼承體系,而是使用原型物件(prototype)實作繼承體系,具體說是利用「建構器」實作類別的功能。

首先解釋下原型繼承中的兩個重要概念:原型屬性、原型物件(實例)。

就js物件系統而言,建立的每個函數(建構器)都有prototype原型屬性,同時,透過建構器建立的每個物件實例也包含一個_proto_屬性,prototype和_proto_屬性是一個指針,指向原型物件。普通函數與建構函數的唯一差異就是,其原型屬性prototype是不是一個有意義的值。

原型屬性prototype所指向的原型是一個物件實例(Object instance)。具體如下圖所示,若建構器Animal()有一個原型物件B,則由該建構器所建立的實例都必然複製於B。即:Animal()的實例a1的_proto_屬性也會指向原型物件B。因此,實例a1能夠繼承B的所有屬性、方法和其他性質。

圖1 js物件實例化實作

#二、空的物件

在javascript中,「空的物件」是整個原型繼承體系的根基,是所有物件的基礎。在介紹「空的物件」之前,必須先介紹下「空物件(null)」。

空物件null

  null不是“空的物件”,作為javascript中的一個保留字,其意義是:

  (1)屬於物件類型

  (2)物件是空值

作為一個物件類型,可以使用for…in去列舉它,但是作為一個空值,null沒有任何方法和屬性(包括constructor、_proto_等屬性),因此列舉不到任何內容。如下例所示:  


var num=0;
  for(var propertyName in null)
  {
  num++;
  }

  Alert(num);//顯示值為0

最重要的一點是null沒有原型,它並不是自Object()建構器(或其子類別)實例化而來,對其進行instanceof 運算會傳回false。

  2.「空的物件」

  「空的物件」是指一個標準的、經由Object()建構的物件實例。例如:


obj=new Object();或 obj={};

  「空的物件」具有「物件」的一切特性,因此可以存取toString()、valueof等預先定義的屬性和方法。

  3.「空的物件」與null的關係

   如下圖2中紅線所示路徑,當透過」Object.prototype._proto_」取得Object原型物件的-proto-屬性時,將會得到”null”,由於null物件沒有任何屬性,也就是說”Object {}”

#  原型物件就是原型鏈的終點了。

圖2 js類別繼承系統

#三、Javascript繼承的實作以及原型鏈維護

(1)繼承的實作

  第一節說過javascript中類別繼承是透過修改建構子的原型屬性prototype實現的。如下程式碼所示:


function Animal() {
this.name = 'Animal';
};
function Dog() {
};
  Dog.prototype = new Animal();
var d = new Dog();
console.log(d.name);//'Animal'

  透過建立一個Animal類型的實例並將其賦值給建構函式Dog()的prototype屬性,從而實現型別繼承,即Animal是Dog的父親類。這樣Dog類型的實例d也能存取Animal類型的name屬性。

(2)原型鏈

JS物件繼承體系中有兩種原型鏈:「內部原型鏈」和「建構器原型鏈」。如圖3所示,黑色箭頭指示路徑是透過建構函數的prototype屬性保持的「構造器原型鏈」。紅色箭頭指示路徑是透過物件實例的_proto_屬性保持的「內部原型鏈」。

圖3 原型鏈

(3)原型鏈維護

圖3說明建構器透過顯示的prototype建構了一個原型鏈,而物件實例也透過_ proto _屬性建構了一個原型鏈。由於_ proto _是一個無法存取的內部屬性(Chrome中可以查看物件_ proto _屬性的值,但不可以修改),因此無法從子類別(Dog)的實例dog1開始存取整個原型鏈。因此,我們需要從圖3中的「內部原型鏈」和「建構器原型鏈」中找到一個連接點,使得實例不能存取obj._proto_的情況下透過建構器存取內部原型鏈(將兩個原型鏈串聯起來)。

若要從子類別的實例開始存取整個原型鏈,需要使用實例的constructor屬性來維護原型鏈。

其实,JavaScript已经为构造器维护了原型属性,根据如下测试代码,当我们自定义一个构造器时,其原型对象是一个Object()类型的实例,但是其原型对象的constructor属性默认总是指向构造器自身,而非指向其父类Object。如图4中构造器实例中蓝色框中的constructor属性,该constructor属性继承自原型对象,因此可以得出一个自定义的构造器产生的实例,其constructor属性默认总是指向该构造器。


function Animal() {
};
var a = new Animal();
console.log(Animal.prototype);//Object(){}
console.log(Animal.prototype.constructor === Animal);//true//true

  

图4

  因此,在_proto_属性不可访问时,可通过a1.constructor.prototype获取实例a1的原型对象。然而,当我们自定义一个构造函数Dog(),并且手动指定其prototype属性值为Animal,即指定Dog的父类为Animal。此时访问d1.constructor值为Animal,而不是Dog;由图5可以看出,Dog的原型对象和dog分别由Animal()和Dog()两个不同的构造器产生,然而他们的constructor属性指向了相同的构造器(Animal),这样就与使用constructor属性串联两种原型链的设想冲突了。

图5

  是构造器出问题还是原型出了问题?图5可以看出,原型继承要求的“复制行为”已经正确实现,能够从子类实例中访问原型对象属性,问题是在给子类构造器Dog()赋予一个原型对象时应该“修正”该原型对象的构造属性值(constructor)。ECMAScript 3标准提供的方法是:保持原型的构造器属性,在子类构造器中初始化其实例对象的构造属性。代码如下: 


function Dog () {
  //初始化constructor属性
   this.constructor=Dog; //或 this.constructor=arguments.callee;
  };
  Dog.prototype = new Animal();//赋予原型对象,实现继承

图6

对constructor属性“修正”后效果如图6所示,在子类构造器Dog中初始化其实例对象的constructor属性后,Dog的实例对象的constructor都指向Dog,而Dog的原型对象的constructor仍然指向父类型构造器Animal。这样就可以实现利用constructor属性串联起原型链,可以从子类实例开始回溯整个原型链。

总结

以上是JavaScript繼承體系的深入淺出的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn