Bài 6.5: Bài tập thực hành ES6+

Chào mừng trở lại với chuỗi bài viết về Lập trình Web Front-end! Sau khi đã tìm hiểu lý thuyết về các tính năng mạnh mẽ của ES6+ (phiên bản JavaScript hiện đại), đã đến lúc chúng ta xắn tay áo lên và đi sâu vào thực hành!

Việc thực hành không chỉ giúp bạn ghi nhớ cú pháp, mà quan trọng hơn là giúp bạn hiểu tại sao chúng ta nên sử dụng các tính năng mới này và khi nào thì chúng phát huy hiệu quả nhất. Bài viết này sẽ cung cấp cho bạn các ví dụ thực tế và giải thích ngắn gọn để bạn có thể áp dụng ngay lập tức.

Hãy cùng bắt đầu hành trình làm chủ ES6+ bằng cách thực hành các tính năng quan trọng nhất nhé!

1. Biến với letconst - Sức mạnh của Scope

Một trong những thay đổi cơ bản và quan trọng nhất trong ES6 là cách khai báo biến với letconst, thay thế cho var truyền thống. Sự khác biệt nằm ở phạm vi (scope) và khả năng gán lại giá trị.

  • let: Biến có phạm vi khối (block scope). Có thể gán lại giá trị.
  • const: Biến có phạm vi khối (block scope). Không thể gán lại giá trị (đối với kiểu dữ liệu nguyên thủy) hoặc không thể gán lại tham chiếu (đối với đối tượng/mảng).

Bài tập 1: Hiểu về Block Scope

Hãy xem ví dụ kinh điển này để thấy sự khác biệt rõ rệt giữa varlet trong vòng lặp và bất đồng bộ (sử dụng setTimeout).

console.log("--- Bài tập 1: Scope của let vs var ---");

// Sử dụng var
for (var i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log('Giá trị của i (var):', i);
    }, 100 * i); // Dùng 100*i để thấy rõ kết quả theo thứ tự
}

// Sử dụng let
for (let j = 0; j < 3; j++) {
    setTimeout(() => {
        console.log('Giá trị của j (let):', j);
    }, 100 * j);
}

Giải thích:

  • Với vòng lặp var i, biến i có phạm vi là toàn bộ hàm hoặc phạm vi global (nếu không nằm trong hàm). Khi setTimeout được gọi, vòng lặp đã kết thúc và giá trị cuối cùng của i3. Do đó, tất cả các hàm callback của setTimeout đều tham chiếu đến cùng một biến i và in ra 3.
  • Với vòng lặp let j, từ khóa let tạo ra một biến j mới cho mỗi lần lặp. Do đó, mỗi hàm callback của setTimeout sẽ đóng lại (closure) giá trị j tương ứng với lần lặp mà nó được tạo ra (0, 1, 2), và in ra đúng các giá trị này.

Bài tập 2: Hiểu về const

console.log("\n--- Bài tập 2: Sử dụng const ---");

const myConstant = 10;
// myConstant = 20; // Lỗi! Không thể gán lại cho const kiểu nguyên thủy

const myArray = [1, 2, 3];
myArray.push(4); // OK! Có thể thay đổi nội dung mảng
console.log('Mảng sau khi push:', myArray); // Mảng sau khi push: [ 1, 2, 3, 4 ]

// myArray = [5, 6]; // Lỗi! Không thể gán lại tham chiếu mới cho biến const

const myObject = { name: 'Alice' };
myObject.age = 30; // OK! Có thể thay đổi thuộc tính đối tượng
console.log('Đối tượng sau khi thêm thuộc tính:', myObject); // Đối tượng sau khi thêm thuộc tính: { name: 'Alice', age: 30 }

// myObject = { name: 'Bob' }; // Lỗi! Không thể gán lại tham chiếu mới cho biến const

Giải thích:

  • const ngăn việc gán lại bản thân biến với một giá trị hoặc tham chiếu mới.
  • Tuy nhiên, đối với các kiểu dữ liệu phức tạp như mảng (Array) và đối tượng (Object), const chỉ đảm bảo rằng biến đó luôn trỏ đến cùng một mảng hoặc đối tượng đó. Bạn hoàn toàn có thể thay đổi nội dung bên trong mảng hoặc đối tượng mà biến const đang trỏ tới.

Lời khuyên: Luôn sử dụng const khi khai báo biến nếu bạn biết rằng giá trị của nó sẽ không thay đổi. Chỉ sử dụng let khi bạn chắc chắn rằng biến đó cần được gán lại giá trị sau này. Hạn chế tối đa việc sử dụng var trong code ES6+.

