JavaScript繼承與原型鏈

對於那些熟悉基於類的面嚮對象語言(Java 或者 C++)的開發者來講,JavaScript 的語法是比較怪異的,這是因爲 JavaScript 是一門動態語言,並且它沒有類的概念( ES6 新增了class 關鍵字,但只是語法糖,JavaScript 仍舊是基於原型)。javascript

涉及到繼承這一塊,Javascript 只有一種結構,那就是:對象。在 javaScript 中,每一個對象都有一個指向它的原型(prototype)對象的內部連接。這個原型對象又有本身的原型,直到某個對象的原型爲 null 爲止(也就是再也不有原型指向),組成這條鏈的最後一環。這種一級一級的鏈結構就稱爲原型鏈(prototype chain)java

雖然,原型繼承常常被視做 JavaScript 的一個弱點,但事實上,原型繼承模型比經典的繼承模型更強大。舉例來講,在原型模型的頂端創建一個典型的模型是至關容易的。數組

基於原型鏈的繼承

繼承屬性

JavaScript 對象是動態的屬性「包」(指其本身的屬性)。JavaScript 對象有一個指向一個原型對象的鏈。當試圖訪問一個對象的屬性時,它不只僅在該對象上搜尋,還會搜尋該對象的原型,以及該對象的原型的原型,依此層層向上搜索,直到找到一個名字匹配的屬性或到達原型鏈的末尾。app

 
根據 ECMAScript 標準,someObject.[[Prototype]] 符號是用於指派 someObject 的原型。這個等同於 JavaScript 的 __proto__  屬性(現已棄用)。從 ECMAScript 6 開始, [[Prototype]] 能夠用Object.getPrototypeOf()和Object.setPrototypeOf()訪問器來訪問。

下面的代碼將演示,當訪問一個對象的屬性時會發生的行爲:框架

// 假定有一個對象 o, 其自身的屬性(own properties)有 a 和 b:
// {a: 1, b: 2}
// o 的原型 o.[[Prototype]]有屬性 b 和 c:
// {b: 3, c: 4}
// 最後, o.[[Prototype]].[[Prototype]] 是 null.
// 這就是原型鏈的末尾,即 null,
// 根據定義,null 沒有[[Prototype]].
// 綜上,整個原型鏈以下: 
// {a:1, b:2} ---> {b:3, c:4} ---> null

console.log(o.a); // 1
// a是o的自身屬性嗎?是的,該屬性的值爲1

console.log(o.b); // 2
// b是o的自身屬性嗎?是的,該屬性的值爲2
// o.[[Prototype]]上還有一個'b'屬性,可是它不會被訪問到.這種狀況稱爲"屬性遮蔽 (property shadowing)".

console.log(o.c); // 4
// c是o的自身屬性嗎?不是,那看看o.[[Prototype]]上有沒有.
// c是o.[[Prototype]]的自身屬性嗎?是的,該屬性的值爲4

console.log(o.d); // undefined
// d是o的自身屬性嗎?不是,那看看o.[[Prototype]]上有沒有.
// d是o.[[Prototype]]的自身屬性嗎?不是,那看看o.[[Prototype]].[[Prototype]]上有沒有.
// o.[[Prototype]].[[Prototype]]爲null,中止搜索,
// 沒有d屬性,返回undefined

建立一個對象它本身的屬性的方法就是設置這個對象的屬性。惟一例外的獲取和設置的行爲規則就是當有一個 getter或者一個setter 被設置成繼承的屬性的時候。ide

繼承方法

JavaScript 並無其餘基於類的語言所定義的「方法」。在 JavaScript 裏,任何函數均可以添加到對象上做爲對象的屬性。函數的繼承與其餘的屬性繼承沒有差異,包括上面的「屬性遮蔽」(這種狀況至關於其餘語言的方法重寫)。函數

當繼承的函數被調用時,this 指向的是當前繼承的對象,而不是繼承的函數所在的原型對象。性能

 

var o = {
  a: 2,
  m: function(){
    return this.a + 1;
  }
};

