JavaScript 的内置构造函数很难进行子类化。 本章解释了原因并提出了解决方案。
我们使用短语 对内置对象进行子类化 并避免使用术语 扩展,因为它在 JavaScript 中有其他含义
A 进行子类化A 的子构造函数 B。 B 的实例也是 A 的实例。obj对内置对象进行子类化有两个障碍:具有内部属性的实例和不能作为函数调用的构造函数。
大多数内置构造函数 的实例都具有所谓的 内部属性(参见 属性的种类),其名称用双括号括起来,如下所示:[[PrimitiveValue]]。内部属性由 JavaScript 引擎管理,通常不能在 JavaScript 中直接访问。JavaScript 中的常规子类化技术是使用子构造函数的 this 将超构造函数作为函数调用(参见 第 4 层:构造函数之间的继承)
functionSuper(x,y){this.x=x;// (1)this.y=y;// (1)}functionSub(x,y,z){// Add superproperties to subinstanceSuper.call(this,x,y);// (2)// Add subpropertythis.z=z;}
大多数内置对象会忽略作为 this 传入的子实例 (2),下一节将介绍此障碍。此外,通常不可能将内部属性添加到现有实例 (1) 中,因为它们往往会从根本上改变实例的性质。因此,(2) 处的调用不能用于添加内部属性。以下构造函数的实例具有内部属性
Boolean、Number 和 String 的实例包装了原始值。它们都具有内部属性 [[PrimitiveValue]],其值由 valueOf() 返回;String 还有两个额外的实例属性
Boolean:内部实例属性 [[PrimitiveValue]]。Number:内部实例属性 [[PrimitiveValue]]。String:内部实例属性 [[PrimitiveValue]],自定义内部实例方法 [[GetOwnProperty]],普通实例属性 length。当使用数组索引时,[[GetOwnProperty]] 允许通过从包装的字符串中读取来对字符进行索引访问。Array
[[DefineOwnProperty]] 拦截要设置的属性。它通过在添加数组元素时保持 length 最新并在 length 变小时删除多余的元素,来确保 length 属性正常工作。Date
[[PrimitiveValue]] 存储日期实例表示的时间(自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数)。Function
[[Call]](调用实例时要执行的代码)以及其他可能的属性。RegExp
内部实例属性 [[Match]],以及两个非内部实例属性。根据 ECMAScript 规范
[[Match]]内部属性的值是RegExp对象的模式的实现相关表示。
唯一没有内部属性的内置构造函数是 Error 和 Object。
MyArray 是 Array 的子类。它有一个 getter size,它返回数组中的实际元素,忽略空洞(length 会考虑空洞)。实现 MyArray 所用的技巧是创建一个数组实例并将其方法复制到其中:[22]
functionMyArray(/*arguments*/){vararr=[];// Don’t use Array constructor to set up elements (doesn’t always work)Array.prototype.push.apply(arr,arguments);// (1)copyOwnPropertiesFrom(arr,MyArray.methods);returnarr;}MyArray.methods={getsize(){varsize=0;for(vari=0;i<this.length;i++){if(iinthis)size++;}returnsize;}}
此代码使用辅助函数 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 和 子类没有具有内部属性的实例,您仍然无法轻松地对其进行子类化,因为标准的子类化模式将不起作用(从前面重复):
functionSuper(x,y){this.x=x;this.y=y;}functionSub(x,y,z){// Add superproperties to subinstanceSuper.call(this,x,y);// (1)// Add subpropertythis.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)时,子类化模式才会起作用。
在子构造函数内部,创建一个新的超实例并将其自身属性复制到子实例
functionMyError(){// Use Error as a functionvarsuperInstance=Error.apply(null,arguments);copyOwnPropertiesFrom(this,superInstance);}MyError.prototype=Object.create(Error.prototype);MyError.prototype.constructor=MyError;
辅助函数 copyOwnPropertiesFrom() 在 复制对象 中显示。试用 MyError
try{thrownewMyError('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
委托是子类化的一种非常简洁的替代方案。 例如,要创建自己的数组构造函数,您可以将数组保存在一个属性中:
functionMyArray(/*arguments*/){this.array=[];Array.prototype.push.apply(this.array,arguments);}Object.defineProperties(MyArray.prototype,{size:{get:function(){varsize=0;for(vari=0;i<this.array.length;i++){if(iinthis.array)size++;}returnsize;}},length:{get:function(){returnthis.array.length;},set:function(value){returnthis.array.length=value;}}});
明显的限制是您不能通过方括号访问 MyArray 的元素;您必须使用方法来访问
MyArray.prototype.get=function(index){returnthis.array[index];}MyArray.prototype.set=function(index,value){returnthis.array[index]=value;}
Array.prototype 的普通方法可以通过以下元编程片段进行传输
['toString','push','pop'].forEach(function(key){MyArray.prototype[key]=function(){returnArray.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'