2. Hàm Mũi Tên (=>) - Ngắn Gọn và Mạnh Mẽ

Hàm mũi tên (Arrow Functions) cung cấp một cú pháp ngắn gọn hơn để viết các hàm biểu thức và có sự khác biệt đáng kể trong cách xử lý từ khóa this.

Bài tập 3: Cú pháp cơ bản

console.log("\n--- Bài tập 3: Cú pháp hàm mũi tên ---");

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

// Hàm mũi tên - Cú pháp ngắn gọn với implicit return
const sumArrow = (a, b) => a + b;
console.log('Tổng (mũi tên):', sumArrow(5, 7)); // 12

// Hàm mũi tên - Có khối lệnh (cần return tường minh)
const multiply = (a, b) => {
  console.log(`Nhân ${a}${b}`);
  return a * b;
};
console.log('Tích (mũi tên):', multiply(4, 6)); // Nhận 4 và 6 \n 24

// Hàm mũi tên - Một tham số (có thể bỏ ngoặc đơn)
const square = x => x * x;
console.log('Bình phương của 5:', square(5)); // 25

// Hàm mũi tên - Không tham số
const greet = () => console.log('Xin chào!');
greet(); // Xin chào!

Giải thích:

  • Cú pháp (tham_số) => biểu_thức cho phép trả về kết quả của biểu_thức một cách ngầm định (implicit return) nếu chỉ có một dòng lệnh.
  • Nếu có nhiều dòng lệnh hoặc cần thực hiện các tác vụ khác trước khi trả về, bạn cần sử dụng khối lệnh {} và từ khóa return tường minh.
  • Đối với hàm chỉ có một tham số, bạn có thể bỏ qua cặp ngoặc đơn ().
  • Đối với hàm không có tham số, bạn vẫn cần cặp ngoặc đơn ().

Bài tập 4: Sự khác biệt về this

Đây là điểm quan trọng nhất cần lưu ý khi sử dụng hàm mũi tên, đặc biệt trong lập trình hướng đối tượng hoặc khi xử lý sự kiện. Hàm mũi tên không có this riêng của nó, mà nó kế thừa this từ ngữ cảnh bao quanh (lexical scope).

console.log("\n--- Bài tập 4: this trong hàm mũi tên ---");

const person = {
  name: 'Bob',
  // Phương thức truyền thống
  greetTraditional: function() {
    // 'this' ở đây là đối tượng 'person'
    setTimeout(function() {
      // 'this' ở đây mặc định là global (window trong trình duyệt, undefined trong Node.js strict mode)
      // console.log('Chào từ setTimeout (truyền thống): ' + this.name); // Thường in ra undefined
    }, 100);
    console.log('Chào (truyền thống): ' + this.name); // In ra 'Chào (truyền thống): Bob'
  },
  // Phương thức dùng hàm mũi tên
  greetArrow: function() {
     // 'this' ở đây là đối tượng 'person'
    setTimeout(() => {
      // 'this' ở đây kế thừa từ ngữ cảnh bao quanh (phương thức greetArrow),
      // nên nó vẫn là đối tượng 'person'
      console.log('Chào từ setTimeout (mũi tên): ' + this.name); // In ra 'Chào từ setTimeout (mũi tên): Bob'
    }, 100);
    console.log('Chào (mũi tên): ' + this.name); // In ra 'Chào (mũi tên): Bob'
  }
};

person.greetTraditional();
person.greetArrow();

Giải thích:

  • Trong greetTraditional, hàm truyền thống bên trong setTimeout định nghĩa this của riêng nó, thường trỏ đến đối tượng global (hoặc undefined trong chế độ nghiêm ngặt), không phải đối tượng person.
  • Trong greetArrow, hàm mũi tên bên trong setTimeout không định nghĩa this riêng. Nó nhìn ra ngoài và kế thừa this từ ngữ cảnh chứa nó (phương thức greetArrow), nơi this đúng là đối tượng person.

Lời khuyên: Sử dụng hàm mũi tên khi bạn muốn this được giữ nguyên giá trị từ ngữ cảnh cha, đặc biệt hữu ích trong các hàm callback (như setTimeout, xử lý sự kiện) hoặc khi làm việc với các phương thức của đối tượng (như trong ví dụ trên).

3. Destructuring - Mở Hộp Dữ Liệu

