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:
varkhắp nơi — scope không rõ ràng (mục 2).- Nối chuỗi bằng
+và\n— khó đọc (mục 3). - Chuỗi
user && user.address && ...để null check (mục 7). options = options || {}và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ũ. let và const 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.Vì 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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
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 <script>; 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ộiBa 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)); // 6Mộ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); // trueMuố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?.()); // undefinedBa 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:#ffb3b3Câ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. Map và Set 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")); // falseMộ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 ES5 | ES6+ |
|---|---|
var | let / const |
"a" + x + "b" | `a${x}b` (template literal) |
fn.apply(null, args) / arguments | fn(...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.c | a?.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-string | Map |
dedup thủ công bằng indexOf | Set |
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.