🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[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' ```