在 JavaScript 中,使用最频繁的数组结构就是数组了,只有熟悉 JavaScript 的数组,才能更好地运用 JavaScript 构建强大的应用程序。
数组对象
数组在 JavaScript 中是一种原生对象。
JavaScript 提供原生的数组构造函数 Array
。
|
Reflect
对象是 ES6 引入的对象,定义了一些操作对象的静态方法。如果想了解详情,可阅读阮一峰的《ECMAScript 6 入门》的「Reflect 章节」。
Array
构造函数除了一般构造器拥有的属性外,还增加了以下属性:
- isArray
- from
- of
- Symbol(Symbol.species)
实际上以上四个都是在 ES6 中新增的 Array
的静态方法。
静态方法指定义在构造器上的方法。
先简单介绍以下上面四个静态方法。
|
Symbol
是 ES6 新增的基本类型 -symbol
类型的生成函数。详情可阅读阮一峰的《ECMAScript 6 入门》的「Symbol 章节」。Symbol.species
是Symbol
函数的静态属性,作为一个全局唯一的标识符。
数组原型
数组原型就是数组构造器 Array
用来生成数组实例的对象,通过 Array.prototype
属性引用该对象。
|
数组原型拥有一系列操作数组的方法,这里就不展开介绍,之后讨论数组操作时再详细介绍各个方法。
现在让我们来讨论下在 JavaScript 中的数组结构。
数组结构的实现
在 JavaScript 中,数组是通过对象来实现的。
|
ES5 就已经允许在对象的最后一个属性后加上
','
。
实现数组结构的对象,必须包含 length
属性,如果有数组元素,每个数组元素都应该是对象的属性,并且按照位置顺序,第一个元素应该是 0
属性的值,第二个元素应该是 1
属性的值,第三个元素应该是 2
属性的值,依次类推。
由此我们可以模拟一个最简单的数组结构(不包括任何操作方法)。
|
我们可以使用 Array.prototype
原生的数组操作方法来操作模拟的数组对象,这也说明了我们模拟的数组应该是可以像数组一样操作。
但是,我们不妨把创建的这个对象称为类数组对象。
|
类数组对象终究不是原生的数组对象实例,使用 instanceof
操作符可以知道 arr
并不是数组对象实例,即 arr
的原型不是 Array.prototype
。
当然,我们可以通过 Reflect.setPrototypeOf
手动设置 arr
的原型,让 arr
「变成」数组对象实例,这就创建了一个真正的数组实例了,然而,如果你真的想要一个数组对象实例的话,那最好不要用这么麻烦的方法来创建数组对象实例,用原生的方法就可以简便地创建数组了。
但我们可以利用这个特性,来创建自定义的数组对象。
数组的操作
JavaScript 提供了大量的数组操作方法,这些方法为操作数组提供了极大的便捷。
创建数组
我们可以使用以下方法创建数组对象:
- 数组字面量
new Array()
Array()
Array.of
Array.from
- 扩展运算符
...
|
创建数组最简单的方法就是使用字面量创建,实际等效于使用 new Array
创建,而且即使不使用 new
运算符调用 Array
函数也能成功创建数组。
我们可以简单模拟一下创建过程。
|
...args
为 Rest 参数,详情参阅阮一峰的《ECMAScript 6 入门》的「函数的扩展」。
new.target
是 ES6 新增的属性,用来判断当前函数是否使用 new 运算符调用。详情参阅阮一峰的《ECMAScript 6 入门》的「相关章节」。
上述模拟了 Array
函数的一个特性,当函数参数只有一个数字类型的参数时,将创建一个指定长度的空数组。
但这样的话,就存在一个缺陷,要想创建一个只含有一个数字类型的数组时,就只能用字面量创建了。
|
这样不一致的接口给编码带来了一定的麻烦,所以,ES6 又新增了一个静态方法 Array.of
来统一创建数组的接口。
|
一般情况下,就可以使用 Array.of
代替 Array
创建数组,只有当需要创建一个制定长度的空数组时才使用 Array
创建。
在 ES6 之前,如果想把一个类数组对象转化为数组对象,还是比较麻烦的,为了解决这个问题,ES6 增加了静态方法 Array.from
,专门用于将其他对象转换为数组对象。
|
事实上,Array.from
只能转换两种对象:类数组对象和实现 Iterator
接口的对象。
我们可以自定义一个简单的类数组对象。
|
那么如果创建实现 Iterator
接口的对象呢?
实现 Iterator
接口的对象实际上指有一个方法名为 Symbol.iterator
的对象。
Symbol.iterator
是一个symbol
类型,而不是'Symbol.iterator'
。详情可参阅这里。
所以,我们创建一个拥有该方法名的对象即可,但有一点需要注意,该方法必须返回一个 Iterator
对象。
想了解
Iterator
对象详情,可参阅这里。
|
我们使用属性名表达式 [Symbol.iterator]
来引用全局定义的 symbol
值。
想了解
Iterator
对象详情,可参阅这里。
同时使用 Generator
函数生成 Iterator
对象。
想了解
Generator
函数详情,可参阅这里。
我们可以查看下字符串原型的属性:
|
可以看到 Symbol(Symbol.iterator)
,所以 String。prototype
是实现了 Iterator
接口。
|
同时 String 对象实例还是类数组对象。
另外 ES6 还新增了扩展运算符 ...
,可以将任意实现了 Iterator
接口的对象展开成数组和类似参数列表的形式。
|
实际上,...
会通过 Iterator
遍历对象的属性,并用逗号分隔多个结果。
然而,...
是有使用限制的,目前只允许在以下语法中使用:
其中在数组展开式和参数列表展开式中 ...
运算符操作的对象都必须实现 Iterator
接口,而对象展开式中 ...
运算符可以操作任何对象,但展开规则不依赖 Iterator
接口。
附带一提,函数定义时使用的
...args
是 rest 参数,调用函数时的...'hello'
才是参数列表展开式。
展开运算符 ...
实际提供了一些便捷的操作,这里就不展开讨论了,有兴趣的同学可以多了解一下。
获取数组长度
获取数组最简单的方法就是 length
属性。
|
获取数组元素
获取数组元素的方法比较多。
|
通过 a[0]
访问时,实际上,会将数字 0
转化为字符串 '0'
,从而访问对象的属性 0
(对象的属性标识符只能是 string
和 symbol
)。
因为对象的嵌套特性,我们也可以便捷地访问嵌套数组元素。
|
遍历数组
遍历数组的方法也比较多。
|
除了 forEach
是定义在 Array.prototype
上的方法外,其他都是 JavaScript
提供的原生语法。
for...in
和 for...of
其实都是为简化对象遍历设计的语法,因为在 JavaScript 中数组也是对象,所以也可以使用该语法。
for...in
可以遍历任何对象的可遍历的属性以及该对象的原型链上的对象的可遍历的属性。
可遍历的属性指属性的内部属性
[[Enumberable]]
为true
。
for...of
是 ES6 新增的语法,用于遍历实现 Iterator
接口的对象的可遍历属性。
此时可遍历的属性为对象的
[Symbol.iterator]
方法返回的Iterator
里可遍历的属性。
因为 for...in
和 for...of
是语法,故可以使用 break
和 continue
进行条件遍历。
而 forEach
方法是高阶函数,为每个可遍历元素都生成了一个函数,所以没有简便的方法可以干预其他函数的执行,故不容易实现条件遍历。
增删数组元素
JavaScript 提供以下方法增删数组元素:
- Array.prototype.push
- Array.prototype.pop
- Array.prototype.unshift
- Array.prototype.shift
- Array.prototype.splice
|
其中,push
和 pop
是从数组尾部开始增删;unshift
和 shift
是从数组首部开始增删。
我们可以看到以上四种方法只能从固定的位置添加任意个元素或删除一个元素,虽然简单,但不够灵活。所以,JavaScript 还提供一个更强大的 splice
方法。
splice 原意为剪接,拼接,这也很符合 splice
方法的功能:裁剪数组的一部分,并将剩下的部分拼接起来。
|
splice
方法十分灵活,总共提供三种调用方式:
|
其中,有几种特殊情况需要注意:
- (start >= 0 && start > length - 1) : start = length
- (start < 0 && -start < length) : start = length + start
- (start < 0 && -start >= length) : start = 0
- (deleteCount >= 0 && start + deleteCount > length) : deleteCount = length - start
- (deleteCount < 0) : deleteCount = 0
先计算 start,再计算 deleteCount。
我们不妨用 splice
分别实现 push
、pop
、unshift
和 shift
方法。
|
查找数组元素
JavaScript 提供查找数组元素值和元素索引的方法。
- Array.prototype.indexOf
- Array.prototype.lastIndexOf
- Array.prototype.find
- Array.prototype.findIndex
|
再提一点,find
方法直接返回数组元素的值,如果该元素是一个对象的话,就会直接返回该对象的引用。
|
例如上面这样,我们会操作 b
的属性时,也会操作 a
的属性。
|
提取数组中的元素
使用 splice
方法,我们可以从原数组中剪取数组中的部分(或全部)元素,但会修改源数组,有时,我们希望在不希望修改源数组的情况下,提取数组中的部分元素(或全部)元素。
首先,我们应该可以想到使用 find
方法可以根据条件提取一个元素,而且不会修改源数组。然而,find
方法并不适合提取多个元素,实际上,JavaScript 提供了 slice
方法来完成提取操作。
|
slice
意思为节选、片段,注意与 splice
区别开来。
slice
主要有以下形式:
|
slice
会提取源数组的 [begin, end)
取值范围内的元素。
在实际提取之前,需要对 begin
和 end
参数进行规整:
- if (typeof begin !== ‘number’) begin = Number(begin)
- if (begin >= 0 && begin >= length) begin = length
- if (begin < 0 && -begin <= length) begin = length + begin
if (begin < 0 && -begin > length) begin = 0
if (typeof end !== ‘number’) end = Number(end)
- if (end >= 0 && end >= length) end = length
- if (end < 0 && -end <= length) end = length + end
- if (end < 0 && -end > length) end = 0
另外提取规则的伪代码如下:
除了按位置提取元素外,JavaScript 还提供了条件提取方法 filter
。
|
数组元素判断
判断数组中是否含有特定的元素也是常见的操作,JavaScript 也提供了以下方法进行测试:
- Array.prototype.some
- Array.prototype.every
- Array.prototype.includes
|
数组合并
使用 concat
方法可以将任意的对象以及原始值合并成数组。
|
concat
方法会将数组对象展开,其他原始值和对象都会当作元素合并到返回的新数组中。
concat
不会展开嵌套数组对象。
不难看出,concat
方法也提供了一种创建数组的方式。
数组表示转换
|
join
方法可以将数组转换为字符串表示形式,默认情况下等价于调用 toString
方法。
|
但 join
方法允许自定义连接符,修改默认连接符 ','
。
|
MapReduce
MapReduce 是一种算法,或者说是一种处理一组数据的流程。
在 JavaScript 中,分别实现了 Map 和 Reduce。
既然 MapReduce 是针对一组数据进行设计的,那么将 map
和 reduce
方法放在 Array.prototype
上实现也是理所当然的。
map
方法会针对数组中的每个元素执行一些操作,这些操作可以通过 callback
函数指定,然后将执行结果组成一个新的数组返回。
|
上面的代码就按顺序遍历了数组 a 的每个元素,对每个元素都执行了平方,并将结果返回。
再看看一个更复杂点的例子。
|
上面的例子将源对象的两个属性合并成了一个属性(即 K-V 的键值对结构)。
因为 map
方法接受一个回调函数,所以,你可以传入任何函数,包括原生函数,但是必须注意一点,那就是回调函数参数个数,在调用原生的函数之前,必须保证回调函数参数不会破坏原生函数的正确性。
|
如果我们想将数组中的每个数字字符串转换成数字,当然会想到原生的 Number.parseInt
静态方法(也可以使用全局的 parseInt
方法)。但执行结果确并非所愿,这是因为没有考虑函数参数不匹配的问题。
|
Number.parseInt
方法接受两个参数,string
和 redix
。
string
为要转换的字符串。redix
为字符串的进制,默认情况下为 10,即默认认为该字符串为十进制数。
|
map
的回调函数接受三个参数 value
、index
和 array
。
value
为当前元素的值。index
为当前元素的索引。array
为源数组的引用。
那么我们可以分析将 Number.parseInt
作为 map
的回调函数时的执行流程。
|
接下来再介绍 reduce
方法,reduce
方法其实和 map
方法很相似,但不同的是返回值的处理流程。
map
方法会将每个元素对应的回调函数的返回值收集到一个新数组中,最后返回该数组。
reduce
方法则是将每个元素对应的回调函数的返回值传递给下一个元素对应的回调函数中,最终返回的是最后一个回调函数的返回值。
正因为 reduce
的回调函数需要接收上个回调函数的返回值,相比于 map
方法的回调函数,我们需要新增一个 accumulator
参数来接收返回值,并且 reduce
方法还增加了一个 initialValue
参数,用来传递给第一个元素对应的回调函数的 accumulator
。
实际上,这就是将每次回调函数的执行结果进行链式传递。
|
上面两种写法,执行结果都是一样的,但实际执行过程是不一样的。
|
当没有提供 initialValue
参数时,就不执行第一个元素对应的回调函数,而是将第一个元素作为第二个元素的 accumulator
参数,直接执行第二个元素对应的回调函数,这样就可以节省一次执行时间。
结语
掌握以上数组操作方法,基本上可以满足大部分的日常工作啦。