Chương 05 · Arrays, Iterables & Iteration Protocols

Mảng, iterable và giao thức lặp

90% công việc thực tế là biến đổi danh sách: lọc user, tính tổng, gom nhóm, đếm tần suất. JavaScript có bộ tool rất mạnh — map/filter/reduce, spread/rest/destructuring, Set/Map, iterator/generator. Chương này biến những thứ đó thành phản xạ.

Độ dài: ~1000 dòng Bài tập: 5 Quiz: 8 Prerequisites: Chương 1-4
🎯 Mục tiêu chương
  • Tạo array đúng cách, hiểu vấn đề sparse array với new Array(n).
  • Phân biệt rạch ròi method mutate vs non-mutate; biết các bản ES2023 (toSorted, toReversed).
  • Master bộ ba map / filter / reduce. Đặc biệt là reduce — pattern gom nhóm, đếm, build object.
  • Biết khi nào chọn forEach, for/of, for — và vì sao for/of nên là default.
  • Hiểu spread/rest cùng cú pháp ... nhưng khác ngữ cảnh.
  • Destructuring nâng cao: nested, rename, default, rest.
  • Iteration protocol: tự build iterable, dùng function* để tạo sequence lười.
  • Phân biệt Set/Map/WeakSet/WeakMap — chọn đúng cho từng tình huống.

1. Tạo array — và cái bẫy sparse

JS có nhiều cách tạo array, nhưng không phải cái nào cũng tương đương.

Các cách tạo array
// 1. Literal — cách dùng 99% trường hợp
const a = [1, 2, 3];

// 2. new Array(n) — tạo array length n nhưng RỖNG (sparse) 🔥
const b = new Array(3);
console.log(b);          // [ <3 empty items> ]
console.log(b.length);   // 3
console.log(b[0]);     // undefined — nhưng KHÔNG phải undefined thật

// 3. Array.of — fix bug new Array(n)
Array.of(3);          // [3]   — luôn coi arg như phần tử
Array.of(1, 2, 3);    // [1, 2, 3]

// 4. Array.from — convert iterable hoặc array-like sang array thật
Array.from('abc');                   // ['a', 'b', 'c']
Array.from({ length: 3 });            // [undefined, undefined, undefined]
Array.from({ length: 3 }, (_, i) => i);  // [0, 1, 2]

// 5. Spread cho iterable → array
[...'abc'];                // ['a', 'b', 'c']
[...new Set([1, 2, 2])];   // [1, 2]  ← dedupe trick
🔥 Gotcha — Sparse array khác array chứa undefined

new Array(3) tạo sparse array: slot rỗng (empty), khác với [undefined, undefined, undefined]. Sparse slot bị nhiều method bỏ qua:

const sparse = new Array(3);
const dense  = [undefined, undefined, undefined];

sparse.map((x) => 1);   // [ <3 empty> ]  ← KHÔNG chạy callback!
dense.map((x) => 1);    // [1, 1, 1]

sparse.forEach((x) => console.log(x));  // không in gì
dense.forEach((x) => console.log(x));   // in 3 lần undefined

Quy tắc: tránh new Array(n). Cần khởi tạo array length n thì dùng Array.from({ length: n }, (_, i) => ...).

lengthproperty writable, không phải read-only:

const arr = [1, 2, 3, 4, 5];
arr.length = 2;          // truncate
console.log(arr);       // [1, 2]

arr.length = 5;          // extend với empty slots — TẠO SPARSE!
console.log(arr);       // [1, 2, <3 empty>]

2. Mutating vs non-mutating — bảng cheat

Đây là kiến thức bắt buộc. Trộn lẫn 2 nhóm này gây bug khó nhất trong JS (mutate state vô tình).

Mutating (sửa mảng gốc)Non-mutating (trả mảng mới)
push, popconcat
shift, unshiftslice
splicemap, filter, flat, flatMap
sorttoSorted (ES2023)
reversetoReversed (ES2023)
fill, copyWithintoSpliced (ES2023)
with(i, v) (ES2023) thay arr[i] = v immutable
Mutate vs immutable
const a = [3, 1, 2];

