Bài 6.4: Thực hành sử dụng ES6+ features

Chào mừng trở lại với series blog của chúng ta! Hôm nay, chúng ta sẽ cùng nhau đi sâu vào thế giới đầy màu sắc của JavaScript hiện đại với các tính năng từ ES6+ (ECMAScript 2015 và các phiên bản sau). Những cải tiến này không chỉ giúp code của chúng ta ngắn gọn, dễ đọcmạnh mẽ hơn, mà còn mở ra nhiều cách tiếp cận mới trong lập trình.

ES6 đánh dấu một bước ngoặt lớn cho JavaScript, mang đến hàng loạt tính năng đột phá. Từ đó đến nay, mỗi năm lại có thêm các bản cập nhật nhỏ hơn (ES7, ES8, ES9,...) bổ sung những cải tiến mới. Việc nắm vữngáp dụng các tính năng này là thiết yếu cho bất kỳ lập trình viên Front-end hiện đại nào.

Hãy cùng thực hành ngay thôi nào!

1. letconst: Quản lý Biến Thông Minh Hơn

Trước ES6, chúng ta chỉ có var. var có phạm vi hoạt động (scope) là toàn cục (global) hoặc hàm (function). Điều này đôi khi dẫn đến những lỗi không mong muốn do biến bị ghi đè.

ES6 giới thiệu letconst với phạm vi hoạt động theo khối (block scope).

  • let: Dùng để khai báo biến có thể thay đổi giá trị sau khi khai báo. Phạm vi là khối ({}).
  • const: Dùng để khai báo biến có giá trị không thể gán lại sau khi khai báo. Phạm vi là khối ({}). Tuy nhiên, với đối tượng (object) hoặc mảng (array), bạn vẫn có thể thay đổi nội dung bên trong nó, chỉ không thể gán lại biến đó sang một đối tượng/mảng khác.

Ví dụ:

function demonstrateScope() {
  // Sử dụng var (cách cũ)
  var oldVariable = "I am var";
  if (true) {
    var oldVariable = "I am var inside if"; // Ghi đè biến bên ngoài
    console.log("Inside if (var):", oldVariable); // Output: Inside if (var): I am var inside if
  }
  console.log("Outside if (var):", oldVariable); // Output: Outside if (var): I am var inside if (bị ghi đè)

  console.log("---");

  // Sử dụng let và const (ES6+)
  let newVariable = "I am let";
  const constantValue = "I am const";

  if (true) {
    let newVariable = "I am let inside if"; // Biến mới, phạm vi chỉ trong if
    const anotherConstant = "Another const inside if"; // Biến mới, phạm vi chỉ trong if
    console.log("Inside if (let):", newVariable); // Output: Inside if (let): I am let inside if
    console.log("Inside if (const):", constantValue); // Output: Inside if (const): I am const (truy cập từ ngoài)
    console.log("Inside if (const block):", anotherConstant); // Output: Inside if (const block): Another const inside if
  }

  console.log("Outside if (let):", newVariable); // Output: Outside if (let): I am let (không bị ghi đè)
  console.log("Outside if (const):", constantValue); // Output: Outside if (const): I am const

  // console.log(anotherConstant); // Lỗi: anotherConstant is not defined (ngoài phạm vi khối if)

  const myArray = [1, 2, 3];
  myArray.push(4); // Hợp lệ: Thay đổi nội dung mảng
  console.log("Modified const array:", myArray); // Output: Modified const array: [1, 2, 3, 4]

  // myArray = [5, 6]; // Lỗi: Assignment to constant variable. (Không thể gán lại biến const)
}

demonstrateScope();

Giải thích: letconst giúp chúng ta tạo ra các biến có phạm vi hẹp hơn, tránh được các lỗi không đáng có do ghi đè biến. Luôn ưu tiên sử dụng const khi giá trị không đổi, và let khi cần thay đổi, hạn chế sử dụng var.

2. Arrow Functions (=>): Viết Hàm Ngắn Gọn và Hiểu this Dễ Hơn

Arrow functions cung cấp một cú pháp ngắn gọn hơn để viết các biểu thức hàm (function expression). Đặc biệt, chúng có cách xử lý từ khóa this khác biệt so với các hàm truyền thống. Arrow functions khôngthis của riêng chúng; thay vào đó, chúng kế thừa this từ ngữ cảnh (scope) bao quanh gần nhất (lexical scoping).

