回答

收藏

在 JavaScript 中的数组上的 For-each

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

如何使用 JavaScript 遍历数组的所有条目?
, R: B0 Q, s7 z: o  H! s/ q我以为是这样的:
* o0 e0 T: r3 a! X) @
    forEach(instance in theArray)
    , l8 r: E! G2 H6 Z
theArray我的数组在哪里,但这似乎是不正确的。
% i; P: V" ]! Q1 E: s6 M+ \                                                               
. \0 g' n+ R! L/ n- j    解决方案:                                                                $ g* }7 a- ]+ w
                                                                TL; DR
* o; [; Z; j3 X, x9 X, K你最好通常是/ Z  `8 h* F2 \9 J8 t5 H# }
一个for-of循环(限 ES2015 ;规范| MDN)——简单且async友好
4 O/ U0 O/ @5 T6 Sfor (const element of theArray)       ...use `element`...  }  n8 R; o2 i8 q
forEach(ES5  只;规格| MDN)(或其亲属some和这样) -    async-友好(见详情)6 x  S3 @: z5 G+ k( w
theArray.forEach(element =>        ...use `element`...   };
# J# `, C; b) I$ W4 }* D# s# S简单的老式for循环——async友好9 W2 o3 S+ E! L7 H
for (let index = 0; index
1 n! @; [" s. z/ ?! r(很少)    for-in 有保护措施- -async友好  H2 }; _2 v4 F  q! L9 j
for (const propertyName in theArray)    if (/*...is an array element property (see below)...*/)        const element = theArray[propertyName];     ...use `element`...   一些快速的不:6 {0 u4 ?) E4 \  H1 D7 Y
不要使用,for-in除非你使用它有安全措施,或者至少知道它为什么会咬你。% Y( p* {2 f' G; K
不要使用map,如果你不使用它的返回值
) Z7 r: ]! Y, c: ?(可悲的是,有人在那里教书。map[ spec / MDN 似乎是这样forEach -但正如我在我的博客上写的,这不是它的用途。如果您不使用它创建的数组,请不要使用它map。)不要使用forEach,如果回调不是异步工作,希望forEach等到工作完成(因为不会)。
但是还有很多东西要探索,请继续阅读…
- r- I; S' c3 \! |JavaScript 有很强的语义来循环遍历数组和类数组对象。我将答案分为两部分:真实数组的选项和类似例如,数组选项arguments其他可迭代对象 (ES2015 )、DOM 集合等。
# z+ T  l) W5 I: B( B好吧,让我们看看我们的选择:
+ d3 q9 g# u1 N) _. \: m: J; V& s对际数组你有五个选项(两个基本上是永久性的,另一个是 ECMAScript 5 [“ES5 添加,另外两个在 ECMAScript 2015(“ES2015”,又名“ES6”)中添加:
" f$ R3 z5 _) x1 `: s[ol]使用for-of(隐式使用迭代器)(ES2015 )
& ~, H% _7 O' P使用forEach及相关(ES5 )
4 `8 [5 Q# J  L$ c8 y% E7 D* q使用简单的for循环8 J% k$ r" M  B0 D) a) c! Y8 ^
正确使用for-in
; n) [# t8 e* a迭代器用于显式(ES2015 )[/ol](你可以看到这里的旧规范:ES5,ES2015年,但两者都被取代了;目前编辑的草稿总是在这里。6 O2 P! P& ^8 t
细节:
, V) X1 u7 @$ f& ?( l7 e$ a/ c% w5 V1.使用for-of(隐式使用迭代器)(ES2015 )ES2015向 JavaScript添加了迭代器和可迭代对象。数组是可迭代的(字符串,Maps 和Sets 以及 DOM 集合和列表也是如此,以后你会看到的)。迭代对象为其值提供迭代器。newfor-of语句循环遍历迭代器返回的值:
9 L7 S: `/ C" u8 s+ S. P
    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
    # d7 q' d" G8 T) B' \# c* l
没有比这更简单的了!在幕后,它从数组中获得一个迭代器,并通过迭代器返回。数组提供的迭代器从头到尾提供数组元素的值。4 k) L- o9 g. F8 k5 o% `+ C7 W9 ~
注意element每个循环迭代的范围是什么;尝试element循环结束后使用失败,因为它不存在于循环体之外。) X5 X  u, [% S, l
理论上,一个for-of循环涉及多个函数调用(一个用于获取迭代器,另一个用于获取每个值)。即使这是真的,也没有什么可担心的。JavaScript 引擎中的函数调用非常便宜(它困扰着我forEach[below] 直到我研究它;细节)。但此外,在处理数组等本机迭代器时,JavaScript 发动机将优化这些调用程序关键性能代码中)。0 I' E6 v/ Q) o
for-of是完全async友好的。如果你需要在循环中串联(而不是并行)来完成工作,那么循环中的一个awaitin 循环体将等待承诺在继续之前解决。这是一个愚蠢的例子:
& a% m7 v7 h0 Xfunction 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请注意这些词是如何在每个词之前延迟的。
$ F+ b+ k# p& M2 I) W- e2 ?2 o这是编码风格的问题,但是for-of这是我在遍历任何可迭代的东西时首先要接触的东西。
" b. \* f& U3 Q  c2 H% U& I2.用途forEach及相关在任何可访问的地方ArrayES5 添加的功能模糊了现代环境(因此,不是 IE8)如果只处理同步代码(或者不需要等待),可以使用forEach( spec | MDN )循环期间完成的异步过程:
) A  C  t9 X8 B7 }# Z9 w
    const a = ["a","b","c"];a.forEach((element) =>    console.log(element);});0 A1 B8 h9 H& s0 n/ G
forEach接受回调函数和可选值this调用回调时使用的值(上述未使用)。调用数组中的每个元素,并按顺序跳过稀疏数组中不存在的元素。虽然我只使用了上述参数,但回调函数调用了三个参数:迭代元素、元素索引和正在迭代的数组(以防止您的函数不方便)。
: s2 H7 c# g  i. s; I& O像for-of,forEach优点是不需要在包含范围内声明索引和值变量;在这种情况下,它们被提供为迭代函数的参数,并且很好地限制在迭代中。
/ ~3 i! x/ W% ~& S" r, J与for-of,forEach缺点是不懂async函数和await.如果您使用 async函数作为回调,forEach则在继续之前等待函数的承诺解决。这是替代的async示例- 请注意初始延迟是如何发生的,但所有文本都会立即出现,而不是等待:for-of``forEach
" z% S# D6 c7 z9 f  ~! p; 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 定义了其他几个有用的遍历数组并执行操作函数,包括:
4 s1 ~. [3 U" o$ G/ u- L& n' T; oevery( spec | MDN ) - 第一次回调假值时停止循环
+ ?- P* f; |6 J% P9 O: l9 P& Gsome( spec | MDN ) - 回调第一次回到真实值时停止循环
+ V& G6 Y8 Y  X% ^5 wfilter( spec | MDN ) - 创建一个新的数组,包括回调返回真实值的元素,省略不返回真实值的元素* Z# R3 U4 k* N8 v( ~
map( spec | MDN ) - 从回调返回的值创建一个新数组
1 {5 ~+ J" l$ ~; N" ]reduce( spec | MDN ) - 通过重复调用回调建立一个值,并将其传输到以前的值;详情请参考规范% _* S6 P+ s* h$ h% T
reduceRight( spec | MDN ) - 类似reduce,但是按照降序而不是升序
和 一样forEach,若使用一个async作为您的回调函数,函数不会等待函数的承诺完成。这意味着:
7 e% C" {& ?5 _8 ?( S# q# C使用一个async回调函数从未通过适当的回调函数every,some以及filter因为他们将承诺返回,就像一个truthy值; 他们不会等待承诺解决,然后使用履行价值。
0 Q  k: A* o  t2 _使用async函数回调通常适用于map,如果目标是将某个数组转换为promise可能是数组传递给 promise 组合器函数(Promise.all、Promise.race、promise.allSettled、 或Promise.any)。# D5 n1 m* N+ Q# k/ w% s
使用async函数回调很少适合reduceor reduceRight,因为(再次)回调将永远回到承诺中。但有一种习惯用法是从使用中使用。reduce( const promise = array.reduce((p,element) => p.then(/*...something usingelement...*/);)数组构建承诺链,但通常在这些情况下,函数中for-oforfor循环async调试更清晰、更容易。
3. 使用简单for循环有时旧的方法是最好的:2 F! g2 M6 N1 c4 S/ r% c
    ( q" G" H2 D7 \) X
  • const a = ["a","b","c"];for (let index = 0; index 如果数组的长度在循环过程中不会改变,那么它可能是一个稍微复杂的版本,在高性能敏感的代码中抓住长度达阵很小的有点快:" A: y$ U- `+ Z" n; H# i
  • [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 n; l+ o  b( y7 T/ N
但现代 JavaScript 引擎,您很少需要勉强挤出最后一点力气。
5 n, ^/ z3 m9 `: |0 i1 D6 c在 ES在2015 之前,包含作用域必须存在循环变量var只有函数级作用域,没有块级作用域。但正如你在上面的例子中看到的,你可以let在 内使用for将变量范围限制为循环。index每次循环迭代都会重新创建变量,这意味着在循环中创建的闭包将被保留index引用特定迭代,解决了旧的循环封闭问题:$ h1 A  _" W) X2 j
// (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指数)。/ E/ D. d+ @1 k# @1 p2 K: F
就像for-of,for循环在async函数运行良好。这是使用。for早期循环示例:
% N6 k# K0 }3 ~$ FShow code snippet9 m: b# x& I- S8 [
4、正确使用for-infor-in它不是用于遍历数组,而是用于遍历对象属性的名称。作为对象的副产品,它似乎经常用于循环遍历数组,但它不仅循环遍历数组索引,还循环遍历对象所有可枚举属性(包括继承属性)。
7 I6 I- j" n1 h- X7 |for-in数组中唯一真实的用例是:. ]; a* v* b: ^. Z9 i7 f
它是一个稀疏其中有数组大量间隙,或者
- p. K2 w  v# t! V# h在数组对象中使用非元素属性,并希望将其包含在循环中
查看第一个示例:for-in若使用适当的保护措施,可使用访问稀疏数组元素:
0 |/ {8 s3 S- Q
    1 E0 U2 l& c1 F  y$ m
  • // `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 注意三项检查:" |3 [* M0 ]6 K+ b( w/ f. _7 Q
  • [ol]
    2 I( e3 h5 D( k% w0 g' c  h2 v
  • 对象有这个名字自己的属性(不是它的原型继承的属性;a.hasOwnProperty(name)但 ES2022 添加了Object.hasOwn更可靠),0 U; Y9 G8 K5 D: S% v+ ^# Y
  • 名称为十进制数字(如正常字符串形式,而不是科学记数法),以及+ A! y9 Z; I  z* [$ R( ]. n
  • 数字时名称的强制值为 [/ol]…尽管如此,大多数代码只进行hasOwnProperty检查。/ a9 U4 W4 h; j: d+ k) T/ K
  • 当然,你不会在内联代码中这样做。您将编写实用程序函数。
    2 L  m- a  w2 h) Q! r( q
  • // 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若工作需串行完成,则在异步函数中效果良好。3 x% |9 [) c: Y1 q: p+ N. f$ [
  • 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);}, [8 Z! s: _8 ]$ P$ j
迭代器是符合规范中迭代器定义的对象。每次调用它next在方法上,它会回到一个新的结果对象。结果对象有属性 ,done告诉我们它是否完成了,还有另一个属性value包含迭代值。(done如果是false,value是可选的,如果是,是可选的undefined。)
+ p8 N/ U/ S$ U: B你得到的value这取决于迭代器。在阵列中,缺少迭代器提供每个阵列元素的值("a","b",和"c"前面的例子)。数组还有三种返回迭代器的方法:4 u  R- H5 }: \) `
values():这是[Symbol.iterator]返回默认迭代器方法的别名。
6 _, D: l" p3 `( l8 A0 c3 ^keys(): 返回一个迭代器,它在数组中提供每个键(索引)。它将在上述示例中提供"0",then "1",then "2"(是的,作为字符串)。/ N  M4 |+ o* {
entries(): 返回一个提供[key,value]数组的迭代器。
因为迭代器对象在调用 之前不会前进next,因此它们在async函数循环运行良好。这是以前的for-of明确使用迭代器的示例:! x! u! I  f! m3 d1 O
function 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 等等。我们如何遍历他们的内容?
6 e+ m4 z/ x$ L9 Y; L: b使用上述大多数选项至少有一些上述数组方法,可能是大多数甚至全部,也适用于类似数组的对象:
* g* O! [9 e; q% O6 C6 Q. I[ol]使用for-of(隐式使用迭代器)(ES2015 )[/ol]for-of使用对象提供的迭代器(如有)。这包括主机提供的对象(如 DOM 集合和列表)。HTMLCollection来自getElementsByXYZ方法NodeList例子和来源querySelectorAll两者的s 实例支持迭代。(这是 HTML 和 DOM 规范非常巧妙定义。基本上,任何东西都有length和 索引访问的对象是自动可迭代的。不必被标记iterable;除了用于集合,除了可迭代,还支持forEach,values,keys,和entriesmethods. NodeListdo; HTMLCollectionnot,但两者都是可迭代的。- E; b3 o$ `. K
以下是循环遍历div元素的例子:
/ H1 }; [2 h2 u: o, e& N2 ]8 V% @0 DShow code snippet
! ?7 u1 p5 Q' Y& Q/ h, d* y" S( f[ol]使用forEach及相关(ES5 )[/ol]on 各种函数Array.prototype可通过有意通用Function#call( spec | MDN ) 或Function#apply( spec | MDN )用于类数组对象。(如果你必须处理 IE8 或更早版本 [哎哟],请参阅本答案末尾的主机提供对象的警告,但这对模糊的现代浏览器来说不是问题。
. ~& \! Z- \. u3 _3 W假设您想forEach在 aNode的childNodes集合使用(作为 ,本机HTMLCollection没有forEach)。你会这样做:* ]- C8 a0 u( F) c( E
js   Array.prototype.forEach.call(node.childNodes,(child) => {       // Do something with `child`  };
6 s5 Z8 H; c% `(但请注意,您只能使用它for-ofon node.childNodes。)# Q! |+ i! t& a  U8 j- h$ O
如果您计划经常这样做,您可能希望将函数引用的副本抓取到变量中进行重用,例如:+ ^5 g$ f3 t1 `" C' O- L, A
```js- H* `# _) U# E* F' _
            // (This is all presumably in a module or some scoping function)
% P% Q0 _, _2 J, W$ M4 r            const forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);
/ Q/ M, x3 u2 h8 d& P// Then later…
9 x# O% s  W6 T  w0 v" u) h5 a            forEach(node.childNodes,(child) => {- F$ k) N1 @; h
                            Do something with child3 y. K5 ~2 [5 |
            };+ U# O% G- a" C
            ```
9 B; |1 D; X5 C[ol]使用简单的for循环[/ol]也许很明显,简单for循环适用于类似数组的对象。: t. z2 O% U1 O& t. F! \
[ol]迭代器用于显式(ES2015 )[/ol]见#1。# ^$ s$ |/ Y' }
可能能够逃脱for-in(使用安全措施),但没有理由尝试所有这些更合适的选项。
/ X* {2 H; }& Z' z+ O创建一个真正的数组在其他情况下,您可能希望将类似数组的对象转换为真实数组。这样做很简单:& P* G3 A$ F' s5 x
[ol]利用 Array.from[/ol]Array.from (规格) | (MDN)(ES2015 ,但容易填充)从类似数组的对象中创建数组,可以选择先通过映射函数传输条目。& S) R* q+ {* A+ G& e, ~2 V
js   const divs = Array.from(document.querySelectorAll("div"));
' v# ^/ o+ y3 u7 M- W' […获取NodeListfromquerySelectorAll并创建数组。
/ l$ V* m( y% Q5 _如果你打算以某种方式映射内容,映射功能将非常方便。例如,如果您想获得具有给定类别元素的标签名称数组:. U6 r$ A# O5 ]" C4 m1 c5 d
```js
: b7 C% Z4 b( g6 H* x* z            // Typical use (with an arrow function):9 g* U2 J" f1 L) `* |# H- g
            const divs = Array.from(document.querySelectorAll(“.some-class”),element => element.tagName);, R2 s$ x. U3 h0 q4 O. Z) ?
// Traditional function (since Array.from can be polyfilled):
/ G; T. e( \& C; C            var divs = Array.from(document.querySelectorAll(“.some-class”),function(element) {
$ k2 {% v9 F9 H5 C                            return element.tagName;
, P/ |/ d" g' t3 u            };4 @8 E0 A+ ~0 \8 y. W0 J  w) s. D
            ```4 q2 D7 q7 S9 {; K
[ol]使用扩展语法 ( ...)[/ol]也可使用 ES2015 扩展语法。for-of,使用对象提供的迭代器(见上节 #1):
: ?. X# a* h5 h9 ^. fjs   const trueArray = [...iterableObject];, R. M- X4 Q, }' ]! |  u" _* z) P
例如,如果我们想要 a 转换NodeList对于一个真正的数组来说,使用扩展语法将变得非常简单:0 g- j6 E  K9 j4 X/ N, p
js   const divs = [...document.querySelectorAll("div")];* {3 u& c; X' O
[ol]使用slice数组的方法[/ol]我们可以用slice和上面提到的其他方法一样,数组的方法是有意通用的,所以可以和类似数组的对象一起使用,如下所示:
: M" L) u5 T  J8 F1 njs   const trueArray = Array.prototype.slice.call(arrayLikeObject);4 a: U" W" `& o, u
例如,如果我们想要 aNodeList我们可以把它转换成真正的数组:" z, K8 x! _; T/ z$ a
js   const divs = Array.prototype.slice.call(document.querySelectorAll("div"));7 z" P9 n) f& c
(如果你还必须处理 IE8 [哎哟],会失败;IE8 不允许你像this使用主机提供的对象。& R& M+ B. O) x$ Y
主机提供的对象的警告如果您使用主机提供类似数组的对象Array.prototype函数(例如,DOM 由浏览器组成,而不是 JavaScript 发动机提供),如 IE8 过时的浏览器可能无法处理,所以如果你必须支持它们,请确保在你的目标环境中进行测试。但这对模糊的现代浏览器来说并不是一个问题。(对于非浏览器环境,它自然会因环境而异。
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则