// ❌ mutate: thay đổi a
a.sort();
console.log(a);  // [1, 2, 3]  ← a đã bị đổi!

// ✅ immutable: a giữ nguyên
const b = [3, 1, 2];
const sorted = b.toSorted();
console.log(b);       // [3, 1, 2]
console.log(sorted);  // [1, 2, 3]

// with(i, v) — replace 1 phần tử, trả mảng mới
const c = ['a', 'b', 'c'];
const d = c.with(1, 'B');   // ['a', 'B', 'c']
console.log(c);             // ['a', 'b', 'c'] — c không đổi
💡 Mẹo nhớ

ES2023 đặt tên bằng tiền tố to-: toSorted, toReversed, toSpliced. Tiền tố to = "trả về phiên bản mới". Trong React/Redux/Vuex, mọi update state nên non-mutate.

3. Bộ ba map / filter / reduce

3 method functional cốt lõi. Học thuộc signature, gotcha, và pattern.

3.1. map — biến đổi từng phần tử

map signature
// arr.map(callback(element, index, array)) → array mới CÙNG length

const nums = [1, 2, 3, 4];

nums.map((x) => x * 2);              // [2, 4, 6, 8]
nums.map((x, i) => `${i}: ${x}`);  // ['0: 1', '1: 2', ...]

// Map object → property
const users = [{ name: 'An', age: 20 }, { name: 'Bình', age: 25 }];
users.map((u) => u.name);  // ['An', 'Bình']

3.2. filter — giữ phần tử match predicate

// arr.filter(predicate(element, index, array)) → array mới (length ≤ gốc)

[1, 2, 3, 4, 5].filter((x) => x % 2 === 0);  // [2, 4]

users.filter((u) => u.age >= 21);  // [{ name: 'Bình', age: 25 }]

// Boolean shortcut — bỏ falsy
[1, 0, 'a', '', null, 5].filter(Boolean);  // [1, 'a', 5]

3.3. reduce — gộp về 1 giá trị (Swiss-army knife)

reduce signature
// arr.reduce(callback(accumulator, element, index, array), initialValue)

// Sum
[1, 2, 3, 4].reduce((acc, x) => acc + x, 0);  // 10

// Max
[3, 7, 1, 9, 4].reduce((m, x) => x > m ? x : m, -Infinity);  // 9

4 pattern kinh điển dùng reduce:

Pattern 1 — Sum theo property
const orders = [
  { id: 1, total: 120 },
  { id: 2, total: 80 },
  { id: 3, total: 200 },
];

const revenue = orders.reduce((acc, o) => acc + o.total, 0);
// 400
Pattern 2 — Group by
const people = [
  { name: 'An',   city: 'HN' },
  { name: 'Bình', city: 'HCM' },
  { name: 'Cường', city: 'HN' },
];

const byCity = people.reduce((acc, p) => {
  (acc[p.city] ??= []).push(p);
  return acc;
}, {});
// { HN: [{...An}, {...Cường}], HCM: [{...Bình}] }

// ES2024 có Object.groupBy built-in:
Object.groupBy(people, (p) => p.city);  // same result
Pattern 3 — Count / tần suất
const votes = ['A', 'B', 'A', 'C', 'A', 'B'];

const tally = votes.reduce((acc, v) => {
  acc[v] = (acc[v] ?? 0) + 1;
  return acc;
}, {});
// { A: 3, B: 2, C: 1 }
Pattern 4 — Array of pairs → object
const pairs = [['a', 1], ['b', 2], ['c', 3]];

const obj = pairs.reduce((acc, [k, v]) => {
  acc[k] = v;
  return acc;
}, {});
// { a: 1, b: 2, c: 3 }

// Built-in tương đương:
Object.fromEntries(pairs);  // { a: 1, b: 2, c: 3 }
🔥 Gotcha — reduce KHÔNG initial value

Nếu không truyền initialValue, reduce dùng phần tử đầu làm acc và bắt đầu từ index 1. Trên array rỗng: TypeError: Reduce of empty array with no initial value.

