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ộtcomment 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()
và 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
and3
6
- B:
2
NaN
and3
NaN
- C:
2
Error
and3
6
- D:
2
4
and3
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.
-
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ủaadd(num)
, tức làadd(2)
, which equals2 + 2 = 4
.- Output:
2 4
-
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ủaadd(num)
, nhưng lúc nàynum
là 3, nênadd(3)
equals3 + 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.
#number
là một private field, chỉ có thể truy cập từ bên trong class.- Phương thức
increment()
có thể truy cập và tăng giá trị của#number
. - Phương thức
getNum()
có thể truy cập và trả về giá trị của#number
. - 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ộtSyntaxError
.
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.
getMembers
là một generator function, yield từng thành viên trong mảngmembers
.getTeams
là một generator function khác, cần yield các thành viên từ mỗi team.- 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 chogetMembers
.- Khi sử dụng
yield*
, mỗi giá trị được yield bởigetMembers
sẽ được yield bởigetTeams
.
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:
-
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 khiObject.freeze()
ngăn chặn mọi thay đổi trên object (nhưng chỉ ở mức nông). -
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.
-
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.
-
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:
- Có một private field
#balance
để lưu số dư tài khoản. - Có các phương thức public
deposit(amount)
vàwithdraw(amount)
để gửi và rút tiền. - 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. - 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ộtcomment 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