Arrays are exotic objects that give special treatment to a certain class of property names
数组是一个对一类属性做了特殊处理的奇异对象
https://tc39.es/ecma262/#sec-array-objects
简化一下,可以理解为“数组是对象”,不过这个对象是“奇异对象”。这也比较符合我们的直观理解,因为我们可以给数组存放不同类型的数据,也可以随意添加属性,这些显然是一个对象的性质,比如:
属性不是一个静态值,而是一个 getter返回的值,返回的是比所有数字类型的key都大1的一个值。所以严格来说不是修改数组的同时改了length,而是length每次都是根据最大的index动态计算的结果。
a. The "length" property of an Array instance is a data property whose value is always numerically greater than the name of every configurable own property whose name is an array index.
const a = ['a', ,'c']; // 稀疏数组
// 不可遍历属性
Object.defineProperty(a, 'foo1', {
value: 'foo1',
enumerable: false
});
// 可遍历属性
Object.defineProperty(a, 'foo2', {
value: 'foo2',
enumerable: true
});
// 原型上加属性
Array.prototype.foo3 = 'foo3';
// 下面四个输出的分别是什么?
a.forEach((i) => console.log(i));
for(let i in a) console.log(a[i]);
for(let i of a) console.log(i);
Object.keys(a).forEach(i => console.log(a[i]));
先别急着回答,为了搞清楚这四种方式的输出,我们分别看看对应方法的实现
先看第1个,forEach的定义如下:
forEach是通过递增index来实现的,只会输出数字类型的值,而且是会用 HasProperty判断当前的 index是不是存在,所以这个方法不会输出的是 a[1],因为 a并没有1这个属性,那么输出的最终结果就是 a, c
第2个方法,for in在 TC39上的定义实在太长看不懂,直接看MDN的说明:
The for...in statement iterates over all enumerable properties of an object that are keyed by strings (ignoring ones keyed by Symbols), including inherited enumerable properties.
for...in 语句会遍历一个对象所有的非Symbols的、可以被遍历的key,包括继承来的
那么上面的例子中,除了 foo1无法被遍历外,其他都可以输出,最终输出结果是 a, c, foo2, foo3
第3个方法,for of实际上是对 iterator进行遍历,iterator的实现如下所示,注意关键的一行伪代码是 Set index to index+1:
因此for of循环在遍历数组的时候,会从 0一直遍历到 length-1为止,并且并不会判断 index是否存在,因此会输出 a[1]的值,显然就是 undefined,那么最终输出结果就是 a, undefined, c
最后第4个方法, Object.keys(),这个函数的定义是这样的:
Returns an array containing the names of all of the given object's own enumerable string properties.
所以他不会输出原型上的 foo3,不会输出不能遍历的 foo1,不会输出不存在的 1,最终输出结果是 a,c,foo2
所以总结一下,这4种不同的遍历姿势,出来的结果全都不一样:
const a = ['a', ,'c']; // 稀疏数组
// 不可遍历属性
Object.defineProperty(a, 'foo1', {
value: 'foo1',
enumerable: false
});
// 可遍历属性
Object.defineProperty(a, 'foo2', {
value: 'foo2',
enumerable: true
});
// 原型上加属性
Array.prototype.foo3 = 'foo3';
// 下面四个输出的分别是什么?
a.forEach((i) => console.log(i)); // a, c
for(let i in a) console.log(a[i]); // a, c, foo2, foo3
for(let i of a) console.log(i); // a, undefined, c
Object.keys(a).forEach(i => console.log(a[i])); // a, c, foo2
数组不是数组
为什么数组不是数组呢?因为JS中一切皆对象,所以数组其实是对象。当然这只是个段子。这里说的数组不是数组的含义是:JS数组在内存中的存储方式不同于我们在数据结构课程上学习的数组。 数据结构中的数组是指存储在一个连续的内存空间中的具有相同类型的数据集合。
首先我们可以看看TC39中对数组的定义:
elements
是存储对象的数字类型的key
的,数组的key
都是数字类型,显然存储在这里。如果是这样的对象{ 1: 'x'}
那么这个x
的值也会被存储在elements
中,大家可以自己验证下properties
是存储对象的非数字类型key
的,#length
作为数组的一个内置属性就被存储在这里,同样,我们自己添加的foo
也会被存储在这里length
是一个优化结果,V8会把部分常用属性直接存在在对象上,以提升性能V8对对象存储做了诸多优化,不是一言两语可以讲清楚的(当然我也没这个能力)。比如
elements
会根据不同情况选择FixedArray
或者NumberDictionary
,以获得最好的性能。为什么要有这些不同的结构呢?哈希表比数组查询速度慢一些,因为数组的index
不用计算可以直接寻址,而哈希表的哈希值是需要计算的,但是哈希表却可以方便进行增删,而且可以不用按数字顺序存储。有兴趣的同学可以把这个对象的结构打印出来,和上面的数组对比一下,看看是不是几乎一样:
稀疏数组
JS中有一种特殊数组叫“稀疏数组”,也就是数组的 index不是连续的,存在一些空白。我们可以用如下几种方式创建稀疏数组:
稀疏数组和插入了 null值的数组是不同的,比如下面这两个:
打印出来可以看到稀疏数组中存储的不是
undefined
或者null
,而是一个V8内部的特殊值the_hole
。 为什么专门说稀疏数组,是因为跟下面要讲的数组遍历关系很大。遍历数组的4种姿势
先来看一段数组遍历的代码:
先别急着回答,为了搞清楚这四种方式的输出,我们分别看看对应方法的实现 先看第1个,
forEach
的定义如下:forEach
是通过递增index
来实现的,只会输出数字类型的值,而且是会用HasProperty
判断当前的index
是不是存在,所以这个方法不会输出的是a[1]
,因为a
并没有1
这个属性,那么输出的最终结果就是a, c
第2个方法,
for in
在 TC39上的定义实在太长看不懂,直接看MDN的说明:第3个方法,
for of
实际上是对iterator
进行遍历,iterator
的实现如下所示,注意关键的一行伪代码是Set index to index+1
: 因此for of
循环在遍历数组的时候,会从0
一直遍历到length-1
为止,并且并不会判断index
是否存在,因此会输出a[1]
的值,显然就是undefined
,那么最终输出结果就是a, undefined, c
最后第4个方法,
Object.keys()
,这个函数的定义是这样的:所以总结一下,这4种不同的遍历姿势,出来的结果全都不一样:
参考文献