Home / Fundamentals / Refactor code ES5 sang ES6+: destructuring, spread, Map/Set
Fundamentals

Refactor code ES5 sang ES6+: destructuring, spread, Map/Set

Refactor code ES5 sang ES6+ từng bước: let/const, destructuring, spread, optional chaining ?., nullish coalescing ??.

Bạn vừa nhận bàn giao một file JavaScript viết từ thời ES5: var rải khắp nơi, nối chuỗi bằng + ba tầng, null check kiểu user && user.address && user.address.city, merge config bằng options = options || {}. Code vẫn chạy, nhưng mỗi lần sửa là một lần dò mìn.

Bài này modernize đúng file đó, từng bước một. Mỗi feature ES6+ chỉ xuất hiện khi nó làm một đoạn code cụ thể gọn và an toàn hơn — không phải danh sách changelog. Cuối bài có cheat sheet pattern cũ sang mới để bạn áp vào dự án.

1. Đoạn code ES5 bạn vừa thừa kế

Đây là hàm bạn phải sống chung: formatOrderSummary in ra tóm tắt một đơn hàng. Nó chạy đúng, nhưng đọc mệt và giấu một cái bug.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function formatOrderSummary(user, items, options) {
  options = options || {};
  var taxRate = options.taxRate || 0.1;
  var currency = options.currency || "USD";
  var name = user && user.name ? user.name : "Khách";
  var city = user && user.address && user.address.city ? user.address.city : "N/A";
  var subtotal = 0;
  var lines = "";
  for (var i = 0; i < items.length; i++) {
    subtotal += items[i].price * items[i].qty;
    lines += "- " + items[i].name + " x" + items[i].qty + "\n";
  }
  var tax = subtotal * taxRate;
  var total = subtotal + tax;
  return (
    "Đơn hàng của " + name + " (" + city + ")\n" +
    lines +
    "Tạm tính: " + subtotal + " " + currency + "\n" +
    "Thuế: " + tax + " " + currency + "\n" +
    "Tổng: " + total + " " + currency
  );
}

Có bốn mùi code ở đây, mỗi mùi được một feature xử lý trong các mục sau:

  • var khắp nơi — scope không rõ ràng (mục 2).
  • Nối chuỗi bằng +\n — khó đọc (mục 3).
  • Chuỗi user && user.address && ... để null check (mục 7).
  • options = options || {}options.taxRate || 0.1 để merge default (mục 5 và 7).

Riêng dòng var taxRate = options.taxRate || 0.1 giấu một bug: nếu đơn được miễn thuế (taxRate truyền vào là 0), 0 || 0.1 cho ra 0.1 — hàm tự cộng thêm 10% thuế vào đơn lẽ ra miễn phí. Ta gỡ nó ở mục 7.2.

Hàm này còn dùng function truyền thống và không có class; hai chủ đề đó đã có bài riêng — arrow function xem 3 lưu ý khi chuyển từ ES5, class syntax xem OOP và Prototypes hiện đại. Bài này tập trung vào các feature còn lại.

2. var sang let/const: scope thật và TDZ

var là nguồn của những bug scope khó chịu nhất trong code cũ. letconst sửa gốc rễ, đổi lại chúng “khó tính” hơn một chút — và sự khó tính đó là có lý do.

2.1. var rò rỉ scope và khai báo trùng

var có function scope, không phải block scope: biến khai báo trong for vẫn sống sau khi vòng lặp kết thúc. var cũng cho phép khai báo trùng tên mà không báo lỗi, nên một biến bị ghi đè âm thầm. let/const có block scope và cấm redeclare, nên đọc biến ngoài block của nó là lỗi ngay.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (var i = 0; i < 3; i++) {
  // ...
}
console.log("var i sau loop:", i); // 3 — i lộ ra ngoài for

var count = 1;
var count = 2; // redeclare cùng tên: không lỗi
console.log("redeclare var:", count); // 2

for (let j = 0; j < 3; j++) {
  // ...
}
try {
  console.log(j); // j block-scoped, không tồn tại ngoài loop
} catch (err) {
  console.log("let j sau loop:", err.name); // ReferenceError
}

