回答

收藏

在 JavaScript 中的数组上的 For-each

技术问答 技术问答 365 人阅读 | 0 人回复 | 2023-09-11

如何使用 JavaScript 遍历数组的所有条目?
% M: s7 W9 l8 U  s6 S我以为是这样的:& m9 R- e" }: T  O0 ?& ^! K
    forEach(instance in theArray)
    ; \! d' p3 P  o+ ?$ I
theArray我的数组在哪里,但这似乎是不正确的。# T, R7 D6 [" m: U; t' L$ a
                                                               
' l; K# g; q( W: a: c1 t    解决方案:                                                               
9 }0 y9 O+ \0 p7 O                                                                TL; DR& y4 T; p4 c( j6 H% G8 V" |
你最好通常是: D' d0 Y  G- ~; t2 G! g, e
一个for-of循环(限 ES2015 ;规范| MDN)——简单且async友好; O4 Z; }0 `/ N7 F
for (const element of theArray)       ...use `element`...  }" w1 w( n7 j9 h) _" D+ y/ r
forEach(ES5  只;规格| MDN)(或其亲属some和这样) -    async-友好(见详情)
) v( i1 d* U6 u6 ]' AtheArray.forEach(element =>        ...use `element`...   };5 T* g8 s3 }; O% U% ]! L
简单的老式for循环——async友好
* c) A! z# P# ], z" ?- ~5 r+ ~for (let index = 0; index
# ?* b+ f2 @. ^. O! r$ h& u(很少)    for-in 有保护措施- -async友好# Q; E: O( Y7 v& r
for (const propertyName in theArray)    if (/*...is an array element property (see below)...*/)        const element = theArray[propertyName];     ...use `element`...   一些快速的不:
1 w' h5 k1 C$ B/ ~0 g不要使用,for-in除非你使用它有安全措施,或者至少知道它为什么会咬你。
$ K/ K: D* t) `. K+ s. O0 ?5 ~% @7 M3 Y不要使用map,如果你不使用它的返回值
; M! v5 p, I- y5 ~4 ]6 n% p: O" M(可悲的是,有人在那里教书。map[ spec / MDN 似乎是这样forEach -但正如我在我的博客上写的,这不是它的用途。如果您不使用它创建的数组,请不要使用它map。)不要使用forEach,如果回调不是异步工作,希望forEach等到工作完成(因为不会)。
但是还有很多东西要探索,请继续阅读…; ^, P9 Y* L- y' R6 {* T' V  g% t
JavaScript 有很强的语义来循环遍历数组和类数组对象。我将答案分为两部分:真实数组的选项和类似例如,数组选项arguments其他可迭代对象 (ES2015 )、DOM 集合等。: w4 \+ e) T' f6 D! {
好吧,让我们看看我们的选择:
$ C! t! P3 _) {7 b对际数组你有五个选项(两个基本上是永久性的,另一个是 ECMAScript 5 [“ES5 添加,另外两个在 ECMAScript 2015(“ES2015”,又名“ES6”)中添加:
9 ~/ x" K; E7 p7 T6 s[ol]使用for-of(隐式使用迭代器)(ES2015 )9 F6 t4 _' T- n6 {4 U
使用forEach及相关(ES5 ); Z, k2 `1 D0 P3 N5 z
使用简单的for循环
2 k7 `3 F4 }' q6 A正确使用for-in
4 i' p; T+ u$ J% ^4 G- {* V) W迭代器用于显式(ES2015 )[/ol](你可以看到这里的旧规范:ES5,ES2015年,但两者都被取代了;目前编辑的草稿总是在这里。
: S/ K$ K, E" Q' m! q2 n细节:: t+ d* ?: F' R; U. z# c6 F
1.使用for-of(隐式使用迭代器)(ES2015 )ES2015向 JavaScript添加了迭代器和可迭代对象。数组是可迭代的(字符串,Maps 和Sets 以及 DOM 集合和列表也是如此,以后你会看到的)。迭代对象为其值提供迭代器。newfor-of语句循环遍历迭代器返回的值:
9 r2 ~# _2 R6 {* S
    const a = ["a","b","c"];for (const element of a) { // You can use `let` instead of `const` if you like    console.log(element);}// a// b// c# x2 \/ _# l5 R- i3 j
没有比这更简单的了!在幕后,它从数组中获得一个迭代器,并通过迭代器返回。数组提供的迭代器从头到尾提供数组元素的值。8 y0 `" S7 c* A
注意element每个循环迭代的范围是什么;尝试element循环结束后使用失败,因为它不存在于循环体之外。# N& p" M! T3 D4 g8 ^) G9 e) \' ]- v9 n
理论上,一个for-of循环涉及多个函数调用(一个用于获取迭代器,另一个用于获取每个值)。即使这是真的,也没有什么可担心的。JavaScript 引擎中的函数调用非常便宜(它困扰着我forEach[below] 直到我研究它;细节)。但此外,在处理数组等本机迭代器时,JavaScript 发动机将优化这些调用程序关键性能代码中)。
) H8 J0 l& \- q$ j4 m" Vfor-of是完全async友好的。如果你需要在循环中串联(而不是并行)来完成工作,那么循环中的一个awaitin 循环体将等待承诺在继续之前解决。这是一个愚蠢的例子:6 M( x7 x6 d4 z: y% y
function delay(ms)    return new Promise(resolve =>        setTimeout(resolve,ms);}async function showSlowly(messages)    for (const message of messages)        await delay(400)console.log(message);   showSlowly(    "So","long","and","thanks","for","all","the","fish!"]);// `.catch` omitted because we know it never rejects请注意这些词是如何在每个词之前延迟的。
3 e' m  o2 l- w# ~, n8 Q6 p; E这是编码风格的问题,但是for-of这是我在遍历任何可迭代的东西时首先要接触的东西。# x9 ]  j5 A! c- _
2.用途forEach及相关在任何可访问的地方ArrayES5 添加的功能模糊了现代环境(因此,不是 IE8)如果只处理同步代码(或者不需要等待),可以使用forEach( spec | MDN )循环期间完成的异步过程:
5 c  w" _' q: a3 j" j5 p
    const a = ["a","b","c"];a.forEach((element) =>    console.log(element);});
    ( g  K$ I$ H# ]- U0 x
forEach接受回调函数和可选值this调用回调时使用的值(上述未使用)。调用数组中的每个元素,并按顺序跳过稀疏数组中不存在的元素。虽然我只使用了上述参数,但回调函数调用了三个参数:迭代元素、元素索引和正在迭代的数组(以防止您的函数不方便)。8 L, i1 _  K! \$ _  W% t% `
像for-of,forEach优点是不需要在包含范围内声明索引和值变量;在这种情况下,它们被提供为迭代函数的参数,并且很好地限制在迭代中。& W* E1 V9 D3 R
与for-of,forEach缺点是不懂async函数和await.如果您使用 async函数作为回调,forEach则在继续之前等待函数的承诺解决。这是替代的async示例- 请注意初始延迟是如何发生的,但所有文本都会立即出现,而不是等待:for-of``forEach
* T( ~( @$ ^* {& U" v# kfunction delay(ms)    return new Promise(resolve =>        setTimeout(resolve,ms);}async function showSlowly(messages)    // INCORRECT,doesn't wait before continuing,   // doesn't handle promise rejections    messages.forEach(async message =>        await delay(400)console.log(message);}showSlowly(    "So","long","and","thanks","for","all","the","fish!"]);// `.catch` omitted because we know it never rejectsforEach 是循环遍历所有函数,但 ES5 定义了其他几个有用的遍历数组并执行操作函数,包括:& r- i& ~2 b2 q. W0 p" ^& N& o7 e
every( spec | MDN ) - 第一次回调假值时停止循环
4 F/ Z0 c1 T. E# R1 ysome( spec | MDN ) - 回调第一次回到真实值时停止循环
" R/ o+ R* i* Q- Vfilter( spec | MDN ) - 创建一个新的数组,包括回调返回真实值的元素,省略不返回真实值的元素& R  n0 |4 \' G! L( {
map( spec | MDN ) - 从回调返回的值创建一个新数组% q  \- }4 _3 U) ]( D( A2 X% \
reduce( spec | MDN ) - 通过重复调用回调建立一个值,并将其传输到以前的值;详情请参考规范
* Y$ M9 Y* F- F2 k2 ]reduceRight( spec | MDN ) - 类似reduce,但是按照降序而不是升序
和 一样forEach,若使用一个async作为您的回调函数,函数不会等待函数的承诺完成。这意味着:" R5 E* P' m! I2 a% \, _! G2 Q& E
使用一个async回调函数从未通过适当的回调函数every,some以及filter因为他们将承诺返回,就像一个truthy值; 他们不会等待承诺解决,然后使用履行价值。
+ U/ d( C0 s) T+ M$ t4 m4 L5 K使用async函数回调通常适用于map,如果目标是将某个数组转换为promise可能是数组传递给 promise 组合器函数(Promise.all、Promise.race、promise.allSettled、 或Promise.any)。
( ~4 d9 P: {+ N. s使用async函数回调很少适合reduceor reduceRight,因为(再次)回调将永远回到承诺中。但有一种习惯用法是从使用中使用。reduce( const promise = array.reduce((p,element) => p.then(/*...something usingelement...*/);)数组构建承诺链,但通常在这些情况下,函数中for-oforfor循环async调试更清晰、更容易。
3. 使用简单for循环有时旧的方法是最好的:7 i/ t& C8 d$ L& P
    % ~5 T  J8 O4 C) K" p. I8 R
  • const a = ["a","b","c"];for (let index = 0; index 如果数组的长度在循环过程中不会改变,那么它可能是一个稍微复杂的版本,在高性能敏感的代码中抓住长度达阵很小的有点快:3 L' i  _3 _! w7 g
  • [code]const a = ["a","b","c"];for (let index = 0,len = a.length; index And/or 倒数:[code]const a = ["a","b","c"];for (let index = a.length - 1; index >= 0; --index)    const element = a[index];    console.log(element);}
    5 j( h- o* U# t4 V9 R
但现代 JavaScript 引擎,您很少需要勉强挤出最后一点力气。
5 w* h' v# p/ @! J. B: l在 ES在2015 之前,包含作用域必须存在循环变量var只有函数级作用域,没有块级作用域。但正如你在上面的例子中看到的,你可以let在 内使用for将变量范围限制为循环。index每次循环迭代都会重新创建变量,这意味着在循环中创建的闭包将被保留index引用特定迭代,解决了旧的循环封闭问题:
; i0 k6 m) M3 h) e( l; ~// (The `NodeList` from `querySelectorAll` is array-like)const divs = document.querySelectorAll("div");for (let index = 0; index         console.log("Index is: "   index);}zeroonetwothreefour如果你点击上面的第一个,你会得到Index is: 0Index is: 4”。这并没有,假如你使用的工作var,而不是let(你总是看:5指数)。5 s/ l" l+ P: V7 z" m
就像for-of,for循环在async函数运行良好。这是使用。for早期循环示例:4 H  e# d8 R9 y! M
Show code snippet2 v5 @# n. A8 p, U1 x
4、正确使用for-infor-in它不是用于遍历数组,而是用于遍历对象属性的名称。作为对象的副产品,它似乎经常用于循环遍历数组,但它不仅循环遍历数组索引,还循环遍历对象所有可枚举属性(包括继承属性)。$ O# E' N, T* f$ M+ H7 E
for-in数组中唯一真实的用例是:7 V( h& C: t6 S# R
它是一个稀疏其中有数组大量间隙,或者
) F" m) U5 ]( v: N" O2 H在数组对象中使用非元素属性,并希望将其包含在循环中
查看第一个示例:for-in若使用适当的保护措施,可使用访问稀疏数组元素:0 |" n1 v4 m( d! G% R$ }
    - z9 [# x1 Q( h
  • // `a` is a sparse arrayconst a = [];a[0] = "a";a[10] = "b";a[10000] = "c";for (const name in a)    if (Object.hasOwn(a,name) &&    These checks are      ^0$|^[1-9]\d*$/.test(name) &&  // explained        name 注意三项检查:4 I$ h  a, W! V. A& V0 j: ]
  • [ol]
    * T1 B/ Y& e2 U: ~% D- L
  • 对象有这个名字自己的属性(不是它的原型继承的属性;a.hasOwnProperty(name)但 ES2022 添加了Object.hasOwn更可靠),
    0 p6 U, Q  E0 H9 f- |
  • 名称为十进制数字(如正常字符串形式,而不是科学记数法),以及
    ( B" {) U; w& v7 Q
  • 数字时名称的强制值为 [/ol]…尽管如此,大多数代码只进行hasOwnProperty检查。8 U* `* r! s1 Y; I
  • 当然,你不会在内联代码中这样做。您将编写实用程序函数。
    ; V) x/ V6 L, i
  • // Utility function for antiquated environments without `forEach`const hasOwn = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);const rexNum = /^0$|^[1-9]\d*$/;function sparseEach(array,callback,thisArg)    for (const name in array)        const index =  name;        if (hasOwn(a,name) &&            rexNum.test(name) &&            index     console.log("Value at "   index   " is "   value);});像for,for-in若工作需串行完成,则在异步函数中效果良好。
      `# O) s, J& s1 i1 m" O) e( |4 j" m" i
  • function delay(ms)    return new Promise(resolve =>        setTimeout(resolve,ms);}async function showSlowly(messages)    for (const name in messages)        if (messages.hasOwnProperty(name)) { // Almost always this is the only check people do            const message = messages[name];            await delay(400);        console.log(message);      showSlowly(    "So","long","and","thanks","for","all","the","fish!"]);// `.catch` omitted because we know it never rejects迭代器用于显式(ES2015 )for-of隐式使用迭代器为您完成所有 scut 工作。有时候,你可能希望显式使用迭代器。它看起来像这样:[code]const a = ["a","b","c"];const it = a.values(); // Or `const it = a[Symbol.iterator]();` if you likelet entry;while (!(entry = it.next()).done) {    const element = entry.value;    console.log(element);}
    7 |/ O, G( ?" {& i  v2 n
迭代器是符合规范中迭代器定义的对象。每次调用它next在方法上,它会回到一个新的结果对象。结果对象有属性 ,done告诉我们它是否完成了,还有另一个属性value包含迭代值。(done如果是false,value是可选的,如果是,是可选的undefined。)
8 K# s3 i5 j. H; z/ S/ k) H你得到的value这取决于迭代器。在阵列中,缺少迭代器提供每个阵列元素的值("a","b",和"c"前面的例子)。数组还有三种返回迭代器的方法:. g3 K1 C) X0 g0 Z# x
values():这是[Symbol.iterator]返回默认迭代器方法的别名。
( o: i8 m3 p6 M# a/ D/ Ikeys(): 返回一个迭代器,它在数组中提供每个键(索引)。它将在上述示例中提供"0",then "1",then "2"(是的,作为字符串)。- M  t: k$ b" G  j
entries(): 返回一个提供[key,value]数组的迭代器。
因为迭代器对象在调用 之前不会前进next,因此它们在async函数循环运行良好。这是以前的for-of明确使用迭代器的示例:
% m+ D: H# o0 ~! Y7 Vfunction delay(ms)    return new Promise(resolve =>        setTimeout(resolve,ms);}async function showSlowly(messages)    const it = messages.values()    while (!(entry = it.next()).done)        await delay(400)const element = entry.value;        console.log(element);   showSlowly(    "So","long","and","thanks","for","all","the","fish!"]);// `.catch` omitted because we know it never rejects类数组对象除了真正的数组,还有类似数组的他们有一个对象length属性及全数字名称属性:NodeListinstances、HTMLCollectioninstances、argumentsobject 等等。我们如何遍历他们的内容?/ M6 j+ ~& B& @+ ?
使用上述大多数选项至少有一些上述数组方法,可能是大多数甚至全部,也适用于类似数组的对象:
5 M$ D4 J/ s5 o[ol]使用for-of(隐式使用迭代器)(ES2015 )[/ol]for-of使用对象提供的迭代器(如有)。这包括主机提供的对象(如 DOM 集合和列表)。HTMLCollection来自getElementsByXYZ方法NodeList例子和来源querySelectorAll两者的s 实例支持迭代。(这是 HTML 和 DOM 规范非常巧妙定义。基本上,任何东西都有length和 索引访问的对象是自动可迭代的。不必被标记iterable;除了用于集合,除了可迭代,还支持forEach,values,keys,和entriesmethods. NodeListdo; HTMLCollectionnot,但两者都是可迭代的。
+ ^& q- }* n0 |0 C: V" i以下是循环遍历div元素的例子:
/ E) E5 B6 j- Z, W" `& _$ _5 JShow code snippet' Y2 d0 @1 c" J, b$ j
[ol]使用forEach及相关(ES5 )[/ol]on 各种函数Array.prototype可通过有意通用Function#call( spec | MDN ) 或Function#apply( spec | MDN )用于类数组对象。(如果你必须处理 IE8 或更早版本 [哎哟],请参阅本答案末尾的主机提供对象的警告,但这对模糊的现代浏览器来说不是问题。
5 m9 E+ Q- R  g假设您想forEach在 aNode的childNodes集合使用(作为 ,本机HTMLCollection没有forEach)。你会这样做:
2 H! d5 j) z% Y$ |js   Array.prototype.forEach.call(node.childNodes,(child) => {       // Do something with `child`  };6 ~7 o0 N6 F" {4 e! a
(但请注意,您只能使用它for-ofon node.childNodes。)
# O0 {$ I/ m9 t* \/ X8 l$ E& m如果您计划经常这样做,您可能希望将函数引用的副本抓取到变量中进行重用,例如:& `8 q: z' D# S: _
```js2 U2 F  V5 l% T: G) G
            // (This is all presumably in a module or some scoping function)
7 I% u3 N  O" g) I            const forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);
+ W" g) z5 M" H) c- F1 M4 }// Then later…, I- u. Z+ Z% c7 }6 w
            forEach(node.childNodes,(child) => {' P, O- z& ~7 p8 V2 `
                            Do something with child# X4 q, \  Z! z, x1 k5 {; E
            };* v2 C& v% ~* \# j( L& i, H
            ```, n9 k+ Y  c3 ^! }. o& Q7 N
[ol]使用简单的for循环[/ol]也许很明显,简单for循环适用于类似数组的对象。
% G: L+ b: W& ]/ R4 P) E[ol]迭代器用于显式(ES2015 )[/ol]见#1。/ o0 @% J' J. B% S& r9 N
可能能够逃脱for-in(使用安全措施),但没有理由尝试所有这些更合适的选项。
* h4 r* h, d. n创建一个真正的数组在其他情况下,您可能希望将类似数组的对象转换为真实数组。这样做很简单:7 U# [* c1 p' x1 L
[ol]利用 Array.from[/ol]Array.from (规格) | (MDN)(ES2015 ,但容易填充)从类似数组的对象中创建数组,可以选择先通过映射函数传输条目。
0 l6 X4 L; w7 s6 U4 mjs   const divs = Array.from(document.querySelectorAll("div"));
! I0 U( |# X+ M6 C! v…获取NodeListfromquerySelectorAll并创建数组。
5 M* _9 n$ u1 o  ^) |' Z如果你打算以某种方式映射内容,映射功能将非常方便。例如,如果您想获得具有给定类别元素的标签名称数组:
; V0 u0 `. A! E) C```js+ `2 }$ |& {9 ]9 u1 F) X7 E  w! H
            // Typical use (with an arrow function):
* m( j7 n- f* U3 s            const divs = Array.from(document.querySelectorAll(“.some-class”),element => element.tagName);, Y6 i8 [. ]+ t
// Traditional function (since Array.from can be polyfilled):
8 h& w- w/ @3 q7 Y            var divs = Array.from(document.querySelectorAll(“.some-class”),function(element) {
9 h) ~$ z8 }4 Q; E                            return element.tagName;
+ w0 s/ B3 B+ v# R+ k" a/ {            };
3 o, @4 C- b+ P1 N# K            ```
! M! q6 E7 I- U! H+ J[ol]使用扩展语法 ( ...)[/ol]也可使用 ES2015 扩展语法。for-of,使用对象提供的迭代器(见上节 #1):" ~1 V, X9 Y+ G) I3 J" p
js   const trueArray = [...iterableObject];
' W3 i% |3 m9 q) g+ n例如,如果我们想要 a 转换NodeList对于一个真正的数组来说,使用扩展语法将变得非常简单:1 W; h. L, J" |' I: O
js   const divs = [...document.querySelectorAll("div")];
' m& d3 a' P% W3 @0 L; D[ol]使用slice数组的方法[/ol]我们可以用slice和上面提到的其他方法一样,数组的方法是有意通用的,所以可以和类似数组的对象一起使用,如下所示:! e1 Z  t/ M9 D
js   const trueArray = Array.prototype.slice.call(arrayLikeObject);3 A6 x. }& B6 w$ E2 G3 k( Q
例如,如果我们想要 aNodeList我们可以把它转换成真正的数组:- t7 e2 {. v' W  J) r4 q% ~1 D
js   const divs = Array.prototype.slice.call(document.querySelectorAll("div"));
# r8 S$ X1 P% H(如果你还必须处理 IE8 [哎哟],会失败;IE8 不允许你像this使用主机提供的对象。
( ~) Q2 h# L' ?* v主机提供的对象的警告如果您使用主机提供类似数组的对象Array.prototype函数(例如,DOM 由浏览器组成,而不是 JavaScript 发动机提供),如 IE8 过时的浏览器可能无法处理,所以如果你必须支持它们,请确保在你的目标环境中进行测试。但这对模糊的现代浏览器来说并不是一个问题。(对于非浏览器环境,它自然会因环境而异。
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则