遊戲開發之旅-JavaScript原型鏈與繼承

本節是第四講的第二十九小節,上一節我們為大家介紹了JavaScript的一個重要概念閉包,本節將繼續介紹一個重要或者說JavaScript面向物件程式設計的關鍵概念原項鍊與繼承。

繼承與原型鏈(Inheritance and the prototype chain)

對於使用過基於類的語言 (如 Java 或 C++) 的開發人員來說,JavaScript 有點令人困惑,因為它是動態的,並且本身不提供一個 class 實現。(在 ES2015/ES6 中引入了 class 關鍵字,但那只是語法糖,JavaScript 仍然是基於原型的)。

當談到繼承時,JavaScript 只有一種結構:物件。每個例項物件( object )都有一個私有屬性(稱之為 __proto__ )指向它的建構函式的原型物件(prototype )。該原型物件也有一個自己的原型物件( __proto__ ) ,層層向上直到一個物件的原型物件為 null。根據定義,null 沒有原型,並作為這個原型鏈中的最後一個環節。

幾乎所有 JavaScript 中的物件都是位於原型鏈頂端的 Object 的例項。

儘管這種原型繼承通常被認為是 JavaScript 的弱點之一,但是原型繼承模型本身實際上比經典模型更強大。例如,在原型模型的基礎上構建經典模型相當簡單。

繼承屬性

JavaScript 物件是動態的屬性“包”(指其自己的屬性)。JavaScript 物件有一個指向一個原型物件的鏈。當試圖訪問一個物件的屬性時,它不僅僅在該物件上搜尋,還會搜尋該物件的原型,以及該物件的原型的原型,依次層層向上搜尋,直到找到一個名字匹配的屬性或到達原型鏈的末尾。

遵循ECMAScript標準,someObject。[[Prototype]] 符號是用於指向 someObject 的原型。從 ECMAScript 6 開始,[[Prototype]] 可以透過 Object。getPrototypeOf() 和 Object。setPrototypeOf() 訪問器來訪問。這個等同於 JavaScript 的非標準但許多瀏覽器實現的屬性 __proto__。

但它不應該與建構函式 func 的 prototype 屬性相混淆。被建構函式建立的例項物件的 [[Prototype]] 指向 func 的 prototype 屬性。Object。prototype 屬性表示 Object 的原型物件。

// 讓我們從一個函數里建立一個物件o,它自身擁有屬性a和b的:

let f = function () {

this。a = 1;

this。b = 2;

}

/* 這麼寫也一樣

function f() {

this。a = 1;

this。b = 2;

}

*/

let o = new f(); // {a: 1, b: 2}

// 在f函式的原型上定義屬性

f。prototype。b = 3;

f。prototype。c = 4;

// 不要在 f 函式的原型上直接定義 f。prototype = {b:3,c:4};這樣會直接打破原型鏈

// o。[[Prototype]] 有屬性 b 和 c

//  (其實就是 o。__proto__ 或者 o。constructor。prototype)

// o。[[Prototype]]。[[Prototype]] 是 Object。prototype。

// 最後o。[[Prototype]]。[[Prototype]]。[[Prototype]]是null

// 這就是原型鏈的末尾,即 null,

// 根據定義,null 就是沒有 [[Prototype]]。

// 綜上,整個原型鏈如下:

// {a:1, b:2} ——-> {b:3, c:4} ——-> Object。prototype——-> null

console。log(o。a); // 1

// a是o的自身屬性嗎?是的,該屬性的值為 1

console。log(o。b); // 2

// b是o的自身屬性嗎?是的,該屬性的值為 2

// 原型上也有一個‘b’屬性,但是它不會被訪問到。

// 這種情況被稱為“屬性遮蔽 (property shadowing)”

console。log(o。c); // 4

// c是o的自身屬性嗎?不是,那看看它的原型上有沒有

// c是o。[[Prototype]]的屬性嗎?是的,該屬性的值為 4

console。log(o。d); // undefined

// d 是 o 的自身屬性嗎?不是,那看看它的原型上有沒有

// d 是 o。[[Prototype]] 的屬性嗎?不是,那看看它的原型上有沒有

// o。[[Prototype]]。[[Prototype]] 為 null,停止搜尋

// 找不到 d 屬性,返回 undefined

繼承方法

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是一個繼承自 o 的物件

p。a = 4; // 建立 p 的自身屬性 ‘a’

console。log(p。m()); // 5

