[TOC]
# 子類化內置構造函數
JavaScript內置的構造函數很難子類化。這一章解釋了原因并提出了解決方案。
## 術語
我們使用了一個*內置的子類*,避免了術語擴展,因為它是用JavaScript編寫的:
* 子類化一個內置的`A`
創建一個給定的內置構造函數`A`的子構造函數`B`。`B`的實例也是`a`的實例。
* 擴展`obj`對象
復制一個對象屬性到另一個對象中。 `Underscore.js`[使用了這個術語](http://underscorejs.org/#extend),延續了`Prototype`框架建立的傳統。
子類化內置對象有兩個障礙:具有內部屬性的實例和不能作為函數調用的構造函數。
## 障礙1:具有內部屬性的實例
大多數內置構造函數都有*具有所謂的內部屬性*(見[屬性種類](###第17章))的實例,其名稱用雙方括號表示,如下所示:`[[PrimitiveValue]]`。內部屬性由JavaScript引擎管理,通常不能直接通過JavaScript訪問。JavaScript中的正常子類化技術是傳入子構造函數中的`this`來調用父級構造函數。(請參閱[第4節:構造函數之間的繼承](###第17章)):
```js
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;
}
```
大多數內置函數忽略了`(2)`中作為`this`傳入的子實例,這是下一節中描述的一個障礙。此外,將內部屬性添加到現有實例`(1)`通常是不可能的,因為它們傾向于從根本上改變實例的性質。因此,`(2)`的調用不能用于添加內部屬性。以下構造函數都有*內部屬性*的實例:
**包裝器構造函數**
`Boolean`、`Number`和`String`的實例其實包裝了原始值.它們都具有通過`valueOf()`返回的 `[[PrimitiveValue]]`這個內部屬性。
`String`有兩個附加的實例屬性:
* Boolean:內部實例屬性`[[PrimitiveValue]]`。
* Number:內部實例屬性`[[PrimitiveValue]]`。
* String:內部實例屬性`[[PrimitiveValue]]`,自定義內部實例方法`[[GetOwnProperty]]`,普通實例屬性`length`。當使用數組索引時,`[[GetOwnProperty]]`可以實現通過從包裝好的字符串中讀取數據,對字符進行索引訪問。
1. Array:
自定義內部實例方法`[[DefineOwnProperty]]`可以阻止屬性被設置。它確保屬性`length`正常工作,可以在添加數組元素時保持`length`處于最新值,并在`length`變小時刪除多余的元素。
2. Date:
內部實例屬性`[[PrimitiveValue]]`存儲了由日期實例表示的時間(自1970年1月1日00:00:00 UTC的**毫秒數**)。
3. Function:
內部實例屬性`[[Call]]`(當一個實例被調用時執行的該代碼)和其他可能的代碼
4. RegExp:
內部實例屬性 `[[Match]]`,加上兩個非內部的實例屬性。下面是來自ECMAScript的規范:
> 內部實例屬性`[[Match]]`的值是RegExp對象的模式的實現依賴表示。
**唯一沒有內部屬性的內置構造函數是`Error`和`Object`。**
### 障礙1的解決方法
`MyArray`是`Array`的子類。它有一個名為`size`的 getter ,返回了數組中的實際元素,忽略了漏洞(在這里`length`考慮了漏洞)。實現`MyArray`的技巧是它創建一個數組實例并將其方法復制到其中(受到Ben Nadel的一篇[博客文章](https://www.bennadel.com/blog/2292-extending-javascript-arrays-while-keeping-native-bracket-notation-functionality.htm)的啟發):
```js
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;
}
}
```
這個代碼使用了輔助函數`copyownproperties()`,這個函數在[復制對象](###第17章#code_copyOwnPropertiesFrom)章節中說過了。
我們不會在行`(1)`中調用`Array`的構造函數,因為一個怪癖:如果用一個參數來調用它,那么這個數字就不會成為一個元素,只是一個空數組的長度(參見用[元素(注意事項!)初始化一個數組](###第18章#avoid_array_constructor))。
這是交互運行結果:
```js
> var a = new MyArray('a', 'b')
> a.length = 4;
> a.length
4
> a.size
2
```
### 注意
將方法復制到實例會導致冗余,這可以通過原型來避免(如果我們有這個選擇)。此外,`MyArray`創建的對象不是它的實例:
```js
> a instanceof MyArray
false
> a instanceof Array
true
```
## 障礙2:內置的構造函數不能作為方法調用
即使`Error`和子類沒有具有內部屬性的實例,您仍然無法輕松地對其進行子類化,因為子類化的標準模式行不通(上述代碼重復):
```js
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`:
```js
> var e = {};
> Object.getOwnPropertyNames(Error.call(e)) // new instance
[ 'stack', 'arguments', 'type' ]
> Object.getOwnPropertyNames(e) // unchanged
[]
```
在前面的交互中,`Error`返回一個具有自己屬性的實例,但它是一個新的實例,而不是`e`。子類化模式只有在`Error`將自己的屬性添加到`this`(`e`,在前面的例子中)時才會起作用。
### 障礙2的解決方法
在子構造函數中,創建一個新的父級實例并將其自己的屬性復制到子實例:
```js
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;
```
再次使用提到過的的`copyownproperties()`。嘗試`MyError`:
```js
try {
throw new MyError('Something happened');
} catch (e) {
console.log('Properties: '+Object.getOwnPropertyNames(e));
}
```
下面是在`node.js`下的輸出:
~~~
Properties: stack,arguments,message,type
~~~
該實例之間的關系是:
```js
> new MyError() instanceof Error
true
> new MyError() instanceof MyError
true
```
## 另一種解決方案:委托
委托可以非常干凈的替代子類。例如,要創建自己的數組構造函數,您需要在屬性中保留一個數組:。
```js
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`的元素;您必須使用這樣的方法:
```js
MyArray.prototype.get = function (index) {
return this.array[index];
}
MyArray.prototype.set = function (index, value) {
return this.array[index] = value;
}
```
可以通過以下元編程來傳輸`Array.prototype`上的普通方法:
```js
[ 'toString', 'push', 'pop' ].forEach(function (key) {
MyArray.prototype[key] = function () {
return Array.prototype[key].apply(this.array, arguments);
}
});
```
通過存儲在`MyArray`實例中的數組`this.array`上調用它們,我們從`Array`的方法中獲得了`MyArray`方法。
使用`MyArray`:
```js
> 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'
```
- 本書簡介
- 前言
- 關于這本書你需要知道些什么
- 如何閱讀本書
- 目錄
- I. JavaScript的快速入門
- 第1章 基礎的JavaScript
- II. 背景知識
- 第2章 為什么選擇JavaScript?
- 第3章 JavaScript的性質
- 第4章 JavaScript是如何創造出來的
- 第5章 標準化:ECMAScript
- 第6章 JavaScript的歷史里程碑
- III. 深入JavaScript
- 第7章 JavaScript語法
- 第8章 值
- 第9章 運算符
- 第10章 布爾值
- 第11章 數字
- 第12章 字符串
- 第13章 語句
- 第14章 異常捕獲
- 第15章 函數
- 第16章 變量:作用域、環境和閉包
- 第17章 對象和繼承
- 第18章 數組
- 第19章 正則表達式
- 第20章 Date
- 第21章 Math
- 第22章 JSON
- 第23章 標準全局變量
- 第24章 編碼和JavaScript
- 第25章 ECMAScript 5中的新功能
- IV. 技巧、工具和類庫
- 第26章 元代碼樣式指南
- 第27章 調試的語言機制
- 第28章 子類化內置構造函數
- 第29章 JSDoc:生成API文檔
- 第30章 類庫
- 第31章 模塊系統和包管理器
- 第32章 其他工具
- 第33章 接下來該做什么
- 著作權