console.log(o.m()); // 3
// 當調用 o.m 時,'this'指向了o.

var p = Object.create(o);
// p是一個對象, p.[[Prototype]]是o.

p.a = 12; // 建立 p 的自身屬性a.
console.log(p.m()); // 13
// 調用 p.m 時, 'this'指向 p. 
// 又由於 p 繼承 o 的 m 函數
// 此時的'this.a' 即 p.a,即 p 的自身屬性 'a'

使用不一樣的方法來建立對象和生成原型鏈

使用普通語法建立對象

var o = {a: 1};

// o這個對象繼承了Object.prototype上面的全部屬性
// 因此能夠這樣使用 o.hasOwnProperty('a').
// hasOwnProperty 是Object.prototype的自身屬性。
// Object.prototype的原型爲null。
// 原型鏈以下:
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];

// 數組都繼承於Array.prototype 
// (indexOf, forEach等方法都是從它繼承而來).
// 原型鏈以下:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
  return 2;
}

// 函數都繼承於Function.prototype
// (call, bind等方法都是從它繼承而來):
// f ---> Function.prototype ---> Object.prototype ---> null

使用構造器建立對象

在 JavaScript 中,構造器其實就是一個普通的函數。當使用 new 操做符 來做用這個函數時,它就能夠被稱爲構造方法(構造函數)。優化

function Graph() {
  this.vertexes = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v){
    this.vertexes.push(v);
  }
};

var g = new Graph();
// g是生成的對象,他的自身屬性有'vertexes'和'edges'.
// 在g被實例化時,g.[[Prototype]]指向了Graph.prototype.

 原型繼承:ui

 function Animal(name){
 2        this.name = name;
 3    }
 4    function Tiger(color){
 5        this.color = color;
 6    }
 7 //   var tiger = new Tiger('yellow');
 8 //   console.log(tiger.color);
 9 //   console.log(tiger.name);  //undefined
10 //      Tiger.prototype = new Animal('老虎');   //一種方式
11    Object.prototype.name = '大老虎';   //第二種方式
12         var tiger = new Tiger('yellow');
13         console.log(tiger.color);
14         console.log(tiger.name);

值得注意的是,這裏存在兩個主要的問題: ①它不方便給父級類型傳遞參數;②父級類型當中的引用類型被全部的實例共享

使用 Object.create 建立對象

ECMAScript 5 中引入了一個新方法:Object.create()。能夠調用這個方法來建立一個新對象。新對象的原型就是調用 create 方法時傳入的第一個參數:

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (繼承而來)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 由於d沒有繼承Object.prototype


使用 class 關鍵字

ECMAScript6 引入了一套新的關鍵字用來實現 class。使用基於類語言的開發人員會對這些結構感到熟悉,但它們是不同的。 JavaScript 仍然是基於原型的。這些新的關鍵字包括 class,constructorstaticextends, 和 super.

"use strict";

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);

性能

在原型鏈上查找屬性比較耗時,對性能有反作用,這在性能要求苛刻的狀況下很重要。另外,試圖訪問不存在的屬性時會遍歷整個原型鏈。

遍歷對象的屬性時,原型鏈上的每一個可枚舉屬性都會被枚舉出來。

檢測對象的屬性是定義在自身上仍是在原型鏈上,有必要使用 hasOwnProperty 方法,全部繼承自 Object.proptotype 的對象都包含這個方法

hasOwnProperty 是 JavaScript 中惟一一個只涉及對象自身屬性而不會遍歷原型鏈的方法。

 
注意:僅僅經過判斷值是否爲 undefined 還不足以檢測一個屬性是否存在,一個屬性可能存在而其值剛好爲 undefined。

很差的實踐:擴展原生對象的原型

一個常常被用到的錯誤實踐是去擴展 Object.prototype 或者其餘內置對象的原型。

該技術被稱爲 monkey patching,它破壞了原型鏈的密封性。儘管,一些流行的框架(如 Prototype.js)在使用該技術,可是並無足夠好的理由要用其餘非標準的方法將內置的類型系統搞亂。