// 呼叫 p。m 時,‘this’ 指向了 p

// 又因為 p 繼承了 o 的 m 函式

// 所以,此時的 ‘this。a’ 即 p。a,就是 p 的自身屬性 ‘a’

在 JavaScript 中使用原型

正如之前提到的,在 JavaScript 中,函式(function)是允許擁有屬性的。所有的函式會有一個特別的屬性 —— prototype 。請注意,以下的程式碼是獨立的(出於嚴謹,假定頁面沒有其他的JavaScript程式碼)。為了最佳的學習體驗,我們強烈建議閣下開啟瀏覽器的控制檯(在Chrome和火狐瀏覽器中,按Ctrl+Shift+I即可),進入“console”選項卡,然後把如下的JavaScript程式碼複製貼上到視窗中,最後透過按下回車鍵執行程式碼。

function doSomething(){}

console。log( doSomething。prototype );

// 和宣告函式的方式無關,

// JavaScript 中的函式永遠有一個預設原型屬性。

var doSomething = function(){};

console。log( doSomething。prototype );

{    constructor: ƒ doSomething(),

__proto__: {

constructor: ƒ Object(),

hasOwnProperty: ƒ hasOwnProperty(),

isPrototypeOf: ƒ isPrototypeOf(),

propertyIsEnumerable: ƒ propertyIsEnumerable(),

toLocaleString: ƒ toLocaleString(),

toString: ƒ toString(),

valueOf: ƒ valueOf()

}}

我們可以給doSomething函式的原型物件新增新屬性,如下:

function doSomething(){}

doSomething。prototype。foo = “bar”;

console。log( doSomething。prototype );

{    foo: “bar”,

constructor: ƒ doSomething(),

__proto__: {

constructor: ƒ Object(),

hasOwnProperty: ƒ hasOwnProperty(),

isPrototypeOf: ƒ isPrototypeOf(),

propertyIsEnumerable: ƒ propertyIsEnumerable(),

toLocaleString: ƒ toLocaleString(),

toString: ƒ toString(),

valueOf: ƒ valueOf()

}}

function doSomething(){}

doSomething。prototype。foo = “bar”; // add a property onto the prototype

var doSomeInstancing = new doSomething();

doSomeInstancing。prop = “some value”; // add a property onto the object

console。log( doSomeInstancing );

{

prop: “some value”,

__proto__: {

foo: “bar”,

constructor: ƒ doSomething(),

__proto__: {

constructor: ƒ Object(),

hasOwnProperty: ƒ hasOwnProperty(),

isPrototypeOf: ƒ isPrototypeOf(),

propertyIsEnumerable: ƒ propertyIsEnumerable(),

toLocaleString: ƒ toLocaleString(),

toString: ƒ toString(),

valueOf: ƒ valueOf()

}

}

}

如上所示, doSomeInstancing 中的__proto__是 doSomething。prototype。 但這是做什麼的呢?當你訪問doSomeInstancing 中的一個屬性,瀏覽器首先會檢視doSomeInstancing 中是否存在這個屬性。

如果 doSomeInstancing 不包含屬性資訊, 那麼瀏覽器會在 doSomeInstancing 的 __proto__ 中進行查詢(同 doSomething。prototype)。 如屬性在 doSomeInstancing 的 __proto__ 中查詢到,則使用 doSomeInstancing 中 __proto__ 的屬性。

否則,如果 doSomeInstancing 中 __proto__ 不具有該屬性,則檢查doSomeInstancing 的 __proto__ 的  __proto__ 是否具有該屬性。預設情況下,任何函式的原型屬性 __proto__ 都是 window。Object。prototype。 因此, 透過doSomeInstancing 的 __proto__ 的  __proto__  ( 同 doSomething。prototype 的 __proto__ (同  Object。prototype)) 來查詢要搜尋的屬性。

如果屬性不存在 doSomeInstancing 的 __proto__ 的  __proto__ 中, 那麼就會在doSomeInstancing 的 __proto__ 的  __proto__ 的  __proto__ 中查詢。然而, 這裡存在個問題:doSomeInstancing 的 __proto__ 的  __proto__ 的  __proto__ 其實不存在。因此,只有這樣,在 __proto__ 的整個原型鏈被檢視之後,這裡沒有更多的 __proto__ , 瀏覽器斷言該屬性不存在,並給出屬性值為 undefined 的結論。

function doSomething(){}