Refactor: đổi mọi var thành const mặc định, chỉ dùng let khi biến thật sự cần gán lại. Sau bước này, mỗi biến chỉ sống đúng trong block bạn muốn.

2.2. TDZ: vì sao let/const “khó tính” hơn var

var được hoisting: khai báo được kéo lên đầu scope và khởi tạo undefined ngay, nên đọc trước dòng khai báo trả về undefined chứ không lỗi. let/const cũng được hoisting nhưng không khởi tạo — chúng nằm trong Temporal Dead Zone (TDZ) từ đầu block tới đúng dòng khai báo. Đọc biến trong khoảng đó ném ReferenceError.

TDZ chính là lý do let/const chặt hơn: thay vì âm thầm trả undefined (rồi crash ở chỗ khác khó lần), chúng báo lỗi ngay tại nơi bạn dùng sai thứ tự.

1
2
3
4
5
6
7
8
9
10
console.log("var trước khai báo:", hoisted); // undefined — var được hoisting
var hoisted = 1;

try {
  console.log(price); // TDZ: price chưa được khởi tạo
  let price = 100;
} catch (err) {
  // message theo V8; Firefox/Safari wording khác
  console.log("let trong TDZ:", err.name + ": " + err.message);
}

Dòng cuối in ReferenceError: Cannot access 'price' before initialization. Nội dung message tùy engine, nhưng err.name luôn là ReferenceError.

2.3. const không phải immutable

Hiểu nhầm phổ biến: const không làm object immutable. Nó chỉ khoá binding — bạn không gán lại biến sang giá trị khác được, nhưng vẫn mutate được nội dung object mà biến đang trỏ tới.

1
2
3
const config = { retries: 3 };
config.retries = 5; // mutate thuộc tính: OK
console.log("mutate const object:", config); // { retries: 5 }

Ngược lại, gán lại chính binding thì luôn ném TypeError: Assignment to constant variable. ngay khi dòng đó chạy — không phụ thuộc vào input:

1
2
const PI = 3.14;
PI = 3.14159; // ❌ TypeError: Assignment to constant variable.

const chỉ là shallow — nó khoá binding chứ không đóng băng nội dung — nên chuyện copy rồi mutate object vẫn có bẫy riêng, ta gặp lại ở mục 5.3.

3. Nối chuỗi sang template literals

Cục "..." + name + "..." trong hàm legacy là kiểu code dễ sai dấu cách và khó đọc nhất. Template literal (dấu backtick) thay nó bằng interpolation ${} và cho phép xuống dòng thật.

3.1. Interpolation và multiline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const name = "Alice";
const city = "Hà Nội";
const subtotal = 250000;
const currency = "VND";

// ES5: nối chuỗi bằng + và \n
const oldStr =
  "Đơn của " + name + " (" + city + ")\n" +
  "Tạm tính: " + subtotal + " " + currency;

// ES6: template literal — ${} và xuống dòng thật, không cần \n
const newStr = `Đơn của ${name} (${city})
Tạm tính: ${subtotal} ${currency}`;

console.log("giống nhau?", oldStr === newStr); // true
console.log(newStr);

Hai chuỗi bằng nhau tuyệt đối (true), nhưng bản template đọc ra ngay layout của chuỗi output. Trong formatOrderSummary, cả khối return ba tầng + gộp lại thành một template literal duy nhất.

3.2. Tagged template: escape HTML tự động

Template literal còn có dạng tagged: đặt một hàm ngay trước backtick, hàm đó nhận mảng phần literal và các giá trị ${} tách riêng, tự do xử lý trước khi ghép. Đây là lúc bạn chèn logic vào giữa quá trình nội suy — ví dụ escape input người dùng để chống XSS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function safeHtml(strings, ...values) {
  const escape = (s) =>
    String(s)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  return strings.reduce(
    (out, str, i) => out + str + (i < values.length ? escape(values[i]) : ""),
    ""
  );
}

const userInput = "<script>alert('xss')</script>";

// tag escape phần ${} nhưng giữ nguyên phần literal
console.log(safeHtml`<p>Bình luận: ${userInput}</p>`);
console.log(`raw: <p>Bình luận: ${userInput}</p>`);

