This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.

Kể từ phiên bản ES2015 (ES6), JavaScript đã trải qua một cuộc cách mạng cú pháp. Mỗi năm, ủy ban TC39 lại bổ sung các tính năng mới giúp mã nguồn ngắn gọn, an toàn và dễ đọc hơn. Bài viết này tổng hợp những cú pháp hiện đại quan trọng nhất từ ES2015 đến ES2023 mà bất kỳ lập trình viên JavaScript nào cũng cần làm chủ: từ let/const, template literals, destructuring, spread/rest, arrow functions cho tới optional chaining và nullish coalescing.

1. let/const & Block Scope

Trước ES2015, JavaScript chỉ có var với function scope (phạm vi hàm) và cơ chế hoisting đầy bẫy. ES2015 giới thiệu letconst với block scope (phạm vi khối { }), giải quyết hàng loạt lỗi kinh điển.

Vì sao nên tránh var? Biến var "rò rỉ" ra ngoài khối lệnh và bị hoisting lên đầu hàm với giá trị undefined, dễ gây lỗi trong vòng lặp và điều kiện. Trong khi đó, letconst bị giữ trong vùng Temporal Dead Zone (TDZ): chúng vẫn được hoisting nhưng không thể truy cập trước dòng khai báo, nếu cố truy cập sẽ ném ReferenceError ngay lập tức — một cơ chế bảo vệ giúp bạn phát hiện lỗi sớm.

Lưu ý quan trọng: const không bất biến sâu (deep immutable). Nó chỉ ngăn việc gán lại tham chiếu của biến, còn nội dung bên trong object hay array vẫn có thể thay đổi. Muốn thực sự đóng băng, bạn cần Object.freeze().

Block scope trong vòng lặp
// var rò rỉ ra ngoài và chia sẻ chung một biến
for (var i = 0; i < 3; i++) { /* ... */ }
console.log(i); // 3 (var lọt ra ngoài vòng lặp)

// let tạo binding mới cho mỗi vòng lặp
for (let j = 0; j < 3; j++) { /* ... */ }
// console.log(j); // ReferenceError: j is not defined
const không bất biến sâu
const user = { name: "An" };
user.name = "Bình";     // OK - sửa nội dung được
// user = {};           // TypeError - không gán lại được

const frozen = Object.freeze({ age: 20 });
frozen.age = 99;        // bị bỏ qua (silent ở mode thường)
console.log(frozen.age); // 20

2. Template Literals & Tagged Templates

