跳到正文
DEV Community· JKC·· 4 小時前AI 評分60

JavaScript Iterator.concat() 惰性串接可迭代物件,避免先展開成陣列

Stop Spreading Iterables Into Arrays to Join Them. Iterator.concat() Is Here

AI 導讀

JavaScript 的 Iterator.concat() 可依序串接多個可迭代物件,並回傳惰性迭代器,避免先把所有項目複製到臨時陣列。文中例子以兩個各產生 1000 列的生成器為資料來源,只取首 3 列時,展開寫法會產生 2000 列,Iterator.concat().take(3) 則只產生 3 列。

正文

TL;DR: Iterator.concat(a, b, c) walks any iterables one after another and returns a lazy iterator. No temporary array, no hand-written generator, and it has been Baseline Newly available since March 2026.

The array you build just to throw away

You have an array, a Set, and a generator. You want to loop over all of them in order. So you write the line everyone writes:

for (const item of [...pinned, ...recentSet, ...loadMore()]) {
  render(item);
}

It works. It also copies every item into a brand new array, runs the generator to the very end, and then hands the array to the garbage collector a moment later. If you only needed the first few items, you paid for all of them.

The careful version is a generator with yield*:

function* concat(...iterables) {
  for (const iterable of iterables) {
    yield* iterable;
  }
}

Correct, lazy, and copy-pasted into a utils folder near you.

What it is

Iterator.concat() is that generator, built into the language. It takes any number of iterables and returns an iterator that yields from each one in turn:

const array = [1, 2, 3];
const set = new Set([4, 5]);
function* gen() {
  yield 6;
  yield 7;
}

console.log([...Iterator.concat(array, set, gen())]);
// [1, 2, 3, 4, 5, 6, 7]

The result is a real iterator, so the iterator helpers work on it directly:

const evensTimesTen = Iterator.concat([1, 2, 3], new Set([4, 5, 6]))
  .filter((n) => n % 2 === 0)
  .map((n) => n * 10)
  .toArray();

console.log(evensTimesTen); // [20, 40, 60]

Maps are iterables of [key, value] pairs, which makes "defaults, then overrides" a one-liner:

const defaults = new Map([["theme", "light"], ["lang", "en"]]);
const user = new Map([["theme", "dark"]]);

const settings = new Map(Iterator.concat(defaults, user));
console.log(Object.fromEntries(settings)); // { theme: "dark", lang: "en" }

Later keys win, because that is how the Map constructor already behaves.

The realistic one: take only what you need

Here is where laziness pays. Two sources of 1000 rows each, and a UI that shows the first three:

let pulled = 0;

function* page(start) {
  for (let i = start; i < start + 1000; i++) {
    pulled++;
    yield i;
  }
}

const spread = [...page(0), ...page(1000)].slice(0, 3);
console.log(spread, pulled); // [0, 1, 2] 2000

pulled = 0;

const lazy = Iterator.concat(page(0), page(1000)).take(3).toArray();
console.log(lazy, pulled); // [0, 1, 2] 3

Same answer. The spread version produced 2000 rows to keep 3. The concat version produced 3. Replace "row" with "parsed log line" or "decoded image" and that gap stops being academic.

It is also lazy about opening each source. Logging every [Symbol.iterator]() call on two iterables gave this order:

created
open a
next 1
next 2
open b
next 3

Nothing is opened when you call concat, and b is not touched until a runs out. That also means infinite sources are fine, as long as you stop early:

function* naturals() {
  let i = 1;
  while (true) yield i++;
}

console.log(Iterator.concat(["start"], naturals()).take(5).toArray());
// ["start", 1, 2, 3, 4]

Can I use it?

Where Status
Baseline Newly available since March 2026
Chrome and Edge 146
Firefox 147
Safari (macOS and iOS) 26.4
Node.js 24 Not yet: typeof Iterator.concat is "undefined" in v24.11.0
Spec TC39 Iterator Sequencing proposal, Stage 4

Everything in this post ran in Chrome 156. If you need it in Node today, keep the five-line generator from above as a fallback.

Gotchas

  1. Strings are rejected, even though they are iterable. Iterator.concat([1], "ab") throws TypeError: Iterator.concat called on non-object right away, at the call, not later during iteration. null throws the same error. Wrap a string first: Iterator.from("bc") or [..."bc"] both worked.

  2. Bare iterators need Iterator.from(). An object with only a next() method is not iterable, and passing one threw TypeError: #<Object> is not iterable. Built-in iterators like [1, 2, 3].values() are fine, because they are iterable too.

  3. The result is single use. Spreading the same concat result twice gave [1, 2, 3] the first time and [] the second. It is an iterator, not an array, so call Iterator.concat again if you need a second pass.

  4. Breaking early closes what it opened. With two generators that log in finally, a for...of that broke on the first item of the second one logged closed a when a ran out and closed b on the break. Your cleanup code runs, which is exactly what you want.

Wrap-up

Iterator.concat() is the yield* helper you kept rewriting, now with iterator helpers attached. Next time you type [...a, ...b] only to loop over it once, try Iterator.concat(a, b) instead.

Sources

This article was written with the help of AI and checked against the sources above.

來源:DEV Community · dev.to