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]]
。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]
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'