Ví dụ:

// Hàm truyền thống
const sumTraditional = function(a, b) {
  return a + b;
};
console.log("Traditional sum:", sumTraditional(5, 7)); // Output: Traditional sum: 12

// Arrow function - cú pháp ngắn gọn
const sumArrow = (a, b) => a + b;
console.log("Arrow sum:", sumArrow(5, 7)); // Output: Arrow sum: 12

// Arrow function với 1 tham số (có thể bỏ ngoặc đơn)
const square = x => x * x;
console.log("Square of 4:", square(4)); // Output: Square of 4: 16

// Arrow function không tham số
const sayHello = () => console.log("Hello from arrow function!");
sayHello(); // Output: Hello from arrow function!

// Arrow function trả về object literal (cần ngoặc đơn bao quanh object)
const createObject = (name, age) => ({ name: name, age: age });
console.log("Created object:", createObject("Bob", 28)); // Output: Created object: { name: 'Bob', age: 28 }

// Ví dụ về 'this' binding (rất quan trọng)
class Timer {
  constructor() {
    this.seconds = 0;
  }

  start() {
    setInterval(function() {
      // Ở đây, 'this' trong hàm truyền thống thường trỏ ra ngoài (ví dụ: global object trong non-strict mode, hoặc undefined trong strict mode)
      // console.log(this.seconds++); // Sẽ gây lỗi hoặc hành vi không mong muốn
    }, 1000);

    setInterval(() => {
      // Arrow function KHÔNG có 'this' riêng, nó kế thừa 'this' từ ngữ cảnh 'start' (đó là instance của Timer)
      this.seconds++; // Hoạt động đúng!
      console.log("Seconds:", this.seconds);
    }, 1000);
  }
}

// const timer = new Timer();
// timer.start(); // Chạy thử và quan sát output sau mỗi giây

Giải thích: Arrow functions rất tiện lợi cho các hàm đơn giản hoặc khi cần giữ ngữ cảnh this từ bên ngoài (như trong các hàm callback cho setTimeout hay addEventListener). Hãy thận trọng khi sử dụng chúng làm phương thức của object literal hoặc class, hoặc khi cần this riêng của hàm.

3. Destructuring Assignment: "Phá Vỡ" Cấu Trúc Dữ Liệu

Destructuring assignment cho phép bạn giải nén các giá trị từ mảng (array) hoặc thuộc tính từ đối tượng (object) và gán chúng vào các biến riêng biệt một cách nhanh chóngtrực quan.

Ví dụ với Object Destructuring:

const person = {
  firstName: "John",
  lastName: "Doe",
  age: 35,
  country: "USA"
};

// Cách cũ
// const firstName = person.firstName;
// const age = person.age;

// Cách dùng Destructuring
const { firstName, age } = person;
console.log(firstName); // Output: John
console.log(age);      // Output: 35

// Đổi tên biến khi Destructuring
const { lastName: familyName, country } = person;
console.log(familyName); // Output: Doe
console.log(country);    // Output: USA

// Cung cấp giá trị mặc định
const { city = "Unknown" } = person;
console.log(city); // Output: Unknown (vì không có thuộc tính city trong object person)

Ví dụ với Array Destructuring:

const colors = ["red", "green", "blue", "yellow"];

// Cách cũ
// const firstColor = colors[0];
// const thirdColor = colors[2];

// Cách dùng Destructuring
const [firstColor, secondColor] = colors;
console.log(firstColor);  // Output: red
console.log(secondColor); // Output: green

// Bỏ qua các phần tử
const [, , thirdColor] = colors; // Bỏ qua phần tử 0 và 1
console.log(thirdColor); // Output: blue

// Lấy phần còn lại của mảng (sử dụng Rest parameter, xem mục tiếp theo)
const [primaryColor, ...otherColors] = colors;
console.log("Primary:", primaryColor); // Output: Primary: red
console.log("Others:", otherColors); // Output: Others: ["green", "blue", "yellow"]

Giải thích: Destructuring làm cho việc truy cập các phần tử trong cấu trúc dữ liệu trở nên ngắn gọndễ đọc hơn rất nhiều, đặc biệt hữu ích khi làm việc với các object cấu hình hoặc kết quả từ API.

4. Spread và Rest Operators (...): Linh Hoạt Với Mảng và Đối Tượng