[].reduce((a, b) => a + b);     // ❌ TypeError
[].reduce((a, b) => a + b, 0);  // ✅ 0

Luôn truyền initial value — vừa an toàn, vừa rõ type của accumulator.

4. forEach vs for/of vs for

3 cách duyệt array, mỗi cái có ưu/nhược.

forEachfor/ofcổ điển for
break được không❌ (chỉ return bên trong callback)
await tuần tự❌ (callback chạy đồng thời)
Truy cập index(v, i) =>arr.entries()biến i
Skip sparse slotBỏ quaTrả undefinedTrả undefined
Iterable nói chungChỉ Array✅ mọi iterableChỉ index được
PerformanceChậm hơn chútNhanhNhanh nhất (vi mô)
break / async issue với forEach
// ❌ forEach không break được
[1, 2, 3, 4].forEach((x) => {
  if (x === 3) return;  // chỉ skip phần tử này, không stop loop
  console.log(x);
});

// ✅ for/of break được
for (const x of [1, 2, 3, 4]) {
  if (x === 3) break;
  console.log(x);
}

// ❌ forEach KHÔNG await tuần tự
async function bad() {
  [url1, url2, url3].forEach(async (u) => {
    await fetch(u);  // 3 request chạy đồng thời, "bad" return trước khi xong
  });
}

// ✅ for/of await tuần tự
async function good() {
  for (const u of [url1, url2, url3]) {
    await fetch(u);  // chạy lần lượt
  }
}
💡 Recommend mặc định

Dùng for/of khi cần side-effect / loop bình thường. Dùng map/filter/reduce khi muốn transform / lọc / gộp. Tránh forEach — không có ưu điểm gì mà mất khả năng break, async.

Truy cập index với for/of
const items = ['a', 'b', 'c'];

for (const [i, v] of items.entries()) {
  console.log(i, v);  // 0 'a', 1 'b', 2 'c'
}

5. find / some / every / includes — early return

Khác map/filter ở chỗ dừng sớm khi tìm thấy / không match.

MethodTrả vềDừng khiMảng rỗng
find(pred)Phần tử đầu match, hoặc undefinedMatch đầu tiênundefined
findIndex(pred)Index của match đầu, hoặc -1Match đầu tiên-1
findLast / findLastIndex (ES2023)Như trên, từ phải quaMatch đầu (từ cuối)
some(pred)boolean — có ít nhất 1 matchMatch đầu tiênfalse
every(pred)boolean — TẤT CẢ matchKhông-match đầu tiêntrue 🔥
includes(v)boolean — có v khôngMatch đầu (so sánh SameValueZero)false
Examples
const nums = [1, 2, 3, 4, 5];

nums.find((x) => x > 3);        // 4
nums.findIndex((x) => x > 3);   // 3
nums.findLast((x) => x < 4);    // 3

nums.some((x) => x > 10);       // false
nums.every((x) => x > 0);        // true

nums.includes(3);                // true
nums.includes(NaN);              // (nếu array có NaN: true, vì SameValueZero)
[NaN].indexOf(NaN);             // -1 🔥 (indexOf dùng ===)
🔥 every trên array rỗng = true

"Mọi phần tử đều match" trên 0 phần tử thì vacuously true (logic toán học). Cẩn thận khi check "tất cả user đã verified" mà list rỗng — bạn vẫn được "yes".

6. flat / flatMap

flat(depth) làm phẳng nested array. flatMap = map().flat(1) nhưng hiệu quả hơn (1 lần duyệt).

[1, [2, [3, [4]]]].flat();           // [1, 2, [3, [4]]]   (depth=1)
[1, [2, [3, [4]]]].flat(2);          // [1, 2, 3, [4]]
[1, [2, [3, [4]]]].flat(Infinity);   // [1, 2, 3, 4]

// flatMap — phổ biến: 1 element → nhiều element
const sentences = ['I love JS', 'You too'];
sentences.flatMap((s) => s.split(' '));
// ['I', 'love', 'JS', 'You', 'too']

// Filter + map gộp 1 lần:
[1, 2, 3, 4].flatMap((x) => x % 2 ? [x * 10] : []);
// [10, 30]   ← chỉ giữ odd, nhân 10