Destructuring Assignment (Gán phá hủy cấu trúc) là một cú pháp tiện lợi cho phép "giải nén" các giá trị từ mảng hoặc thuộc tính từ đối tượng vào các biến riêng biệt một cách dễ dàng.

Bài tập 5: Destructuring Mảng

console.log("\n--- Bài tập 5: Destructuring Mảng ---");

const rgb = ['red', 'green', 'blue'];

// Gán các phần tử mảng vào biến
const [color1, color2, color3] = rgb;
console.log(color1, color2, color3); // red green blue

// Bỏ qua phần tử
const [,, thirdColor] = rgb;
console.log('Màu thứ ba:', thirdColor); // Màu thứ ba: blue

// Kết hợp với Rest operator (xem phần sau)
const [primary, ...secondary] = rgb;
console.log('Màu chính:', primary); // Màu chính: red
console.log('Màu phụ:', secondary); // Màu phụ: [ 'green', 'blue' ]

// Gán với giá trị mặc định
const [c1, c2, c3, c4 = 'black'] = rgb;
console.log('Màu mặc định:', c4); // Màu mặc định: black

// Hoán đổi giá trị
let a = 1, b = 2;
[a, b] = [b, a];
console.log('Sau khi hoán đổi:', a, b); // Sau khi hoán đổi: 2 1

Giải thích:

  • Sử dụng cú pháp [] ở vế trái của phép gán để chỉ định các biến sẽ nhận giá trị từ mảng ở vế phải, theo vị trí tương ứng.
  • Bạn có thể dùng dấu phẩy để bỏ qua các phần tử không mong muốn.
  • Kết hợp với Rest operator (...) để thu thập các phần tử còn lại vào một mảng mới.
  • Có thể cung cấp giá trị mặc định (biến = giá_trị_mặc_định) cho các biến trong trường hợp phần tử tương ứng không tồn tại trong mảng nguồn.
  • Destructuring mảng là một cách cực kỳ gọn gàng để hoán đổi giá trị của hai biến.

Bài tập 6: Destructuring Đối Tượng

console.log("\n--- Bài tập 6: Destructuring Đối Tượng ---");

const userProfile = {
  userId: 101,
  userName: 'Charlie',
  userAge: 25,
  city: 'New York'
};

// Gán thuộc tính đối tượng vào biến cùng tên
const { userName, userAge } = userProfile;
console.log(userName, userAge); // Charlie 25

// Gán thuộc tính và đổi tên biến
const { userId: id, city: location } = userProfile;
console.log(id, location); // 101 New York

// Gán với giá trị mặc định (khi thuộc tính không tồn tại)
const { userName: nameDefault, country = 'USA' } = userProfile;
console.log('Tên và quốc gia mặc định:', nameDefault, country); // Tên và quốc gia mặc định: Charlie USA

// Destructuring trong tham số hàm
function displayUser({ userName, userAge, city = 'Unknown' }) {
  console.log(`Người dùng: ${userName}, Tuổi: ${userAge}, Thành phố: ${city}`);
}
displayUser(userProfile); // Người dùng: Charlie, Tuổi: 25, Thành phố: New York
displayUser({ userName: 'David', userAge: 35 }); // Người dùng: David, Tuổi: 35, Thành phố: Unknown

Giải thích:

  • Sử dụng cú pháp {} ở vế trái của phép gán để chỉ định các thuộc tính cần trích xuất từ đối tượng ở vế phải. Các biến mới sẽ có tên giống với tên thuộc tính mặc định, trừ khi bạn đổi tên rõ ràng (tenThuocTinh: tenBienMoi).
  • Có thể cung cấp giá trị mặc định cho các biến trong trường hợp thuộc tính tương ứng không tồn tại.
  • Destructuring đối tượng cực kỳ hữu ích khi truyền đối tượng cấu hình làm tham số cho hàm, giúp code đọc dễ hiểu hơn nhiều.

Lời khuyên: Destructuring giúp code của bạn gọn gàng, dễ đọc và dễ bảo trì hơn rất nhiều khi làm việc với cấu trúc dữ liệu như mảng và đối tượng. Hãy làm quen và sử dụng nó thường xuyên!

4. Spread & Rest Operators (...) - Phép Thuật Với Mảng và Đối Tượng

Toán tử ba chấm (...) trong ES6+ có hai vai trò chính: Spread (rải) và Rest (thu thập).

Bài tập 7: Spread Operator (Rải)

Spread operator được dùng để "mở rộng" hoặc "rải" các phần tử của một iterable (như mảng, chuỗi) hoặc các thuộc tính của một đối tượng vào nơi cần dùng.

