[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章 接下来该做什么
- 著作权