7. sort gotcha — default là so sánh STRING

Đây là bug kinh điển bắt mọi JS developer ít nhất 1 lần.

sort default
[10, 1, 2, 25, 3].sort();
// [1, 10, 2, 25, 3]   🔥 SAI! sort by string

// Vì sao? Default sort convert mỗi phần tử sang string, so sánh code point.
// '10' < '2' khi so chuỗi (ký tự đầu '1' < '2').

// Cách đúng — truyền comparator:
[10, 1, 2, 25, 3].sort((a, b) => a - b);
// [1, 2, 3, 10, 25]  ✅

// Comparator rule:
//   trả < 0  → a đứng trước b
//   trả > 0  → b đứng trước a
//   trả = 0  → giữ nguyên thứ tự tương đối (stable từ 2019)

// Sort theo property:
users.sort((a, b) => a.age - b.age);  // asc
users.sort((a, b) => b.age - a.age);  // desc

// Sort string đúng locale (tiếng Việt có dấu):
['Đào', 'An', 'Bình'].sort((a, b) => a.localeCompare(b, 'vi'));
⚠️ sort mutate — dùng toSorted nếu không muốn đổi gốc
const a = [3, 1, 2];
const b = a.toSorted((x, y) => x - y);
// a = [3, 1, 2]  (giữ nguyên)
// b = [1, 2, 3]

8. Spread (...arr) và Rest (...args) — cùng cú pháp khác ngữ cảnh

Cùng dùng dấu ... nhưng:

  • Spread: "trải" iterable ra — dùng ở function call, array literal, object literal.
  • Rest: "gom" arg/element vào array — dùng ở function param, destructuring.
Spread
// Spread trong function call: array → individual args
Math.max(...[3, 1, 4, 1, 5]);   // 5

// Spread trong array literal: clone / merge
const a = [1, 2];
const b = [3, 4];
const merged = [...a, ...b];      // [1, 2, 3, 4]
const cloned = [...a];            // shallow clone

// Spread trong object literal (ES2018):
const o1 = { a: 1, b: 2 };
const o2 = { ...o1, c: 3 };    // { a: 1, b: 2, c: 3 }
const o3 = { ...o1, b: 99 };   // { a: 1, b: 99 }  ← override
Rest
// Rest trong function param: gom args còn lại vào array
function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4);  // 10

// Rest sau named param
function log(level, ...messages) {
  console.log(`[${level}]`, ...messages);
}
log('INFO', 'a', 'b', 'c');  // [INFO] a b c

// Rest trong destructuring
const [first, ...others] = [1, 2, 3, 4];
// first = 1, others = [2, 3, 4]

const { a, ...restObj } = { a: 1, b: 2, c: 3 };
// a = 1, restObj = { b: 2, c: 3 }
🧠 Mental model — đứng bên trái hay bên phải dấu =?

Nếu ... đứng bên trái (param, destructuring) → rest: gom vào.
Nếu ... đứng bên phải hoặc trong call/literal → spread: trải ra.

9. Destructuring nâng cao

Đã gặp ở chương 3-4. Đây là full reference.

Array destructuring
const [a, b, c] = [1, 2, 3];
// a=1, b=2, c=3

// Skip phần tử
const [, , third] = [1, 2, 3];  // third=3

// Default value khi undefined
const [x = 10, y = 20] = [1];  // x=1, y=20

// Rest
const [head, ...tail] = [1, 2, 3, 4];
// head=1, tail=[2,3,4]

// Swap không cần biến temp
let p = 1, q = 2;
[p, q] = [q, p];  // p=2, q=1
Object destructuring
const user = { name: 'An', age: 20, city: 'HN' };

// Cơ bản
const { name, age } = user;

// Rename — y trong object → newName trong scope
const { name: userName } = user;  // userName = 'An'

// Default
const { phone = 'N/A' } = user;  // phone = 'N/A'

// Rename + default
const { phone: tel = 'N/A' } = user;

// Nested
const obj = { a: { b: { c: 42 } } };
const { a: { b: { c } } } = obj;  // c = 42

