JavaScript Nâng Cao - Kỳ 29
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. Tham chiếu và giá trị mặc định trong hàm
Output của đoạn code bên dưới là gì?
const person = {
name: "Lydia Hallie",
hobbies: ["coding"]
};
function addHobby(hobby, hobbies = person.hobbies) {
hobbies.push(hobby);
return hobbies;
}
addHobby("running", []);
addHobby("dancing");
addHobby("baking", person.hobbies);
console.log(person.hobbies);
- A:
["coding"]
- B:
["coding", "dancing"]
- C:
["coding", "dancing", "baking"]
- D:
["coding", "running", "dancing", "baking"]
Đá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é ❓️
1.1. Phân tích vấn đề
Để hiểu được kết quả này, chúng ta cần phân tích từng lời gọi hàm addHobby
:
-
addHobby("running", [])
:- Ở đây, chúng ta truyền một mảng rỗng
[]
làm đối số thứ hai. - Hàm sẽ thêm "running" vào mảng rỗng này và trả về
["running"]
. - Tuy nhiên, điều này không ảnh hưởng đến
person.hobbies
.
- Ở đây, chúng ta truyền một mảng rỗng
-
addHobby("dancing")
:- Ở đây, chúng ta không truyền đối số thứ hai.
- Do đó, hàm sẽ sử dụng giá trị mặc định
person.hobbies
. - "dancing" được thêm vào
person.hobbies
.
-
addHobby("baking", person.hobbies)
:- Ở đây, chúng ta trực tiếp truyền
person.hobbies
làm đối số thứ hai. - "baking" được thêm vào
person.hobbies
.
- Ở đây, chúng ta trực tiếp truyền
1.2. Giải thích chi tiết
Điểm quan trọng cần lưu ý là khi chúng ta sử dụng person.hobbies
làm giá trị mặc định cho tham số hobbies
, chúng ta đang tạo một tham chiếu đến mảng gốc trong object person
.
Khi gọi addHobby("dancing")
, do không có đối số thứ hai, hàm sử dụng giá trị mặc định person.hobbies
. Điều này có nghĩa là "dancing" được thêm trực tiếp vào mảng hobbies
của person
.
Tương tự, khi gọi addHobby("baking", person.hobbies)
, chúng ta lại trực tiếp thao tác trên mảng hobbies
của person
.
1.3. Kết luận
Vì vậy, sau tất cả các lời gọi hàm, person.hobbies
sẽ chứa ["coding", "dancing", "baking"]
. Lời gọi đầu tiên với mảng rỗng không ảnh hưởng đến person.hobbies
, trong khi hai lời gọi sau đó đều thêm phần tử vào mảng gốc.
Đây là một ví dụ điển hình về cách mà tham chiếu (reference) hoạt động trong JavaScript và cách mà giá trị mặc định trong hàm có thể ảnh hưởng đến kết quả.
2. Kế thừa và Constructor trong Class
Output của đoạn code bên dưới là gì?
class Bird {
constructor() {
console.log("I'm a bird. 🦢");
}
}
class Flamingo extends Bird {
constructor() {
console.log("I'm pink. 🌸");
super();
}
}
const pet = new Flamingo();
- A:
I'm pink. 🌸
- B:
I'm pink. 🌸
I'm a bird. 🦢
- C:
I'm a bird. 🦢
I'm pink. 🌸
- D: Nothing, we didn't call any method
Đá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é ❓️
2.1. Phân tích vấn đề
Để hiểu được kết quả này, chúng ta cần phân tích cách mà kế thừa và constructor hoạt động trong JavaScript:
- Chúng ta có hai class:
Bird
(lớp cha) vàFlamingo
(lớp con kế thừa từBird
). - Mỗi class đều có constructor riêng.
- Trong constructor của
Flamingo
, chúng ta gọisuper()
.
2.2. Giải thích chi tiết
Khi chúng ta tạo một instance mới của Flamingo
bằng cách gọi new Flamingo()
, điều gì xảy ra?
- Constructor của
Flamingo
được gọi đầu tiên. - Dòng
console.log("I'm pink. 🌸");
trong constructor củaFlamingo
được thực thi. - Sau đó,
super()
được gọi.super()
ở đây có nghĩa là gọi constructor của lớp cha (Bird). - Constructor của
Bird
được thực thi, in ra"I'm a bird. 🦢"
.
2.3. Vai trò của super()
super()
là một phương thức đặc biệt được sử dụng để gọi constructor của lớp cha. Trong JavaScript, khi một class kế thừa từ một class khác, nó phải gọi super()
trong constructor của nó trước khi sử dụng this
.
Nếu chúng ta không gọi super()
trong constructor của lớp con, JavaScript sẽ throw một ReferenceError.
2.4. Kết luận
Vì vậy, khi chúng ta tạo một instance mới của Flamingo
, chúng ta sẽ thấy hai dòng output:
"I'm pink. 🌸"
(từ constructor của Flamingo)"I'm a bird. 🦢"
(từ constructor của Bird, được gọi thông quasuper()
)
3. Thao tác với mảng và hằng số
Câu lệnh nào sẽ bị lỗi?
const emojis = ["🎄", "🎅🏼", "🎁", "⭐"];
/* 1 */ emojis.push("🦌");
/* 2 */ emojis.splice(0, 2);
/* 3 */ emojis = [...emojis, "🥂"];
/* 4 */ emojis.length = 0;
- A: 1
- B: 1 and 2
- C: 3 and 4
- D: 3
Đá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é ❓️
3.1. Phân tích vấn đề
Để hiểu được kết quả này, chúng ta cần phân tích từng câu lệnh và hiểu về cách const
hoạt động với mảng trong JavaScript:
emojis.push("🦌")
: Thêm một phần tử vào cuối mảng.emojis.splice(0, 2)
: Xóa 2 phần tử đầu tiên của mảng.emojis = [...emojis, "🥂"]
: Cố gắng gán lại giá trị choemojis
.emojis.length = 0
: Đặt độ dài của mảng về 0, xóa tất cả phần tử.
3.2. Giải thích chi tiết
Khi chúng ta khai báo một mảng với const
, điều đó có nghĩa là chúng ta không thể gán lại biến đó cho một giá trị mới. Tuy nhiên, nó không ngăn chúng ta thay đổi nội dung của mảng.
-
Câu lệnh 1 và 2 hoàn toàn hợp lệ vì chúng chỉ thay đổi nội dung của mảng, không gán lại biến
emojis
. -
Câu lệnh 3 sẽ gây ra lỗi vì nó cố gắng gán lại giá trị cho
emojis
. Mặc dù chúng ta đang sử dụng spread operator...
để tạo một mảng mới, nhưng việc gán lại giá trị cho một biếnconst
là không được phép. -
Câu lệnh 4 là hợp lệ vì nó chỉ thay đổi thuộc tính
length
của mảng, không gán lại giá trị cho biếnemojis
.
3.3. Hiểu về const
với mảng và object
Điều quan trọng cần nhớ là const
chỉ ngăn chặn việc gán lại biến, không ngăn chặn việc thay đổi nội dung của mảng hoặc object. Đây là một điểm gây nhầm lẫn phổ biến trong JavaScript.
Với mảng và object, const
đảm bảo rằng biến luôn trỏ đến cùng một vùng nhớ, nhưng không đảm bảo rằng nội dung của vùng nhớ đó không thay đổi.
3.4. Kết luận
Vì vậy, chỉ có câu lệnh 3 sẽ gây ra lỗi. Các câu lệnh khác, mặc dù thay đổi nội dung của mảng, vẫn là hợp lệ khi làm việc với một mảng được khai báo bằng const
.
4. Iterator và Symbol.iterator
Ta cần thêm gì vào object person
để khi gọi [...person]
sẽ cho kết quả là ["Lydia Hallie", 21]
?
const person = {
name: "Lydia Hallie",
age: 21
}
[...person] // ["Lydia Hallie", 21]
- A: Nothing, object are iterable by default
- B:
*[Symbol.iterator]() { for (let x in this) yield* this[x] }
- C:
*[Symbol.iterator]() { for (let x in this) yield* Object.values(this) }
- D:
*[Symbol.iterator]() { for (let x in this) yield this }
Đá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é ❓️
4.1. Phân tích vấn đề
Để hiểu được kết quả này, chúng ta cần biết về:
- Cách spread operator
...
hoạt động - Khái niệm về iterable trong JavaScript
- Cách sử dụng
Symbol.iterator
để tạo một iterable object trong JavaScript
4.2. Giải thích chi tiết
-
Spread operator và iterable:
- Spread operator
...
hoạt động với các đối tượng có thể lặp (iterable). - Mặc định, object trong JavaScript không phải là iterable.
- Spread operator
-
Symbol.iterator:
- Để làm cho một object trở thành iterable, chúng ta cần định nghĩa phương thức
Symbol.iterator
. - Phương thức này phải trả về một iterator object, thường là một generator.
- Để làm cho một object trở thành iterable, chúng ta cần định nghĩa phương thức
-
Generator function:
- Generator function được định nghĩa bằng cách sử dụng dấu
*
. - Nó cho phép chúng ta sử dụng từ khóa
yield
để trả về các giá trị tuần tự.
- Generator function được định nghĩa bằng cách sử dụng dấu
-
Phân tích các lựa chọn:
- A: Không đúng vì object không phải iterable mặc định.
- B: Gần đúng, nhưng
yield* this[x]
sẽ không hoạt động như mong muốn. - C: Đúng,
yield* Object.values(this)
sẽ trả về tất cả giá trị của object. - D: Không đúng,
yield this
sẽ trả về toàn bộ object thay vì các giá trị riêng lẻ.
4.3. Giải pháp chi tiết
Đáp án C là chính xác:
const person = {
name: "Lydia Hallie",
age: 21,
*[Symbol.iterator]() {
yield* Object.values(this);
}
};
*[Symbol.iterator]()
: Định nghĩa một generator function làm phương thức iterator.yield* Object.values(this)
: Sử dụngyield*
để yield tất cả giá trị từObject.values(this)
.Object.values(this)
trả về một mảng chứa tất cả giá trị của object.
Khi sử dụng [...person]
, JavaScript sẽ gọi phương thức Symbol.iterator
, và generator sẽ yield lần lượt các giá trị "Lydia Hallie" và 21.
5. Truthy và Falsy trong JavaScript
Output là gì?
let count = 0;
const nums = [0, 1, 2, 3];
nums.forEach(num => {
if (num) count += 1
})
console.log(count)
- A: 1
- B: 2
- C: 3
- D: 4
Đá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é ❓️
5.1. Phân tích vấn đề
Để hiểu được kết quả này, chúng ta cần xem xét:
- Cách hoạt động của phương thức
forEach
- Khái niệm về truthy và falsy trong JavaScript
- Cách điều kiện
if
đánh giá các giá trị
5.2. Giải thích chi tiết
-
Phương thức
forEach
:forEach
lặp qua từng phần tử trong mảngnums
.- Với mỗi phần tử, nó gọi hàm callback được cung cấp.
-
Truthy và Falsy:
- Trong JavaScript, mọi giá trị đều có tính chất boolean ngầm định.
- Giá trị 0 là falsy, mọi số khác 0 đều là truthy.
-
Điều kiện
if
:if (num)
sẽ đánh giánum
như một giá trị boolean.- Nếu
num
là truthy, khối lệnh bên trongif
sẽ được thực thi.
-
Phân tích từng lần lặp:
- Với 0:
if(0)
là false, không tăngcount
- Với 1:
if(1)
là true,count
tăng lên 1 - Với 2:
if(2)
là true,count
tăng lên 2 - Với 3:
if(3)
là true,count
tăng lên 3
- Với 0:
5.3. Kết luận
Sau khi lặp qua tất cả các phần tử, count
sẽ có giá trị là 3. Điều này là vì có 3 số trong mảng nums
(1, 2, và 3) được đánh giá là truthy.
Một số điểm cần nhớ:
- Số 0 là falsy
- Chuỗi rỗng "" là falsy
- null và undefined đều là falsy
- Mọi số khác 0 (kể cả số âm) đều là truthy
- Mọi chuỗi không rỗng đều là truthy
- Tất cả các object và array (kể cả rỗng) đều là truthy
Hiểu rõ về các khái niệm này sẽ giúp bạn viết code JavaScript hiệu quả và tránh được nhiều lỗi tiềm ẩn.
Đó là tất cả cho JavaScript Nâng Cao - Kỳ 29. Hy vọng qua bài viết này, các bạn đã hiểu rõ hơn về cách hoạt động của tham chiếu và giá trị mặc định trong hàm, kế thừa và constructor trong class, thao tác với mảng và hằng số, iterator và Symbol.iterator, cũng như truthy và falsy trong JavaScript.
Nếu có bất kỳ câu hỏi hoặc thắc mắc nào, đừng ngại comment bên dưới nhé. Hẹn gặp lại các bạn trong các bài viết tiếp theo của series JavaScript Nâng Cao!
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