+9

JavaScript Nâng Cao - Kỳ 28

Có một câu nói vui là: Trên đời chỉ có thứ nhiều người chửi và thứ không ai thèm dùng.

Javascript là một ví dụ điển hình, nó có một số điểm thú vị nhưng cũng khiến chúng ta phải đau đầu. Lý thuyết thì dễ hiểu, nhưng khi thực hành là cả một vấn đề. Vậy nên, mình sẽ cùng các bạn đi sâu vào từng ví dụ cụ thể và phân tích, mổ xẻ nó để hiểu hơn về Javascript nhé.

Series này có thể sẽ khá dài mình không biết sẽ có bao nhiêu Kỳ tuy nhiên để tiện cho các bạn nào không đọc các bài trước đó của mình về JS thì trong loạt bài này mình sẽ giải thích lại toàn bộ. Các lý thuyết trong loạt bài này mình cũng có thể sẽ giải thích lại nhiều lần (tùy hứng) để các bạn có thể nắm rõ nó hơn nhé.

Ok vào bài thôi nào... GÉT GÔ 🚀

Nếu có bất kỳ câu hỏi nào đừng ngại hãy bình luận dưới phần comment nhé. Hoặc chỉ cần để lại một comment chào mình là đã giúp mình có thêm động lực hoàn thành series này. Cảm ơn các bạn rất nhiều. 🤗

1. Object.seal() và Object.freeze()

Trước khi đi vào ví dụ cụ thể, chúng ta hãy cùng tìm hiểu về hai phương thức quan trọng trong JavaScript: Object.seal()Object.freeze().

1.1. Object.seal()

Object.seal() là một phương thức trong JavaScript được sử dụng để "niêm phong" một object. Khi một object bị niêm phong:

  • Không thể thêm thuộc tính mới vào object.
  • Không thể xóa các thuộc tính hiện có của object.
  • Có thể thay đổi giá trị của các thuộc tính hiện có.

Ví dụ:

const person = { name: "Lydia Hallie" };

Object.seal(person);

person.name = "Evan Bacon"; // Được phép
person.age = 21; // Không được phép
delete person.name; // Không được phép

console.log(person); // { name: "Evan Bacon" }

1.2. Object.freeze()

Object.freeze() là một phương thức mạnh mẽ hơn Object.seal(). Khi một object bị đóng băng:

  • Không thể thêm thuộc tính mới vào object.
  • Không thể xóa các thuộc tính hiện có của object.
  • Không thể thay đổi giá trị của các thuộc tính hiện có.

Tuy nhiên, Object.freeze() chỉ đóng băng nông (shallow freeze). Điều này có nghĩa là nếu object có các thuộc tính là object con, các object con này vẫn có thể bị thay đổi.

Ví dụ:

const person = {
  name: "Lydia Hallie",
  address: {
    street: "100 Main St"
  }
};

Object.freeze(person);

person.name = "Evan Bacon"; // Không được phép
person.age = 21; // Không được phép
delete person.name; // Không được phép
person.address.street = "101 Main St"; // Được phép (vì là object con)

console.log(person);
// { 
//   name: "Lydia Hallie", 
//   address: { street: "101 Main St" } 
// }

Giờ chúng ta hãy cùng xem xét các ví dụ cụ thể nhé!

2. Ví dụ về Object.seal()

const person = { name: "Lydia Hallie" };

Object.seal(person);

Câu hỏi: Cách nào sau đây sẽ thay đổi object person?

  • A: person.name = "Evan Bacon"
  • B: person.age = 21
  • C: delete person.name
  • D: Object.assign(person, { age: 21 })
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A

Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️

2.1. Phân tích

Như đã giải thích ở phần trước, Object.seal() cho phép chúng ta thay đổi giá trị của các thuộc tính hiện có, nhưng không cho phép thêm mới hoặc xóa thuộc tính.

  • Đáp án A: person.name = "Evan Bacon" - Đúng, vì đây là thay đổi giá trị của thuộc tính hiện có.
  • Đáp án B: person.age = 21 - Sai, vì đây là thêm mới một thuộc tính.
  • Đáp án C: delete person.name - Sai, vì đây là xóa một thuộc tính.
  • Đáp án D: Object.assign(person, { age: 21 }) - Sai, vì đây cũng là thêm mới một thuộc tính.

2.2. Kết luận

Vì vậy, chỉ có đáp án A là có thể thay đổi object person sau khi đã được seal.