// Destructure trong param + default
function greet({ name = 'guest', lang = 'vi' } = {}) {
  console.log(name, lang);
}
greet();                       // 'guest' 'vi'
greet({ name: 'An' });          // 'An' 'vi'
greet({ name: 'An', lang: 'en' });
💡 Pattern hay — destructure return value
function parseUrl(url) {
  const u = new URL(url);
  return { host: u.host, path: u.pathname, query: u.search };
}

const { host, path } = parseUrl('https://example.com/x?y=1');
// host = 'example.com', path = '/x'

10. Iteration Protocol — vì sao for/of hoạt động

Một object là iterable nếu có method [Symbol.iterator](), method này trả về một iterator. Iterator là object có method next() trả về { value, done }.

Iterator của Array (built-in)
const arr = ['a', 'b', 'c'];
const it = arr[Symbol.iterator]();

it.next();  // { value: 'a', done: false }
it.next();  // { value: 'b', done: false }
it.next();  // { value: 'c', done: false }
it.next();  // { value: undefined, done: true }

Built-in iterables: Array, String, Map, Set, NodeList, arguments, TypedArray. Plain object thì không.

Tự build iterable
const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
};

for (const n of range) console.log(n);  // 1 2 3 4 5
[...range];                              // [1, 2, 3, 4, 5]
Array.from(range);                       // [1, 2, 3, 4, 5]
🔥 for/of trên plain object → lỗi
const obj = { a: 1, b: 2 };
for (const x of obj) { }   // TypeError: obj is not iterable

// Cách đúng:
for (const k of Object.keys(obj)) { }
for (const v of Object.values(obj)) { }
for (const [k, v] of Object.entries(obj)) { }

// Hoặc for/in (duyệt key, kể cả inherited - cẩn thận):
for (const k in obj) { }

11. Generator — function*

Generator là function dừng/tiếp được. Khi gọi, nó không chạy — trả về một generator object (vừa là iterator vừa là iterable). Mỗi lần next() chạy đến yield tiếp theo rồi pause.

Generator cơ bản
function* simple() {
  yield 1;
  yield 2;
  yield 3;
}

const g = simple();
g.next();  // { value: 1, done: false }
g.next();  // { value: 2, done: false }
g.next();  // { value: 3, done: false }
g.next();  // { value: undefined, done: true }

// Generator object là iterable
[...simple()];          // [1, 2, 3]
for (const v of simple()) console.log(v);
Range bằng generator (gọn hơn)
function* range(from, to, step = 1) {
  for (let i = from; i <= to; i += step) {
    yield i;
  }
}

[...range(1, 5)];           // [1, 2, 3, 4, 5]
[...range(0, 10, 2)];        // [0, 2, 4, 6, 8, 10]
Infinite sequence (lazy)
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

// Lấy 5 số đầu — KHÔNG vô hạn loop vì generator lazy
function* take(iter, n) {
  for (const v of iter) {
    if (n-- <= 0) return;
    yield v;
  }
}

[...take(naturals(), 5)];  // [1, 2, 3, 4, 5]
🧠 Vì sao generator?
  • Lazy: chỉ tính khi cần — viết được sequence vô hạn.
  • Composable: take, filter, map bằng generator tạo pipeline streaming, không cần buộc array trung gian.
  • State machine: viết logic kiểu "đợi event, rồi đến event tiếp" tự nhiên hơn.
  • Là nền tảng để hiểu async generator (chương 6).

12. Set / Map / WeakSet / WeakMap

4 collection ES6 ngoài array/object. Mỗi cái có chỗ dùng riêng.

SetMapWeakSetWeakMap
LưuGiá trị uniqueCặp key→valueObject uniqueCặp object→value
Key typeBất kỳ (kể cả object)Object only
LookupO(1) — hasO(1) — get/hasO(1)O(1)
Iterable✅ insertion order✅ insertion order
size property
GC referenceStrongStrongWeak (cho phép GC)Weak

12.1. Set — collection unique