console.log("\n--- Bài tập 7: Spread Operator ---");

// Spread với mảng - Sao chép mảng (shallow copy)
const originalArray = [1, 2, 3];
const copyArray = [...originalArray];
console.log('Sao chép mảng:', copyArray); // Sao chép mảng: [ 1, 2, 3 ]
console.log('Hai mảng có giống nhau không (về tham chiếu)?', originalArray === copyArray); // false

// Spread với mảng - Ghép mảng
const arrA = [1, 2];
const arrB = [3, 4];
const combinedArray = [...arrA, ...arrB, 5, 6];
console.log('Ghép mảng:', combinedArray); // Ghép mảng: [ 1, 2, 3, 4, 5, 6 ]

// Spread với đối tượng - Sao chép đối tượng (shallow copy)
const originalObject = { x: 1, y: 2 };
const copyObject = { ...originalObject };
console.log('Sao chép đối tượng:', copyObject); // Sao chép đối tượng: { x: 1, y: 2 }
console.log('Hai đối tượng có giống nhau không (về tham chiếu)?', originalObject === copyObject); // false

// Spread với đối tượng - Ghép đối tượng
const objA = { a: 1, b: 2 };
const objB = { c: 3, d: 4 };
const combinedObject = { ...objA, ...objB, e: 5 };
console.log('Ghép đối tượng:', combinedObject); // Ghép đối tượng: { a: 1, b: 2, c: 3, d: 4, e: 5 }

// Spread với đối tượng - Ghi đè thuộc tính
const defaultSettings = { theme: 'dark', fontSize: 14 };
const userSettings = { fontSize: 16, language: 'vi' };
const finalSettings = { ...defaultSettings, ...userSettings }; // userSettings sẽ ghi đè defaultSettings nếu có thuộc tính trùng
console.log('Cài đặt cuối cùng:', finalSettings); // Cài đặt cuối cùng: { theme: 'dark', fontSize: 16, language: 'vi' }

// Spread với chuỗi
const greetingString = 'Hello';
const chars = [...greetingString];
console.log('Rải chuỗi thành mảng ký tự:', chars); // Rải chuỗi thành mảng ký tự: [ 'H', 'e', 'l', 'l', 'o' ]

// Sử dụng Spread trong gọi hàm (truyền các phần tử mảng làm đối số)
const numbers = [1, 2, 3];
function addThreeNumbers(n1, n2, n3) {
  return n1 + n2 + n3;
}
console.log('Gọi hàm với spread:', addThreeNumbers(...numbers)); // Gọi hàm với spread: 6

Giải thích:

  • Khi ... được sử dụng ở vế phải của phép gán hoặc trong tham số khi gọi hàm, nó hoạt động như Spread operator, "rải" các phần tử/thuộc tính ra.
  • Rất hữu ích để tạo bản sao nông (shallow copy) của mảng/đối tượng, kết hợp nhiều mảng/đối tượng, hoặc truyền các phần tử mảng làm đối số riêng lẻ cho hàm.

Bài tập 8: Rest Parameters (Thu thập)

Rest parameters được sử dụng trong định nghĩa hàm để "thu thập" tất cả các đối số còn lại (sau khi đã gán cho các tham số tường minh) vào một mảng duy nhất.

console.log("\n--- Bài tập 8: Rest Parameters ---");

// Thu thập tất cả các đối số
function sum(...args) {
  console.log('Các đối số được thu thập:', args); // args là một mảng
  return args.reduce((total, current) => total + current, 0);
}

console.log('Tổng các số:', sum(1, 2, 3, 4, 5)); // Các đối số được thu thập: [ 1, 2, 3, 4, 5 ] \n Tổng các số: 15
console.log('Tổng các số:', sum(10, 20));      // Các đối số được thu thập: [ 10, 20 ] \n Tổng các số: 30

// Kết hợp tham số tường minh và Rest
function greetUser(greeting, ...names) {
  console.log('Lời chào:', greeting);
  console.log('Danh sách tên:', names); // names là mảng các tên còn lại
  names.forEach(name => console.log(`${greeting}, ${name}`));
}

greetUser('Hello', 'Anna', 'Ben', 'Cindy');
// Lời chào: Hello
// Danh sách tên: [ 'Anna', 'Ben', 'Cindy' ]
// Hello, Anna
// Hello, Ben
// Hello, Cindy

greetUser('Hi'); // names sẽ là mảng rỗng
// Lời chào: Hi
// Danh sách tên: []

