JavaScript 的內建建構函式難以建立子類別。本章說明原因並提出解決方案。
我們使用片語 建立內建子類別,避免使用術語 延伸,因為 JavaScript 中已採用此術語
A
的子類別
A
的子建構函式 B
。 B
的執行個體也是 A
的執行個體。
obj
建立內建子類別有兩個障礙:具有內部屬性的執行個體和無法作為函式呼叫的建構函式。
大多數內建建構函式 具有所謂的 內部屬性 的執行個體(請參閱 屬性的種類),其名稱以雙方括號撰寫,如下所示: [[PrimitiveValue]]
。內部屬性由 JavaScript 引擎管理,通常無法在 JavaScript 中直接存取。JavaScript 中正常的子類別技術是將超建構函式作為函式呼叫,並使用子建構函式的 this
(請參閱 第 4 層:建構函式之間的繼承)
function
Super
(
x
,
y
)
{
this
.
x
=
x
;
// (1)
this
.
y
=
y
;
// (1)
}
function
Sub
(
x
,
y
,
z
)
{
// Add superproperties to subinstance
Super
.
call
(
this
,
x
,
y
);
// (2)
// Add subproperty
this
.
z
=
z
;
}
大多數內建函式會忽略作為 this
傳遞的子執行個體 (2),下一節會說明此障礙。此外,通常無法將內部屬性新增至現有執行個體 (1),因為這往往會徹底改變執行個體的本質。因此,無法使用 (2) 的呼叫來新增內部屬性。下列建構函式具有內部屬性的執行個體
Boolean
、Number
和 String
的執行個體會包裝基本型別。它們都具有內部屬性 [[PrimitiveValue]]
,其值由 valueOf()
傳回;String
有兩個額外的執行個體屬性
Boolean
:內部執行個體屬性 [[PrimitiveValue]]
。
Number
:內部執行個體屬性 [[PrimitiveValue]]
。
字串
:內部實例屬性 [[PrimitiveValue]]
、自訂內部實例方法 [[GetOwnProperty]]
、一般實例屬性 length
。當使用陣列索引時,[[GetOwnProperty]]
可透過從包裝字串中讀取來啟用字元的索引存取。
陣列
[[DefineOwnProperty]]
會攔截正在設定的屬性。它會確保 length
屬性運作正確,方法是在新增陣列元素時保持 length
為最新狀態,以及在縮小 length
時移除多餘元素。
日期
[[PrimitiveValue]]
會儲存日期實例所表示的時間(以自 1970 年 1 月 1 日 00:00:00 UTC 以來的毫秒數表示)。
函式
[[Call]]
(在呼叫實例時要執行的程式碼)以及其他可能的屬性。
正規表示式
內部實例屬性 [[Match]]
,加上兩個非內部實例屬性。根據 ECMAScript 規範
[[Match]]
內部屬性的值是RegExp
物件的模式的實作依賴表示法。
沒有內部屬性的內建建構函式只有 Error
和 Object
。
MyArray
是 Array
的子類別。它有一個 getter size
,會傳回陣列中的實際元素,忽略孔洞(length
會考慮孔洞)。實作 MyArray
所使用的技巧是建立一個陣列實例,並將其方法複製到其中:[22]
function
MyArray
(
/*arguments*/
)
{
var
arr
=
[];
// Don’t use Array constructor to set up elements (doesn’t always work)
Array
.
prototype
.
push
.
apply
(
arr
,
arguments
);
// (1)
copyOwnPropertiesFrom
(
arr
,
MyArray
.
methods
);
return
arr
;
}
MyArray
.
methods
=
{
get
size
()
{
var
size
=
0
;
for
(
var
i
=
0
;
i
<
this
.
length
;
i
++
)
{
if
(
i
in
this
)
size
++
;
}
return
size
;
}
}
這段程式碼使用輔助函式 copyOwnPropertiesFrom()
,其顯示和說明如下:複製物件。
我們沒有在第 (1) 行呼叫 Array
建構函式,因為有一個怪癖:如果呼叫時只有一個數字參數,該數字並不會變成元素,而是決定空陣列的長度(請參閱初始化包含元素的陣列(避免!))。
以下是互動
> var a = new MyArray('a', 'b') > a.length = 4; > a.length 4 > a.size 2
複製方法到一個實例會導致冗餘,而這可以用原型來避免(如果我們有選項可以使用一個原型)。此外,MyArray
會建立不是其實例的物件
> a instanceof MyArray false > a instanceof Array true
即使 Error
和 子類別沒有具有內部屬性的實例,你仍然無法輕鬆地對它們進行子類別化,因為子類別化的標準模式無法運作(從前面重複):
function
Super
(
x
,
y
)
{
this
.
x
=
x
;
this
.
y
=
y
;
}
function
Sub
(
x
,
y
,
z
)
{
// Add superproperties to subinstance
Super
.
call
(
this
,
x
,
y
);
// (1)
// Add subproperty
this
.
z
=
z
;
}
問題在於 Error
即使當作函式呼叫(1)也會產生一個新實例;也就是說,它會忽略透過 call()
傳遞給它的參數 this
> var e = {}; > Object.getOwnPropertyNames(Error.call(e)) // new instance [ 'stack', 'arguments', 'type' ] > Object.getOwnPropertyNames(e) // unchanged []
在前面的互動中,Error
會傳回一個具有自身屬性的實例,但它是一個新實例,不是 e
。子類別化模式只有在 Error
將自身屬性新增到 this
(在前述情況中為 e
)時才會運作。
在子建構函式內部,建立一個新的超級實例,並將其自身屬性複製到子實例
function
MyError
()
{
// Use Error as a function
var
superInstance
=
Error
.
apply
(
null
,
arguments
);
copyOwnPropertiesFrom
(
this
,
superInstance
);
}
MyError
.
prototype
=
Object
.
create
(
Error
.
prototype
);
MyError
.
prototype
.
constructor
=
MyError
;
輔助函式 copyOwnPropertiesFrom()
顯示在 複製一個物件 中。嘗試 MyError
try
{
throw
new
MyError
(
'Something happened'
);
}
catch
(
e
)
{
console
.
log
(
'Properties: '
+
Object
.
getOwnPropertyNames
(
e
));
}
以下是 Node.js 上的輸出
Properties: stack,arguments,message,type
instanceof
關係就像它應該的那樣
> new MyError() instanceof Error true > new MyError() instanceof MyError true
委派是子類別化一個非常乾淨的替代方案。例如,要建立你自己的陣列建構函式,你會在一個屬性中保留一個陣列:
function
MyArray
(
/*arguments*/
)
{
this
.
array
=
[];
Array
.
prototype
.
push
.
apply
(
this
.
array
,
arguments
);
}
Object
.
defineProperties
(
MyArray
.
prototype
,
{
size
:
{
get
:
function
()
{
var
size
=
0
;
for
(
var
i
=
0
;
i
<
this
.
array
.
length
;
i
++
)
{
if
(
i
in
this
.
array
)
size
++
;
}
return
size
;
}
},
length
:
{
get
:
function
()
{
return
this
.
array
.
length
;
},
set
:
function
(
value
)
{
return
this
.
array
.
length
=
value
;
}
}
});
顯而易見的限制是,你無法透過方括號存取 MyArray
的元素;你必須使用函式才能這麼做
MyArray
.
prototype
.
get
=
function
(
index
)
{
return
this
.
array
[
index
];
}
MyArray
.
prototype
.
set
=
function
(
index
,
value
)
{
return
this
.
array
[
index
]
=
value
;
}
Array.prototype
的一般函式可以透過以下的元程式化片段轉移
[
'toString'
,
'push'
,
'pop'
].
forEach
(
function
(
key
)
{
MyArray
.
prototype
[
key
]
=
function
()
{
return
Array
.
prototype
[
key
].
apply
(
this
.
array
,
arguments
);
}
});
我們透過在儲存在 MyArray
實例中的陣列 this.array
上呼叫 Array
函式來衍生 MyArray
函式。
使用 MyArray
> var a = new MyArray('a', 'b'); > a.length = 4; > a.push('c') 5 > a.length 5 > a.size 3 > a.set(0, 'x'); > a.toString() 'x,b,,,c'