const s = new Set();
s.add(1); s.add(2); s.add(2); s.add(3);
s.size;            // 3
s.has(2);          // true
s.delete(2);
[...s];            // [1, 3]

// Dedupe array đơn giản:
const uniq = [...new Set([1, 2, 2, 3, 3, 3])];  // [1, 2, 3]

// So sánh performance: includes O(n) vs Set.has O(1)
const bigArr = Array.from({ length: 100000 }, (_, i) => i);
const bigSet = new Set(bigArr);

bigArr.includes(99999);   // chậm — duyệt 100k phần tử
bigSet.has(99999);        // nhanh — hash lookup

12.2. Map — key bất kỳ + insertion order

const m = new Map();
m.set('name', 'An');
m.set(42, 'số 42');
m.set(Symbol('id'), 1);

const obj = {};
m.set(obj, 'value cho object');   // Map dùng object làm key — khác plain Object!

m.get('name');   // 'An'
m.has(42);       // true
m.size;          // 4

// Iterate theo INSERTION ORDER
for (const [k, v] of m) { }
for (const k of m.keys()) { }
for (const v of m.values()) { }

// Khởi tạo từ array of pairs
const m2 = new Map([
  ['a', 1],
  ['b', 2],
]);
⚖️ Map vs plain Object
  • Key type: Map cho phép key bất kỳ. Object chỉ string/Symbol.
  • Order: Map đảm bảo insertion order. Object — chỉ đảm bảo từ ES2020, và integer-like key bị sort.
  • Size: Map có .size. Object phải Object.keys(o).length.
  • Iterate: Map iterable trực tiếp. Object cần Object.entries.
  • Performance: Map tối ưu cho thêm/xóa thường xuyên.
  • Prototype pollution: Object có inherited key như toString, __proto__. Map sạch.
  • Khi nào dùng Object: structured record (user, config). Khi nào dùng Map: dictionary động, cache, lookup table với key động.

12.3. WeakSet / WeakMap — không giữ GC reference

Strong reference (Map, Set, biến thường) chặn garbage collector. Weak reference cho phép object bị GC nếu không có chỗ nào khác giữ nó.

let user = { name: 'An' };
const wm = new WeakMap();
wm.set(user, 'metadata');

user = null;   // không còn strong ref → entry trong WeakMap sẽ bị GC

// WeakMap KHÔNG iterable, không size, không keys() — vì entries có thể biến mất bất kỳ lúc nào
// Chỉ có: set / get / has / delete
💡 Use case WeakMap

Gắn metadata cho object mà không "giữ" object đó sống mãi. Pattern: private fields trước khi class có #field (chương 4), memoize theo object key, cache DOM node mà không leak memory khi node bị remove.

Bài tập

Bài 1 — Phân tích danh sách user

Cho mảng:

const users = [
  { name: 'An',    age: 25 },
  { name: 'Bình',  age: 17 },
  { name: 'Cường', age: 30 },
  { name: 'An',    age: 21 },
  { name: 'Bảo',   age: 19 },
];

Viết các tính toán sau (chain filter/map/reduce, không loop tay):

  1. Danh sách tên những user ≥ 21 tuổi.
  2. Tuổi trung bình (làm tròn 1 chữ số thập phân).
  3. Group user theo ký tự đầu của tên: { A: [...], B: [...], C: [...] }.
Đáp án
// 1.
const adultNames = users
  .filter((u) => u.age >= 21)
  .map((u) => u.name);   // ['An', 'Cường', 'An']

// 2.
const avgAge = +(
  users.reduce((s, u) => s + u.age, 0) / users.length
).toFixed(1);   // 22.4

// 3.
const byLetter = users.reduce((acc, u) => {
  const k = u.name[0];
  (acc[k] ??= []).push(u);
  return acc;
}, {});

// Hoặc dùng Object.groupBy (ES2024):
Object.groupBy(users, (u) => u.name[0]);