Bản safeHtml biến <script> thành &lt;script&gt;; bản template thường ghép thẳng, giữ nguyên thẻ nguy hiểm. Đây chính là ý tưởng đứng sau các lib như lit-html. Lý do vì sao phải escape input đọc thêm ở bài chặn XSS với CSP.

4. Destructuring: bóc dữ liệu theo shape

Bộ ba var name = user.name; var age = user.age; ... là code lặp thuần tuý. Destructuring bóc nhiều thuộc tính trong một dòng, theo đúng shape của dữ liệu.

4.1. Object và array destructuring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const user = {
  name: "Alice",
  age: 30,
  address: { city: "Hà Nội", zip: "10000" },
};

// object destructuring + rename + default cho field thiếu
const { name, age, role = "user" } = user;
console.log(name, age, role); // Alice 30 user

const { name: userName } = user; // rename khi bóc
console.log("renamed:", userName); // Alice

// array destructuring + rest (teaser cho mục 5)
const [first, second, ...rest] = [90, 85, 70, 60];
console.log("first/second/rest:", first, second, rest); // 90 85 [ 70, 60 ]

// nested
const {
  address: { city },
} = user;
console.log("city:", city); // Hà Nội

Ba tính năng gộp lại: default (role = "user") điền chỗ thiếu, rename (name: userName) tránh trùng tên, rest (...rest) gom phần đuôi vào một array — ta khai thác ... kỹ hơn ở mục 5.

4.2. Nested và function parameters

Destructuring dùng ngay ở tham số hàm: phần options của formatOrderSummary trở thành destructured params, kèm default cho cả object lồng.

1
2
3
4
5
6
7
8
const user = { name: "Alice", address: { city: "Hà Nội" } };

function formatUser({ name, address: { city } = {} } = {}) {
  return `${name} - ${city ?? "N/A"}`;
}

console.log(formatUser(user)); // Alice - Hà Nội
console.log(formatUser({ name: "Bob" })); // Bob - N/A (address thiếu -> default {})

Bản thân cơ chế default parameter (chỉ kích hoạt khi giá trị là undefined) đã có bài riêng: viết hàm an toàn hơn với default parameters. Ở đây ta chỉ dùng nó để lấp giá trị thiếu khi bóc.

4.3. Gotcha: destructure undefined là TypeError

Cái bẫy quan trọng nhất của destructuring: bóc một giá trị null hoặc undefined ném TypeError. Rất hay gặp khi API trả về thiếu data.

1
2
3
4
5
6
7
8
9
10
11
12
const response = { data: undefined }; // API trả thiếu data

try {
  const { items } = response.data; // destructure undefined -> throw
  console.log(items);
} catch (err) {
  console.log(err.name + ": " + err.message);
}

// Fix: chuẩn hoá bằng ?? {} rồi mới bóc
const { items = [] } = response.data ?? {};
console.log("sau fix, items:", items); // []

err.message (theo V8) là Cannot destructure property 'items' of 'response.data' as it is undefined. — wording tùy engine, nhưng err.name luôn là TypeError. Cách fix chuẩn: chuẩn hoá về {} bằng ?? {} trước khi destructure. Toán tử ?? đó được giải thích đầy đủ ở mục 7.

5. Spread và rest: thay arguments, concat, Object.assign

Dấu ... làm hai việc ngược nhau tuỳ vị trí: rest gom nhiều thứ vào một, spread trải một thứ ra thành nhiều. Cả hai xoá được cả loạt API cũ rườm rà.

5.1. Rest parameters thay arguments

Helper legacy dùng arguments — một object array-like không có .map/.reduce. Rest parameter (...nums) cho bạn một array thật.

1
2
3
4
5
6
7
8
9
10
11
12
13
// ES5: arguments là array-like, không có .reduce
function sumOld() {
  var total = 0;
  for (var i = 0; i < arguments.length; i++) total += arguments[i];
  return total;
}
// ES6: rest -> nums là array thật
function sumNew(...nums) {
  return nums.reduce((a, b) => a + b, 0);
}

console.log("sumOld:", sumOld(1, 2, 3)); // 6
console.log("sumNew:", sumNew(1, 2, 3)); // 6