doSomething。prototype。foo = “bar”;

var doSomeInstancing = new doSomething();

doSomeInstancing。prop = “some value”;

console。log(“doSomeInstancing。prop:      ” + doSomeInstancing。prop);

console。log(“doSomeInstancing。foo:       ” + doSomeInstancing。foo);

console。log(“doSomething。prop:           ” + doSomething。prop);

console。log(“doSomething。foo:            ” + doSomething。foo);

console。log(“doSomething。prototype。prop: ” + doSomething。prototype。prop);

console。log(“doSomething。prototype。foo:  ” + doSomething。prototype。foo);

doSomeInstancing。prop:      some value

doSomeInstancing。foo:       bar

doSomething。prop:           undefined

doSomething。foo:            undefined

doSomething。prototype。prop: undefined

doSomething。prototype。foo:  bar

使用語法結構建立的物件

var o = {a: 1};

// o 這個物件繼承了 Object。prototype 上面的所有屬性

// o 自身沒有名為 hasOwnProperty 的屬性

// hasOwnProperty 是 Object。prototype 的屬性

// 因此 o 繼承了 Object。prototype 的 hasOwnProperty

// Object。prototype 的原型為 null

// 原型鏈如下:

// o ——-> Object。prototype ——-> null

var a = [“yo”, “whadup”, “?”];

// 陣列都繼承於 Array。prototype

// (Array。prototype 中包含 indexOf, forEach 等方法)

// 原型鏈如下:

// a ——-> Array。prototype ——-> Object。prototype ——-> null

function f(){

return 2;

}

// 函式都繼承於 Function。prototype

// (Function。prototype 中包含 call, bind等方法)

// 原型鏈如下:

// f ——-> Function。prototype ——-> Object。prototype ——-> null

使用構造器建立的物件

function Graph() {

this。vertices = [];

this。edges = [];

}

Graph。prototype = {

addVertex: function(v){

this。vertices。push(v);

}};

var g = new Graph();

// g 是生成的物件,他的自身屬性有 ‘vertices’ 和 ‘edges’。

// 在 g 被例項化時,g。[[Prototype]] 指向了 Graph。prototype。

使用 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

效能

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

遍歷物件的屬性時,原型鏈上的每個可列舉屬性都會被枚舉出來。要檢查物件是否具有自己定義的屬性,而不是其原型鏈上的某個屬性,則必須使用所有物件從 Object。prototype 繼承的 hasOwnProperty 方法。下面給出一個具體的例子來說明它:

console。log(g。hasOwnProperty(‘vertices’));// true

console。log(g。hasOwnProperty(‘nope’));// false

console。log(g。hasOwnProperty(‘addVertex’));// false

console。log(g。__proto__。hasOwnProperty(‘addVertex’));// true

hasOwnProperty 是 JavaScript 中唯一一個處理屬性並且不會遍歷原型鏈的方法。

注意:檢查屬性是否為 undefined 是不能夠檢查其是否存在的。該屬性可能已存在,但其值恰好被設定成了 undefined。

錯誤實踐:擴充套件原生物件的原型

經常使用的一個錯誤實踐是擴充套件 Object。prototype 或其他內建原型。

這種技術被稱為猴子補丁並且會破壞封裝。儘管一些流行的框架(如 Prototype。js)在使用該技術,但仍然沒有足夠好的理由使用附加的非標準方法來混入內建原型。

擴充套件內建原型的唯一理由是支援 JavaScript 引擎的新特性,如 Array。forEach。

結論

在使用原型繼承編寫複雜程式碼之前,理解原型繼承模型是至關重要的。此外,請注意程式碼中原型鏈的長度,並在必要時將其分解,以避免可能的效能問題。此外,原生原型不應該被擴充套件,除非它是為了與新的 JavaScript 特性相容。

function A(a){

this。varA = a;

}

// 以上函式 A 的定義中,既然 A。prototype。varA 總是會被 this。varA 遮蔽,// 那麼將 varA 加入到原型(prototype)中的目的是什麼?

A。prototype = {

varA : null,

/*既然它沒有任何作用,幹嘛不將 varA 從原型(prototype)去掉 ?

也許作為一種在隱藏類中最佳化分配空間的考慮 ?

如果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();

以上內容部分摘自影片課程04網頁遊戲程式設計JavaScript-28閉包,更多示例請參見網站示例。跟著張員外講程式設計,學習更輕鬆,不花錢還能學習真本領。