3. Ví dụ về Object.freeze()

const person = {
  name: "Lydia Hallie",
  address: {
    street: "100 Main St"
  }
};

Object.freeze(person);

Câu hỏi: Cách nào sau đây có thể thay đổi object person?

  • A: person.name = "Evan Bacon"
  • B: delete person.address
  • C: person.address.street = "101 Main St"
  • D: person.pet = { name: "Mara" }
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: C

Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️

3.1. Phân tích

Object.freeze() tạo ra một "shallow freeze" trên object. Điều này có nghĩa là:

  • Đáp án A: person.name = "Evan Bacon" - Sai, vì không thể thay đổi thuộc tính trực tiếp của object đã freeze.
  • Đáp án B: delete person.address - Sai, vì không thể xóa thuộc tính của object đã freeze.
  • Đáp án C: person.address.street = "101 Main St" - Đúng, vì address là một object con và Object.freeze() không ảnh hưởng đến các thuộc tính bên trong object con.
  • Đáp án D: person.pet = { name: "Mara" } - Sai, vì không thể thêm thuộc tính mới vào object đã freeze.

3.2. Kết luận

Chỉ có đáp án C là có thể thay đổi object person sau khi đã được freeze, vì nó thay đổi thuộc tính của object con, không phải object chính.

4. Giá trị mặc định và Arrow Function

const add = x => x + x;

function myFunc(num = 2, value = add(num)) {
  console.log(num, value);
}

myFunc();
myFunc(3);

Câu hỏi: Output là gì?

  • A: 2 4 and 3 6
  • B: 2 NaN and 3 NaN
  • C: 2 Error and 3 6
  • D: 2 4 and 3 Error
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A

Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️

4.1. Phân tích

Trong ví dụ này, chúng ta có một arrow function add và một function myFunc với hai tham số có giá trị mặc định.

  1. Khi gọi myFunc():

    • num nhận giá trị mặc định là 2.
    • value nhận giá trị mặc định là kết quả của add(num), tức là add(2), which equals 2 + 2 = 4.
    • Output: 2 4
  2. Khi gọi myFunc(3):

    • num nhận giá trị 3.
    • value vẫn nhận giá trị mặc định là kết quả của add(num), nhưng lúc này num là 3, nên add(3) equals 3 + 3 = 6.
    • Output: 3 6

4.2. Kết luận

Vì vậy, output sẽ là 2 4 cho lần gọi đầu tiên và 3 6 cho lần gọi thứ hai.

5. Private Fields trong Class

class Counter {
  #number = 10

  increment() {
    this.#number++
  }

  getNum() {
    return this.#number
  }
}

const counter = new Counter()
counter.increment()