Một lưu ý khi refactor: arrow function không có arguments riêng, nên rest parameter là cách duy nhất gom biến số tham số trong arrow — chi tiết ở 3 lưu ý về arrow function.

5.2. Spread: copy, gộp array và object

Spread thay ba API cũ trong một cú pháp: arr1.concat(arr2), Math.max.apply(null, nums), và Object.assign({}, defaults, options).

1
2
3
4
5
6
7
8
9
10
const a = [1, 2];
const b = [3, 4];
console.log("concat:", [...a, ...b]); // [ 1, 2, 3, 4 ]

console.log("Math.max:", Math.max(...[5, 1, 9, 3])); // 9

// object spread thay Object.assign({}, defaults, options)
const defaults = { currency: "USD", taxRate: 0.1 };
const options = { taxRate: 0.2 };
console.log("merge:", { ...defaults, ...options }); // { currency: 'USD', taxRate: 0.2 }

Chính dòng cuối là bản refactor cho options = options || {} trong hàm legacy: { ...defaults, ...options } gộp default với input, key sau ghi đè key trước.

Lưu ý phiên bản

Array spread và rest parameter thuộc ES2015, nhưng object spread ({ ...a, ...b }) mới thêm ở ES2018 — dễ nhầm hai cái cùng tuổi. Cả hai giờ đều Baseline nên dùng thẳng không cần lo.

5.3. Gotcha: spread là shallow copy

Spread chỉ copy một tầng. Object lồng bên trong vẫn là chung một reference, nên mutate bản copy làm bản gốc đổi theo.

1
2
3
4
5
6
7
8
9
10
11
12
13
const original = {
  name: "Alice",
  address: { city: "Hà Nội" }, // nested object
};

const copy = { ...original }; // shallow copy
copy.name = "Bob"; // đổi primitive tầng ngoài: original an toàn
copy.address.city = "Đà Nẵng"; // đổi nested: original đổi theo!

console.log("copy.name:", copy.name); // Bob
console.log("original.name:", original.name); // Alice
console.log("original.address.city:", original.address.city); // Đà Nẵng (leak!)
console.log("cùng reference?", copy.address === original.address); // true

Muốn copy sâu (deep copy) thì structuredClone là công cụ built-in cho việc đó; còn ES2023 thêm nhóm method immutable (toSorted, toReversed, with) giúp bớt kiểu copy-rồi-sửa. Cả hai là chuyện của một bài khác — ở đây chỉ cần nhớ spread không cứu bạn khỏi mutation ở tầng sâu.

6. Object literal hiện đại: shorthand và computed properties

Object literal ES6+ bỏ được hai kiểu lặp: viết name: name khi tên biến trùng tên key, và cú pháp key: function () {} cho method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createUser(name, role) {
  return {
    name, // shorthand thay name: name
    role,
    greet() {
      // method shorthand thay greet: function () {}
      return `${this.name} (${this.role})`;
    },
  };
}

const u = createUser("Alice", "admin");
console.log("user:", u);
console.log("greet:", u.greet()); // Alice (admin)

// computed property: build key động ngay trong literal
function buildFilter(field, value) {
  return { [field]: value };
}
console.log("filter:", buildFilter("status", "active")); // { status: 'active' }

Computed property ({ [field]: value }) thay kiểu “tạo object rỗng rồi bracket-assign từng key”: thay vì const f = {}; f[field] = value;, bạn khai báo key động ngay trong literal. Hữu ích khi build query hoặc filter mà tên key đến từ biến.

7. Optional chaining ?. và nullish coalescing ??

Đây là cặp toán tử gỡ đúng hai mùi code còn lại trong hàm legacy: chuỗi && để null check, và || default để điền giá trị mặc định.

7.1. ?. thay chuỗi && dài

user && user.address && user.address.city là cách ES5 tránh crash khi truy cập sâu. Optional chaining ?. làm y hệt trong một biểu thức: gặp null/undefined giữa chừng thì dừng và trả undefined, không đọc tiếp.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const users = [
  { name: "Alice", address: { city: "Hà Nội" }, greet: () => "hi" },
  { name: "Bob" }, // không có address, không có greet
];