Toán tử ba chấm (...) là một trong những tính năng đa năng nhất của ES6+, được sử dụng cho hai mục đích chính:

  • Spread Operator: "Trải" (expand) một iterable (như mảng, chuỗi) hoặc object literal thành các phần tử hoặc cặp key-value riêng lẻ.
  • Rest Parameter: "Thu gom" (collect) nhiều phần tử riêng lẻ (thường là các tham số còn lại trong một hàm) thành một mảng.

Ví dụ với Spread Operator:

// Copy mảng (shallow copy)
const originalArray = [1, 2, 3];
const copiedArray = [...originalArray];
console.log("Copied array:", copiedArray); // Output: Copied array: [1, 2, 3]

// Kết hợp mảng
const array1 = [1, 2];
const array2 = [3, 4];
const combinedArray = [...array1, ...array2];
console.log("Combined array:", combinedArray); // Output: Combined array: [1, 2, 3, 4]

// Copy object (shallow copy)
const originalObject = { a: 1, b: 2 };
const copiedObject = { ...originalObject };
console.log("Copied object:", copiedObject); // Output: Copied object: { a: 1, b: 2 }

// Kết hợp object
const object1 = { a: 1, b: 2 };
const object2 = { c: 3, d: 4 };
const combinedObject = { ...object1, ...object2 };
console.log("Combined object:", combinedObject); // Output: Combined object: { a: 1, b: 2, c: 3, d: 4 }

// Thêm phần tử/thuộc tính mới khi copy/kết hợp
const updatedObject = { ...originalObject, b: 20, e: 5 }; // Ghi đè 'b', thêm 'e'
console.log("Updated object:", updatedObject); // Output: Updated object: { a: 1, b: 20, e: 5 }

// Sử dụng Spread để truyền tham số cho hàm
function multiply(x, y, z) {
  return x * y * z;
}
const numbersToMultiply = [2, 3, 4];
console.log("Multiply with spread:", multiply(...numbersToMultiply)); // Output: Multiply with spread: 24

Ví dụ với Rest Parameter:

// Thu thập các tham số còn lại thành một mảng
function displayArguments(firstArg, ...restArgs) {
  console.log("First argument:", firstArg);
  console.log("Rest arguments:", restArgs); // restArgs là một MẢNG chứa 2, 3, 4, 5
}

displayArguments(1, 2, 3, 4, 5);
// Output:
// First argument: 1
// Rest arguments: [ 2, 3, 4, 5 ]

// Destructuring kết hợp với Rest
const [head, ...tail] = [10, 20, 30, 40];
console.log("Head:", head); // Output: Head: 10
console.log("Tail:", tail); // Output: Tail: [ 20, 30, 40 ]

Giải thích: Spread và Rest operators mang lại sự linh hoạt đáng kinh ngạc khi làm việc với mảng và đối tượng. Spread giúp tạo bản sao, kết hợp hoặc truyền dữ liệu dễ dàng. Rest giúp gom nhóm các phần tử hoặc tham số một cách gọn gàng.

