JavaScript — Iterators & Iterables
The iteration protocol is one of the most fundamental contracts in JavaScript. It defines how objects produce sequences of values and how consumers can traverse those sequences uniformly. Understanding iteration unlocks the full power of for...of, spread syntax, destructuring, and many built-in APIs.
The protocol consists of two core concepts: iterables (objects that can produce a sequence of values) and iterators (objects that manage the traversal). An iterable has a [Symbol.iterator]() method that returns an iterator. The iterator has a next() method that returns { value, done } objects.
This protocol is the backbone of JavaScript's iteration ecosystem. Arrays, Strings, Maps, Sets, TypedArrays, NodeLists, and even the arguments object are iterable. Generators produce iterators. Custom objects can opt into iteration by implementing the protocol. Once something is iterable, it works with all language features designed for iteration.
| 1 | // The iteration protocol at a glance |
| 2 | const array = [10, 20, 30]; |
| 3 | |
| 4 | // Array is iterable — it has Symbol.iterator |
| 5 | const iterator = array[Symbol.iterator](); |
| 6 | |
| 7 | console.log(iterator.next()); // { value: 10, done: false } |
| 8 | console.log(iterator.next()); // { value: 20, done: false } |
| 9 | console.log(iterator.next()); // { value: 30, done: false } |
| 10 | console.log(iterator.next()); // { value: undefined, done: true } |
| 11 | |
| 12 | // All of these work because arrays are iterable: |
| 13 | for (const val of array) { /* ... */ } |
| 14 | const copy = [...array]; |
| 15 | const [first, second] = array; |
| 16 | const fromArray = Array.from(array); |
The iteration protocol is actually two protocols working together: the iterable protocol and the iterator protocol. Any object that implements [Symbol.iterator]() is iterable. The method must return an iterator — an object with a next() method that returns { value: any, done: boolean }.
| Protocol | Interface | Returns | Description |
|---|---|---|---|
| Iterable | [Symbol.iterator]() | Iterator | Factory method that returns a new iterator each time |
| Iterator | next() | { value, done } | Returns the next value or signals completion |
| Iterator (opt) | return() | { value, done } | Called when iteration is terminated early (break, throw) |
| Iterator (opt) | throw() | { value, done } | Called to signal an error to the iterator |
| 1 | // The protocol explained step by step |
| 2 | // 1. An iterable is any object with [Symbol.iterator] |
| 3 | const iterable = { |
| 4 | [Symbol.iterator]: function() { |
| 5 | // 2. Returns an iterator |
| 6 | let count = 0; |
| 7 | return { |
| 8 | next: function() { |
| 9 | count++; |
| 10 | if (count <= 3) { |
| 11 | return { value: count * 10, done: false }; |
| 12 | } |
| 13 | return { value: undefined, done: true }; |
| 14 | }, |
| 15 | // Optional: called on early termination |
| 16 | return: function() { |
| 17 | console.log("Iterator cleaned up"); |
| 18 | return { value: undefined, done: true }; |
| 19 | } |
| 20 | }; |
| 21 | } |
| 22 | }; |
| 23 | |
| 24 | for (const val of iterable) { |
| 25 | console.log(val); // 10, 20, 30 |
| 26 | } |
| 27 | |
| 28 | // Early break calls return() |
| 29 | for (const val of iterable) { |
| 30 | if (val === 20) break; // triggers return() |
| 31 | } |
info
Any object can become iterable by implementing [Symbol.iterator](). This is one of the most practical patterns in JavaScript — it allows custom data structures to participate in the standard iteration ecosystem (for...of, spread, destructuring, Array.from).
| 1 | // Making a simple collection iterable |
| 2 | class Team { |
| 3 | constructor(name) { |
| 4 | this.name = name; |
| 5 | this.members = []; |
| 6 | } |
| 7 | |
| 8 | addMember(name) { |
| 9 | this.members.push(name); |
| 10 | return this; |
| 11 | } |
| 12 | |
| 13 | // Make it iterable |
| 14 | [Symbol.iterator]() { |
| 15 | let index = 0; |
| 16 | const members = this.members; |
| 17 | return { |
| 18 | next() { |
| 19 | if (index < members.length) { |
| 20 | return { value: members[index++], done: false }; |
| 21 | } |
| 22 | return { value: undefined, done: true }; |
| 23 | } |
| 24 | }; |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | const team = new Team("Devs"); |
| 29 | team.addMember("Alice").addMember("Bob").addMember("Charlie"); |
| 30 | |
| 31 | // Now it works with all iteration features |
| 32 | for (const member of team) { |
| 33 | console.log(member); // "Alice", "Bob", "Charlie" |
| 34 | } |
| 35 | |
| 36 | console.log([...team]); // ["Alice", "Bob", "Charlie"] |
| 37 | const [lead, ...rest] = team; |
| 38 | console.log(lead); // "Alice" |
| 39 | console.log(rest); // ["Bob", "Charlie"] |
| 40 | |
| 41 | // You can also make plain objects iterable |
| 42 | const range = { |
| 43 | from: 1, |
| 44 | to: 5, |
| 45 | [Symbol.iterator]() { |
| 46 | let current = this.from; |
| 47 | const end = this.to; |
| 48 | return { |
| 49 | next() { |
| 50 | if (current <= end) { |
| 51 | return { value: current++, done: false }; |
| 52 | } |
| 53 | return { value: undefined, done: true }; |
| 54 | } |
| 55 | }; |
| 56 | } |
| 57 | }; |
| 58 | |
| 59 | console.log([...range]); // [1, 2, 3, 4, 5] |
| 1 | // Iterable with generator (cleaner syntax) |
| 2 | class PaginatedResult { |
| 3 | constructor(data, pageSize = 10) { |
| 4 | this.data = data; |
| 5 | this.pageSize = pageSize; |
| 6 | } |
| 7 | |
| 8 | *[Symbol.iterator]() { |
| 9 | for (let i = 0; i < this.data.length; i += this.pageSize) { |
| 10 | yield this.data.slice(i, i + this.pageSize); |
| 11 | } |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | const allItems = Array.from({ length: 25 }, (_, i) => `item-${i}`); |
| 16 | const paginated = new PaginatedResult(allItems, 10); |
| 17 | |
| 18 | for (const page of paginated) { |
| 19 | console.log("Page:", page); |
| 20 | // ["item-0", ..., "item-9"] |
| 21 | // ["item-10", ..., "item-19"] |
| 22 | // ["item-20", ..., "item-24"] |
| 23 | } |
| 24 | |
| 25 | // Practical: iterable matrix |
| 26 | class Matrix { |
| 27 | constructor(rows, cols, fill = 0) { |
| 28 | this.data = Array.from({ length: rows }, () => |
| 29 | Array.from({ length: cols }, () => fill) |
| 30 | ); |
| 31 | } |
| 32 | |
| 33 | *[Symbol.iterator]() { |
| 34 | for (const row of this.data) { |
| 35 | yield* row; |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | *rows() { |
| 40 | yield* this.data; |
| 41 | } |
| 42 | |
| 43 | *columns() { |
| 44 | for (let col = 0; col < this.data[0].length; col++) { |
| 45 | yield this.data.map(row => row[col]); |
| 46 | } |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | const m = new Matrix(3, 3); |
| 51 | console.log([...m]); // [0, 0, 0, 0, 0, 0, 0, 0, 0] |
| 52 | console.log([...m.rows()]); // [[0,0,0], [0,0,0], [0,0,0]] |
| 53 | console.log([...m.columns()]); // [[0,0,0], [0,0,0], [0,0,0]] |
pro tip
JavaScript provides many built-in iterables. Each collection type implements the iterable protocol, and they can be used interchangeably with any language feature that consumes iterables.
| Type | Iterates | Example | Notes |
|---|---|---|---|
| Array | Elements in index order | [1, 2, 3] | Most common iterable |
| String | Characters (code points) | "abc" | Iterates by Unicode code points, not UTF-16 code units |
| Map | [key, value] entries | new Map() | Insertion order |
| Set | Values in insertion order | new Set() | Each value appears once |
| TypedArray | Numeric elements | Uint8Array | Binary data buffers |
| NodeList | DOM nodes | document.querySelectorAll() | Iterable in modern browsers |
| arguments | Function arguments | arguments | Legacy, avoid in favor of rest params |
| Generator | Yielded values | function*() | Both iterable and iterator |
| 1 | // Array — iterate elements |
| 2 | for (const el of [10, 20, 30]) console.log(el); |
| 3 | |
| 4 | // String — iterate Unicode code points |
| 5 | for (const char of "hello") console.log(char); |
| 6 | for (const emoji of "👍🎉") console.log(emoji); // Works correctly |
| 7 | // String iterator handles surrogate pairs correctly |
| 8 | console.log([..."👍🎉"]); // ["👍", "🎉"] |
| 9 | |
| 10 | // Map — iterate [key, value] pairs |
| 11 | const map = new Map([["a", 1], ["b", 2]]); |
| 12 | for (const [key, value] of map) { |
| 13 | console.log(key, value); // "a" 1, "b" 2 |
| 14 | } |
| 15 | console.log([...map.keys()]); // ["a", "b"] |
| 16 | console.log([...map.values()]); // [1, 2] |
| 17 | |
| 18 | // Set — iterate unique values |
| 19 | const set = new Set([1, 1, 2, 3, 3]); |
| 20 | for (const val of set) console.log(val); // 1, 2, 3 |
| 21 | console.log([...set]); // [1, 2, 3] |
| 22 | |
| 23 | // NodeList — iterate DOM nodes |
| 24 | document.querySelectorAll("div"); // iterate with for...of |
| 25 | |
| 26 | // TypedArray |
| 27 | const bytes = new Uint8Array([0x48, 0x65, 0x6C, 0x6C, 0x6F]); |
| 28 | console.log([...bytes].map(c => String.fromCharCode(c)).join("")); // "Hello" |
info
The for...of loop is syntactic sugar over the iteration protocol. Understanding what it does internally helps you debug iteration issues and appreciate what the protocol provides for free.
| 1 | // for...of is syntactic sugar for this pattern: |
| 2 | const iterable = [10, 20, 30]; |
| 3 | |
| 4 | // What for...of does internally: |
| 5 | const iterator = iterable[Symbol.iterator](); |
| 6 | let result = iterator.next(); |
| 7 | |
| 8 | while (!result.done) { |
| 9 | const value = result.value; |
| 10 | console.log(value); // 10, 20, 30 |
| 11 | |
| 12 | // If break is encountered, iterator.return() is called |
| 13 | // if (condition) { iterator.return?.(); break; } |
| 14 | |
| 15 | result = iterator.next(); |
| 16 | } |
| 17 | // Iterator is exhausted — all values consumed |
| 18 | |
| 19 | // With early termination protection |
| 20 | function iterateSafely(iterable) { |
| 21 | const iterator = iterable[Symbol.iterator](); |
| 22 | try { |
| 23 | let result = iterator.next(); |
| 24 | while (!result.done) { |
| 25 | // Process result.value |
| 26 | console.log(result.value); |
| 27 | result = iterator.next(); |
| 28 | } |
| 29 | } finally { |
| 30 | // Ensure cleanup on early exit or error |
| 31 | if (iterator.return) iterator.return(); |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | // This is why for...of calls return() when you break: |
| 36 | const withCleanup = { |
| 37 | [Symbol.iterator]() { |
| 38 | let i = 0; |
| 39 | return { |
| 40 | next() { return i < 3 ? { value: i++, done: false } : { done: true }; }, |
| 41 | return() { console.log("Cleanup!"); return { done: true }; } |
| 42 | }; |
| 43 | } |
| 44 | }; |
| 45 | |
| 46 | for (const v of withCleanup) { |
| 47 | if (v === 1) break; // Logs "Cleanup!" |
| 48 | } |
best practice
The spread operator (...) works with any iterable. It calls [Symbol.iterator]() and consumes all values into the target context. This is why spread works not just on arrays, but on strings, sets, maps, and any custom iterable.
| 1 | // Spread works with any iterable |
| 2 | const arr1 = [1, 2, 3]; |
| 3 | const arr2 = [4, 5, 6]; |
| 4 | const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6] |
| 5 | |
| 6 | // Strings are iterable — spread into characters |
| 7 | const chars = [..."hello"]; // ["h", "e", "l", "l", "o"] |
| 8 | const emojis = [..."🎉🚀💡"]; // ["🎉", "🚀", "💡"] |
| 9 | |
| 10 | // Sets are iterable — spread into array (deduplicate) |
| 11 | const unique = [...new Set([1, 2, 2, 3, 3, 4])]; // [1, 2, 3, 4] |
| 12 | |
| 13 | // Maps spread into [key, value] pairs |
| 14 | const map = new Map([["a", 1], ["b", 2]]); |
| 15 | const entries = [...map]; // [["a", 1], ["b", 2]] |
| 16 | const keys = [...map.keys()]; // ["a", "b"] |
| 17 | |
| 18 | // Works with custom iterables |
| 19 | const range = { |
| 20 | from: 1, to: 5, |
| 21 | [Symbol.iterator]() { |
| 22 | let i = this.from; |
| 23 | return { next: () => i <= this.to ? { value: i++, done: false } : { done: true } }; |
| 24 | } |
| 25 | }; |
| 26 | console.log([...range]); // [1, 2, 3, 4, 5] |
| 27 | |
| 28 | // Spread in function calls |
| 29 | function sum(...numbers) { |
| 30 | return numbers.reduce((a, b) => a + b, 0); |
| 31 | } |
| 32 | const nums = [1, 2, 3, 4, 5]; |
| 33 | console.log(sum(...nums)); // 15 |
| 34 | |
| 35 | // Spread in object literals (own properties, NOT iteration) |
| 36 | const obj1 = { x: 1, y: 2 }; |
| 37 | const obj2 = { ...obj1, z: 3 }; // { x: 1, y: 2, z: 3 } |
| 38 | // Note: object spread uses own property enumeration, NOT iteration |
info
Array.from() converts any iterable (or array-like object) into a real array. It is safer and more explicit than spread for some cases, especially when dealing with array-like objects that are not iterable (like the old arguments object in non-strict mode).
| 1 | // Array.from with iterables |
| 2 | const set = new Set([1, 2, 3, 3, 4]); |
| 3 | const arr1 = Array.from(set); // [1, 2, 3, 4] |
| 4 | const arr2 = [...set]; // [1, 2, 3, 4] (same result) |
| 5 | |
| 6 | // Array.from accepts a map function (second argument) |
| 7 | const doubled = Array.from(set, x => x * 2); // [2, 4, 6, 8] |
| 8 | |
| 9 | // Create arrays from range |
| 10 | const range = Array.from({ length: 5 }, (_, i) => i + 1); // [1, 2, 3, 4, 5] |
| 11 | |
| 12 | // From string |
| 13 | const chars = Array.from("hello"); // ["h", "e", "l", "l", "o"] |
| 14 | |
| 15 | // From NodeList (safer than spread in older environments) |
| 16 | // const divs = Array.from(document.querySelectorAll("div")); |
| 17 | |
| 18 | // Array.from with array-like objects (not iterable) |
| 19 | const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 }; |
| 20 | const realArray = Array.from(arrayLike); // ["a", "b", "c"] |
| 21 | // Spread would fail here — array-likes are not iterable |
| 22 | |
| 23 | // Practical: generate sequences |
| 24 | function range(start, end, step = 1) { |
| 25 | const length = Math.ceil((end - start) / step); |
| 26 | return Array.from({ length }, (_, i) => start + i * step); |
| 27 | } |
| 28 | console.log(range(0, 10, 2)); // [0, 2, 4, 6, 8] |
| 29 | |
| 30 | // Practical: grid initialization |
| 31 | const grid = Array.from({ length: 3 }, () => |
| 32 | Array.from({ length: 3 }, () => 0) |
| 33 | ); |
| 34 | // [[0,0,0], [0,0,0], [0,0,0]] |
| 35 | |
| 36 | // Array.from vs spread: |
| 37 | // - Array.from accepts a mapFn — one fewer iteration |
| 38 | // - Array.from handles array-likes (non-iterable) |
| 39 | // - Spread is more concise for simple cases |
| 40 | // - Array.from is slightly faster for large iterables |
best practice
Beyond making objects iterable, you can write standalone iterators that encapsulate complex traversal logic. Common patterns include infinite iterators, transforming iterators (map, filter), and iterators that manage external resources.
| 1 | // Standalone iterator factory |
| 2 | function createIterator(data) { |
| 3 | let index = 0; |
| 4 | return { |
| 5 | next() { |
| 6 | if (index < data.length) { |
| 7 | return { value: data[index++], done: false }; |
| 8 | } |
| 9 | return { value: undefined, done: true }; |
| 10 | }, |
| 11 | // Make it iterable too |
| 12 | [Symbol.iterator]() { return this; } |
| 13 | }; |
| 14 | } |
| 15 | |
| 16 | const it = createIterator(["a", "b", "c"]); |
| 17 | console.log(it.next().value); // "a" |
| 18 | console.log(it.next().value); // "b" |
| 19 | console.log(it.next().value); // "c" |
| 20 | console.log(it.next().done); // true |
| 21 | |
| 22 | // Transforming iterator: map |
| 23 | function mapIterator(iterator, fn) { |
| 24 | return { |
| 25 | next() { |
| 26 | const { value, done } = iterator.next(); |
| 27 | return done ? { done } : { value: fn(value), done: false }; |
| 28 | }, |
| 29 | [Symbol.iterator]() { return this; } |
| 30 | }; |
| 31 | } |
| 32 | |
| 33 | const base = createIterator([1, 2, 3]); |
| 34 | const doubled = mapIterator(base, x => x * 2); |
| 35 | console.log([...doubled]); // [2, 4, 6] |
| 36 | |
| 37 | // Zip two iterators |
| 38 | function zipIterator(...iterables) { |
| 39 | const iterators = iterables.map(i => i[Symbol.iterator]()); |
| 40 | return { |
| 41 | next() { |
| 42 | const values = []; |
| 43 | for (const it of iterators) { |
| 44 | const result = it.next(); |
| 45 | if (result.done) return { value: undefined, done: true }; |
| 46 | values.push(result.value); |
| 47 | } |
| 48 | return { value: values, done: false }; |
| 49 | }, |
| 50 | [Symbol.iterator]() { return this; } |
| 51 | }; |
| 52 | } |
| 53 | |
| 54 | const names = ["Alice", "Bob", "Charlie"]; |
| 55 | const ages = [30, 25, 35]; |
| 56 | const zipped = zipIterator(names, ages); |
| 57 | console.log([...zipped]); // [["Alice", 30], ["Bob", 25], ["Charlie", 35]] |
| 1 | // Fibonacci iterator (infinite) |
| 2 | function fibonacciIterator() { |
| 3 | let a = 0, b = 1; |
| 4 | return { |
| 5 | next() { |
| 6 | const value = a; |
| 7 | [a, b] = [b, a + b]; |
| 8 | return { value, done: false }; // Never done! |
| 9 | }, |
| 10 | [Symbol.iterator]() { return this; } |
| 11 | }; |
| 12 | } |
| 13 | |
| 14 | const fib = fibonacciIterator(); |
| 15 | console.log(fib.next().value); // 0 |
| 16 | console.log(fib.next().value); // 1 |
| 17 | console.log(fib.next().value); // 1 |
| 18 | console.log(fib.next().value); // 2 |
| 19 | console.log(fib.next().value); // 3 |
| 20 | |
| 21 | // Take helper for infinite iterators |
| 22 | function take(n, iterator) { |
| 23 | let count = 0; |
| 24 | return { |
| 25 | next() { |
| 26 | if (count++ >= n) return { done: true }; |
| 27 | return iterator.next(); |
| 28 | }, |
| 29 | [Symbol.iterator]() { return this; } |
| 30 | }; |
| 31 | } |
| 32 | |
| 33 | const first10Fib = [...take(10, fibonacciIterator())]; |
| 34 | console.log(first10Fib); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] |
| 35 | |
| 36 | // Paging iterator (external resource management) |
| 37 | function pagingIterator(fetchPage, options = {}) { |
| 38 | const { maxPages = Infinity } = options; |
| 39 | let page = 0; |
| 40 | let index = 0; |
| 41 | let currentPage = []; |
| 42 | |
| 43 | return { |
| 44 | async next() { |
| 45 | if (index >= currentPage.length) { |
| 46 | if (page >= maxPages) return { done: true }; |
| 47 | currentPage = await fetchPage(++page); |
| 48 | index = 0; |
| 49 | } |
| 50 | return { value: currentPage[index++], done: false }; |
| 51 | } |
| 52 | }; |
| 53 | } |
pro tip
Perhaps the most important benefit of the iterator protocol is lazy evaluation. Values are produced on demand, not upfront. This means you can work with sequences that are infinite, or with large datasets that would not fit in memory if materialized eagerly. The iteration protocol enables this at the language level.
| 1 | // Lazy vs eager: memory comparison |
| 2 | |
| 3 | // Eager — materializes entire result |
| 4 | function eagerMap(arr, fn) { |
| 5 | return arr.map(fn); // Entire new array created |
| 6 | } |
| 7 | function eagerFilter(arr, pred) { |
| 8 | return arr.filter(pred); // Entire new array created |
| 9 | } |
| 10 | |
| 11 | // Chaining creates multiple intermediate arrays |
| 12 | const bigData = Array.from({ length: 1000000 }, (_, i) => i); |
| 13 | const result = eagerFilter(eagerMap(bigData, x => x * 2), x => x > 10); |
| 14 | // 3 arrays: original, mapped, filtered — millions of items |
| 15 | |
| 16 | // Lazy — compose iterators without materializing |
| 17 | function lazyMap(iterable, fn) { |
| 18 | const iter = iterable[Symbol.iterator](); |
| 19 | return { |
| 20 | next() { |
| 21 | const { value, done } = iter.next(); |
| 22 | return done ? { done } : { value: fn(value), done: false }; |
| 23 | }, |
| 24 | [Symbol.iterator]() { return this; } |
| 25 | }; |
| 26 | } |
| 27 | |
| 28 | function lazyFilter(iterable, pred) { |
| 29 | const iter = iterable[Symbol.iterator](); |
| 30 | return { |
| 31 | next() { |
| 32 | let result = iter.next(); |
| 33 | while (!result.done) { |
| 34 | if (pred(result.value)) return { value: result.value, done: false }; |
| 35 | result = iter.next(); |
| 36 | } |
| 37 | return { done: true }; |
| 38 | }, |
| 39 | [Symbol.iterator]() { return this; } |
| 40 | }; |
| 41 | } |
| 42 | |
| 43 | // No intermediate arrays — values flow through the pipeline |
| 44 | const pipeline = lazyFilter(lazyMap(bigData, x => x * 2), x => x > 10); |
| 45 | // Process just the first 5 values |
| 46 | const first5 = [...take(5, pipeline)]; |
| 47 | console.log(first5); // Only 5 values computed, not 1 million |
| 48 | |
| 49 | // This is the same principle as LINQ, Java streams, and functional pipelines |
info
| 1 | // Quick reference: iteration patterns |
| 2 | |
| 3 | // 1. Make any object iterable with generator |
| 4 | class MyCollection { |
| 5 | *[Symbol.iterator]() { |
| 6 | yield* this.items; |
| 7 | } |
| 8 | } |
| 9 | |
| 10 | // 2. Safe infinite iterator consumption |
| 11 | function* naturalNumbers() { |
| 12 | let n = 1; |
| 13 | while (true) yield n++; |
| 14 | } |
| 15 | function* take(n, iterable) { |
| 16 | let count = 0; |
| 17 | for (const val of iterable) { |
| 18 | if (count++ >= n) return; |
| 19 | yield val; |
| 20 | } |
| 21 | } |
| 22 | const first100 = [...take(100, naturalNumbers())]; |
| 23 | |
| 24 | // 3. Lazy pipeline composition |
| 25 | const pipeline = (iterable) => |
| 26 | take(5, map(x => x * 2, filter(x => x > 10, iterable))); |
| 27 | |
| 28 | // 4. Cleanup with return() |
| 29 | function fileLinesIterator(filePath) { |
| 30 | let fileHandle; |
| 31 | return { |
| 32 | next() { /* read next line */ }, |
| 33 | return() { fileHandle?.close(); return { done: true }; }, |
| 34 | [Symbol.iterator]() { return this; } |
| 35 | }; |
| 36 | } |
| 37 | |
| 38 | // 5. Check if something is iterable |
| 39 | function isIterable(obj) { |
| 40 | return obj != null && typeof obj[Symbol.iterator] === "function"; |
| 41 | } |
warning