Skip to content

Latest commit

 

History

History
170 lines (141 loc) · 7.86 KB

File metadata and controls

170 lines (141 loc) · 7.86 KB

6.2 The gotchas!(陷阱)

前2个陷阱比较简单,而且不容易遇到我就一笔带过了。

  1. 不要扩展Object对象。因为扩展Object之后,会使得for循环多遇到一个元素,尽管可以通过this.hasOwnProperty(i)来判断是否忽略,还是很不方便
  2. 不要扩展Number对象。否则会抛出异常

6.2.3 Subclassing native objects

下面讲第三个:继承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属性非常特殊。可以看到,只有第一个测试失败了。 继承Array失败

为了解决以上问题,我们需要分别绑定每个方法:

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,只是模拟了一个

6.2.4 Instantiation issues

可以看出,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对象产生影响。【原文中的测试是第三个,原著说会失败,其实不会。译者注】。

mis_init

以上暴漏了不用new调用构造函数的2个问题:

  1. 返回未定义对象。
  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:)。

不过,作为忍者,我们还需要停下来想想,这样做有什么问题?

  1. arguments.callee是js不提倡的,在strict mode中他会失效。【可以直接使用函数名,如listing6.19,译者注】
  2. 这真的是一个好的code practice吗?抛出异常是不是更好?估计会有很多争议。
  3. 这会不会违背用户的意愿?如果用户就是不想用new呢?

好了,继承相关的坑就说到这里。下面我们来看看如何实现一个完整的继承架构。