探秘数组的push方法

今天看到下面这段有趣的代码(据说是出自jQuery的作者哦),让我对push方法的执行机制产生了兴趣,决定一探究竟。。。

var elems = {
     length: 0,
     add: function(elem) {
          Array.prototype.push.call(this, elem);
     }
};

一. 疑问

在上段代码中,通过elems.add添加东西后,elems对象会变成了啥样呢?为什么要指定length属性?我们来操作看看:

// 分别打印出初始状态和操作后的elems,方便对比
console.dir(elems);
elems.add(1);
elems.add(2);
console.dir(elems);

结果如下图:

对拥有length的对象进行push操作

我们看到:元素被成功添加(各浏览器均如此),且elems对象的length属性值发生了改变,其值即为添加项的数量,为什么会这样???

二. 解惑

看来一切都和push方法有关,我们到MDC中看看push方法的相关说明,我们着重看看下面几点说明:

(1) This method can be called or applied to objects resembling arrays;
理解:类数组的对象可以通过call或apply方法来调用push方法. (这里的类数组对象,指的应该是拥有length属性的对象,不过函数和字符串是个例外,这个后面再讲)
(2) The return value of this method is the new length property of the object upon which the method was called;
理解:push方法的返回值,是调用对象在执行push操作后(即调用push方法的对象)的length属性值(执行push操作后,length属性值会被更新,为什么length会被更新,可以从明城的《Array.prototype 的泛型应用》一文的评论中找到答案).

// 验证下看看:elems对象的length属性值,默认为0,按照说明,插入一个值后,elems.length的值应该为1
var elems = {
     length: 0,
     add: function(elem) {
          Array.prototype.push.call(this, elem);
     }
};
elems.add('test');
alert(elems.length);

// 浏览器给出的结果:
IE 6,7, 8: 1
FF 3.6: 1
Chrome 4: 1
Safari 4: 1
Opera 10: 1

// O(∩_∩)O,验证通过!!!
(3) The push method relies on a length property to determine where to start inserting the given values;
理解:push方法通过对象的length属性来决定,在什么位置插入给定的值.

// 我们来验证一下:elems的length属性被改成了1,按照上面的说明,如果我调用一次elems.add方法后,elems.length应该是2
var elems = {
     length: 1,
     add: function(elem) {
          Array.prototype.push.call(this, elem);
     }
};
elems.add('test');
alert(elems.length);

// 浏览器给出的结果:
IE 6,7, 8: 2
FF 3.6: 2
Chrome 4: 2
Safari 4: 2
Opera 10: 2

// O(∩_∩)O,验证通过!!!
(4) If the length property cannot be converted into a number, the index used is 0;
理解:如果对象的length属性不能被转换成数字,那么插入值时,位置从0开始.

// 照常验证:我们将elems对象的length属性改为字符串,然后插入一个值,按照说明,elems.length的值应该为1
var elems = {
     length: 'aa',
     add: function(elem) {
          Array.prototype.push.call(this, elem);
     }
};
elems.add('test');
alert(elems.length);

// 浏览器给出的结果:
IE 6,7, 8: 1
FF 3.6: 1
Chrome 4: 1
Safari 4: 1
Opera 10: 1

// O(∩_∩)O,再次验证通过!!!
(5) This includes the possibility of length being nonexistent, in which case length will also be created;
理解:当调用push方法的对象没有length属性时,就创建一个length属性(MD,这里没有说明这个创建的length属性,默认值是多少,NND,不知道这会导致兼容性问题么。。。).

// 继续验证:去掉elems对象的length属性,然后插入一个值,按照说明,此时elems.length的值应该为1(开始祷告浏览器都兼容吧。。。)
var elems = {
     add: function(elem) {
          Array.prototype.push.call(this, elem);
     }
};
elems.add('test');
alert(elems.length);

// 浏览器给出的结果:
IE 6,7: undefined
IE 8: 1
FF 3.6: 1
Chrome 4: 1
Safari 4: 1
Opera 10: 1

// ( ⊙o⊙ ),不幸被咱言中,由此可见,把话说清楚是多么的重要啊。。。

三. 例外

上面提到:函数和字符串在调用push方法时,有些例外,这是指它们在调用push方法后,其length属性值不会改变,MDC中也给出了相应的说明:

// 函数(函数的length属性):
(1) length is external to a function, and indicates how many arguments the function expects, i.e. the number of formal parameters.
理解:函数对象的length属性,表示的是函数的形参个数,是个只读属性.(试想一下,对于一个给定的函数,它的形参数量还能被改变么?)

// 字符串:
(2) The only native, array-like objects are strings, although they are not suitable in applications of this method, as strings are immutable.
理解:对于一个给定的字符串,它的长度是固定的(除非你去改动它),所以其length属性也是只读的(试想一下,通过修改一个字符串的length属性值,你能改变字符串的长度吗?)

// 我们还是验证一下:
function testFunc(a, b) { // 初始length为2
    // code
}
var str = 'test'; // 初始length为4
Array.prototype.push.call(testFunc, 1);
Array.prototype.push.call(str, 1);
alert('func length: ' + testFunc.length + ', str length: ' + str.length);

// 浏览器给出的结果:
IE 6,7, 8: "func length: 2, str length: 4"
FF 3.6: "func length: 2, str length: 4"
Chrome 4: "func length: 2, str length: 4"
Safari 4: "func length: 2, str length: 4"
Opera 10: 直接报JS错误,提示"Uncaught exception: TypeError: Array.prototype.push: failed to update length"(个人认为Opera的处理更合理,只读属性就不应该被修改)

现在你应该明白,本文开始的那个例子里面,为什么要加上length属性了吧(对,为了兼容性)。好了,哥累了,今天就写这么多,回家咯。。。

Comments

jumkey at 2010年05月26日 9:47 上午

学习了,JavaScript还是很深奥的啊

涵宇 at 2010年05月26日 1:50 下午

太帅了!感觉到哥的一气呵成了,佩服!
console.dir用的更帅,我又觅到好东西了!

vapour at 2010年05月26日 3:49 下午

通过这个这个模仿数组,然后对这个模仿数组进行扩展,而不影响原生的Array对象。

Author comment by Tcer at 2010年05月27日 8:43 下午

@vapour,不错的想法。。。

Leave a comment!