Template literals dùng dấu backtick ` thay cho dấu nháy đơn/kép, mở ra ba khả năng mạnh mẽ: nội suy biểu thức bằng ${...}, viết chuỗi nhiều dòng không cần ký tự \n, và xây dựng tagged templates.

Nội suy không chỉ chèn biến mà còn chạy được bất kỳ biểu thức JavaScript nào: phép tính, gọi hàm, toán tử ba ngôi. Chuỗi nhiều dòng giúp viết HTML hoặc SQL gọn gàng, giữ nguyên định dạng xuống dòng.

Tagged template là một hàm đặt ngay trước template literal. Hàm này nhận mảng các phần chuỗi tĩnh và các giá trị nội suy riêng biệt, cho phép bạn xử lý tùy biến — cực kỳ hữu ích cho i18n (đa ngôn ngữ), sanitize (chống XSS), highlight cú pháp, hay styled-components.

Nội suy & nhiều dòng
const name = "An", price = 1500;
const msg = `Xin chào ${name}, tổng tiền: ${(price * 1.1).toFixed(0)}đ`;

const html = `
  <div class="card">
    <h2>${name}</h2>
  </div>`;
console.log(msg);
Tagged template để sanitize
function safe(strings, ...values) {
  return strings.reduce((out, str, i) => {
    const val = values[i - 1]
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
    return out + val + str;
  });
}
const input = "<script>alert(1)</script>";
console.log(safe`Bình luận: ${input}`);
// Bình luận: &lt;script&gt;alert(1)&lt;/script&gt;

3. Destructuring (Phân rã cấu trúc)

Destructuring cho phép "bóc tách" giá trị từ array hoặc thuộc tính từ object ra các biến riêng lẻ chỉ trong một dòng. Đây là một trong những cú pháp được dùng nhiều nhất trong JavaScript hiện đại.

Với array destructuring, thứ tự quyết định: phần tử đầu vào biến đầu. Bạn có thể bỏ qua phần tử bằng cách để trống dấu phẩy, và dùng kỹ thuật này để hoán đổi (swap) hai biến mà không cần biến tạm.

Với object destructuring, tên thuộc tính quyết định (không phải thứ tự). Bạn có thể đổi tên (rename) biến bằng cú pháp { a: newName }, gán giá trị mặc định bằng = khi thuộc tính là undefined, và bóc tách lồng nhau (nested) nhiều cấp.

Ứng dụng mạnh nhất là destructuring trong tham số hàm: thay vì nhận một object rồi truy cập từng thuộc tính, bạn bóc tách ngay tại chữ ký hàm, kết hợp giá trị mặc định để có API gọn gàng, dễ đọc.

Array destructuring & swap
const colors = ["đỏ", "lục", "lam"];
const [first, , third] = colors;   // bỏ qua phần tử giữa
console.log(first, third);         // đỏ lam

let a = 1, b = 2;
[a, b] = [b, a];                   // hoán đổi không cần biến tạm
console.log(a, b);                 // 2 1
Object: rename, default, nested
const res = {
  data: { user: { name: "An", role: "admin" } },
  status: 200
};
// rename name -> userName, default cho lang
const { data: { user: { name: userName } }, lang = "vi" } = res;
console.log(userName, lang); // An vi
Destructuring trong tham số hàm
function createUser({ name, age = 18, active = true } = {}) {
  return `${name}, ${age} tuổi, ${active ? "hoạt động" : "khóa"}`;
}
console.log(createUser({ name: "Bình", age: 25 }));
console.log(createUser({ name: "Chi" }));   // dùng default
▶ Thử chạy: Destructuring

4. Spread & Rest

Cùng dùng ký hiệu ba chấm ... nhưng hai toán tử này hoạt động ngược chiều nhau. Spread (trải) "bung" một iterable hoặc object ra thành các phần tử riêng lẻ; Rest (gom) thu thập nhiều phần tử lại thành một mảng.

Spread giúp sao chép (copy)gộp (merge) array hay object một cách bất biến. Khi gộp object, thuộc tính sau ghi đè thuộc tính trước — rất tiện để cập nhật state. Một ứng dụng kinh điển là truyền mảng vào hàm nhận tham số rời, ví dụ Math.max(...arr).

Lưu ý: spread chỉ tạo bản sao nông (shallow clone). Các object/array lồng bên trong vẫn dùng chung tham chiếu, nên sửa cấp sâu sẽ ảnh hưởng cả bản gốc.

Rest parameters cho phép hàm nhận số lượng đối số tùy ý, gom chúng vào một mảng thực sự (khác với đối tượng arguments cũ kỹ), nhờ đó dùng được trực tiếp các phương thức như reduce().

Spread copy & merge
const base = { theme: "dark", lang: "vi" };
const merged = { ...base, lang: "en" }; // lang bị ghi đè -> en
console.log(merged); // { theme: "dark", lang: "en" }

const arr1 = [1, 2], arr2 = [3, 4];
const all = [...arr1, ...arr2];          // [1,2,3,4]
console.log(Math.max(...all));           // 4
Shallow clone — cảnh báo
const original = { name: "An", meta: { score: 10 } };
const copy = { ...original };
copy.meta.score = 99;            // sửa cả original.meta!
console.log(original.meta.score); // 99 (dùng chung tham chiếu)
Rest parameters
function sum(...nums) {
  return nums.reduce((t, n) => t + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

const [head, ...tail] = [10, 20, 30, 40];
console.log(head, tail); // 10 [20,30,40]
▶ Thử chạy: Spread & Rest

5. Arrow Functions & this

Arrow function (hàm mũi tên) mang lại cú pháp ngắn gọn: bỏ từ khóa function, và nếu thân hàm chỉ là một biểu thức thì có thể bỏ luôn return và dấu ngoặc nhọn (implicit return). Với một tham số, có thể bỏ cả dấu ngoặc đơn.

Khác biệt cốt lõi: arrow function không có this riêng. Nó "mượn" this từ phạm vi từ vựng (lexical this) nơi nó được định nghĩa. Đây là lý do arrow function cực kỳ tiện trong callback và setTimeout, giúp loại bỏ thủ thuật const self = this hay .bind(this) ngày xưa.

Tuy nhiên, chính đặc điểm này khiến bạn KHÔNG nên dùng arrow function trong vài trường hợp: làm method của object (vì this sẽ không trỏ tới object đó), làm constructor (arrow không thể gọi với new), hay khi cần đối tượng arguments và dynamic this (ví dụ event handler của DOM cần this là phần tử).

Lexical this giải cứu callback
const counter = {
  count: 0,
  start() {
    setInterval(() => {
      this.count++;        // this trỏ đúng counter nhờ lexical this
      console.log(this.count);
    }, 1000);
  }
};
// Nếu dùng function thường, this trong setInterval sẽ là undefined/window
Khi KHÔNG nên dùng arrow
const obj = {
  name: "An",
  // SAI: this không trỏ tới obj mà ra phạm vi ngoài
  greetBad: () => `Chào ${this.name}`,
  // ĐÚNG: method shorthand giữ this động
  greetGood() { return `Chào ${this.name}`; }
};
console.log(obj.greetGood()); // Chào An

6. Optional Chaining ?. & Nullish Coalescing ??

Hai toán tử của ES2020 này giải quyết triệt để vấn đề truy cập dữ liệu sâu mà không bị crash bởi lỗi kinh điển "Cannot read properties of undefined".

Optional chaining ?. truy cập thuộc tính một cách an toàn: nếu giá trị bên trái là null hoặc undefined, biểu thức dừng ngắn mạch và trả về undefined thay vì ném lỗi. Nó hoạt động với cả thuộc tính (a?.b), gọi hàm (obj.fn?.() — chỉ gọi nếu hàm tồn tại) và truy cập mảng (arr?.[0]).

Nullish coalescing ?? trả về vế phải chỉ khi vế trái là null hoặc undefined. Điều này khác biệt then chốt với toán tử ||: || trả về vế phải với mọi giá trị falsy (bao gồm 0, "", false). Vì vậy khi 0 hay chuỗi rỗng là giá trị hợp lệ, hãy dùng ?? để có giá trị mặc định an toàn. Kết hợp ?. với ?? tạo nên mẫu code truy cập sâu cực kỳ vững chắc.

Optional chaining an toàn
const user = { profile: { social: null } };
console.log(user.profile?.social?.twitter); // undefined (không crash)
console.log(user.settings?.theme);          // undefined

// Gọi hàm an toàn
const api = {};
api.onError?.("lỗi"); // không chạy, không lỗi

// Truy cập mảng an toàn
const data = null;
console.log(data?.[0]); // undefined
?? vs || với 0 và ""
const count = 0;
console.log(count || 10);  // 10  (0 là falsy -> bị thay)
console.log(count ?? 10);  // 0   (0 hợp lệ -> giữ nguyên)

const label = "";
console.log(label || "N/A"); // "N/A"
console.log(label ?? "N/A"); // ""  (chuỗi rỗng hợp lệ)
Kết hợp ?. và ??
function getCity(user) {
  return user?.address?.city ?? "Chưa cập nhật";
}
console.log(getCity({ address: { city: "Đà Nẵng" } })); // Đà Nẵng
console.log(getCity({}));                                // Chưa cập nhật
console.log(getCity(null));                              // Chưa cập nhật
▶ Thử chạy: Optional Chaining & Nullish

7. Các tính năng ES2020+

Mỗi năm TC39 lại bổ sung các tiện ích nhỏ nhưng vô cùng hữu ích. Dưới đây là những tính năng đáng nhớ nhất từ ES2020 trở đi:

  • globalThis (ES2020): tham chiếu global thống nhất cho mọi môi trường (browser, Node, worker), thay cho window/global/self.
  • Array.flat() & flatMap() (ES2019): làm phẳng mảng lồng nhau theo độ sâu chỉ định.
  • Object.fromEntries() (ES2019): chuyển mảng cặp [key, value] (hoặc Map) thành object — đảo ngược Object.entries().
  • String.replaceAll() (ES2021): thay tất cả lần xuất hiện mà không cần regex global.
  • Numeric separators (ES2021): dùng _ ngăn cách chữ số cho dễ đọc, ví dụ 1_000_000.
  • Logical assignment (ES2021): ??=, ||=, &&= gán có điều kiện gọn gàng.
  • Array.at() (ES2022): truy cập phần tử với chỉ số âm, arr.at(-1) lấy phần tử cuối.
  • structuredClone() (ES2022): tạo deep clone thật sự cho object phức tạp mà không cần thư viện ngoài.
flat, fromEntries, replaceAll, at
console.log([1, [2, [3]]].flat(2));          // [1,2,3]
console.log([1, 2].flatMap(x => [x, x * 10])); // [1,10,2,20]

const pairs = [["a", 1], ["b", 2]];
console.log(Object.fromEntries(pairs));        // {a:1, b:2}

console.log("a-b-c".replaceAll("-", "+"));     // a+b+c
console.log([10, 20, 30].at(-1));              // 30
Logical assignment, separators, deep clone
const config = { retries: 0 };
config.timeout ??= 3000;   // chỉ gán nếu null/undefined
config.retries ||= 5;      // gán vì 0 là falsy -> 5
console.log(config);       // { retries: 5, timeout: 3000 }

const budget = 1_000_000;  // dễ đọc hơn 1000000
console.log(budget);

const nested = { a: { b: [1, 2] } };
const deep = structuredClone(nested);
deep.a.b.push(3);
console.log(nested.a.b);   // [1,2] (không bị ảnh hưởng)
Phiên bản Năm Tính năng tiêu biểu
ES2015 (ES6) 2015 let/const, arrow, template literals, destructuring, spread/rest, class, module
ES2017 2017 async/await, Object.entries/values
ES2019 2019 Array.flat/flatMap, Object.fromEntries, trimStart/End
ES2020 2020 optional chaining ?., nullish ??, globalThis, BigInt
ES2021 2021 replaceAll, logical assignment, numeric separators
ES2022 2022 Array.at, structuredClone, top-level await, class fields
ES2023 2023 Array.findLast, toSorted/toReversed (immutable)

8. Câu hỏi trắc nghiệm ôn tập

Câu 1 — const & tính bất biến

Câu lệnh nào sau đây sẽ ném ra lỗi TypeError?

Câu 2 — ?? vs ||

Kết quả của console.log(0 ?? 5)console.log(0 || 5) lần lượt là?

Câu 3 — Arrow function & this

Vì sao không nên dùng arrow function làm method của object khi cần truy cập this?

Tải file code thực hành minh họa bài học

Chúng tôi đã viết sẵn một file script hoàn chỉnh tổng hợp toàn bộ cú pháp hiện đại: block scope, template literals, destructuring, spread/rest, arrow & this, optional chaining, nullish coalescing và các tính năng ES2020+:

Tải về js_modern.js

Related Articles

Bài viết liên quan trong series

Lesson 3: How JS Works: Engine, Execution Context & Closures Bài 3: Cách JavaScript Hoạt Động: JS Engine, Execution Context & Closures Lesson 1: JavaScript Fundamentals: Types, Coercion & Equality Bài 1: Nền Tảng JavaScript: Kiểu Dữ Liệu, Ép Kiểu & So Sánh Back to JavaScript Series Overview Quay lại Lộ trình JavaScript Series