Bài 2 — Implement unique, chunk, zip không dùng lodash

  • unique(arr) trả mảng các phần tử unique (giữ thứ tự xuất hiện đầu tiên).
  • chunk(arr, size) chia mảng thành mảng các "chunk" độ dài size; chunk cuối có thể ngắn hơn. VD chunk([1,2,3,4,5], 2)[[1,2],[3,4],[5]].
  • zip(...arrs) ghép nhiều mảng cùng index: zip([1,2,3], ['a','b','c'])[[1,'a'],[2,'b'],[3,'c']]. Nếu length khác, dừng ở mảng ngắn nhất.
Đáp án
const unique = (arr) => [...new Set(arr)];

const chunk = (arr, size) =>
  Array.from(
    { length: Math.ceil(arr.length / size) },
    (_, i) => arr.slice(i * size, i * size + size)
  );

const zip = (...arrs) => {
  const minLen = Math.min(...arrs.map((a) => a.length));
  return Array.from({ length: minLen }, (_, i) => arrs.map((a) => a[i]));
};

unique([1, 2, 2, 3]);              // [1, 2, 3]
chunk([1, 2, 3, 4, 5], 2);          // [[1,2],[3,4],[5]]
zip([1, 2, 3], ['a', 'b', 'c']);   // [[1,'a'],[2,'b'],[3,'c']]

Bài 3 — Generator Fibonacci + take

Viết:

  1. function* fib() sinh ra dãy Fibonacci vô hạn: 0, 1, 1, 2, 3, 5, 8, 13, ...
  2. function* take(iter, n) lấy n phần tử đầu của iterable.

Test: [...take(fib(), 10)][0, 1, 1, 2, 3, 5, 8, 13, 21, 34].

Đáp án
function* fib() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

function* take(iter, n) {
  for (const v of iter) {
    if (n-- <= 0) return;
    yield v;
  }
}

[...take(fib(), 10)];
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Generator vô hạn hoạt động được vì lazy: yield dừng tới khi next() mới chạy tiếp. take ngắt mạch sớm.

Bài 4 — Refactor imperative → functional

Code dưới đang dùng loop + push. Refactor thành chain filter/map/reduce đọc rõ ràng hơn:

function totalDiscountedPrice(items) {
  const instock = [];
  for (const it of items) {
    if (it.stock > 0) instock.push(it);
  }
  const prices = [];
  for (const it of instock) {
    prices.push(it.price * (1 - it.discount));
  }
  let total = 0;
  for (const p of prices) total += p;
  return total;
}
Đáp án
function totalDiscountedPrice(items) {
  return items
    .filter((it) => it.stock > 0)
    .map((it) => it.price * (1 - it.discount))
    .reduce((s, p) => s + p, 0);
}

Code ngắn hơn ~⅓, đọc được như "lọc còn hàng → tính giá sau giảm → cộng tổng".

Bài 5 — Generic groupBy(arr, keyFn)

Viết groupBy(arr, keyFn) nhận mảng + function tính key, trả object mỗi key → mảng phần tử match.

groupBy([1, 2, 3, 4, 5], (n) => n % 2 === 0 ? 'even' : 'odd');
// { odd: [1, 3, 5], even: [2, 4] }

groupBy(words, (w) => w.length);
// { 3: [...], 4: [...], ... }
Đáp án
function groupBy(arr, keyFn) {
  return arr.reduce((acc, item) => {
    const k = keyFn(item);
    (acc[k] ??= []).push(item);
    return acc;
  }, {});
}

// ES2024 native: Object.groupBy(arr, keyFn)  — kết quả tương tự.
// Hoặc Map.groupBy nếu cần key bất kỳ (object key).

Quiz

Q1

[1, 2, 3].map(parseInt) trả về?

Xem đáp án
✓ Đáp án

[1, NaN, NaN]. Vì parseInt nhận (value, radix), map gọi callback với (value, index, array). Kết quả: parseInt('1', 0) = 1 (radix 0 = mặc định 10); parseInt('2', 1) = NaN (không có radix 1); parseInt('3', 2) = NaN ('3' không hợp lệ trong nhị phân).

Fix: [1, 2, 3].map((x) => parseInt(x)) hoặc .map(Number).

Q2

Array.from({ length: 3 }, (_, i) => i) trả về?

Xem đáp án
✓ Đáp án