Giải thích:

  • Khi ... được sử dụng trong danh sách tham số khi định nghĩa hàm, nó hoạt động như Rest parameters, thu thập tất cả các đối số còn lại vào một mảng.
  • Rest parameters chỉ có thể xuất hiện một lầnluôn ở cuối cùng trong danh sách tham số.

Lời khuyên: Sử dụng Spread để "bung" ra và Rest để "gom lại". Chúng cực kỳ hữu ích khi làm việc với số lượng đối số/phần tử không xác định hoặc để thao tác với mảng/đối tượng một cách bất biến (tạo bản sao mới thay vì sửa trực tiếp).

5. Template Literals - Chuỗi Dễ Đọc Hơn Bao Giờ Hết

Template Literals (hay Template Strings) cho phép bạn làm việc với chuỗi một cách linh hoạt hơn, hỗ trợ nội suy biến (embedding expressions) và chuỗi nhiều dòng mà không cần ký tự thoát đặc biệt.

Bài tập 9: Nội suy biến và Chuỗi nhiều dòng

console.log("\n--- Bài tập 9: Template Literals ---");

const subject = 'JavaScript';
const topic = 'ES6+';
const version = 'modern';

// Nội suy biến (Interpolation)
const greeting = `Chào mừng bạn đến với ${subject} ${version}, đặc biệt là ${topic}!`;
console.log(greeting); // Chào mừng bạn đến với JavaScript modern, đặc biệt là ES6+!

// Có thể nhúng các biểu thức JavaScript
const price = 10;
const vatRate = 0.1;
const total = `Tổng cộng: ${price * (1 + vatRate)} USD`;
console.log(total); // Tổng cộng: 11 USD

// Chuỗi nhiều dòng (Multi-line strings)
const multiLinePoem = `Dòng một
Dòng hai
  Dòng ba (có thụt lề)`;
console.log(multiLinePoem);
// Dòng một
// Dòng hai
//   Dòng ba (có thụt lề)

// Sử dụng với các ký tự đặc biệt không cần thoát
const specialChars = `Thẻ HTML: <div class="container">
Dấu nháy đơn: '
Dấu nháy kép: "
Backticks: \``; // Cần thoát backtick nếu muốn hiển thị backtick thật
console.log(specialChars);