console.log(counter.#number)

Câu hỏi: Output là gì?

  • A: 10
  • B: 11
  • C: undefined
  • D: SyntaxError
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: D

Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️

5.1. Phân tích

Trong ví dụ này, chúng ta đang sử dụng một tính năng mới của JavaScript: private fields trong class. Private fields được khai báo bằng dấu # trước tên field.

  1. #number là một private field, chỉ có thể truy cập từ bên trong class.
  2. Phương thức increment() có thể truy cập và tăng giá trị của #number.
  3. Phương thức getNum() có thể truy cập và trả về giá trị của #number.
  4. Tuy nhiên, khi chúng ta cố gắng truy cập counter.#number từ bên ngoài class, JavaScript sẽ throw một SyntaxError.

5.2. Kết luận

Vì vậy, khi chúng ta cố gắng in ra counter.#number, JavaScript sẽ throw một SyntaxError, vì chúng ta không thể truy cập private fields từ bên ngoài class.

Để truy cập giá trị của #number, chúng ta nên sử dụng phương thức getNum():

console.log(counter.getNum()); // 11

Đây là cách JavaScript đảm bảo tính encapsulation trong OOP, giúp bảo vệ dữ liệu và ngăn chặn truy cập trực tiếp từ bên ngoài class.

6. Generator Functions

const teams = [
  { name: "Team 1", members: ["Paul", "Lisa"] },
  { name: "Team 2", members: ["Laura", "Tim"] }
];

function* getMembers(members) {
  for (let i = 0; i < members.length; i++) {
    yield members[i];
  }
}

function* getTeams(teams) {
  for (let i = 0; i < teams.length; i++) {
    // ✨ SOMETHING IS MISSING HERE ✨
  }
}

const obj = getTeams(teams);
obj.next(); // { value: "Paul", done: false }
obj.next(); // { value: "Lisa", done: false }

Câu hỏi: Câu lệnh còn thiếu là gì?

  • A: yield getMembers(teams[i].members)
  • B: yield* getMembers(teams[i].members)
  • C: return getMembers(teams[i].members)
  • D: return yield getMembers(teams[i].members)
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: B

Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️

6.1. Phân tích

Trong ví dụ này, chúng ta đang làm việc với generator functions. Generator functions là một loại hàm đặc biệt trong JavaScript, cho phép chúng ta tạm dừng và tiếp tục thực thi hàm.

  1. getMembers là một generator function, yield từng thành viên trong mảng members.
  2. getTeams là một generator function khác, cần yield các thành viên từ mỗi team.
  3. Chúng ta cần điền vào phần còn thiếu để getTeams có thể yield từng thành viên của mỗi team.

Đáp án B: yield* getMembers(teams[i].members) là chính xác vì:

  • yield* cho phép chúng ta "ủy quyền" việc yielding cho một generator function khác. Trong trường hợp này, chúng ta ủy quyền cho getMembers.
  • Khi sử dụng yield*, mỗi giá trị được yield bởi getMembers sẽ được yield bởi getTeams.

Lý do tại sao các đáp án khác không đúng:

  • Đáp án A: yield getMembers(teams[i].members) - Sai vì nó sẽ yield toàn bộ generator object từ getMembers, không phải từng giá trị riêng lẻ.
  • Đáp án C: return getMembers(teams[i].members) - Sai vì return sẽ kết thúc generator function ngay lập tức, không yield từng giá trị.
  • Đáp án D: return yield getMembers(teams[i].members) - Sai vì nó sẽ yield generator object và sau đó kết thúc function.

6.2. Kết luận

Sử dụng yield* là cách chính xác để "phẳng hóa" (flatten) các giá trị từ một generator function khác vào trong generator function hiện tại. Điều này cho phép chúng ta duyệt qua tất cả các thành viên của tất cả các team một cách liền mạch.

7. Tổng kết

Trong bài này, chúng ta đã đi sâu vào một số khía cạnh nâng cao của JavaScript:

  1. Object.seal() và Object.freeze(): Hai phương thức này giúp chúng ta kiểm soát việc thay đổi object. Object.seal() ngăn chặn thêm/xóa thuộc tính nhưng cho phép thay đổi giá trị, trong khi Object.freeze() ngăn chặn mọi thay đổi trên object (nhưng chỉ ở mức nông).

  2. Giá trị mặc định và Arrow Function: Chúng ta đã thấy cách giá trị mặc định trong tham số hàm hoạt động, và cách chúng tương tác với các arrow function.

  3. Private Fields trong Class: Tính năng mới này cho phép chúng ta tạo ra các thuộc tính private trong class, tăng cường tính encapsulation trong OOP JavaScript.

  4. Generator Functions: Chúng ta đã học cách sử dụng generator functions và từ khóa yield* để tạo ra các iterator phức tạp.

Những khái niệm này là một phần quan trọng của JavaScript hiện đại và thường xuyên xuất hiện trong các ứng dụng thực tế. Hiểu rõ về chúng sẽ giúp bạn viết code JavaScript hiệu quả và mạnh mẽ hơn.

8. Thử thách

Để củng cố kiến thức, hãy thử tạo một class BankAccount với các yêu cầu sau:

  1. Có một private field #balance để lưu số dư tài khoản.
  2. Có các phương thức public deposit(amount)withdraw(amount) để gửi và rút tiền.
  3. Sử dụng Object.freeze() để ngăn chặn việc thêm hoặc xóa các phương thức của class sau khi đã tạo.
  4. Tạo một generator function transactionHistory() để lưu và trả về lịch sử giao dịch.

Hãy thử implement class này và kiểm tra xem nó hoạt động như mong đợi không nhé!

Chúc các bạn học tập hiệu quả và hẹn gặp lại trong bài học tiếp theo! 👋

Nếu có bất kỳ câu hỏi nào đừng ngại hãy bình luận dưới phần comment nhé. Hoặc chỉ cần để lại một comment chào mình là đã giúp mình có thêm động lực hoàn thành series này. Cảm ơn các bạn rất nhiều. 🤗


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí