Skip to content
This page has been auto-translated and may contain errors.View in English

数组方法

docs.scrimba.com

你有一个价格列表,想要第二个列表,其中每个价格都添加了税。你可以编写一个 for 循环来遍历数组、计算总和,然后将每个结果推送到新数组中。这样可以工作,但对于一个小想法来说需要太多的代码:取每个项目并改变它。数组方法正是为这类工作而设计的内置工具。它们让你说出你想对列表做什么,而不需要详细说明实现循环,本章涵盖了你每天都会用到的那些。它们直接基于数组函数构建,所以把两者都放在手边。

map:改变每个项目

map 是一个**方法**:一个属于数组的函数,对其进行操作。map 通过将每个项目运行通过你给它的一个小函数来构建一个新数组,并收集结果。你传递给它那个函数,它一次手交给你一个项目。

假设你有一些价格,想要每个都翻倍:

js
const prices = [2, 4, 6];
const doubled = prices.map((price) => price * 2);
console.log(doubled); // [4, 8, 12]

(price) => price * 2 部分是一个箭头函数:一个短函数,它接受一个项目(price)并返回它应该变成什么(price * 2)。map 对每个项目调用一次,并将每个答案放在新数组的相同位置。

map 是一个数组**方法,将一个列表转换为一个相同长度的新列表。你给它一个回调**,一个 map 为每个项目运行一次的函数,该回调返回的任何内容都成为结果的相同位置处的项目。

js
const prices = [2, 4, 6];
const doubled = prices.map((price) => price * 2);
console.log(doubled); // [4, 8, 12]

与手动版本进行比较,它用更多的移动部分做同样的事情:

js
const prices = [2, 4, 6];
const doubled = [];
for (let i = 0; i < prices.length; i += 1) {
  doubled.push(prices[i] * 2);
}
console.log(doubled); // [4, 8, 12]

循环让你管理索引、新数组和 pushmap 表述意图,"把每个价格变成价格乘以二",并为你处理簿记。

map 通过将**回调**(你传入供它调用的函数)应用于每个元素来将数组转换为相同长度的新数组。回调在索引 i 处的返回值变成结果的索引 i 处的元素。没有跳过任何内容,也没有添加任何内容,所以形状得到保留,只有值改变。

js
const prices = [2, 4, 6];
const withTax = prices.map((price) => price * 1.2);
console.log(withTax); // [2.4, 4.8, 7.2]

回调还接收索引和整个数组作为第二个和第三个参数,所以 array.map((value, index) => ...) 在位置重要时可用。它为你保留的值是意图:map 一目了然地说"相同长度,每个项目转换",而循环让读者从索引、计数器和 push 重新构建。

Junomapmap 获取一个列表,给你一个相同长度的新列表,每个项目都通过你的小函数运行。箭头函数说出每个项目变成什么,map 按顺序对所有项目做这件事。每当你发现自己写一个循环只是为了构建第二个列表时,就可以使用它。
Junomapmap 是"将每个项目转换为相同长度的新数组"。回调为每个项目运行一次,其返回值落在相同位置。一旦你看到它作为循环加 push 模式的替代品,一半的循环就变成了一行可读的代码。
Junomapmap 保留长度和位置:回调在索引 i 处的返回值变成结果的索引 i。回调也获取索引和数组,用于位置重要的情况。它读起来像意图,而循环让你从计数器和 push 重新组装意图。

filter:保留通过测试的项目

map 改变每个项目,而 filter 只保留其中一些。你给它一个函数,为每个项目回答是或否,filter 从只有回答"是"的项目中构建一个新数组。

js
const scores = [40, 75, 90, 55, 88];
const passing = scores.filter((score) => score >= 60);
console.log(passing); // [75, 90, 88]

函数 (score) => score >= 60 为每个分数返回 truefalsefilter 保留返回 true 的项目,丢弃其余的。新数组可以比原数组更短,它永远不会改变顺序。

filter 返回一个新数组,仅包含其回调返回 true 的项目。回调是一个**谓词**:一个其工作是为每个项目回答是或否问题的函数。

js
const scores = [40, 75, 90, 55, 88];
const passing = scores.filter((score) => score >= 60);
console.log(passing); // [75, 90, 88]

结果通常比输入更短,它保留原始顺序。与循环加 if 相比,filter 将测试放在前面和中心,并去掉它周围的仪式。如果回调为所有内容返回 true,你会得到一个完整副本;如果它为所有内容返回 false,你会得到一个空数组。

filter 返回一个新数组,包含满足**谓词**(其工作是返回真或假答案的回调)的元素。长度不保留:结果从空运行到完整副本,顺序保留。

js
const scores = [40, 75, 90, 55, 88];
const passing = scores.filter((score) => score >= 60);
console.log(passing); // [75, 90, 88]

有两件事值得区分。首先,filter 决定一个项目是否存活;它永远不会改变项目,所以当你需要选择和转换时,将其与 map 结合。其次,回调的返回被强制为布尔值,所以一个谓词意外返回 0undefined 会丢弃你想保留的项目。返回显式比较,整类 bug 消失。

Junofilterfilter 保留通过测试的项目,丢弃其余的,所以新数组通常更短。你的函数为每个项目回答是或否,只有"是"的项目才能通过。它永远不会重新排序任何内容,所以存活的项目保持开始时的顺序。
Junofilterfilter 保留其回调返回 true 的项目,给你一个相同长度或更短的新数组。回调是一个谓词:一个是或否的测试,不是转换器。所有 true 给出完整副本,所有 false 给出空数组,顺序总是保留。
Junofilterfilter 选择,map 转换,保持这种分离整洁是为什么它们链接得如此好的原因。回调的返回被强制为布尔值,所以一个谓词滑出 0undefined 会悄悄丢弃你想要的项目。返回显式比较,整个类别的 bug 消失。

reduce:化简为一个值

mapfilter 都交给你一个数组。reduce 是将数组塌缩为一个单一值的:一个总和、一个最大值、一个连接的字符串。它比其他的更容易让人困惑,所以值得慢慢来。

reduce 遍历数组,同时携带一个运行结果。在每个项目上,它问:给定到目前为止的结果,以及这个项目,新结果是什么?

js
const prices = [10, 20, 30];
const total = prices.reduce((sum, price) => sum + price, 0);
console.log(total); // 60

函数得到两件事:sum,到目前为止的运行总和,和 price,当前项目。它返回 sum + price,这成为下一个项目的新运行总和。末尾的 0 是总和开始的地方。所以它是 0 + 10,然后 10 + 20,然后 30 + 30,落在 60

reduce 通过携带**累加器**:一个运行结果,每一步更新并传递,将整个数组变成一个值。回调接收累加器和当前项目,并为下一步返回累加器。

js
const prices = [10, 20, 30];
const total = prices.reduce((sum, price) => sum + price, 0);
console.log(total); // 60

两个论点要理清:sum 是累加器(到目前为止构建的值),price 是当前项目。reduce 的第二个论点,0,是累加器的起始值。一步一步:从 0 开始,加 10 得到 10,加 20 得到 30,加 30 得到 60。名称 sum 这里只是一个标签;累加器可以是任何类型,包括你不断追加的字符串或运行最大值。

reduce 通过线程化**累加器**(运行结果)通过数组来将数组折叠为一个单一值。回调是 (accumulator, currentItem) => nextAccumulator,其返回变成传递给下一步的累加器。reduce 的第二个论点是初始累加器。

js
const prices = [10, 20, 30];
const total = prices.reduce((sum, price) => sum + price, 0);
console.log(total); // 60

两个实用规则可以避免大多数 reduce 错误。始终传递初始值:没有它,reduce 使用第一个元素作为种子并在第二个开始,这在空数组上抛出,当你的累加器类型不同于元素类型时会悄悄改变行为。回调的每条路径都必须返回累加器;在多行体中忘记 return 会将 undefined 作为累加器留给下一步。reduce 是这些方法中最通用的,mapfilter 都可以写成 reduce,这正是为什么它也最容易变得难以理解。当你塌缩为一个值时使用它,当你不需要时可以选择更简单的东西。

Junoreducereduce 通过列表携带一个运行结果,并在每个项目处更新它。回调获取到目前为止的结果加上当前项目,并返回新结果。最后一个论点是你开始的地方,通常是 0 作为总和,当我大声读出来一步一步时这个对我点击了。
Junoreducereduce 使用累加器将数组化简为一个值:一个运行结果,每一步更新并传递。回调是 (accumulator, item),最后一个论点作为累加器的种子。它不必是一个数字;累加器可以是一个你构建的字符串或运行最大值。
Junoreduce 始终传递初始值,否则 reduce 从第一个元素作为种子,在空数组上抛出。回调的每条路径都必须返回累加器,否则下一步获取 undefined。这是这些方法中最通用的,也是最容易变得难以理解的,所以当你塌缩为一个值时使用它,当你不需要时选择更简单的东西。

find、some 和 every

这三个是 filter 的近亲:它们在数组上运行测试,但不是一个新数组,它们回答一个更狭义的问题。

find 给你通过测试的第一个项目,不是所有项目的列表。someevery 回答关于整个数组的是或否问题。

js
const scores = [40, 75, 90, 55];

const firstPass = scores.find((score) => score >= 60);
console.log(firstPass); // 75

const anyPass = scores.some((score) => score >= 60);
console.log(anyPass); // true

const allPass = scores.every((score) => score >= 60);
console.log(allPass); // false

find 返回第一个匹配的项目(75)。some 问"有任何项目通过吗?"并返回 true,因为至少有一个通过。every 问"它们都通过了吗?"并返回 false,因为 4055 没有通过。

filter 相同的谓词风格,三个不同的答案。find 返回**第一个匹配项目**(如果都不匹配则为 undefined)。some 返回 true 如果至少一个项目通过。every 返回 true 仅当所有项目都通过。

js
const scores = [40, 75, 90, 55];

console.log(scores.find((score) => score >= 60)); // 75
console.log(scores.some((score) => score >= 60)); // true
console.log(scores.every((score) => score >= 60)); // false

当你想要一个项目而不是一个过滤后的列表时选择 find,当你只需要关于集合的是或否答案时选择 someevery。使用 filter(...).length > 0,其中 some 会做的工作,但它检查每个项目并构建一个一次性数组来回答 some 直接回答的问题。

三个谓词驱动的查询,停止早期而不是构建新数组。find 返回**第一个匹配元素**或 undefinedsome 在第一个通过的元素上返回 trueevery 在第一个失败的元素上返回 false

js
const scores = [40, 75, 90, 55];

console.log(scores.find((score) => score >= 60)); // 75
console.log(scores.some((score) => score >= 60)); // true
console.log(scores.every((score) => score >= 60)); // false

短路就是要点:someevery 在答案决定时停止,所以当你只需要布尔值时它们比在整个数组上 filter 更便宜。两个边界情况要知道:find 返回 undefined 与找到实际 undefined 无法区分,所以当可能时测试你关心的谓词而不是返回值。在空数组上 some 总是 falseevery 总是 true(空真地,因为没有项目要失败),这是正确的但第一次让人惊讶。

Junofind, some, and everyfind 给你通过的第一个项目,不是整个列表。some 问"有任何通过吗?"而 every 问"都通过了吗?",两者都用 truefalse 回答。当你想要一个项目或一个是或否答案,不是一个过滤后的数组时使用这些。
Junofind, some, and everyfilter 相同的谓词,三个更狭义的答案:find 给出第一个匹配或 undefinedsome 是如果有任何通过则为 true,every 仅当所有通过时为 true。当你想要一个项目或布尔值时选择这些而不是 filter(...).length,因为它们说明意图并跳过构建一次性数组。
Junofind, some, and everysomeevery 短路,在答案决定时停止,所以它们击败在整个数组上过滤以获取布尔值。注意两个边:find 返回 undefined 看起来与找到实际 undefined 相同,在空数组上 some 总是 false 而 every 总是 true。两者都是正确的,两者第一次让人惊讶。

链接和保持可读性

上面的方法共享一个设计,一旦你结合它们就会获得回报:每一个都返回一个新值并让原始数组不动。

mapfilter 和其他的永远不会改变你开始的数组。它们给你一个新数组(或一个值)并让原始的单独。

js
const prices = [10, 20, 30];
const doubled = prices.map((price) => price * 2);
console.log(doubled); // [20, 40, 60]
console.log(prices); // [10, 20, 30]

因为 mapfilter 都交给你一个数组,你可以把它们排成一队:先过滤,然后映射幸存者。

js
const prices = [10, 20, 30, 40];
const bigDoubled = prices.filter((price) => price >= 20).map((price) => price * 2);
console.log(bigDoubled); // [40, 60, 80]

这些方法返回新数组和值而不改变原始的,一个称为**不可变性**的属性:你的源数据保持原样。这就是使链接安全的原因。因为 filter 返回一个数组,你可以直接在其结果上调用 map

js
const prices = [10, 20, 30, 40];
const bigDoubled = prices.filter((price) => price >= 20).map((price) => price * 2);
console.log(bigDoubled); // [40, 60, 80]

从上到下阅读一个链作为一个管道:保留 20 或更多的价格,然后翻倍每个幸存者。顺序重要,因为首先 filter 意味着 map 在更少项目上运行。链接读起来最多两三步不错;超过那,一个命名的中间变量或一个普通循环通常比一个长链更清晰。

这些方法不可变:它们返回新数组和值,让源不动。那个**不可变性**是什么使 filter(...).map(...) 链安全地作为一个管道读,因为没有步骤回到并在你下面编辑一个共享数组。

js
const prices = [10, 20, 30, 40];
const bigDoubled = prices.filter((price) => price >= 20).map((price) => price * 2);
console.log(bigDoubled); // [40, 60, 80]

两个判断决定一个链是帮助还是伤害。首先,成本:每一步遍历数组并分配一个新的,所以一个三步链进行三次通过和两个中间数组。在你每天遇到的大小上那是无关的,可读性赢;在一个大数组上的热路径上它可以重要,单一循环或一个在一次通过中做工作的 reduce 是答案。其次,清晰:一个链在每一步是一个明确想法时赚得它的位置。一旦回调增长一个有分支的体,或链超过三步,一个普通的 for 循环有一个命名变量通常读得更好。更喜欢一个队友最快理解的版本,不是适合一行的。

JunoChaining and staying readable 这些方法永远不会改变你开始的数组,它们给你一个新的,所以原始的总是安全的。因为 filtermap 都返回数组,你可以链接它们:先过滤,然后映射剩下的。从上到下,一次一步读链。
JunoChaining and staying readable 不可变性,让原始数组单独,是使链接安全的原因。从上到下阅读 filter(...).map(...) 作为一个管道,记住顺序重要因为首先过滤意味着映射更少项目。超过两三步,一个命名变量或一个普通循环通常读得比一个长链更清晰。
JunoChaining and staying readable 每一步是一个通过和分配,所以一个链在日常大小上很好,在热路径上值得崩溃到一个循环或一个 reduce,在大数据上。清晰是另一个决定:一个链在每一步是一个明确想法时赚得它的位置,一个普通循环在回调增长分支时赢。写一个队友最快读的版本,不是最短的。

接下来去哪里

mapfilterreduce 涵盖了你在列表中做的大多数事情:转换它、缩小它或塌缩它。findsomeevery 回答更狭义的问题。一旦这些感到自然,你数组上的大多数 for 循环变成一行或两行,说出你的意思。

自然的下一步是对象,其中数组开始保留比普通数字和字符串更丰富的项目,这些相同的方法在它们上继续工作。如果这里的任何回调语法感到摇摇欲坠,通过函数的一次通过使其余的点击到位。