Giải thích:

  • Template Literals được bao bọc bởi cặp ký tự backticks (``), không phải nháy đơn (') hay nháy kép (").
  • Bạn có thể nhúng bất kỳ biểu thức JavaScript nào vào trong chuỗi bằng cú pháp ${bieu_thuc_js}. Kết quả của biểu thức sẽ được chuyển đổi thành chuỗi và chèn vào vị trí đó.
  • Chuỗi có thể trải dài trên nhiều dòng trong code nguồn mà không cần sử dụng ký tự xuống dòng \n. Khoảng trắng và ngắt dòng sẽ được giữ nguyên.
  • Hầu hết các ký tự đặc biệt (bao gồm cả dấu nháy đơn, kép) đều có thể sử dụng trực tiếp mà không cần ký tự thoát \. Chỉ cần thoát ký tự backtick nếu muốn hiển thị nó bên trong template literal.

Lời khuyên: Template Literals làm cho việc tạo chuỗi phức tạp (có nhiều biến, nhiều dòng) trở nên dễ đọcdễ viết hơn rất nhiều so với việc nối chuỗi truyền thống bằng toán tử +.

6. Lớp (Classes) - Xây Dựng Đối Tượng Dễ Dàng Hơn

Mặc dù JavaScript là ngôn ngữ dựa trên Prototype, ES6 giới thiệu cú pháp class như một lớp "đường" (syntactic sugar) giúp việc làm việc với các mẫu đối tượng và kế thừa trở nên quen thuộc hơn với những người quen thuộc với các ngôn ngữ hướng đối tượng truyền thống như Java hay C++.

Bài tập 10: Định nghĩa và sử dụng Class cơ bản

console.log("\n--- Bài tập 10: Class cơ bản ---");

// Định nghĩa Class
class Animal {
  // Constructor - Phương thức được gọi khi tạo đối tượng mới
  constructor(name, species) {
    this.name = name; // Thuộc tính
    this.species = species;
  }

  // Phương thức của Class
  introduce() {
    console.log(`Xin chào! Tôi là ${this.name}, thuộc loài ${this.species}.`);
  }

  // Getter (đọc thuộc tính)
  get fullName() {
    return `${this.name} (${this.species})`;
  }

  // Setter (ghi thuộc tính) - Ít phổ biến hơn cho ví dụ đơn giản này
  set species(newSpecies) {
    if (newSpecies.length > 2) { // Kiểm tra đơn giản
        this._species = newSpecies; // Lưu vào thuộc tính nội bộ
    } else {
        console.error("Tên loài quá ngắn!");
    }
  }

  get species() { // Cần getter tương ứng cho setter
      return this._species;
  }
}

// Tạo đối tượng từ Class (Instantiate)
const myPet = new Animal('Max', 'Dog');
const anotherPet = new Animal('Whiskers', 'Cat');

// Sử dụng phương thức và getter
myPet.introduce();      // Xin chào! Tôi là Max, thuộc loài Dog.
anotherPet.introduce(); // Xin chào! Tôi là Whiskers, thuộc loài Cat.

console.log('Tên đầy đủ của Max:', myPet.fullName); // Tên đầy đủ của Max: Max (Dog)

// Sử dụng setter
myPet.species = 'Super Dog'; // Gán giá trị mới qua setter
myPet.introduce(); // Xin chào! Tôi là Max, thuộc loài Super Dog.

myPet.species = 'D'; // Thử gán giá trị không hợp lệ
// console.error("Tên loài quá ngắn!"); (Lỗi được in ra)
console.log('Loài của Max sau khi thử gán lỗi:', myPet.species); // Loài của Max sau khi thử gán lỗi: Super Dog

Giải thích:

  • Sử dụng từ khóa class để định nghĩa một lớp.
  • Phương thức constructor là nơi bạn khởi tạo các thuộc tính của đối tượng khi nó được tạo bằng new.
  • Các phương thức khác được định nghĩa trực tiếp trong thân lớp.
  • Bạn có thể định nghĩa getset để tạo các thuộc tính "ảo" hoặc thêm logic khi truy cập/thay đổi thuộc tính. Lưu ý trong ví dụ setter, tôi đã lưu giá trị vào this._species để tránh lặp vô hạn khi setter gọi lại getter/setter chính.

Bài tập 11: Kế thừa (Inheritance)

Class hỗ trợ kế thừa bằng từ khóa extends.

console.log("\n--- Bài tập 11: Kế thừa Class ---");

// Class con kế thừa từ Animal
class Dog extends Animal {
  constructor(name, breed) {
    // Gọi constructor của Class cha
    super(name, 'Dog'); // Luôn gọi super trước khi dùng 'this' trong constructor của class con
    this.breed = breed; // Thuộc tính riêng của Dog
  }

  // Phương thức riêng của Dog
  bark() {
    console.log(`${this.name} nói: Woof woof!`);
  }

  // Ghi đè phương thức của Class cha
  introduce() {
    console.log(`Xin chào! Tôi là ${this.name}, một chú chó giống ${this.breed}.`);
  }
}

// Tạo đối tượng từ Class con
const myDoggy = new Dog('Buddy', 'Golden Retriever');

// Sử dụng phương thức kế thừa và phương thức riêng
myDoggy.introduce(); // Xin chào! Tôi là Buddy, một chú chó giống Golden Retriever. (Phương thức đã bị ghi đè)
myDoggy.bark();      // Buddy nói: Woof woof!

// Vẫn có thể truy cập các thuộc tính
console.log('Tên của doggy:', myDoggy.name);   // Tên của doggy: Buddy
console.log('Loài của doggy:', myDoggy.species); // Loài của doggy: Dog (set trong super)

Giải thích:

  • Sử dụng extends TenClassCha để tạo một class con kế thừa các thuộc tính và phương thức của class cha.
  • Trong constructor của class con, bạn phải gọi super() trước khi sử dụng từ khóa this. super() gọi constructor của class cha.
  • Class con có thể định nghĩa các thuộc tính và phương thức riêng của nó.
  • Class con có thể ghi đè (override) các phương thức của class cha bằng cách định nghĩa lại phương thức cùng tên.

Lời khuyên: Cú pháp Class giúp tổ chức code hướng đối tượng rõ ràng và dễ hiểu hơn. Sử dụng nó khi bạn cần tạo nhiều đối tượng có cùng cấu trúc và hành vi, hoặc khi mô hình hóa các mối quan hệ kế thừa.

7. Async/Await & Promises - Xử Lý Bất Đồng Bộ Thanh Lịch

JavaScript xử lý nhiều tác vụ bất đồng bộ (như gọi API, đọc file, hẹn giờ). Trước ES6, điều này thường dẫn đến "callback hell". Promises (từ ES6) và Async/Await (từ ES2017) là các công cụ giúp quản lý code bất đồng bộ một cách thanh lịchdễ đọc hơn nhiều.

Bài tập 12: Làm việc với Promises

Promise là một đối tượng đại diện cho kết quả cuối cùng của một thao tác bất đồng bộ. Nó có thể ở một trong ba trạng thái: pending (đang chờ), fulfilled (hoàn thành thành công) hoặc rejected (thất bại).

console.log("\n--- Bài tập 12: Sử dụng Promises ---");

function simulateAsyncOperation(success, delay, data, error) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (success) {
        console.log(`--- Thao tác thành công sau ${delay}ms ---`);
        resolve(data); // Hoàn thành thành công, gọi resolve
      } else {
        console.error(`--- Thao tác thất bại sau ${delay}ms ---`);
        reject(error); // Thất bại, gọi reject
      }
    }, delay);
  });
}