[0, 1, 2]. Array.from chấp nhận array-like (object có length) + map function. Đây là cách tạo array có index 0..n-1 không cần loop.

Q3

Gọi [].reduce((a, b) => a + b) (không initial value, trên array rỗng) — xảy ra gì?

Xem đáp án
✓ Đáp án

TypeError: Reduce of empty array with no initial value. Reduce không có gì để khởi tạo accumulator. Quy tắc: luôn truyền initial value, vừa an toàn vừa tự document type.

Q4

for (const x of obj) với obj = { a: 1, b: 2 } — chạy được không?

Xem đáp án
✓ Đáp án

Không. Plain object không có [Symbol.iterator] → throw TypeError: obj is not iterable. Dùng Object.entries(obj), Object.keys(obj) hoặc Object.values(obj) để có iterable.

Q5

So sánh performance lookup giữa arr.includes(x)set.has(x)?

Xem đáp án
✓ Đáp án

Set.has là O(1) (hash lookup). Array.includes là O(n) (duyệt tuần tự). Khi check membership trong vòng lặp / mảng lớn, chuyển sang Set có thể giảm thời gian từ O(n²) xuống O(n).

Q6

So sánh [...arr1, ...arr2]arr1.concat(arr2) — khác gì?

Xem đáp án
✓ Đáp án

Kết quả giống nhau (đều trả mảng mới, không mutate). Khác nhau ở flexibility: spread chèn được vào giữa [1, ...arr, 99], gom được iterable khác (Set, Map, generator) — concat chỉ ghép array. Performance: spread thường nhanh hơn cho mảng nhỏ; concat có thể nhanh hơn cho mảng rất lớn (engine tối ưu khác nhau).

Q7

arr.sort() không truyền comparator — sort theo gì?

Xem đáp án
✓ Đáp án

Theo string — JS convert mọi phần tử sang string rồi so sánh code point. Đó là vì sao [10, 1, 2].sort() ra [1, 10, 2]. Với số phải truyền: arr.sort((a, b) => a - b).

Q8

WeakMap vs Map — key behavior khác chỗ nào?

Xem đáp án
✓ Đáp án

Map giữ strong reference đến key — chặn GC, miễn map còn sống thì key còn sống. WeakMap giữ weak reference — nếu không nơi nào khác giữ key (chỉ WeakMap có), GC được phép thu hồi, entry biến mất khỏi map. Vì entries có thể biến mất bất kỳ lúc nào, WeakMap không iterable, không có .size, .keys(), .values(). Key phải là object, không nhận primitive.

Tổng kết

Sau chương 5, bạn nên đã master:

  • Tạo array: literal, Array.of, Array.from; tránh new Array(n) sparse trap.
  • Mutate vs non-mutate: thuộc bảng. Trong React/Redux luôn dùng toSorted, toReversed, spread copy.
  • map/filter/reduce: 4 pattern reduce kinh điển (sum, group, count, build object); luôn truyền initial value.
  • for/of là default, forEach tránh, for chỉ khi cần index thực sự.
  • find/some/every/includes: early return; every([]) = true.
  • flat / flatMap: depth, Infinity.
  • sort default string — luôn truyền comparator.
  • Spread vs Rest: cùng ..., khác ngữ cảnh (trái = rest, phải = spread).
  • Destructuring: rename, default, nested, param.
  • Iteration protocol: [Symbol.iterator] + next().
  • Generator: function*, yield, lazy, sequence vô hạn.
  • Set/Map/WeakSet/WeakMap: chọn đúng loại; Set dedupe; Map khi key bất kỳ; Weak khi cần GC-friendly metadata.

Kết nối

  • Chương 6 (Async, Event Loop) — async iterator (for await...of) là phần mở rộng tự nhiên của iteration protocol cho Promise.
  • Chương 7 (Error handling) — xử lý lỗi bên trong reduce/map mà không phá vỡ chain.
  • Chương 8+ (TypeScript) — type cho generic array/iterable, tuple, mapped types đối chiếu với map/filter.
  • Dart Chương 6 (Collections) — đối chiếu List/Set/Map Dart, sync* / async* generator Dart.