遊戲開發之旅-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閉包,更多示例請參見網站示例。跟著張員外講程式設計,學習更輕鬆,不花錢還能學習真本領。