前2个陷阱比较简单,而且不容易遇到我就一笔带过了。
- 不要扩展Object对象。因为扩展Object之后,会使得for循环多遇到一个元素,尽管可以通过
this.hasOwnProperty(i)
来判断是否忽略,还是很不方便 - 不要扩展Number对象。否则会抛出异常
下面讲第三个:继承Array类,这个遇见的概率更大些。而且使用的技巧也容易推广。 但是如果我们这样写:
QUnit.test(
"Simulating Array functionality but without the true subclassing",
function(assert) {
function MyArray0() {}
MyArray0.prototype = new Array();
var mine = new MyArray0();
mine.push(1, 2, 3);
assert.deepEqual(mine.length, 3,
"All the items are in our sub-classed array.");
assert.deepEqual(mine[0], 1,
"first element is 1");
assert.deepEqual(mine[1], 2,
"first element is 2");
assert.deepEqual(mine[2], 3,
"first element is 3");
assert.ok(mine instanceof Array,
"Verify that we implement Array functionality.");
})
以上代码在大部分浏览器上运行正常,除了IE(又是它!)。因为IE的length属性非常特殊。可以看到,只有第一个测试失败了。
为了解决以上问题,我们需要分别绑定每个方法:
Listing 6.15 Simulating Array functionality but without the true subclassing
QUnit.test(
"Simulating Array functionality but without the true subclassing",
function(assert) {
function MyArray() {}
// Defines a new class with a prototyped length property
MyArray.prototype.length = 0;
(function() {
var methods = ['push', 'pop', 'shift', 'unshift',
'slice', 'splice', 'join'
];
// Copies selected array functionality
for (var i = 0; i < methods.length; i++)(function(name) {
MyArray.prototype[name] = function() {
return Array.prototype[name].apply(this, arguments);
};
})(methods[i]);
})();
var mine = new MyArray();
mine.push(1, 2, 3);
assert.deepEqual(mine.length, 3,
"All the items are on our sub-classed array.");
assert.deepEqual(mine[0], 1,
"first element is 1");
assert.deepEqual(mine[1], 2,
"first element is 2");
assert.deepEqual(mine[2], 3,
"first element is 3");
assert.ok(!(mine instanceof Array),
"We aren't subclassing Array, though.");
});
上例中,我们只是给MyArray的原型定义了一个length,因为他是唯一可变的属性,而且IE没有提供该属性。然后我们使用immediate function和第四章学到的apply()技巧给MyArray绑定每个方法。
注意这里我们并没有继承Array,只是模拟了一个
可以看出,js中的函数具有双重身份:普通函数和构造函数。他们的行为完全不同。那么对于用户来说如何区分2种用法,如果他们用错了会如何呢?(比如不小心把new遗漏了)
Listing 6.17 错误初始化
<script type="text/javascript">
QUnit.test("错误初始化", function(assert) {
function User(first, last) {
this.name = first + " " + last;
}
(function() {
this.name = "Rukia";
var user = User("Ichigo", "Kurosaki");
assert.ok(user, "Rukia exists")
assert.deepEqual(this.name, "Rukia",
"Name was set to Rukia.");
})();
var name = "Rukia";
var user = User("Ichigo", "Kurosaki");
assert.deepEqual(name, "Rukia",
"Name was set to Rukia.");
});
</script>
第一个测试失败了。此处User被当成了普通函数调用,user为undefined。 比较有趣的是第二个和第三个:第二个失败了,因为在函数没有明确指定上下文时,this指向全局Window对象,在User不正确调用时,产生了副作用,即把原来附加到window上的name属性更改了!。而第三个成功了,这里的name是全局对象,this指向Object,不会对Window对象产生影响。【原文中的测试是第三个,原著说会失败,其实不会。译者注】。
以上暴漏了不用new调用构造函数的2个问题:
- 返回未定义对象。
- 可能污染全局空间。
这对于初学者来说可能是调试的噩梦。那么,作为一个有责任心的忍者,我们要学会规范/避免这类问题, 关键是如何判断一个构造函数是通过new调用的呢?刚才我们看到,如果错误调用,this会指向Window,或者其他上下文,只有通过new调用this才会指向我们期望的对象。因此我们可以判断this:
6.18 判断构造函数调用
QUnit.test("判断构造函数调用", function(assert)
{
function Test()
{
return this instanceof arguments.callee;
}
assert.ok(!Test(),
"We didn't instantiate, so it returns false.");
assert.ok(new Test(), "We did instantiate, returning true.");
});
上面,我们通过比较this和arguments.callee来判断构造函数是否调用正确。从第4章我们知道,arguments.callee总是引用当前调用函数。而this通常指向全局对象,除非被显示更改。
ok,测试都通过了,那么下一步怎么弄,直接抛出异常?那不是忍者所为,既然他已经被定义为构造函数,那我们就让他总是按照构造函数的模式运行:
listing 6.19 纠正用户错误调用构造函数
<script type="text/javascript">
QUnit.test("纠正用户错误调用构造函数", function(assert) {
function User(first, last) {
if (! (this instanceof User) ){
return new User(first, last);
}
this.name = first + " " + last;
}
(function() {
this.name = "Rukia";
var user = User("Ichigo", "Kurosaki");
assert.ok(user, "Rukia exists")
assert.deepEqual(this.name, "Rukia",
"Name was set to Rukia.");
})();
var name = "Rukia";
var user = User("Ichigo", "Kurosaki");
assert.deepEqual(name, "Rukia",
"Name was set to Rukia.");
});
</script>
好了,这下子3个测试都通过了,对用户也更加友好了(谁说忍者都是冷酷无情的:smirk:)。
不过,作为忍者,我们还需要停下来想想,这样做有什么问题?
- arguments.callee是js不提倡的,在strict mode中他会失效。【可以直接使用函数名,如listing6.19,译者注】
- 这真的是一个好的code practice吗?抛出异常是不是更好?估计会有很多争议。
- 这会不会违背用户的意愿?如果用户就是不想用new呢?
好了,继承相关的坑就说到这里。下面我们来看看如何实现一个完整的继承架构。