for (const user of users) {
  // thay user && user.address && user.address.city
  console.log(user.name, "->", user.address?.city ?? "N/A");
}

// ?.[] cho key động
const key = "city";
console.log("dynamic có:", users[0].address?.[key]); // Hà Nội
console.log("dynamic thiếu:", users[1].address?.[key]); // undefined

// ?.() cho method có thể không tồn tại
console.log("greet có:", users[0].greet?.()); // hi
console.log("greet thiếu:", users[1].greet?.()); // undefined

Ba biến thể: ?. cho thuộc tính, ?.[] cho key động, ?.() cho lời gọi method có thể vắng mặt. Tất cả thuộc ES2020.

7.2. ?? vs ||: đừng để 0 và chuỗi rỗng bị nuốt

Đây là gotcha chủ lực và cũng là chỗ gỡ cái bug cài ở mục 1. Toán tử || fallback khi vế trái falsy — mà 0, '', false đều falsy dù là giá trị hợp lệ. Nullish coalescing ?? chỉ fallback khi vế trái là null hoặc undefined, nên 0 được giữ nguyên.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function calcTotal(subtotal, taxRate) {
  const rateOld = taxRate || 0.1; // BUG: taxRate = 0 bị nuốt -> 0.1
  const rateNew = taxRate ?? 0.1; // đúng: 0 được giữ
  return {
    withOr: subtotal + subtotal * rateOld,
    withNullish: subtotal + subtotal * rateNew,
  };
}

// Đơn miễn thuế: taxRate = 0 hợp lệ
console.log("taxRate = 0:", calcTotal(100, 0));
// { withOr: 110, withNullish: 100 } — || cộng nhầm 10 tiền thuế

// Không truyền taxRate: cả hai fallback 0.1
console.log("taxRate = undefined:", calcTotal(100, undefined));
// { withOr: 110, withNullish: 110 }

Với taxRate: 0, bản || cho tổng 110 (tự thêm thuế), bản ?? cho 100 (đúng). Đây chính là bug options.taxRate || 0.1 ở mục 1: đổi sang options.taxRate ?? 0.1 là xong.

View Mermaid diagram code
flowchart TD
    Start["x ?? fallback"] --> Q{"x là null/undefined?"}
    Q -->|Có| A["trả fallback"]
    Q -->|Không| B["trả x — giữ cả 0, '', false"]

    Start2["x || fallback"] --> Q2{"x là falsy bất kỳ?"}
    Q2 -->|Có| A2["trả fallback — nuốt cả 0, '', false"]
    Q2 -->|Không| B2["trả x"]

    style B fill:#1a2c08,stroke:#c7ff50,stroke-width:1.5px,color:#c7ff50
    style A2 fill:#2a1517,stroke:#ff6b6b,stroke-width:1.5px,color:#ffb3b3

Câu chuyện đầy đủ về việc giá trị nào là falsy nằm ở bài Type Coercion trong JavaScript.

7.3. Logical assignment: ||=, &&=, ??=

ES2021 thêm nhóm logical assignment gộp toán tử logic với phép gán. options.retries = options.retries || 3 rút thành options.retries ??= 3 — và cùng bẫy nuốt-0 như trên: dùng ??= khi 0 là giá trị hợp lệ.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function applyDefaults(options) {
  const withOr = { ...options };
  withOr.retries ||= 3; // retries = 0 (falsy) bị gán đè thành 3 — SAI
  withOr.timeout ||= 5000;

  const withNullish = { ...options };
  withNullish.retries ??= 3; // retries = 0 giữ nguyên — đúng
  withNullish.timeout ??= 5000;

  return { withOr, withNullish };
}

// retries = 0 nghĩa là "không retry" — giá trị hợp lệ
console.log(applyDefaults({ retries: 0 }));

// &&= chỉ gán khi biến hiện tại truthy
const flags = { debug: true };
flags.debug &&= false; // debug đang true -> gán false
console.log("debug &&= false:", flags.debug); // false

||= biến retries: 0 thành 3 (sai), ??= giữ 0 (đúng). &&= thì ngược lại: chỉ gán khi vế trái đang truthy — hữu ích để cập nhật giá trị đã tồn tại.

8. Map và Set: khi object và array không đủ