5. Template Literals (` `${}`): Chuỗi Mẫu Mạnh Mẽ

Template literals (sử dụng dấu backticks ``) cho phép tạo chuỗi một cách trực quanmạnh mẽ hơn so với chuỗi truyền thống (dùng '' hoặc "").

Các tính năng chính:

  • String Interpolation: Nhúng biến hoặc biểu thức trực tiếp vào chuỗi bằng cú pháp ${expression}.
  • Multiline Strings: Viết chuỗi trải dài qua nhiều dòng mà không cần ký tự xuống dòng đặc biệt (\n).

Ví dụ:

const name = "Developer";
const age = 7;

// Cách cũ
// const greetingOld = "Hello, " + name + "! Your age is " + age + ".";
// console.log(greetingOld); // Output: Hello, Developer! Your age is 7.

// Sử dụng Template Literals (interpolation)
const greetingNew = `Hello, ${name}! Your age is ${age}.`;
console.log(greetingNew); // Output: Hello, Developer! Your age is 7.

const price = 10;
const quantity = 3;
const total = `The total cost is ${price * quantity} USD.`; // Nhúng biểu thức
console.log(total); // Output: The total cost is 30 USD.

// Chuỗi nhiều dòng
const multiLineText = `This is the first line.
  This is the second line.
      This is the third line, indented.`;
console.log(multiLineText);
/* Output:
This is the first line.
  This is the second line.
      This is the third line, indented.
*/

Giải thích: Template literals làm cho việc xây dựng các chuỗi phức tạp trở nên đơn giảndễ đọc hơn rất nhiều, loại bỏ nhu cầu nối chuỗi bằng dấu +.

6. Default Parameters: Tham Số Mặc Định Cho Hàm

Tính năng này cho phép bạn gán các giá trị mặc định cho tham số của hàm ngay trong phần khai báo. Nếu khi gọi hàm, tham số tương ứng không được truyền vào (hoặc được truyền giá trị là undefined), giá trị mặc định sẽ được sử dụng.

Ví dụ:

// Hàm với tham số mặc định
function introduce(name = "Guest", greeting = "Hello") {
  console.log(`${greeting}, my name is ${name}.`);
}

introduce("Alice", "Hi"); // Output: Hi, my name is Alice. (Sử dụng cả 2 giá trị truyền vào)
introduce("Bob");       // Output: Hello, my name is Bob. (Sử dụng giá trị mặc định cho greeting)
introduce();            // Output: Hello, my name is Guest. (Sử dụng cả 2 giá trị mặc định)
introduce(undefined, "Hey"); // Output: Hey, my name is Guest. (Sử dụng giá trị mặc định cho name)
introduce("Charlie", undefined); // Output: Hello, my name is Charlie. (Sử dụng giá trị mặc định cho greeting)

Giải thích: Default parameters giúp viết các hàm linh hoạt hơn, không cần kiểm tra if (param === undefined) bên trong thân hàm nữa. Code trở nên sạch sẽdễ hiểu hơn.

7. Classes: Lập Trình Hướng Đối Tượng Rõ Ràng Hơn

ES6 giới thiệu cú pháp class như một lớp syntactic sugar (cú pháp "đường") lên trên cơ chế kế thừa dựa trên prototype vốn có của JavaScript. Nó cung cấp một cách trực quanquen thuộc hơn (đặc biệt với những người từ các ngôn ngữ OOP truyền thống) để định nghĩa các "đối tượng", các phương thức và thực hiện kế thừa.

Ví dụ (Cơ bản):

// Định nghĩa một Class
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound.`);
  }

  // Getter
  get animalName() {
    return this.name;
  }

  // Static method (gọi trực tiếp trên Class, không cần instance)
  static compareAnimals(animal1, animal2) {
    if (animal1.age && animal2.age) {
        return animal1.age - animal2.age; // Giả định có thuộc tính age
    }
    return 0;
  }
}

// Tạo instance (đối tượng) từ Class
const dog = new Animal("Dog");
dog.speak(); // Output: Dog makes a sound.
console.log(dog.animalName); // Output: Dog (sử dụng getter)

// Kế thừa Class (ES6+)
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Gọi constructor của lớp cha (Animal)
    this.breed = breed;
  }

  // Ghi đè phương thức của lớp cha
  speak() {
    console.log(`${this.name} barks!`);
  }

  // Phương thức riêng của lớp con
  displayBreed() {
    console.log(`${this.name} is a ${this.breed}.`);
  }
}

const myDog = new Dog("Buddy", "Golden Retriever");
myDog.speak(); // Output: Buddy barks! (Phương thức của lớp con)
myDog.displayBreed(); // Output: Buddy is a Golden Retriever.
console.log(myDog.animalName); // Output: Buddy (Kế thừa getter từ lớp cha)

// Sử dụng static method
// console.log(Animal.compareAnimals({ age: 3 }, { age: 5 })); // Output: -2

Giải thích: Cú pháp class giúp tổ chức code theo hướng đối tượng rõ ràng hơn. constructor là phương thức được gọi khi tạo đối tượng mới. Từ khóa extends cho phép kế thừa, và super() được dùng để gọi constructor/phương thức của lớp cha. static methods thuộc về Class chứ không phải từng instance.

8. asyncawait: Xử Lý Bất Đồng Bộ Dễ Dàng Hơn

Trước ES6, xử lý bất đồng bộ thường liên quan đến các callback lồng nhau ("callback hell") hoặc sử dụng Promises với .then().catch(). ES6 giới thiệu Promises và ES2017 (ES8) đã mang đến async/await như một cú pháp tuyệt vời dựa trên Promises để viết code bất đồng bộ trông giống code đồng bộ, làm cho nó dễ đọcdễ bảo trì hơn nhiều.

  • async: Từ khóa được đặt trước một hàm để báo hiệu rằng hàm đó sẽ luôn trả về một Promise. Bên trong hàm async, bạn có thể sử dụng await.
  • await: Chỉ được sử dụng bên trong hàm async. Đặt trước một Promise, await sẽ tạm dừng việc thực thi của hàm async cho đến khi Promise đó hoàn thành (resolved hoặc rejected). Giá trị trả về của Promise đã hoàn thành sẽ là kết quả của biểu thức await.

Ví dụ:

// Một hàm trả về Promise (mô phỏng việc gọi API hoặc thao tác mất thời gian)
function fetchData(dataId) {
  return new Promise((resolve, reject) => {
    console.log(`Fetching data for ID: ${dataId}...`);
    setTimeout(() => {
      if (dataId === 1) {
        resolve(`Data from server for ${dataId}`);
      } else {
        reject(`Error fetching data for ID: ${dataId}`);
      }
    }, 1000); // Giả lập độ trễ 1 giây
  });
}

// Sử dụng async/await để xử lý Promise
async function processData() {
  try {
    console.log("Starting data processing...");

    // Dòng này chờ fetchData(1) hoàn thành
    const result1 = await fetchData(1);
    console.log("Received:", result1);

    console.log("Continuing processing...");

    // Dòng này chờ fetchData(2) hoàn thành (sẽ bị reject)
    const result2 = await fetchData(2); // Lỗi sẽ xảy ra ở đây
    console.log("Received:", result2); // Dòng này sẽ không chạy nếu fetchData(2) bị reject

  } catch (error) {
    // Bắt lỗi nếu bất kỳ Promise nào bị reject bên trong hàm async
    console.error("An error occurred:", error);
  } finally {
    console.log("Data processing finished.");
  }
}

processData();
console.log("This line runs immediately, before processData completes."); // Minh họa tính bất đồng bộ

Giải thích: Hàm processData được đánh dấu là async. Khi gọi fetchData(1), await tạm dừng thực thi hàm processData cho đến khi fetchData(1) trả về kết quả. Sau khi nhận được kết quả result1, hàm tiếp tục chạy. Khi gọi fetchData(2) và nó bị reject, khối catch sẽ bắt lỗi. Cú pháp này giúp luồng code bất đồng bộ trông rất giống code đồng bộ, loại bỏ sự phức tạp của chuỗi .then().

9. for...of Loop: Lặp Qua Các Iterable

ES6 giới thiệu for...of loop như một cách đơn giảntrực tiếp để lặp qua các giá trị của các đối tượng có thể lặp lại (iterable), bao gồm: Mảng (Arrays), Chuỗi (Strings), Map, Set, Arguments object, TypedArray, NodeList, v.v.

Nó khác với for...in loop, vốn lặp qua các khóa (keys) hoặc chỉ mục (indices) của một đối tượng.

Ví dụ:

// Lặp qua mảng
const fruits = ["apple", "banana", "cherry"];
console.log("Iterating through array:");
for (const fruit of fruits) {
  console.log(fruit);
}
/* Output:
apple
banana
cherry
*/

// Lặp qua chuỗi
const greeting = "Hello";
console.log("Iterating through string:");
for (const char of greeting) {
  console.log(char);
}
/* Output:
H
e
l
l
o
*/

// So sánh với for...in (lặp qua chỉ mục/khóa)
console.log("Iterating with for...in (array):");
for (const index in fruits) {
  console.log(index); // Output: 0, 1, 2 (các chỉ mục)
  console.log(fruits[index]); // Output: apple, banana, cherry
}

const obj = { a: 1, b: 2, c: 3 };
// for (const key of obj) { } // Lỗi: obj is not iterable
// for...of không dùng trực tiếp cho object literal thông thường
console.log("Iterating with for...in (object):");
for (const key in obj) {
    console.log(key, obj[key]);
}
/* Output:
a 1
b 2
c 3
*/

Giải thích: for...of loop là cách ưu việt để lặp qua các giá trị trong mảng hoặc các cấu trúc dữ liệu tương tự. Nó trực tiếp truy cập giá trị của từng phần tử, làm code dễ hiểu hơn khi bạn chỉ quan tâm đến các phần tử đó. Hãy nhớ rằng nó không hoạt động trực tiếp trên các object literal thông thường, lúc đó bạn vẫn cần for...in hoặc các phương thức như Object.keys(), Object.values(), Object.entries().

Comments

There are no comments at the moment.