在编写 JavaScript 的过程中,总是会遇到类数组对象,比如函数执行的时候的
arguments
对象,还有一些 DOM 方法也会返回类数组对象,比如 getElementsByClassName 就会返回一个类数组对象,表示一个 DOM 集合。接触 JavaScript 一年多了,把这段时间里对它的认识写了一下,我认为类数组对象的存在是合理的,不应该算是 JavaScript 设计的槽点,原因在于 JavaScript 的数组是用对象实现的,而不是我们寻常知道的经典数组 ———— 由一个索引偏移,还有对应其基地址的指针构成的线性空间。
# 类数组对象 ↵
类数组对象指的是形如下面的这类对象:
00var likeArray = { 01 0: 'A', 02 1: 'B', 03 2: 'C', 04 length: 3 05}
它们不是真正的数组,但是却异常地像数组:
00console.log( 01 likeArray[0] 02); 03// => 04// 'A'
稍加操作即可让类数组对象使用数组原生方法:
00var print = x => console.log(x); 01Array.prototype.forEach.call(likeArray, print); 02// => 03// 'A' 04// 'B' 05// 'C'
利用
slice
或者 Array.from
可以由其生成一个真正的数组:00console.log( 01 Array.from(likeArray), 02 Array.prototype.slice.call(likeArray) 03);
# 真实的数组 ↵
我认为,像 C 语言那样的真实线性内存空间所代表的可存储区域,才叫做真实的数组,而 JavaScript 的数组是用对象的属性查找实现的,也就是说,在执行表达式
likeArray[0]
的时候,其实就是 likeArray.0
这跟 likeArray["key2property"]
没什么两样,只是属性名不一样。这样来看,JavaScript 数组的性能会比经典数组要慢很多。
# 尝试创建一个尽可能像数组的类数组对象 ↵
数组之所以是数组,不过是 length 及其不同项的索引在其实例上对应着不同的值而已。
如果想要让某个类数组对象很像很像很像数组,只需要具备以下条件:
- 该类数组对象的原型链应该最终可以抵达 Array.prototype
- 需要有索引属性以及对应的正确的 length 属性
满足上述两点,根据数组的对象本质,这时候我们大可以合理认为,这种类数组已经跟普通数组一样了。
00// FakeArray.js 01function FakeArray(arr){ 02 // 各索引值 03 arr.forEach((val, idx) => { 04 this[idx] = val; 05 }); 06 07 // 长度 08 this.length = arr.length; 09} 10 11// 创建一个对象继承自 Array.prototype 12var FakeArrayProto = Object.create(Array.prototype); 13 14// 连接到构造器上 15FakeArray.prototype = FakeArrayProto 16 17module.exports = FakeArray;
00var likeArray = new FakeArray(['i', 'am', 'fake', 'array']); 01 02// forEach 03likeArray.forEach(e => console.log(e)); 04 05// join 06likeArray.join(' '); 07 08Array.isArray(likeArray); 09// => 10// false

创建 FakeArray 对象
而且,甚至浏览器在打印我们的假数组的时候也把它作为真数组来打印了,当然
Array.isArray
还是能正常工作的,它能够判断到底是不是真的数组。# 对象本质 ↵
JavaScript 数组是用对象实现的,它跟其他对象并没有什么本质不同,唯一的区别在于它用整数来作为他的属性名,并假装成数组的索引来使用,而对于如何取得索引对应的值,经典数组是用基地址 + 索引偏移来寻找数组元素的,反观对象数组,它是利用哈希算法来将值放到堆的某个地方的,抛却如何寻找数组元素这一层来看,不论是经典数组还是对象数组,其本质在于把一个整数区间映射到一组值上,这是它们两者更加深层的内在本质。
相比经典数组,对象数组的优势在于:
- 其本身就是稀疏数组来用
- 其本身就是变长数组
- 其本身就是一个对象,可以直接添加方法
# 性能 ↵
关于数组的性能我主要参考了这篇文章 Let’s get those Javascript Arrays to work fast
# 使用有类型数组 (Typed Array)
具体请看 类型化数组架构 这个我曾在之前做 HTML5 Audio API 那里的波形频域时域信息就是用这种数组保存的。
# [] 比 {} 要好
作者认为 [] 比 {} 要好,因为他认为数组是用 number 类型的值做属性名而不是对象的字符串,不过,我认为这两者应该是没有区别的,在做
[0][0]
的时候我认为其中会发生类型转化,表达式将会被解释成 [0]["0"]
也就是说并无二致,数组的性能说不定还要更差一些。# 不要反过来遍历数组
00var i = myArray.length; 01while ( i-- ) { 02 process myArray[i] ... 03}
this will be much slower than using the right order. Because all CPU caches in the world expect the processing to be ‘straight’, you will have cache misses again and again, and a 2X slow down is what you’ll get when you are lucky.
作者认为,上述的代码性能会很差,因为这会造成 CPU Cache 命中率大幅降低,因为这个世界上所有的 CPU 都期望执行过程是直线的 (straight)
不过我自己写的简单测试却显示几乎没有什么区别,作者的说法有可能是错误的。
# 不要随意改变数组的长度
每一次的数组长度变化都可能是一次内存分配或者回收,如果频繁这样操作可能导致数组性能大幅度降低。
但是总不能就是不改长度把,对于这一点,坐着给出两个比较可行的策略:
- 任何时候都尽量操作固定长度的数组
- 使用长度比较大的数组来对付需要改变长度的任务 (Using over-sized array is way better than changing often the size.)
对于第二点他给出了一段代码:
00var B = [ 0, 0, 0, 0, 0 ] ; // allocated array of length 5. 01var BLength = 0 ; 02// to do a 'push', do 03B[ BLength++ ] = 1; 04// to do a 'pop' : 05var last = B[ --BLength ] ;
# 任何时候尽量使用预定义的数组
00var n = 1000; 01var myArray = new Array(n); 02var i=0; 03while (i<n) { myArray[i] =0; i++; }
这是作者给出的代码,并认为这段代码会比用
push
快十倍 push-allocated-vs-dynamic# 用自己写的 push unshift splice 等等这些原生方法
他认为这些都是函数调用,函数调用是有代价的,应该用简单的表达式取代他们。
本人不敢苟同这点,这样把抽象带来的好处都丢了。
# 空间换时间
原标题是
Could i never de-allocate my arrays ? Yes, you can !
这段作者认为栈操作可以写下面这样
00var myStack = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]; 01var myStacksLength = 0; 02// to do a push : 03myStack[myStacksLenght++] = value; 04// to do a pop : 05var val = myStack[myStacksLength--]; // you might want to check 06 // myStacksLenght before...
这样可以尽可能的避免分配和收回内存了。
# Think It As An Object ↵
因为 JavaScript 的数组其实就是用对象来实现的,因此类数组对象的存在完全合理,也不应该是所谓 “糟粕” 。
通过构造器
FakeArray
我们可以构造出极像数组的类数组对象,它完全拥有数组的全部原生方法,在一般情况下,我们可以合理认为,它跟普通数组并无二致了。后面的大部分在讨论性能问题,我刚刚自己试了下主要的结论是:
push
pop
这些改变数组长度的操作确实挺慢的- 反过来遍历几乎不会有什么影响
arr[0]
VSarr["0"]
几乎没有区别([])[0]
VS({})[0]
几乎没有区别