Object và array giải quyết hầu hết nhu cầu, nhưng có hai chỗ chúng đuối: object làm dictionary với key không phải string, và array kiểm tra trùng/tồn tại. MapSet sinh ra cho đúng hai việc đó.

8.1. Map: key không còn bị ép thành string

Dùng plain object làm dictionary có ba vấn đề: key không phải string bị ép thành string (mọi object thành "[object Object]" nên va nhau), không có .size (phải Object.keys().length), và lẫn với key kế thừa từ prototype. Map sửa cả ba: key kiểu bất kỳ, có .size, và duyệt theo đúng insertion order (thứ tự chèn — được spec đảm bảo).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const keyA = { id: 1 };
const keyB = { id: 2 };

// object: hai object key khác nhau nhưng đều stringify thành "[object Object]"
const obj = {};
obj[keyA] = "A";
obj[keyB] = "B"; // ghi đè "A" vì cùng một string key
console.log("object key collision:", obj); // { '[object Object]': 'B' }

// Map: key giữ nguyên identity, không va nhau
const map = new Map();
map.set(keyA, "A");
map.set(keyB, "B");
console.log("map.get(keyA):", map.get(keyA)); // A
console.log("map.get(keyB):", map.get(keyB)); // B
console.log("map.size:", map.size); // 2

// duyệt theo insertion order, không tự sort
const order = new Map();
order.set("banana", 1);
order.set("apple", 2);
order.set("cherry", 3);
console.log("insertion order:", [...order.keys()]); // [ 'banana', 'apple', 'cherry' ]

for (const [k, v] of order) {
  console.log(`${k} = ${v}`);
}

Vòng for...of trên Map trả ra từng cặp [key, value]; cơ chế iteration protocol phía sau nằm ở bài Iterables và Iterators, ở đây chỉ cần biết Map duyệt được trực tiếp.

8.2. Set: dedup và kiểm tra tồn tại

Set là tập hợp các giá trị không trùng. Nó xoá hai đoạn code kinh điển: dedup bằng vòng lặp indexOf, và membership check bằng array.includes() gọi lặp lại.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const tags = ["js", "ts", "js", "css", "ts", "js"];

// ES5: dedup thủ công bằng indexOf
const uniqueOld = [];
for (var i = 0; i < tags.length; i++) {
  if (uniqueOld.indexOf(tags[i]) === -1) uniqueOld.push(tags[i]);
}
console.log("dedup thủ công:", uniqueOld); // [ 'js', 'ts', 'css' ]

// ES6: Set bỏ trùng, spread về lại array
console.log("dedup bằng Set:", [...new Set(tags)]); // [ 'js', 'ts', 'css' ]

// membership: .has() thay array.includes() gọi lặp lại
const allowed = new Set(["admin", "editor"]);
console.log("has admin:", allowed.has("admin")); // true
console.log("has guest:", allowed.has("guest")); // false

Một lưu ý trước khi dùng Map/Set để cache theo object: cả hai giữ strong reference tới key, nên object dùng làm key không bao giờ được garbage collector dọn chừng nào Map còn sống. Nếu bạn cache metadata theo object và lo memory leak, bản WeakMap/WeakSet mới là thứ cần — xem WeakMap vs WeakSet: tránh memory leak.

9. Cheat sheet: pattern ES5 sang ES6+

Ghép mọi bước lại, đây là formatOrderSummary sau refactor — đặt cạnh bản legacy để chứng minh output y hệt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// ES5 legacy (dán lại từ mục 1 để so trực tiếp)
function formatOrderSummaryOld(user, items, options) {
  options = options || {};
  var taxRate = options.taxRate || 0.1;
  var currency = options.currency || "USD";
  var name = user && user.name ? user.name : "Khách";
  var city = user && user.address && user.address.city ? user.address.city : "N/A";
  var subtotal = 0;
  var lines = "";
  for (var i = 0; i < items.length; i++) {
    subtotal += items[i].price * items[i].qty;
    lines += "- " + items[i].name + " x" + items[i].qty + "\n";
  }
  var tax = subtotal * taxRate;
  var total = subtotal + tax;
  return (
    "Đơn hàng của " + name + " (" + city + ")\n" +
    lines +
    "Tạm tính: " + subtotal + " " + currency + "\n" +
    "Thuế: " + tax + " " + currency + "\n" +
    "Tổng: " + total + " " + currency
  );
}

