JavaScript Nâng Cao - Kỳ 23
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. Temporal Dead Zone và Hoisting
Output của đoạn code bên dưới là gì?
let name = 'Lydia'
function getName() {
console.log(name)
let name = 'Sarah'
}
getName()
- A: Lydia
- B: Sarah
- C:
undefined
- D:
ReferenceError
Đá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é ❓️
1.1. Hoisting và Temporal Dead Zone
Để hiểu được vấn đề này, chúng ta cần nắm rõ hai khái niệm quan trọng trong JavaScript: Hoisting và Temporal Dead Zone (TDZ).
Hoisting
Hoisting là một hành vi trong JavaScript, nơi các khai báo biến và hàm được di chuyển lên đầu scope của chúng trước khi code được thực thi. Tuy nhiên, chỉ có khai báo được hoisted, không phải là khởi tạo.
Temporal Dead Zone (TDZ)
TDZ là khoảng thời gian từ khi biến được hoisted đến khi nó được khởi tạo. Trong khoảng thời gian này, nếu ta cố gắng truy cập biến, JavaScript sẽ throw ra ReferenceError
.
1.2. Phân tích đoạn code
Bây giờ, hãy phân tích đoạn code của chúng ta:
let name = 'Lydia'
function getName() {
console.log(name)
let name = 'Sarah'
}
getName()
Khi hàm getName()
được gọi, JavaScript sẽ tạo ra một scope mới cho hàm này. Trong scope này, biến name
được khai báo bằng let
. Do hoisting, khai báo này sẽ được đưa lên đầu scope của hàm:
function getName() {
let name; // Hoisted
console.log(name)
name = 'Sarah'
}
Tuy nhiên, let
(và const
) không được khởi tạo trong quá trình hoisting như var
. Thay vào đó, chúng được đặt vào TDZ. Khi ta cố gắng truy cập name
trước khi nó được khởi tạo, JavaScript sẽ throw ra ReferenceError
.
1.3. Ví dụ minh họa
Để hiểu rõ hơn về TDZ, hãy xem xét ví dụ sau:
console.log(x); // Throws ReferenceError
let x = 5;
console.log(y); // Outputs: undefined
var y = 10;
Trong ví dụ này, x
được khai báo bằng let
nên nó nằm trong TDZ cho đến khi được khởi tạo. Còn y
được khai báo bằng var
, nên nó được hoisted và khởi tạo với giá trị undefined
.
1.4. Tóm lại
Quay lại với câu hỏi ban đầu, khi gọi getName()
, ta đang cố gắng truy cập biến name
trong khi nó đang ở trong TDZ. Điều này dẫn đến ReferenceError
.
Hiểu về hoisting và TDZ là rất quan trọng trong JavaScript. Nó giúp chúng ta tránh được những lỗi không mong muốn và viết code một cách rõ ràng, dễ đọc hơn.
2. Generator Functions và yield
Output của đoạn code bên dưới là gì?
function* generatorOne() {
yield ['a', 'b', 'c'];
}
function* generatorTwo() {
yield* ['a', 'b', 'c'];
}
const one = generatorOne()
const two = generatorTwo()
console.log(one.next().value)
console.log(two.next().value)
- A:
a
vàa
- B:
a
vàundefined
- C:
['a', 'b', 'c']
vàa
- D:
a
và['a', 'b', 'c']
Đá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é ❓️
2.1. Generator Functions
Generator functions là một tính năng mạnh mẽ trong JavaScript, cho phép chúng ta tạo ra các iterator một cách dễ dàng. Chúng được định nghĩa bằng cách sử dụng dấu sao (*) sau từ khóa function
.
2.2. Từ khóa yield
và yield*
Trong generator functions, chúng ta sử dụng từ khóa yield
để trả về giá trị. Mỗi lần gọi phương thức next()
, generator sẽ thực thi cho đến khi gặp từ khóa yield
tiếp theo.
yield
: Trả về một giá trị đơn lẻ.yield*
: Ủy quyền cho một generator hoặc iterable object khác.
2.3. Phân tích đoạn code
Hãy phân tích từng generator function:
function* generatorOne() {
yield ['a', 'b', 'c'];
}
generatorOne
sử dụng yield
để trả về toàn bộ mảng ['a', 'b', 'c']
như một giá trị duy nhất.
function* generatorTwo() {
yield* ['a', 'b', 'c'];
}
generatorTwo
sử dụng yield*
để ủy quyền cho mảng ['a', 'b', 'c']
. Điều này có nghĩa là nó sẽ lặp qua từng phần tử của mảng.
2.4. Kết quả
Khi chúng ta gọi one.next().value
, nó trả về toàn bộ mảng ['a', 'b', 'c']
.
Khi gọi two.next().value
, nó trả về phần tử đầu tiên của mảng, tức là 'a'
.
2.5. Ví dụ minh họa
Để hiểu rõ hơn sự khác biệt, hãy xem xét ví dụ sau:
const one = generatorOne();
console.log(one.next().value); // ['a', 'b', 'c']
console.log(one.next().value); // undefined
const two = generatorTwo();
console.log(two.next().value); // 'a'
console.log(two.next().value); // 'b'
console.log(two.next().value); // 'c'
console.log(two.next().value); // undefined
Như bạn có thể thấy, generatorOne
chỉ yield một lần, trong khi generatorTwo
yield ba lần, mỗi lần một phần tử của mảng.
2.6. Tóm lại
Hiểu về cách hoạt động của generator functions và sự khác biệt giữa yield
và yield*
là rất quan trọng khi làm việc với các iterator trong JavaScript. Nó cho phép chúng ta tạo ra các sequence phức tạp một cách dễ dàng và hiệu quả.
3. Template Literals và Arrow Functions
Output của đoạn code bên dưới là gì?
console.log(`${(x => x)('I love')} to program`)
- A:
I love to program
- B:
undefined to program
- C:
${(x => x)('I love') to program
- D:
TypeError
Đá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é ❓️
3.1. Template Literals
Template literals là một tính năng trong JavaScript cho phép chúng ta tạo ra các chuỗi đa dòng và nhúng các biểu thức JavaScript vào trong chuỗi. Chúng được bao quanh bởi dấu backtick (`) thay vì dấu nháy đơn hoặc nháy kép.
3.2. Arrow Functions
Arrow functions là một cú pháp ngắn gọn để viết function expressions. Chúng có cú pháp ngắn hơn so với function expressions thông thường và không bind this
của riêng chúng.
3.3. Phân tích đoạn code
Hãy chia nhỏ đoạn code của chúng ta:
`${(x => x)('I love')} to program`
(x => x)
là một arrow function nhận vào một tham sốx
và trả về chínhx
.('I love')
là đối số được truyền vào arrow function.- Kết quả của việc gọi arrow function được nhúng vào template literal bằng cách sử dụng
${}
.
3.4. Cách thức hoạt động
- Arrow function
(x => x)
được gọi với đối số'I love'
. - Function này đơn giản chỉ trả về đối số được truyền vào, tức là
'I love'
. - Kết quả này được nhúng vào template literal.
- Phần còn lại của chuỗi
to program
được thêm vào.
3.5. Ví dụ minh họa
Để hiểu rõ hơn, hãy xem xét một vài ví dụ tương tự:
console.log(`${(x => x * 2)(5)} is ten`); // "10 is ten"
console.log(`Hello, ${(name => `Mr. ${name}`)('John')}`); // "Hello, Mr. John"
Trong ví dụ đầu tiên, arrow function nhân đối số với 2. Trong ví dụ thứ hai, arrow function thêm "Mr." vào trước tên.
3.6. Tóm lại
Sự kết hợp giữa template literals và arrow functions cho phép chúng ta tạo ra các chuỗi động một cách linh hoạt và ngắn gọn. Điều này đặc biệt hữu ích khi chúng ta cần thực hiện các phép tính hoặc biến đổi đơn giản ngay trong chuỗi.
4. setInterval và Garbage Collection
Điều gì sẽ xảy ra với đoạn code sau?
let config = {
alert: setInterval(() => {
console.log('Alert!')
}, 1000)
}
config = null
- A: Callback
setInterval
sẽ không được gọi - B: Callback
setInterval
sẽ được gọi một lần duy nhất - C: Callback
setInterval
vẫn sẽ được gọi mỗi giây một lần - D:
config.alert()
không bao giờ được gọi bởi config lànull
Đá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. setInterval
setInterval
là một hàm trong JavaScript được sử dụng để thực thi một đoạn code lặp đi lặp lại sau một khoảng thời gian nhất định. Nó trả về một ID duy nhất, có thể được sử dụng để hủy interval bằng clearInterval
.
4.2. Garbage Collection
Garbage Collection là quá trình tự động giải phóng bộ nhớ không còn được sử dụng trong chương trình. Trong JavaScript, garbage collector sẽ tự động xóa các object không còn được tham chiếu đến.
4.3. Phân tích đoạn code
Hãy phân tích từng phần của đoạn code:
let config = {
alert: setInterval(() => {
console.log('Alert!')
}, 1000)
}
- Chúng ta tạo một object
config
với một thuộc tínhalert
. - Giá trị của
alert
là kết quả trả về từsetInterval
, là một ID duy nhất. setInterval
được thiết lập để gọi một arrow function mỗi 1000ms (1 giây).
config = null
Dòng này gán null
cho biến config
, nhưng điều này không ảnh hưởng đến interval đã được thiết lập.
4.4. Tại sao interval vẫn tiếp tục chạy?
Mặc dù config
đã được gán null
, interval vẫn tiếp tục chạy vì:
- JavaScript engine vẫn giữ một tham chiếu đến hàm callback trong
setInterval
. - ID trả về bởi
setInterval
vẫn được JavaScript runtime lưu giữ. - Arrow function trong
setInterval
không tạo ra mộtthis
mới, nên nó không bị ràng buộc với objectconfig
.
4.5. Ví dụ minh họa
Để hiểu rõ hơn, hãy xem xét ví dụ sau:
let intervalId = setInterval(() => {
console.log('Still running!');
}, 1000);
// Interval vẫn chạy ngay cả khi ta gán null
intervalId = null;
// Cách đúng để dừng interval
clearInterval(intervalId);
Trong ví dụ này, gán null
cho intervalId
không dừng interval. Để dừng interval, chúng ta cần sử dụng clearInterval
.
4.6. Tóm lại
Hiểu về cách setInterval
hoạt động và mối quan hệ của nó với garbage collection là rất quan trọng trong JavaScript. Nó giúp chúng ta tránh được các lỗi liên quan đến memory leak và quản lý tài nguyên hiệu quả hơn.
Để dừng một interval, luôn nhớ sử dụng clearInterval
với ID mà setInterval
trả về, thay vì chỉ đơn giản gán null
cho biến chứa ID đó.
5. Map và Function References
Những hàm nào sẽ trả về 'Hello world!'
?
const myMap = new Map()
const myFunc = () => 'greeting'
myMap.set(myFunc, 'Hello world!')
//1
myMap.get('greeting')
//2
myMap.get(myFunc)
//3
myMap.get(() => 'greeting')
- A: 1
- B: 2
- C: 2 và 3
- D: Tất cả
Đá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é ❓️
5.1. Map trong JavaScript
Map
là một cấu trúc dữ liệu trong JavaScript cho phép lưu trữ các cặp key-value, trong đó cả key và value có thể là bất kỳ kiểu dữ liệu nào, kể cả object hoặc function.
5.2. Function References
Trong JavaScript, functions là first-class citizens, có nghĩa là chúng có thể được gán cho biến, truyền vào như tham số, và trả về từ các hàm khác.
5.3. Phân tích đoạn code
Hãy phân tích từng phần của đoạn code:
const myMap = new Map()
const myFunc = () => 'greeting'
myMap.set(myFunc, 'Hello world!')
- Chúng ta tạo một
Map
mới. - Định nghĩa một arrow function
myFunc
. - Thêm một cặp key-value vào
Map
, trong đó key làmyFunc
và value là'Hello world!'
.
5.4. Các trường hợp gọi get
-
myMap.get('greeting')
:- Trả về
undefined
vì không có key nào là chuỗi'greeting'
trong Map.
- Trả về
-
myMap.get(myFunc)
:- Trả về
'Hello world!'
vìmyFunc
chính là key đã được set.
- Trả về
-
myMap.get(() => 'greeting')
:- Trả về
undefined
vì đây là một arrow function mới, khác vớimyFunc
.
- Trả về
5.5. Tại sao chỉ có trường hợp 2 trả về đúng?
- Trong JavaScript, hai function có cùng định nghĩa không được coi là bằng nhau.
- Khi sử dụng
Map
, việc so sánh key được thực hiện bằng thuật toán "same-value equality" (tương tự===
). myFunc
và() => 'greeting'
là hai function khác nhau trong bộ nhớ, mặc dù chúng có cùng định nghĩa.
5.6. Ví dụ minh họa
Để hiểu rõ hơn về cách JavaScript so sánh functions, hãy xem xét ví dụ sau:
const func1 = () => 'hello';
const func2 = () => 'hello';
console.log(func1 === func2); // false
console.log(func1 === func1); // true
const myMap = new Map();
myMap.set(func1, 'Value 1');
console.log(myMap.get(func2)); // undefined
console.log(myMap.get(func1)); // 'Value 1'
5.7. Tóm lại
Hiểu về cách Map
hoạt động với function references là rất quan trọng trong JavaScript. Nó giúp chúng ta tránh được các lỗi không mong muốn khi làm việc với Map
và functions.
Khi sử dụng functions làm key trong Map
, luôn nhớ rằng chỉ có chính xác function reference đó mới có thể được sử dụng để truy xuất giá trị, không phải một function khác có cùng định nghĩa.
Kết luận
Qua bài viết này, chúng ta đã đi sâu vào một số khía cạnh nâng cao của JavaScript như Temporal Dead Zone, Generator Functions, Template Literals kết hợp với Arrow Functions, setInterval và Garbage Collection, cũng như cách Map hoạt động với Function References.
Những kiến thức này không chỉ giúp chúng ta hiểu sâu hơn về cách JavaScript hoạt động, mà còn giúp chúng ta viết code hiệu quả và tránh được những lỗi tiềm ẩn. Hy vọng rằng qua những ví dụ và phân tích chi tiết, các bạn đã có thể nắm bắt được những khái niệm này một cách rõ ràng hơn.
Hãy nhớ rằng, việc học JavaScript là một hành trình dài, và những khái niệm nâng cao này đòi hỏi thời gian và thực hành để thực sự thành thạo. Đừng ngại thử nghiệm với code và đặt câu hỏi khi bạn gặp khó khăn.
Trong các bài viết tiếp theo, chúng ta sẽ tiếp tục khám phá những khía cạnh thú vị khác của JavaScript. Hãy tiếp tục theo dõi series này nhé!
Chúc các bạn học tập tốt và luôn giữ được niềm đam mê với lập trình! 🚀🎉
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