js繼承與原型鏈

對於那些熟悉基於類的面嚮對象語言(Java 或者 C++)的開發者來講,JavaScript 的語法是比較怪異的,這是因爲 JavaScript 是一門動態語言,並且它沒有類的概念(雖然 class 是個保留字,不能做爲變量名來使用)。web

繼承方面,JavaScript 中的每一個對象都有一個內部私有的連接指向另外一個對象,這個對象就是原對象的原型。這個原型對象也有本身的原型,直到對象的原型爲 null 爲止(也就是沒有原型)。這種一級一級的鏈結構就稱爲原型鏈數組

雖然這一般會被稱做 JavaScript 的弱點之一,實際上這種原型繼承的模型要比經典的繼承模型還要強大。雖然在原型模型上構建一個經典模型是至關瑣碎的,但若是採起其餘方式實現則會更加困難。框架

基於原型鏈的繼承

繼承屬性

JavaScript 對象有兩種不一樣的屬性,一種是對象自身的屬性,另一種是繼承於原型鏈上的屬性。下面的代碼則演示了當訪問一個對象的屬性時發生的行爲:函數

// 假定咱們有個對象o,而且o所在的原型鏈以下: // {a:1, b:2} ---> {b:3, c:4} ---> null // 'a'和'b'是o自身的屬性.  // 該例中,用"對象.[[Prototype]]"來表示這個對象的原型. // 這只是一個純粹的符號表示(ECMAScript標準中也這樣使用),不能在實際代碼中使用. console.log(o.a); // 1 // a是o的自身屬性嗎?是的,該屬性的值爲1 console.log(o.b); // 2 // b是o的自身屬性嗎?是的,該屬性的值爲2 // o.[[Prototype]]上還有一個'b'屬性,可是它不會被訪問到.這種狀況稱爲"屬性遮蔽". 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
 

繼承方法

JavaScript 並無真正的「方法」,JavaScript 只有函數,並且任何函數均可以添加到對象上做爲對象的屬性。繼承的函數與其餘的屬性是基本沒有差異的,包括「屬性遮蔽」(這種狀況至關於其餘語言的方法重寫)。性能

當繼承的函數被調用時,this 指向的是當前繼承原型的對象,而不是繼承的函數所在的原型對象。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. 'this.a'則是12.
 

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

使用普通語法建立對象

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 操做符 來做用這個函數時,它就能夠被稱爲構造方法(構造函數)。spa

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.
 

使用 Object.create 建立對象

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

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
 

性能

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

遍歷對象的屬性時,原型鏈上的每一個屬性都是可枚舉的。orm

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

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

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

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

一個常用的很差實踐是擴展 Object.prototype 或者其餘內置對象的原型。

該技術被稱爲 monkey patching,它破壞了對象的封裝性。雖然一些流行的框架(如 Prototype.js)在使用該技術,可是該技術依然不是好的實踐,附加的非標準的方法使得內置的類型混亂。

擴展內置對象原型的惟一正當理由是移植較新 JavaScript 引擎的特性,如 Array.forEach

結論

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

相關文章
相關標籤/搜索