// ES6+ modern
function formatOrderSummaryNew(user, items, { taxRate = 0.1, currency = "USD" } = {}) {
  const name = user?.name ?? "Khách";
  const city = user?.address?.city ?? "N/A";
  const subtotal = items.reduce((sum, { price, qty }) => sum + price * qty, 0);
  const lines = items.map(({ name, qty }) => `- ${name} x${qty}`).join("\n");
  const tax = subtotal * taxRate;
  const total = subtotal + tax;
  return `Đơn hàng của ${name} (${city})
${lines}
Tạm tính: ${subtotal} ${currency}
Thuế: ${tax} ${currency}
Tổng: ${total} ${currency}`;
}

const user = { name: "Alice", address: { city: "Hà Nội" } };
const items = [
  { name: "Cà phê", price: 45000, qty: 2 },
  { name: "Bánh mì", price: 25000, qty: 1 },
];

console.log(formatOrderSummaryNew(user, items));
console.log("---");
console.log(
  "legacy === modern?",
  formatOrderSummaryOld(user, items) === formatOrderSummaryNew(user, items)
);

Bản modern ngắn hơn hẳn và không còn cái bug thuế: với input mặc định, output khớp tuyệt đối (true). Bảng dưới gom nhanh các pattern để bạn tra khi refactor:

Pattern ES5ES6+
varlet / const
"a" + x + "b"`a${x}b` (template literal)
fn.apply(null, args) / argumentsfn(...args) / rest ...args
arr1.concat(arr2)[...arr1, ...arr2]
Object.assign({}, a, b){ ...a, ...b }
{ name: name }{ name } (shorthand)
obj[key] = value (object rỗng rồi gán){ [key]: value } (computed)
a && a.b && a.b.ca?.b?.c
x || default (khi 0/'' hợp lệ)x ?? default
x = x || d (khi 0/'' hợp lệ)x ??= d
object làm dictionary key không-stringMap
dedup thủ công bằng indexOfSet

Refactor kiểu này làm dần từng lát, không cần viết lại cả file trong một lần. Mọi feature ở đây đều Baseline từ lâu nên không còn cớ để giữ code ES5 chỉ vì sợ transpiler. Bước tiếp theo của cùng hành trình là generators, nhóm method immutable của ES2023, và structuredClone cho deep copy. Hãy thử mở file ES5 cũ nhất trong dự án của bạn và refactor đúng một hàm theo bài này xem sao.

Table of Contents
  1. 1. Đoạn code ES5 bạn vừa thừa kế
  2. 2. var sang let/const: scope thật và TDZ
    1. 2.1. var rò rỉ scope và khai báo trùng
    2. 2.2. TDZ: vì sao let/const “khó tính” hơn var
    3. 2.3. const không phải immutable
  3. 3. Nối chuỗi sang template literals
    1. 3.1. Interpolation và multiline
    2. 3.2. Tagged template: escape HTML tự động
  4. 4. Destructuring: bóc dữ liệu theo shape
    1. 4.1. Object và array destructuring
    2. 4.2. Nested và function parameters
    3. 4.3. Gotcha: destructure undefined là TypeError
  5. 5. Spread và rest: thay arguments, concat, Object.assign
    1. 5.1. Rest parameters thay arguments
    2. 5.2. Spread: copy, gộp array và object
    3. 5.3. Gotcha: spread là shallow copy
  6. 6. Object literal hiện đại: shorthand và computed properties
  7. 7. Optional chaining ?. và nullish coalescing ??
    1. 7.1. ?. thay chuỗi && dài
    2. 7.2. ?? vs ||: đừng để 0 và chuỗi rỗng bị nuốt
    3. 7.3. Logical assignment: ||=, &&=, ??=
  8. 8. Map và Set: khi object và array không đủ
    1. 8.1. Map: key không còn bị ép thành string
    2. 8.2. Set: dedup và kiểm tra tồn tại
  9. 9. Cheat sheet: pattern ES5 sang ES6+