咱們去擴展內置對象原型的惟一理由是引入新的 JavaScript 引擎的某些新特性,好比Array.forEach

示例

B 將繼承自 A:

function A(a){
  this.varA = a;
}

// 以上函數 A 的定義中,既然 A.prototype.varA 老是會被 this.varA 遮蔽,
// 那麼將 varA 加入到原型(prototype)中的目的是什麼?
A.prototype = {
  varA : null,  // 既然它沒有任何做用,幹嗎不將 varA 從原型(prototype)去掉?
      // 也許做爲一種在隱藏類中優化分配空間的考慮?
      // https://developers.google.com/speed/articles/optimizing-javascript#Initializing instance variables
      // 將會驗證若是 varA 在每一個實例不被特別初始化會是什麼狀況。
  doSomething : function(){
    // ...
  }
}

function B(a, b){
  A.call(this, a);
  this.varB = b;
}
B.prototype = Object.create(A.prototype, {
  varB : {
    value: null, 
    enumerable: true, 
    configurable: true, 
    writable: true 
  },
  doSomething : { 
    value: function(){ // override
      A.prototype.doSomething.apply(this, arguments); // call super
      // ...
    },
    enumerable: true,
    configurable: true, 
    writable: true
  }
});
B.prototype.constructor = B;

var b = new B();
b.doSomething();

 

最重要的部分是:

  • 類型被定義在 .prototype 中
  • 而你用 Object.create() 來繼承

prototype 和 Object.getPrototypeOf

對於從 Java 或 C ++ 轉過來的開發人員來講 JavaScript 會有點讓人困惑,由於它所有都是動態的,都是運行時,並且不存在類(classes)。全部的都是實例(對象)。即便咱們模擬出的 「類(classes)」,也只是一個函數對象。

你可能已經注意到,咱們的函數 A 有一個特殊的屬性叫作原型。這個特殊的屬性與 JavaScript 的 new 運算符一塊兒工做。對原型對象的引用會複製到新實例內部的 [[Prototype]] 屬性。例如,當你這樣: var a1 = new A(), JavaScript 就會設置:a1.[[Prototype]] = A.prototype(在內存中建立對象後,並在運行 this 綁定的函數 A()以前)。而後在你訪問實例的屬性時,JavaScript 首先檢查它們是否直接存在於該對象中(便是否是該對象的自身屬性),若是不是,它會在 [[Prototype]] 中查找。也就是說,你在原型中定義的元素將被全部實例共享,甚至能夠在稍後對原型進行修改,這種變動將影響到全部現存實例。

像上面的例子中,若是你執行 var a1 = new A(); var a2 = new A(); 那麼 a1.doSomething 事實上會指向Object.getPrototypeOf(a1).doSomething,它就是你在 A.prototype.doSomething 中定義的內容。好比:Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething。

簡而言之, prototype 是用於類型的,而 Object.getPrototypeOf() 是用於實例的(instances),二者功能一致。

[[Prototype]] 看起來就像遞歸引用, 如a1.doSomethingObject.getPrototypeOf(a1).doSomethingObject.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething 等等等, 直到它找到 doSomething 這個屬性或者 Object.getPrototypeOf 返回 null。

所以,當你執行:

var o = new Foo();

JavaScript 實際上執行的是:

var o = new Object();
o.[[Prototype]] = Foo.prototype;
Foo.call(o);

(或者相似上面這樣的),而後當你執行:

o.someProp;

它會檢查是否存在 someProp 屬性。若是沒有,它會查找Object.getPrototypeOf(o).someProp 仍舊沒有,它會繼續查找Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp ,一直查找下去,直到它找到這個屬性 或者 Object.getPrototypeOf() 返回 null 。

結論

在用原型繼承編寫複雜代碼前理解原型繼承模型十分重要。同時,還要清楚代碼中原型鏈的長度,並在必要時結束原型鏈,以免可能存在的性能問題。此外,除非爲了兼容新 JavaScript 特性,不然,永遠不要擴展原生的對象原型。

相關文章
相關標籤/搜索