// Sử dụng Promise
console.log('Bắt đầu gọi Promise thành công...');
simulateAsyncOperation(true, 1000, { id: 1, message: 'Data fetched' })
  .then(result => {
    console.log('Promise thành công với dữ liệu:', result);
    // Có thể trả về một Promise khác để chaining
    return simulateAsyncOperation(true, 500, 'Second step complete');
  })
  .then(secondResult => {
      console.log('Bước thứ hai thành công:', secondResult);
  })
  .catch(error => {
    // Xử lý lỗi của bất kỳ Promise nào trong chuỗi
    console.error('Promise thất bại:', error);
  })
  .finally(() => {
    console.log('Promise hoàn thành (dù thành công hay thất bại)');
  });

console.log('\nBắt đầu gọi Promise thất bại...');
simulateAsyncOperation(false, 1500, null, 'Network Error')
  .then(result => {
    console.log('Đây sẽ không chạy vì promise thất bại');
  })
  .catch(error => {
    console.error('Promise thất bại (bị bắt bởi catch):', error); // Bắt lỗi tại đây
  })
  .finally(() => {
    console.log('Promise thất bại hoàn thành (finally)');
  });

console.log('Các Promise đã được gọi (code đồng bộ tiếp tục chạy ngay)');

Giải thích:

  • new Promise((resolve, reject) => { ... }): Tạo một Promise mới. Hàm callback nhận hai tham số resolvereject. Gọi resolve() khi thao tác thành công, gọi reject() khi thất bại.
  • .then(callback): Đăng ký hàm callback sẽ được gọi khi Promise fulfilled. Nó nhận giá trị từ resolve. .then cũng trả về một Promise mới, cho phép chaining (.then().then()...).
  • .catch(callback): Đăng ký hàm callback sẽ được gọi khi Promise rejected. Nó nhận giá trị từ reject. Thường dùng để xử lý lỗi tập trung.
  • .finally(callback): Đăng ký hàm callback sẽ được gọi khi Promise hoàn thành (dù fulfilled hay rejected). Hữu ích cho việc dọn dẹp (ví dụ: ẩn spinner loading).

Bài tập 13: Sử dụng Async/Await

Async/Await là cú pháp giúp làm việc với Promises trở nên gần giống với code đồng bộ hơn, giúp code bất đồng bộ dễ đọcdễ viết hơn đáng kể.

  • async function: Một hàm được đánh dấu là async luôn trả về một Promise. Bên trong hàm async, bạn có thể sử dụng từ khóa await.
  • await: Chỉ được sử dụng bên trong hàm async. Đặt await trước một Promise sẽ tạm dừng việc thực thi của hàm async đó cho đến khi Promise hoàn thành (fulfilled hoặc rejected). Nếu Promise fulfilled, await trả về giá trị resolve. Nếu Promise rejected, await sẽ ném ra một lỗi.
console.log("\n--- Bài tập 13: Sử dụng Async/Await ---");

function simulateFetchData(data, delay) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`Đã lấy dữ liệu:`, data);
      resolve(data);
    }, delay);
  });
}

function simulateProcessData(data, delay) {
   return new Promise((resolve, reject) => {
     setTimeout(() => {
       if (data) {
         console.log(`Đã xử lý dữ liệu: ${typeof data === 'object' ? JSON.stringify(data) : data}`);
         resolve(`Processed: ${typeof data === 'object' ? JSON.stringify(data) : data}`);
       } else {
         reject("Không có dữ liệu để xử lý!");
       }
     }, delay);
   });
}


async function performOperations() {
  console.log('Bắt đầu các thao tác bất đồng bộ với async/await...');

  try {
    // Await sẽ tạm dừng ở đây cho đến khi simulateFetchData hoàn thành
    const userData = await simulateFetchData({ user: 'Eva' }, 1500);
    console.log('Nhận được userData:', userData);

    // Await sẽ tạm dừng ở đây cho đến khi simulateProcessData hoàn thành với userData
    const processedUserData = await simulateProcessData(userData, 1000);
    console.log('Nhận được processedUserData:', processedUserData);

    // Thử một thao tác có thể thất bại
    const result = await simulateProcessData(null, 500);
    console.log('Đây sẽ không chạy nếu dòng trên thất bại:', result);

  } catch (error) {
    // Nếu bất kỳ await nào trong try block ném ra lỗi (Promise bị reject),
    // khối catch này sẽ bắt lỗi đó.
    console.error('Đã xảy ra lỗi trong quá trình thực hiện:', error);
  } finally {
      console.log('Kết thúc hàm performOperations (dù thành công hay thất bại).');
  }

  // Code sau await cuối cùng chỉ chạy khi Promise trước đó resolve thành công
  // Nếu có catch block, code sau catch cũng sẽ chạy
  console.log('Kết thúc hàm async.');
}

performOperations();
console.log('Hàm async đã được gọi (code đồng bộ tiếp tục chạy ngay).');

Giải thích:

  • Hàm performOperations được đánh dấu là async.
  • Bên trong performOperations, chúng ta dùng await trước lời gọi các hàm trả về Promise (simulateFetchData, simulateProcessData).
  • Việc sử dụng await làm cho luồng code trông giống như đồng bộ: lấy dữ liệu người dùng xong mới xử lý dữ liệu người dùng.
  • Khối try...catch được sử dụng để xử lý lỗi một cách đồng bộ hơn. Nếu bất kỳ Promise nào bị reject bởi await, lỗi sẽ được ném ra và bị bắt bởi khối catch tương ứng.
  • Khối finally hoạt động tương tự như với .finally của Promise.

Lời khuyên: Async/Await là cách ưa thích hiện nay để viết code bất đồng bộ trong JavaScript vì nó cải thiện đáng kể tính dễ đọcdễ bảo trì so với việc chỉ dùng .then().catch() chaining, đặc biệt với các luồng logic phức tạp.

8. Tham Số Mặc Định (Default Parameters)

ES6 cho phép bạn chỉ định giá trị mặc định cho các tham số hàm ngay trong định nghĩa hàm. Điều này giúp loại bỏ việc phải kiểm tra if (param === undefined) ở đầu hàm.

Bài tập 14: Sử dụng Default Parameters

console.log("\n--- Bài tập 14: Tham Số Mặc Định ---");

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

greet();             // Hello, Guest! (Sử dụng cả hai giá trị mặc định)
greet('Daniel');     // Hello, Daniel! (Chỉ sử dụng giá trị mặc định cho greeting)
greet('Emily', 'Hi'); // Hi, Emily! (Không sử dụng giá trị mặc định nào)

// Có thể sử dụng kết quả của biểu thức hoặc giá trị của tham số khác
function calculateTotal(price, quantity = 1, discount = price * 0.1) {
  return (price * quantity) - discount;
}

console.log('Tổng đơn hàng 1:', calculateTotal(100));    // Tổng đơn hàng 1: 90 (price=100, quantity=1, discount=100*0.1=10)
console.log('Tổng đơn hàng 2:', calculateTotal(100, 2)); // Tổng đơn hàng 2: 180 (price=100, quantity=2, discount=100*0.1=10)
console.log('Tổng đơn hàng 3:', calculateTotal(100, 2, 5)); // Tổng đơn hàng 3: 195 (price=100, quantity=2, discount=5)

Giải thích:

  • Chỉ cần thêm = giá_trị_mặc_định sau tên tham số trong định nghĩa hàm.
  • Giá trị mặc định chỉ được sử dụng khi tham số tương ứng không được truyền (hoặc được truyền undefined).
  • Giá trị mặc định có thể là bất kỳ biểu thức hợp lệ nào, và các biểu thức này chỉ được đánh giá khi cần thiết.
  • Bạn có thể sử dụng các tham số đứng trước trong biểu thức của tham số mặc định.

Lời khuyên: Sử dụng tham số mặc định để làm cho các hàm của bạn linh hoạt và dễ sử dụng hơn, giảm thiểu các đoạn code kiểm tra giá trị undefined lặp đi lặp lại.

Comments

There are no comments at the moment.