JavaScript Nâng Cao - Kỳ 27
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. Phương thức flat() trong JavaScript
Output của đoạn code bên dưới là gì?
const emojis = ["🥑", ["✨", "✨", ["🍕", "🍕"]]];
console.log(emojis.flat(1));
- A:
['🥑', ['✨', '✨', ['🍕', '🍕']]]
- B:
['🥑', '✨', '✨', ['🍕', '🍕']]
- C:
['🥑', ['✨', '✨', '🍕', '🍕']]
- D:
['🥑', '✨', '✨', '🍕', '🍕']
Đá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é ❓️
1.1. Phương thức flat() là gì?
Phương thức flat()
trong JavaScript được sử dụng để "làm phẳng" một mảng nhiều chiều. Nó tạo ra một mảng mới với tất cả các phần tử của mảng con được nối vào nó một cách đệ quy cho đến độ sâu được chỉ định.
1.2. Cách hoạt động của flat()
Khi bạn gọi flat()
, bạn có thể truyền vào một tham số chỉ định độ sâu mà bạn muốn "làm phẳng". Nếu không truyền tham số, mặc định độ sâu là 1.
Trong ví dụ của chúng ta:
const emojis = ["🥑", ["✨", "✨", ["🍕", "🍕"]]];
console.log(emojis.flat(1));
Chúng ta đang gọi flat(1)
, có nghĩa là chúng ta chỉ muốn làm phẳng mảng ở độ sâu 1.
1.3. Phân tích kết quả
Hãy xem xét mảng ban đầu:
["🥑", ["✨", "✨", ["🍕", "🍕"]]]
- Ở độ sâu 0 (mức ngoài cùng), chúng ta có hai phần tử:
"🥑"
và["✨", "✨", ["🍕", "🍕"]]
- Khi áp dụng
flat(1)
, nó sẽ làm phẳng mảng con ở độ sâu 1:["✨", "✨", ["🍕", "🍕"]]
Kết quả cuối cùng sẽ là:
['🥑', '✨', '✨', ['🍕', '🍕']]
Đây chính là đáp án B.
1.4. Lưu ý
Nếu chúng ta muốn làm phẳng hoàn toàn mảng này, chúng ta có thể sử dụng flat(Infinity)
:
console.log(emojis.flat(Infinity));
// Kết quả: ['🥑', '✨', '✨', '🍕', '🍕']
Đây là một kỹ thuật hữu ích khi bạn không biết chắc độ sâu của mảng hoặc muốn làm phẳng hoàn toàn một mảng nhiều chiều.
2. Tham chiếu và Object trong JavaScript
Output của đoạn code bên dưới là gì?
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
}
}
const counterOne = new Counter();
counterOne.increment();
counterOne.increment();
const counterTwo = counterOne;
counterTwo.increment();
console.log(counterOne.count);
- A:
0
- B:
1
- C:
2
- 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é ❓️
2.1. Class và Object trong JavaScript
Trong JavaScript, class
là một template để tạo ra các object. Khi chúng ta sử dụng từ khóa new
, chúng ta đang tạo ra một instance mới của class đó.
2.2. Tham chiếu trong JavaScript
Khi chúng ta gán một object cho một biến, chúng ta thực sự đang gán tham chiếu của object đó cho biến, chứ không phải copy toàn bộ object.
2.3. Phân tích đoạn code
Hãy đi qua từng bước:
-
Chúng ta tạo một instance
counterOne
của classCounter
:const counterOne = new Counter();
Lúc này,
counterOne.count
= 0 -
Chúng ta gọi
increment()
hai lần trêncounterOne
:counterOne.increment(); counterOne.increment();
Sau hai lần gọi này,
counterOne.count
= 2 -
Chúng ta gán
counterOne
chocounterTwo
:const counterTwo = counterOne;
Điều này không tạo ra một object mới, mà chỉ tạo ra một tham chiếu mới đến cùng một object.
-
Chúng ta gọi
increment()
trêncounterTwo
:counterTwo.increment();
Vì
counterTwo
vàcounterOne
cùng tham chiếu đến một object, nêncount
sẽ tăng lên 3. -
Cuối cùng, khi chúng ta in ra
counterOne.count
, kết quả sẽ là 3.
2.4. Minh họa
Để hiểu rõ hơn, hãy tưởng tượng counterOne
và counterTwo
như hai cái remote cùng điều khiển một chiếc TV. Khi bạn dùng remote nào để tăng âm lượng, thì âm lượng trên TV cũng sẽ tăng, không phân biệt bạn dùng remote nào.
2.5. Lưu ý
Đây là một đặc điểm quan trọng của object trong JavaScript. Khi làm việc với object, luôn nhớ rằng khi bạn gán một object cho một biến mới, bạn đang tạo ra một tham chiếu mới đến cùng một object, chứ không phải tạo ra một bản sao của object đó.
3. Promise, async/await và Event Loop trong JavaScript
Output của đoạn code bên dưới là gì?
const myPromise = Promise.resolve(Promise.resolve("Promise!"));
function funcOne() {
myPromise.then(res => res).then(res => console.log(res));
setTimeout(() => console.log("Timeout!", 0));
console.log("Last line!");
}
async function funcTwo() {
const res = await myPromise;
console.log(await res);
setTimeout(() => console.log("Timeout!", 0));
console.log("Last line!");
}
funcOne();
funcTwo();
- A:
Promise! Last line! Promise! Last line! Last line! Promise!
- B:
Last line! Timeout! Promise! Last line! Timeout! Promise!
- C:
Promise! Last line! Last line! Promise! Timeout! Timeout!
- D:
Last line! Promise! Promise! Last line! Timeout! Timeout!
Đá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. Promise và async/await
Promise là một cơ chế trong JavaScript để xử lý các tác vụ bất đồng bộ. async/await
là một cú pháp "đường mật" (syntactic sugar) giúp làm việc với Promise dễ dàng hơn.
3.2. Event Loop và Call Stack
JavaScript sử dụng một mô hình đơn luồng (single-threaded) với Event Loop để xử lý các tác vụ bất đồng bộ. Call Stack là nơi các hàm được thực thi theo thứ tự LIFO (Last In, First Out).
3.3. Phân tích đoạn code
Hãy đi qua từng bước:
-
funcOne()
được gọi:.then()
củamyPromise
được đưa vào Microtask Queue.setTimeout
được đưa vào Callback Queue.- "Last line!" được in ra ngay lập tức.
-
funcTwo()
được gọi:await myPromise
tạm dừng thực thi của hàm.- Các tác vụ khác tiếp tục thực thi.
-
Call Stack trống, Event Loop kiểm tra Microtask Queue:
- Promise từ
funcOne()
được resolve, in ra "Promise!". - Promise từ
funcTwo()
được resolve, in ra "Promise!".
- Promise từ
-
funcTwo()
tiếp tục thực thi:- "Last line!" được in ra.
-
Call Stack trống, Event Loop kiểm tra Callback Queue:
- Hai
setTimeout
callback được thực thi, in ra "Timeout!" hai lần.
- Hai
3.4. Kết quả cuối cùng
Thứ tự in ra sẽ là:
- "Last line!" (từ
funcOne()
) - "Promise!" (từ
funcOne()
) - "Promise!" (từ
funcTwo()
) - "Last line!" (từ
funcTwo()
) - "Timeout!" (từ
funcOne()
) - "Timeout!" (từ
funcTwo()
)
3.5. Lưu ý
Hiểu về cách JavaScript xử lý các tác vụ bất đồng bộ là rất quan trọng khi làm việc với các ứng dụng phức tạp. Nó giúp bạn dự đoán được thứ tự thực thi của code và tránh được các lỗi liên quan đến timing.
4. Import và Export trong JavaScript
Làm thế nào có thể gọi hàm sum
trong index.js
từ sum.js?
// sum.js
export default function sum(x) {
return x + x;
}
// index.js
import * as sum from "./sum";
- A:
sum(4)
- B:
sum.sum(4)
- C:
sum.default(4)
- D: Default aren't imported with
*
, only named exports
Đá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. Export và Import trong JavaScript
Trong JavaScript modules, chúng ta có hai loại export chính: named exports và default exports.
4.2. Default Export
Khi sử dụng export default
, chúng ta đang xuất một giá trị mặc định từ module. Mỗi module chỉ có thể có một default export.
4.3. Import với dấu *
Khi sử dụng import * as name from './module'
, chúng ta đang import tất cả các exports (cả named và default) từ module dưới dạng một object.
4.4. Phân tích đoạn code
Trong file sum.js
, chúng ta có một default export:
export default function sum(x) {
return x + x;
}
Trong file index.js
, chúng ta import tất cả từ sum.js
:
import * as sum from "./sum";
Khi sử dụng import *
, JavaScript sẽ tạo ra một object chứa tất cả các exports, với default export được gán cho thuộc tính default
.
4.5. Cách gọi hàm sum
Vì chúng ta đã import tất cả các exports dưới tên sum
, và hàm sum
là một default export, nên chúng ta có thể gọi nó thông qua sum.default
:
sum.default(4)
Đây chính là lý do tại sao đáp án C là chính xác.
4.6. Lưu ý
Mặc dù cách import này hoạt động, nhưng nó không phải là cách thông thường để import một default export. Thông thường, chúng ta sẽ import default export như sau:
import sum from './sum';
Sau đó, chúng ta có thể sử dụng nó trực tiếp:
sum(4)
Cách import sử dụng *
thường được dùng khi chúng ta muốn import nhiều named exports cùng một lúc, hoặc khi chúng ta không chắc chắn về cấu trúc của module mà chúng ta đang import.
5. Proxy trong JavaScript
Output của đoạn code bên dưới là gì?
const handler = {
set: () => console.log("Added a new property!"),
get: () => console.log("Accessed a property!")
};
const person = new Proxy({}, handler);
person.name = "Lydia";
person.name;
- A:
Added a new property!
- B:
Accessed a property!
- C:
Added a new property!
Accessed a property!
- D: Nothing gets logged
Đá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. Proxy trong JavaScript
Proxy là một object đặc biệt trong JavaScript cho phép chúng ta tùy chỉnh hành vi cơ bản của một object. Nó cho phép chúng ta định nghĩa các "bẫy" (traps) cho các hoạt động khác nhau trên object.
5.2. Handler object
Handler object chứa các phương thức (traps) định nghĩa hành vi của Proxy. Trong ví dụ này, chúng ta có hai traps:
set
: Được gọi khi một thuộc tính được thiết lập.get
: Được gọi khi một thuộc tính được truy cập.
5.3. Phân tích đoạn code
-
Chúng ta tạo một Proxy với một object rỗng
{}
và handler object:const person = new Proxy({}, handler);
-
Khi chúng ta thiết lập thuộc tính
name
:person.name = "Lydia";
Trap
set
được kích hoạt, in ra "Added a new property!". -
Khi chúng ta truy cập thuộc tính
name
:person.name;
Trap
get
được kích hoạt, in ra "Accessed a property!".
5.4. Kết quả
Do đó, kết quả sẽ là:
Added a new property!
Accessed a property!
5.5. Lưu ý
Proxy là một công cụ mạnh mẽ trong JavaScript, cho phép chúng ta can thiệp vào các hoạt động cơ bản của object. Nó có thể được sử dụng cho nhiều mục đích như validation, logging, formatting, notifications, và nhiều hơn nữa.
Tuy nhiên, cần lưu ý rằng việc sử dụng Proxy có thể ảnh hưởng đến hiệu suất, đặc biệt là khi được sử dụng trên các object lớn hoặc trong các vòng lặp.
Đó là tất cả cho JavaScript Nâng Cao - Kỳ 27. Hy vọng các bạn đã học được nhiều điều mới mẻ về JavaScript. Hãy nhớ rằng, việc hiểu sâu về các khái niệm này không chỉ giúp bạn viết code tốt hơn mà còn giúp bạn debug hiệu quả hơn.
Nếu bạn có bất kỳ câu hỏi nào, đừng ngại comment bên dưới nhé. Hẹn gặp lại các bạn trong kỳ 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