在 JavaScript 中,使用最频繁的数组结构就是数组了,只有熟悉 JavaScript 的数组,才能更好地运用 JavaScript 构建强大的应用程序。

数组对象

数组在 JavaScript 中是一种原生对象。

JavaScript 提供原生的数组构造函数 Array

typeof Array // "function"
Reflect.ownKeys(Array)
// ["length", "name", "arguments","caller", "prototype",
// "isArray", "from", "of",
// Symbol(Symbol.species)]

Reflect 对象是 ES6 引入的对象,定义了一些操作对象的静态方法。如果想了解详情,可阅读阮一峰的《ECMAScript 6 入门》的「Reflect 章节」。

Array 构造函数除了一般构造器拥有的属性外,还增加了以下属性:

  • isArray
  • from
  • of
  • Symbol(Symbol.species)

实际上以上四个都是在 ES6 中新增的 Array 的静态方法。

静态方法指定义在构造器上的方法。

先简单介绍以下上面四个静态方法。

// Array.isArray: 判断对象类型是否为数组
Array.isArray([]) // true
// Array.from: 将类数组对象转化成数组
Array.from('hello') // ["h", "e", "l", "l", "o"]
// Array.of: 统一数组的构造方式
Array.of(3) // [3]
Array(3) // [undefinded x 3]
// Array[Symbol.species]: 构造器将使用该函数作为构造函数生成新的实例
Array[Symbol.species] === Array // true

Symbol 是 ES6 新增的基本类型 - symbol 类型的生成函数。详情可阅读阮一峰的《ECMAScript 6 入门》的「Symbol 章节」。
Symbol.speciesSymbol 函数的静态属性,作为一个全局唯一的标识符。

数组原型

数组原型就是数组构造器 Array 用来生成数组实例的对象,通过 Array.prototype 属性引用该对象。

typeof Array.prototype // "object"
Reflect.ownKeys(Array.prototype)
// ["length", "constructor", "toString", "toLocaleString", "join", "pop", "push", "reverse", "shift", "unshift", "slice", "splice", "sort", "indexOf", "lastIndexOf", "copyWithin", "find", "findIndex", "fill", "includes", "entries", "keys", "concat", "forEach", "filter", "map", "every", "some", "reduce", "reduceRight", Symbol(Symbol.unscopables), Symbol(Symbol.iterator)]

数组原型拥有一系列操作数组的方法,这里就不展开介绍,之后讨论数组操作时再详细介绍各个方法。

现在让我们来讨论下在 JavaScript 中的数组结构。

数组结构的实现

在 JavaScript 中,数组是通过对象来实现的。

[]
// 等价于
{
length: 0,
}
[1, '2', {}]
// 等价于
{
0: 1,
1: '2',
2: {},
length: 3,
}

ES5 就已经允许在对象的最后一个属性后加上','

实现数组结构的对象,必须包含 length 属性,如果有数组元素,每个数组元素都应该是对象的属性,并且按照位置顺序,第一个元素应该是 0 属性的值,第二个元素应该是 1 属性的值,第三个元素应该是 2 属性的值,依次类推。

由此我们可以模拟一个最简单的数组结构(不包括任何操作方法)。

let arr = {
0: '1',
1: '2',
length: 2,
};
Array.prototype.push.call(arr, '3');
arr
// {0: "1", 1: "2", 2: "3", length: 3}

我们可以使用 Array.prototype 原生的数组操作方法来操作模拟的数组对象,这也说明了我们模拟的数组应该是可以像数组一样操作。

但是,我们不妨把创建的这个对象称为类数组对象

arr instanceof Array // false
Reflect.setPrototypeOf(arr, Array.prototype)
arr instanceof Array // true

类数组对象终究不是原生的数组对象实例,使用 instanceof 操作符可以知道 arr 并不是数组对象实例,即 arr 的原型不是 Array.prototype

当然,我们可以通过 Reflect.setPrototypeOf 手动设置 arr 的原型,让 arr 「变成」数组对象实例,这就创建了一个真正的数组实例了,然而,如果你真的想要一个数组对象实例的话,那最好不要用这么麻烦的方法来创建数组对象实例,用原生的方法就可以简便地创建数组了。

但我们可以利用这个特性,来创建自定义的数组对象。

数组的操作

JavaScript 提供了大量的数组操作方法,这些方法为操作数组提供了极大的便捷。

创建数组

我们可以使用以下方法创建数组对象:

  • 数组字面量
  • new Array()
  • Array()
  • Array.of
  • Array.from
  • 扩展运算符 ...
// 使用字面量
[1, 2, 3]; // [1, 2, 3]
// 使用构造器:显式 new
new Array(1, 2, 3); // [1, 2, 3]
new Array(3); // [undefined x 3]
// 使用构造器:隐式 new
Array(1, 2, 3); // [1, 2, 3]
Array(3); // [undefined x 3]
// 使用静态方法 of
Array.of(1, 2, 3); // [1, 2, 3]
Array.of(3); // [3]
// 使用静态方法 from
Array.from('hello'); // ["h", "e", "l", "l", "o"]
// 使用扩展运算符 ...
[...'hello'] // ["h", "e", "l", "l", "o"]
Array(...'hello') // ["h", "e", "l", "l", "o"]
new Array(...'hello') // ["h", "e", "l", "l", "o"]
Array.of(...'hello') // ["h", "e", "l", "l", "o"]

创建数组最简单的方法就是使用字面量创建,实际等效于使用 new Array 创建,而且即使不使用 new 运算符调用 Array 函数也能成功创建数组。

我们可以简单模拟一下创建过程。

function A (...args) {
Reflect.setPrototypeOf(this, Array.prototype);
if (args.length === 1 && typeof args[0] === 'number') {
if (!new.target) {
args.length = args[0];
Reflect.deleteProperty(args, 0);
} else {
this.length = args[0];
}
}
return args;
}

...argsRest 参数,详情参阅阮一峰的《ECMAScript 6 入门》的「函数的扩展」。

new.target 是 ES6 新增的属性,用来判断当前函数是否使用 new 运算符调用。详情参阅阮一峰的《ECMAScript 6 入门》的「相关章节」。

上述模拟了 Array 函数的一个特性,当函数参数只有一个数字类型的参数时,将创建一个指定长度的空数组

但这样的话,就存在一个缺陷,要想创建一个只含有一个数字类型的数组时,就只能用字面量创建了。

Array(3) // [undefinded x 3]
// 等价于
{ length: 3 }
[3] // [3]
// 等价于
{ 0: 3, length: 1 }

这样不一致的接口给编码带来了一定的麻烦,所以,ES6 又新增了一个静态方法 Array.of 来统一创建数组的接口。

Array.of(3) // [3]
// 等价于
{ 0: 3, length: 1 }

一般情况下,就可以使用 Array.of 代替 Array 创建数组,只有当需要创建一个制定长度的空数组时才使用 Array 创建。

在 ES6 之前,如果想把一个类数组对象转化为数组对象,还是比较麻烦的,为了解决这个问题,ES6 增加了静态方法 Array.from,专门用于将其他对象转换为数组对象。

// 字符串 => 数组
// ES5
'hello'.split('') // ["h", "e", "l", "l", "o"]
// ES6
Array.from('hello') // ["h", "e", "l", "l", "o"]
// `arguments` => 数组
// ES5
(function() {
let a = [];
for(let i = 0; i < arguments.length; i++)
a.push(arguments[i]);
return a;
})(1, 2) // [1, 2]
// 可使用 map 简化
(function() {
return Array.prototype.map.call(arguments, e => e);
})(1, 2) // [1, 2]
// ES6
(function() {
return Array.from(arguments);
})(1, 2) // [1, 2]

事实上,Array.from 只能转换两种对象:类数组对象和实现 Iterator 接口的对象。

我们可以自定义一个简单的类数组对象。

var obj = {
0: 'h',
1: 'e',
2: 'l',
3: 'l',
4: 'o',
length: 5,
};
// 或者简写成
var obj = {};
Array.prototype.push.apply(obj, 'hello'.split(''));
obj instanceof Array // false
Array.from(obj) // ["h", "e", "l", "l", "o"]
obj instanceof Array // true

那么如果创建实现 Iterator 接口的对象呢?

实现 Iterator 接口的对象实际上指有一个方法名为 Symbol.iterator 的对象。

Symbol.iterator 是一个 symbol 类型,而不是 'Symbol.iterator'。详情可参阅这里

所以,我们创建一个拥有该方法名的对象即可,但有一点需要注意,该方法必须返回一个 Iterator 对象。

想了解 Iterator 对象详情,可参阅这里

var obj = {
a: 'a',
b: 'b',
c: 'c',
*[Symbol.iterator]() {
for(let key of Reflect.ownKeys(this)) yield this[key];
}
}
Array.from(obj) // ["a", "b", "c", ƒ]

我们使用属性名表达式 [Symbol.iterator] 来引用全局定义的 symbol 值。

想了解 Iterator 对象详情,可参阅这里

同时使用 Generator 函数生成 Iterator 对象。

想了解 Generator 函数详情,可参阅这里

我们可以查看下字符串原型的属性:

Reflect.ownKeys(String.prototype)
// ["length", "constructor", "charAt", "charCodeAt", "concat", "endsWith", "includes", "indexOf", "lastIndexOf", "localeCompare", "normalize", "replace", "slice", "split", "substr", "substring", "startsWith", "toString", "trim", "trimLeft", "trimRight", "toLocaleLowerCase", "toLocaleUpperCase", "toLowerCase", "toUpperCase", "valueOf", "codePointAt", "match", "padEnd", "padStart", "repeat", "search", "link", "anchor", "fontcolor", "fontsize", "big", "blink", "bold", "fixed", "italics", "small", "strike", "sub", "sup", Symbol(Symbol.iterator)]

可以看到 Symbol(Symbol.iterator),所以 String。prototype 是实现了 Iterator 接口。

Reflect.ownKeys(new String('hello'))
// ["0", "1", "2", "3", "4", "length"]

同时 String 对象实例还是类数组对象。

另外 ES6 还新增了扩展运算符 ...,可以将任意实现了 Iterator 接口的对象展开成数组和类似参数列表的形式

[...'hello']
// 等价于
["h", "e", "l", "l", "o"]
Array.of(...'hello')
// 等价于
Array.of("h", "e", "l", "l", "o")
...'hello' // SyntaxError

实际上,... 会通过 Iterator 遍历对象的属性,并用逗号分隔多个结果。

然而,... 是有使用限制的,目前只允许在以下语法中使用:

// 数组展开式
[...'hello']
// 等价于
["h", "e", "l", "l", "o"]
// 对象展开式
{...'hello'}
// 等价于
{0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
// 函数调用时的参数列表展开式
((...args) => args)(...'hello')
// 等价于
((...args) => args)("h", "e", "l", "l", "o")

其中在数组展开式和参数列表展开式中 ... 运算符操作的对象都必须实现 Iterator 接口,而对象展开式中 ...运算符可以操作任何对象,但展开规则不依赖 Iterator 接口。

附带一提,函数定义时使用的 ...argsrest 参数,调用函数时的 ...'hello' 才是参数列表展开式。

展开运算符 ... 实际提供了一些便捷的操作,这里就不展开讨论了,有兴趣的同学可以多了解一下。

获取数组长度

获取数组最简单的方法就是 length 属性。

[1, 2, 3].length // 3

获取数组元素

获取数组元素的方法比较多。

const a = [1, 2, 3];
a[0] // 1
a[1] // 2
a[2] // 3
a[3] // undefined
a['0'] // 1
a['1'] // 2
a['2'] // 3
a['3'] // undefined

通过 a[0] 访问时,实际上,会将数字 0 转化为字符串 '0',从而访问对象的属性 0(对象的属性标识符只能是 stringsymbol)。

因为对象的嵌套特性,我们也可以便捷地访问嵌套数组元素。

const b = [
[1],
[2, 3],
];
b[0][0] // 1
b[1][0] // 2
b[1][1] // 3

遍历数组

遍历数组的方法也比较多。

const a = [1, 2, 3];
// for 循环
for (let i = 0; i < a.length; i++) {
console.log(a[i]);
}
// for...in 循环
for (let i in a) {
console.log(a[i]);
}
// for...of 循环
for (let i of a) {
console.log(i);
}
// forEach 遍历
a.forEach(i => { console.log(i); });
// kyes 遍历
for(let it = a.keys(), state = it.next(); !state.done; state = it.next()) {
const key = state.value;
console.log(a[key]);
}
// entries 遍历
for(let it = a.entries(), state = it.next(); !state.done; state = it.next()) {
const entry = state.value;
console.log(entry[1]);
}
// values 遍历(目前只有 Safari 浏览器支持该方法)
for(let it = a.values(), state = it.next(); !state.done; state = it.next()) {
console.log(state.value);
}

除了 forEach 是定义在 Array.prototype 上的方法外,其他都是 JavaScript 提供的原生语法。

for...infor...of 其实都是为简化对象遍历设计的语法,因为在 JavaScript 中数组也是对象,所以也可以使用该语法。

for...in 可以遍历任何对象的可遍历的属性以及该对象的原型链上的对象的可遍历的属性。

可遍历的属性指属性的内部属性 [[Enumberable]]true

for...of 是 ES6 新增的语法,用于遍历实现 Iterator 接口的对象的可遍历属性。

此时可遍历的属性为对象的 [Symbol.iterator] 方法返回的 Iterator 里可遍历的属性。

因为 for...infor...of 是语法,故可以使用 breakcontinue 进行条件遍历。

forEach 方法是高阶函数,为每个可遍历元素都生成了一个函数,所以没有简便的方法可以干预其他函数的执行,故不容易实现条件遍历。

增删数组元素

JavaScript 提供以下方法增删数组元素:

  • Array.prototype.push
  • Array.prototype.pop
  • Array.prototype.unshift
  • Array.prototype.shift
  • Array.prototype.splice
var a = [];
// push: 从数组尾部添加任意个元素,返回数组的长度
a.push(2, 1); // 2 := a.length
a // [2, 1]
// pop: 从数组尾部删除一个元素,返回删除的元素
a.pop(); // 1 := a.[length-1]
a // [2]
// pop: 空数组返回 undefined
[].pop // undefinded
// unshift: 从数组首部添加任意个元素,返回数组的长度
a.unshift(0, 1); // 3 := a.length
a // [0, 1, 2]
// shift: 从数组首部删除一个元素,返回删除的元素
a.shift(); // 0 := a[0]
a // [1, 2]
// shift: 空数组返回 undefined
[].shift() // undefined

其中,pushpop 是从数组尾部开始增删;unshiftshift 是从数组首部开始增删。

我们可以看到以上四种方法只能从固定的位置添加任意个元素或删除一个元素,虽然简单,但不够灵活。所以,JavaScript 还提供一个更强大的 splice 方法。

splice 原意为剪接,拼接,这也很符合 splice 方法的功能:裁剪数组的一部分,并将剩下的部分拼接起来。

const a = [1, 2, 3, 4];
// 从索引为 2 的元素开始,删除 1 个元素,返回删除的元素组成的数组
a.splice(2, 1) // [3]
a // [1, 2, 4]
// 从索引为 2 的元素开始,删除 2 个元素(超过了实际剩下的元素),返回删除的元素组成的数组
a.splice(2, 2) // [4]
a // [1, 2]
// 从索引为 1 的元素开始,删除 0 个元素,添加元素 3、4,返回删除的元素组成的数组
a.splice(1, 0, 3, 4) // []
a // [1, 3, 4, 2]
// 从索引为 -1 的元素开始(即倒数第 1 个元素),删除 1 个元素,返回删除的元素组成的数组
a.splice(-1, 1) // [1]
a // [1, 3, 4]
// 不执行任何操作
a.splice() // []
a // [1, 3, 4]
// 从索引为 1 的元素开始,删除之后的所有元素,返回删除的元素组成的数组
a.splice(1) // [3, 4]
a // [1]
// 从索引为 -2 的元素开始(不存在倒数第 2 个元素,即从顺数第 0 个元素开始),
// 删除 a.length 个元素,添加 4、3、2、1,返回删除的元素组成的数组
a.splice(-2, a.length, 4, 3, 2, 1) // [1]
a // [4, 3, 2, 1]

splice 方法十分灵活,总共提供三种调用方式:

// 删除从 start 开始的所有元素
array.splice(start)
// 删除从 start 开始的指定数量的元素
array.splice(start, deleteCount)
// 删除从 start 开始的指定数量的元素,并添加任意数量的元素
array.splice(start, deleteCount, item1, item2, ...)

其中,有几种特殊情况需要注意:

  • (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 分别实现 pushpopunshiftshift 方法。

const a = [1, 2, 3, 4]
// a.push(5, 6, 7, 8)
a.splice(a.length, 0, 5, 6, 7, 8)
a // [1, 2, 3, 4, 5, 6, 7, 8]
// a.pop()
a.splice(a.length - 1, 1)
a // [1, 2, 3, 4, 5, 6, 7]
// a.unshift(0)
a.splice(0, 0, -1, 0)
a // [-1, 0, 1, 2, 3, 4, 5, 6, 7]
// a.shift()
a.splice(0, 1)
a // [0, 1, 2, 3, 4, 5, 6, 7]

查找数组元素

JavaScript 提供查找数组元素值和元素索引的方法。

  • Array.prototype.indexOf
  • Array.prototype.lastIndexOf
  • Array.prototype.find
  • Array.prototype.findIndex
const a = [1, '1', 1, '1']
// indexOf: 返回元素在数组中第一次出现的位置
a.indexOf(1) // 0
a.indexOf('1') // 1
[undefinded].indexOf() // 0
// indexOf: 元素不在数组中,返回 -1
a.indexOf(0) // -1
// lastIndexOf: 返回元素在数组中最后一次出现的位置
a.lastIndexOf(1) // 2
a.lastIndexOf('1') // 3
[undefined, undefined].lastIndexOf() // 1
// lastIndexOf: 元素不在数组中,返回 -1
[].lastIndexOf() // -1
// find: 返回符合条件的第一个元素值
a.find((element, index, array) => {
console.log(array === a);
console.log(`${element} === array[${index}] : ${element === array[index]}`;
return typeof element === 'string';
}
// true
// 1 === array[0] : true
// true
// 1 === array[0] : true
// <= "1"
// find: 数组中没有符合条件的元素,返回 undefined
a.find((element, index, array) => {
console.log(a === array);
console.log(`array[${index}] === ${
typeof element === 'string' ? '"'+ element + '"' : element
} : ${element === array[index]}`);
return typeof element === 'boolean';
})
// true
// array[0] === 1 : true
// true
// array[1] === "1" : true
// true
// array[2] === 1 : true
// true
// array[3] === "1" : true
// <= undefined
// findIndex: 返回符合条件的第一个元素的索引
a.findIndex((element, index, array) => typeof element === 'string') // 1
// findIndex: 未找到符合条件的元素,返回 -1
a.findIndex((element, index, array) => typeof element === 'boolean') // -1

再提一点,find 方法直接返回数组元素的值,如果该元素是一个对象的话,就会直接返回该对象的引用。

const a = [ {value: 1} ]
b = a.find( element => typeof element === 'object' )
b.value = 3
a[0].value // 3

例如上面这样,我们会操作 b 的属性时,也会操作 a 的属性。

const a = [ {value: 1} ]
// 让 b 继承了 a[0]
b = Object.create(a.find( element => typeof element === 'object' ))
b.value = 3
a[0].value // 1

提取数组中的元素

使用 splice 方法,我们可以从原数组中剪取数组中的部分(或全部)元素,但会修改源数组,有时,我们希望在不希望修改源数组的情况下,提取数组中的部分元素(或全部)元素。

首先,我们应该可以想到使用 find 方法可以根据条件提取一个元素,而且不会修改源数组。然而,find 方法并不适合提取多个元素,实际上,JavaScript 提供了 slice 方法来完成提取操作。

const a = [1, 2, 3, 4, 5, 6, 7, 8]
a.slice() // [1, 2, 3, 4, 5, 6, 7, 8]
a.slice(4) // [5, 6, 7, 8]
a.slice(8) // []
a.slice(-2) // [7, 8]
a.slice(-9) // [1, 2, 3, 4, 5, 6, 7, 8]
a.slice(1,4) // [2, 3, 4]
// 传入非数字参数
a.slice(null) // [1, 2, 3, 4, 5, 6, 7, 8]
a.slice(undefined) // [1, 2, 3, 4, 5, 6, 7, 8]
a.slice(false) // [1, 2, 3, 4, 5, 6, 7, 8]
a.slice(true) // [2, 3, 4, 5, 6, 7, 8]
a.slice('5') // [6, 7, 8]
a.slice({}) // [1, 2, 3, 4, 5, 6, 7, 8]

slice 意思为节选、片段,注意与 splice 区别开来。

slice 主要有以下形式:

arr.slice()
arr.slice(begin)
arr.slice(begin, end)

slice 会提取源数组的 [begin, end) 取值范围内的元素。
在实际提取之前,需要对 beginend 参数进行规整:

  1. if (typeof begin !== ‘number’) begin = Number(begin)
  2. if (begin >= 0 && begin >= length) begin = length
  3. if (begin < 0 && -begin <= length) begin = length + begin
  4. if (begin < 0 && -begin > length) begin = 0

  5. if (typeof end !== ‘number’) end = Number(end)

  6. if (end >= 0 && end >= length) end = length
  7. if (end < 0 && -end <= length) end = length + end
  8. if (end < 0 && -end > length) end = 0

另外提取规则的伪代码如下:

if (end <= begin) return []
else return [bgein, end)

除了按位置提取元素外,JavaScript 还提供了条件提取方法 filter

const a = [1, '1', 1, '1']
// filter: 提取符合条件的所有元素,返回包含结果的数组
a.filter((element, index, array) => typeof element === 'string') // ["1", "1"]
// filter:没有符合要求的元素,返回空数组
a.filter((element, index, array) => typeof element === 'boolean') // []

数组元素判断

判断数组中是否含有特定的元素也是常见的操作,JavaScript 也提供了以下方法进行测试:

  • Array.prototype.some
  • Array.prototype.every
  • Array.prototype.includes
const a = [1, '2', 3, 4, true, {}]
// 按值比较
a.includes(3) // true
a.includes(2) // false
a.includes({}) // false
// 部分元素满足条件
a.some((currentElement, index, array) => typeof currentElement === 'number') // true
// 所有元素满足条件
a.every((currentElement, index, array) => typeof currentElement === 'number') // false

数组合并

使用 concat 方法可以将任意的对象以及原始值合并成数组。

const a = [1, 2]
const b = [3, 4]
const c = a.concat(b) // [1, 2, 3, 4]
c === a // false
[].concat([1, 2, 3], 4, 5) // [1, 2, 3, 4, 5]
[].concat([1, [2, 3]], true, '43px', {length: 0})
// [1, [2, 3], true, "43px", {length: 0}]

concat 方法会将数组对象展开,其他原始值和对象都会当作元素合并到返回的新数组中。

concat 不会展开嵌套数组对象。

不难看出,concat 方法也提供了一种创建数组的方式。

数组表示转换

[1, 2, 3, 4].join() // "1,2,3,4"
[1, '2', `3`, true, {length: 1}, new Set([1,2,3])].join()
// "1,2,3,true,[object Object],[object Set]"

join 方法可以将数组转换为字符串表示形式,默认情况下等价于调用 toString 方法。

[1, '2', `3`, true, {length: 1}, new Set([1,2,3])].toString()
// "1,2,3,true,[object Object],[object Set]"

join 方法允许自定义连接符,修改默认连接符 ','

[1, {}].join(' ') // "1 [object Object]"
[1, 2, 3].join('') // "123"
[1, 2, 3, 4].join(' + ')
// "1 + 2 + 3 + 4"

MapReduce

MapReduce 是一种算法,或者说是一种处理一组数据的流程。

在 JavaScript 中,分别实现了 MapReduce

既然 MapReduce 是针对一组数据进行设计的,那么将 mapreduce 方法放在 Array.prototype 上实现也是理所当然的。

map 方法会针对数组中的每个元素执行一些操作,这些操作可以通过 callback 函数指定,然后将执行结果组成一个新的数组返回。

const a = [1, 2, 3]
a.map((value, index, array) => value * value)
// [1, 4, 9]
a // [1, 2, 3]

上面的代码就按顺序遍历了数组 a 的每个元素,对每个元素都执行了平方,并将结果返回。

再看看一个更复杂点的例子。

const a = [{name: 'sysiya', id: '8000114137'},
{name: 'akira', id: '8000114410'}]
a.map(e => {
let obj = {};
obj[e.id] = e.name;
return obj;
}
// [ {8000114137: sysiya}, {8000114410: akira}

上面的例子将源对象的两个属性合并成了一个属性(即 K-V 的键值对结构)。

因为 map 方法接受一个回调函数,所以,你可以传入任何函数,包括原生函数,但是必须注意一点,那就是回调函数参数个数,在调用原生的函数之前,必须保证回调函数参数不会破坏原生函数的正确性。

['1', '2', '3'].map(Number.parseInt)
// [1, NaN, NaN]

如果我们想将数组中的每个数字字符串转换成数字,当然会想到原生的 Number.parseInt 静态方法(也可以使用全局的 parseInt 方法)。但执行结果确并非所愿,这是因为没有考虑函数参数不匹配的问题。

Number.parseInt(string,[ radix ])

Number.parseInt 方法接受两个参数,stringredix

  • string 为要转换的字符串。
  • redix 为字符串的进制,默认情况下为 10,即默认认为该字符串为十进制数。
Array.prototype.map(callback)
callback(value, index, array)

map 的回调函数接受三个参数 valueindexarray

  • value 为当前元素的值。
  • index 为当前元素的索引。
  • array 为源数组的引用。

那么我们可以分析将 Number.parseInt 作为 map 的回调函数时的执行流程。

// 01. 等价于 Number.parseInt('1', 10)
Number.parseInt('1', 0) // 1
// 02. '2' 无法作为一进制数进行转换
Number.parseInt('2', 1) // NaN
// 03. '3' 无法作为二进制数进行转换
Number.parseInt('3', 2) // NaN

接下来再介绍 reduce 方法,reduce 方法其实和 map 方法很相似,但不同的是返回值的处理流程。

map 方法会将每个元素对应的回调函数的返回值收集到一个新数组中,最后返回该数组。

reduce 方法则是将每个元素对应的回调函数的返回值传递给下一个元素对应的回调函数中,最终返回的是最后一个回调函数的返回值。

正因为 reduce 的回调函数需要接收上个回调函数的返回值,相比于 map 方法的回调函数,我们需要新增一个 accumulator 参数来接收返回值,并且 reduce 方法还增加了一个 initialValue 参数,用来传递给第一个元素对应的回调函数的 accumulator

实际上,这就是将每次回调函数的执行结果进行链式传递。

[1, 2, 3, 4].reduce((accumulator, value, index, array) => accumulator + value, 0)
// 10
// 可简写为
[1, 2, 3, 4].reduce((accumulator, value) => accumulator + value)
// 10

上面两种写法,执行结果都是一样的,但实际执行过程是不一样的。

[1, 2, 3, 4].reduce((accumulator, value, index, array) => {
console.log({accumulator, value, index, array});
return accumulator + value;
}, 0)
// {accumulator: 0, value: 1, index: 0, array: Array(4)}
// {accumulator: 1, value: 2, index: 1, array: Array(4)}
// {accumulator: 3, value: 3, index: 2, array: Array(4)}
// {accumulator: 6, value: 4, index: 3, array: Array(4)}
// <= 10
[1, 2, 3, 4].reduce((accumulator, value, index, array) => {
console.log({accumulator, value, index, array});
return accumulator + value;
})
// {accumulator: 1, value: 2, index: 1, array: Array(4)}
// {accumulator: 3, value: 3, index: 2, array: Array(4)}
// {accumulator: 6, value: 4, index: 3, array: Array(4)}
// <= 10

当没有提供 initialValue 参数时,就不执行第一个元素对应的回调函数,而是将第一个元素作为第二个元素的 accumulator 参数,直接执行第二个元素对应的回调函数,这样就可以节省一次执行时间。

结语

掌握以上数组操作方法,基本上可以满足大部分的日常工作啦。