Danh mục bài viết
TogglePhần 1: Câu hỏi JavaScript cơ bản
JavaScript là gì?
JavaScript là ngôn ngữ lập trình phía client và server-side, thường được sử dụng để tạo hiệu ứng động trên trang web, tương tác với người dùng và xử lý các sự kiện.
Sự khác nhau giữa let, const, và var là gì?
var
: có phạm vi function-scoped, có thể khai báo lại và không bị block scope.let
: có phạm vi block-scoped và không thể khai báo lại trong cùng một block.const
: giốnglet
nhưng không thể gán lại giá trị sau khi khai báo.
Ví dụ
Var
function myFunction() {
var x = 10;
console.log(x); // Output: 10
if (true) {
var x = 20; // Khai báo lại x trong block, nhưng vẫn giữ giá trị 20 ở ngoài block
console.log(x); // Output: 20
}
console.log(x); // Output: 20
}
myFunction();
- Phạm vi hàm: Biến
x
được khai báo trong hàmmyFunction
, do đó nó chỉ có phạm vi trong hàm này. - Khai báo lại: Chúng ta có thể khai báo lại
x
nhiều lần trong cùng một hàm. Giá trị cuối cùng được gán sẽ được sử dụng. - Không có block scope: Giá trị của
x
được thay đổi bên trong blockif
cũng ảnh hưởng đến giá trị củax
bên ngoài block.
Let
function myFunction() {
let x = 10;
console.log(x); // Output: 10
if (true) {
let x = 20; // Khai báo lại x trong block, tạo một biến x mới
console.log(x); // Output: 20
}
console.log(x); // Output: 10
}
myFunction();
- Phạm vi khối: Biến
x
được khai báo trong blockif
là một biến riêng biệt, không ảnh hưởng đến biếnx
bên ngoài block. - Không thể khai báo lại: Chúng ta không thể khai báo lại
x
trong cùng một block.
Const
function myFunction() {
const x = 10;
console.log(x); // Output: 10
// x = 20; // Lỗi: Cannot assign to constant variable.
const person = { name: "John" };
person.name = "Doe"; // Có thể thay đổi thuộc tính của đối tượng
}
myFunction();
- Phạm vi khối: Giống như
let
. - Không thể gán lại: Chúng ta không thể thay đổi giá trị của
x
sau khi khai báo. Tuy nhiên, nếux
là một đối tượng, chúng ta vẫn có thể thay đổi các thuộc tính của đối tượng đó.
Khi nào nên sử dụng Var, Let & Const?
- var: Nên tránh sử dụng
var
trong các dự án mới vì nó có thể dẫn đến các vấn đề về phạm vi và gây khó khăn trong việc quản lý code. - let: Sử dụng
let
khi bạn cần một biến có phạm vi khối và giá trị có thể thay đổi. - const: Sử dụng
const
khi bạn muốn một giá trị không đổi trong suốt quá trình thực thi.
Lưu ý:
- Hoisting: Cả
var
,let
vàconst
đều được hoisting, nhưng cách chúng được khởi tạo là khác nhau. - Temporal Dead Zone (TDZ): Biến
let
vàconst
sẽ nằm trong TDZ cho đến khi chúng được khai báo. Nếu bạn cố gắng truy cập biến trước khi nó được khai báo, sẽ xảy ra lỗi.
JavaScript là ngôn ngữ đơn luồng (single-threaded) hay đa luồng (multi-threaded)?
JavaScript là ngôn ngữ đơn luồng, sử dụng event loop và callback queue để quản lý các tác vụ bất đồng bộ.
console.log('Start');
setTimeout(() => {
console.log('Callback');
}, 0);
console.log('End');
/*
Kết quả:
Start
End
Callback
*/
- Giải thích:
console.log('Start');
được thực thi ngay lập tức và in ra “Start”.setTimeout()
tạo ra một callback và đưa nó vào Callback Queue, nhưng với thời gian chờ là 0ms.console.log('End');
được thực thi tiếp theo, in ra “End”.- Sau khi Call Stack rỗng, Event Loop lấy callback từ Callback Queue và đưa vào Call Stack để thực thi, in ra “Callback”.
Tại sao lại như vậy?
- Vì
setTimeout
là một hàm bất đồng bộ, nên callback của nó sẽ không được thực thi ngay lập tức mà sẽ được đưa vào Callback Queue. - Event Loop ưu tiên thực hiện các tác vụ đồng bộ trước, sau đó mới đến các tác vụ bất đồng bộ.
Closure là gì?
Closure là một tính năng trong JavaScript, cho phép một hàm ghi nhớ và truy cập phạm vi từ ngữ cảnh ban đầu ngay cả khi hàm đó được thực thi ngoài phạm vi đó.
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // Output: 1
console.log(counter()); // Output: 2
Giải thích:
- Hàm
createCounter
tạo ra một closure. Nó trả về một hàm con (anonymous function) bên trong. - Hàm con này “ghi nhớ” biến
count
của hàm cha, ngay cả khi hàm cha đã hoàn thành việc thực thi. - Mỗi lần gọi hàm con, giá trị của
count
sẽ được tăng lên và trả về.
Arrow function trong JavaScript là gì?
Arrow function là một cú pháp viết hàm ngắn gọn hơn, không có this
riêng mà kế thừa từ ngữ cảnh bao quanh, không có arguments
và constructor
.
Để hiểu rõ hơn về arrow function, chúng ta cùng đi sâu vào từng đặc điểm của nó:
- Cú pháp ngắn gọn:
- Không cần từ khóa
function
: Cú pháp của arrow function đơn giản hơn nhiều so với hàm truyền thống. - Sử dụng dấu
=>
: Dấu mũi tên=>
phân tách danh sách tham số và thân hàm. - Thân hàm:
- Một biểu thức: Nếu thân hàm chỉ là một biểu thức đơn giản, bạn có thể bỏ qua cặp ngoặc nhọn
{}
và từ khóareturn
. Giá trị của biểu thức sẽ tự động được trả về. - Nhiều biểu thức: Nếu thân hàm có nhiều biểu thức, bạn cần sử dụng cặp ngoặc nhọn
{}
và từ khóareturn
để chỉ rõ giá trị trả về.
- Một biểu thức: Nếu thân hàm chỉ là một biểu thức đơn giản, bạn có thể bỏ qua cặp ngoặc nhọn
- Không cần từ khóa
Ví dụ:
// Hàm truyền thống
function sum(a, b) {
return a + b;
}
// Arrow function
const sum = (a, b) => a + b;
Event delegation là gì?
Event delegation là kỹ thuật gán một listener chung cho một phần tử cha để xử lý sự kiện từ các phần tử con, giúp tối ưu hiệu suất và dễ quản lý sự kiện hơn.
Ưu điểm event delegation
- Hiệu suất:
- Giảm số lượng listener: Thay vì gán listener cho từng phần tử con, chúng ta chỉ cần gán một listener duy nhất cho phần tử cha. Điều này giúp giảm thiểu việc tạo ra quá nhiều đối tượng listener, từ đó cải thiện hiệu suất của ứng dụng.
- Tối ưu hóa việc xử lý sự kiện: Khi một sự kiện xảy ra, trình duyệt sẽ bắt đầu quá trình bubbling (sủi bọt) từ phần tử con lên đến phần tử cha. Nhờ đó, chúng ta chỉ cần lắng nghe sự kiện tại phần tử cha mà không cần phải duyệt qua từng phần tử con.
- Quản lý sự kiện dễ dàng:
- Thêm/xóa phần tử động: Khi thêm hoặc xóa các phần tử con, chúng ta không cần phải cập nhật lại các listener. Listener đã được gán cho phần tử cha sẽ tự động hoạt động với các phần tử con mới.
- Tránh rò rỉ bộ nhớ:
- Việc gán quá nhiều listener có thể dẫn đến rò rỉ bộ nhớ. Event delegation giúp giảm thiểu vấn đề này.
Cách thức hoạt động của event delegation
- Chọn một phần tử cha: Lựa chọn một phần tử cha chung cho tất cả các phần tử con mà bạn muốn lắng nghe sự kiện.
- Gán listener cho phần tử cha: Sử dụng
addEventListener
để gán một listener cho phần tử cha. - Xác định phần tử đích: Bên trong hàm xử lý sự kiện, sử dụng
event.target
để xác định phần tử con mà sự kiện đã xảy ra. - Xử lý sự kiện: Kiểm tra xem
event.target
có khớp với phần tử con mà bạn muốn xử lý hay không. Nếu đúng, thực hiện các hành động cần thiết.
// Giả sử chúng ta có một danh sách các phần tử với class là 'item'
const itemList = document.querySelector('.item-list');
itemList.addEventListener('click', (event) => {
// Kiểm tra xem phần tử bị click có class là 'item' hay không
if (event.target.classList.contains('item')) {
console.log('Bạn đã click vào một item');
// Thực hiện các hành động khác khi click vào item
}
});
Trong ví dụ trên, chúng ta đã gán một listener cho phần tử
.item-list
. Khi người dùng click vào bất kỳ phần tử nào có class làitem
, sự kiện sẽ được bubbling lên đến.item-list
và hàm xử lý sự kiện sẽ được thực thi. Bên trong hàm này, chúng ta kiểm tra xem phần tử bị click có phải là một item hay không và thực hiện các hành động tương ứng.
Ứng dụng của event delegation
- Tạo các danh sách động: Khi bạn cần tạo ra một danh sách các phần tử có thể thay đổi, event delegation giúp bạn dễ dàng xử lý các sự kiện trên các phần tử mới được thêm vào.
- Xử lý sự kiện trên các phần tử được tạo ra động: Tương tự như trên, event delegation rất hữu ích khi bạn cần xử lý sự kiện trên các phần tử được tạo ra bằng JavaScript.
- Tối ưu hóa hiệu suất: Đặc biệt khi làm việc với các ứng dụng có nhiều phần tử tương tác, event delegation giúp cải thiện hiệu suất đáng kể.
Phần 2: Câu hỏi JavaScript trung cấp
Sự khác biệt giữa == và === trong JavaScript là gì?
==
là phép so sánh giá trị có ép kiểu, còn ===
so sánh giá trị và kiểu dữ liệu mà không thực hiện ép kiểu.
- Toán tử
==
(so sánh bằng):- Thực hiện so sánh giá trị sau khi ép kiểu cả hai toán hạng về cùng một kiểu dữ liệu.
- Có thể dẫn đến những kết quả bất ngờ do quá trình ép kiểu tự động.
- Ví dụ:
5 == "5"
: Trả vềtrue
vì chuỗi “5” được ép kiểu thành số 5 trước khi so sánh.0 == false
: Trả vềtrue
vì cả 0 và false đều được coi là falsy.
- Toán tử
===
(so sánh bằng nghiêm ngặt):- So sánh cả giá trị và kiểu dữ liệu của hai toán hạng.
- Không thực hiện ép kiểu.
console.log(5 == "5"); // true
console.log(5 === "5"); // false
console.log(null == undefined); // true
console.log(null === undefined); // false
console.log(true == 1); // true
console.log(true === 1); // false
- Sử dụng
===
trong hầu hết các trường hợp:- Đảm bảo tính chính xác và tránh những lỗi không mong muốn do ép kiểu.
- Dễ dàng đọc và hiểu hơn.
- Sử dụng
==
khi:- Bạn chắc chắn về các kiểu dữ liệu đang so sánh và muốn thực hiện ép kiểu ngầm định.
- Bạn muốn so sánh các giá trị null và undefined (chúng bằng nhau khi sử dụng
==
).
Giải thích về hoisting trong JavaScript.
Hoisting là một cơ chế đặc biệt trong JavaScript, nơi các khai báo biến và hàm được “nâng” lên đầu phạm vi của chúng (phạm vi toàn cục hoặc phạm vi hàm) trước khi code được thực thi. Điều này có nghĩa là bạn có thể sử dụng một biến hoặc hàm trước khi nó được khai báo chính thức
Hoisting với biến:
- Khai báo bằng
var
: Khi bạn khai báo một biến bằngvar
, biến đó sẽ được nâng lên đầu phạm vi và gán giá trịundefined
. - Ví dụ:
console.log(x); // Output: undefined
var x = 10;
- Trong ví dụ này, mặc dù
console.log(x)
được thực hiện trước khi khai báox
, nhưng do hoisting, biếnx
đã được nâng lên đầu và có giá trịundefined
.
Hoisting với hàm:
- Khai báo hàm: Cả khai báo hàm và biểu thức hàm đều được hoisting. Tuy nhiên, chỉ có khai báo hàm mới có thể được gọi trước khi khai báo.
- Ví dụ:
console.log(myFunction()); // Output: Hello
function myFunction() {
return "Hello";
}
hàm myFunction
được gọi trước khi được khai báo, nhưng vẫn hoạt động bình thường nhờ hoisting.
Lưu ý quan trọng:
- Chỉ khai báo được nâng lên: Hoisting chỉ nâng lên phần khai báo, không nâng lên phần gán giá trị.
- Biểu thức hàm: Biểu thức hàm (function expression) không được nâng lên hoàn toàn. Bạn không thể gọi một biểu thức hàm trước khi nó được gán.
- let và const: Với
let
vàconst
, biến sẽ được nâng lên nhưng sẽ nằm trong vùng chết (Temporal Dead Zone) cho đến khi đến dòng khai báo. Nếu bạn cố gắng truy cập biến trước khi nó được khai báo, sẽ xảy ra lỗiReferenceError
.
Callback là gì?
Callback là một hàm được truyền vào một hàm khác như một đối số và được gọi lại khi một sự kiện nào đó xảy ra hoặc một tác vụ nào đó hoàn thành. Nói cách khác, callback là một cách để thực thi một đoạn mã sau khi một hành động khác kết thúc.
Tại sao cần sử dụng callback?
- Xử lý các tác vụ bất đồng bộ: Nhiều hoạt động trong JavaScript, như đọc dữ liệu từ server, thao tác DOM, hoặc chờ đợi một sự kiện người dùng, mất một khoảng thời gian nhất định để hoàn thành. Callback cho phép chúng ta xác định một hàm sẽ được gọi lại khi hoạt động đó hoàn tất.
- Tạo ra các hàm linh hoạt: Bằng cách truyền các hàm khác nhau vào một hàm, chúng ta có thể tạo ra các hàm có hành vi khác nhau tùy thuộc vào callback được cung cấp.
function greet(name, callback) {
console.log('Xin chào, ' + name + '!');
callback();
}
function sayGoodbye() {
console.log('Tạm biệt!');
}
greet('Alice', sayGoodbye);
Trong ví dụ trên:
greet
là một hàm nhận hai đối số:name
vàcallback
.sayGoodbye
là một callback được truyền vào hàmgreet
.- Khi hàm
greet
được gọi, nó in ra một thông báo chào mừng và sau đó gọi hàmcallback
(trong trường hợp này làsayGoodbye
).
Các trường hợp sử dụng phổ biến của callback
- setTimeout và setInterval: Để thực thi một đoạn mã sau một khoảng thời gian nhất định.
- Event listeners: Để xử lý các sự kiện người dùng như click, hover, …
- AJAX: Để xử lý dữ liệu trả về từ server sau khi gửi một yêu cầu.
- Thư viện JavaScript: Nhiều thư viện JavaScript như jQuery, React sử dụng callback rộng rãi.
Tại sao callback lại quan trọng?
- Kiến trúc sự kiện: Callback là một khái niệm cốt lõi trong kiến trúc sự kiện, cho phép các phần khác nhau của ứng dụng giao tiếp với nhau một cách linh hoạt.
- Không chặn luồng thực thi: Callback giúp tránh tình trạng chương trình bị treo chờ một tác vụ bất đồng bộ hoàn thành.
- Tạo ra các API linh hoạt: Callback cho phép các API được thiết kế một cách mở rộng và tùy biến.
Nhược điểm của callback?
- Callback hell: Khi lồng quá nhiều callback vào nhau, code trở nên khó đọc và khó bảo trì.
- Khó quản lý luồng điều khiển: Việc theo dõi luồng điều khiển trong các callback lồng nhau có thể trở nên phức tạp.
Giải pháp cho callback hell là gì?
- Promise: Một cách hiện đại hơn để xử lý các tác vụ bất đồng bộ, giúp code trở nên rõ ràng hơn và dễ quản lý hơn.
- Async/await: Một cú pháp dựa trên Promise, giúp code bất đồng bộ trông giống như code đồng bộ.
Sự khác biệt giữa map(), filter(), và reduce() là gì?
map(), filter() và reduce() là ba phương thức rất hữu ích trong JavaScript để thao tác với các mảng. Mặc dù chúng đều nhận một callback function làm đối số, nhưng mỗi phương thức có mục đích và cách hoạt động riêng biệt.
Phương thức | Mục đích | Trả về |
---|---|---|
map() | Tạo mảng mới với các phần tử được biến đổi | Mảng mới có cùng độ dài với mảng ban đầu |
filter() | Tạo mảng mới chứa các phần tử thỏa mãn điều kiện | Mảng mới có thể có độ dài nhỏ hơn mảng ban đầu |
reduce() | Giảm mảng thành một giá trị duy nhất | Một giá trị duy nhất |
Khi nào nên sử dụng phương thức nào?
- map(): Khi bạn muốn tạo ra một mảng mới dựa trên mảng hiện tại, nhưng với các giá trị đã được biến đổi.
- filter(): Khi bạn muốn lọc ra các phần tử thỏa mãn một điều kiện nhất định.
- reduce(): Khi bạn muốn thực hiện một phép tính tích lũy trên toàn bộ mảng (ví dụ: tính tổng, tìm giá trị lớn nhất/nhỏ nhất).
Lưu ý
- Các phương thức này đều trả về một mảng mới và không thay đổi mảng gốc.
- Callback function có thể nhận thêm các tham số tùy chọn.
- Bạn có thể kết hợp các phương thức này để thực hiện các thao tác phức tạp hơn trên mảng.
Ví dụ kết hợp
const numbers = [1, 2, 3, 4, 5];
const sumOfEvenNumbers = numbers
.filter(number => number % 2 === 0)
.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sumOfEvenNumbers); // 6
Trong ví dụ trên, chúng ta đã kết hợp filter()
để lọc ra các số chẵn và reduce()
để tính tổng của chúng.
Prototype trong JavaScript là gì?
Prototype là một khái niệm cốt lõi trong JavaScript, nó đóng vai trò như một khuôn mẫu mà các đối tượng được tạo ra từ đó kế thừa các thuộc tính và phương thức. Nghĩa là, khi bạn tạo một đối tượng mới, nó sẽ có quyền truy cập vào các thuộc tính và phương thức được định nghĩa trong prototype của nó.
Tại sao Prototype lại quan trọng?
- Kế thừa: Prototype cho phép JavaScript thực hiện cơ chế kế thừa, giống như các ngôn ngữ lập trình hướng đối tượng khác.
- Tiết kiệm bộ nhớ: Thay vì sao chép các thuộc tính và phương thức cho từng đối tượng, chúng ta chỉ cần định nghĩa chúng một lần trong prototype.
- Tạo ra các đối tượng có liên quan: Các đối tượng có cùng prototype sẽ chia sẻ các thuộc tính và phương thức chung.
Cách hoạt động của Prototype
- Mỗi hàm là một đối tượng: Trong JavaScript, mọi hàm đều là một đối tượng. Và mỗi hàm có một thuộc tính đặc biệt gọi là
prototype
. - Prototype là một đối tượng: Thuộc tính
prototype
của một hàm là một đối tượng. Các đối tượng được tạo ra từ hàm đó sẽ kế thừa các thuộc tính và phương thức của đối tượng prototype này. - Chuỗi prototype: Các đối tượng có thể có một chuỗi prototype, nghĩa là một đối tượng có thể kế thừa từ prototype của đối tượng khác, và cứ thế.
function Person(name, age) {
this.name = name;
this.age = age;
}
// Thêm một phương thức vào prototype của Person
Person.prototype.greet = function() {
console.log('Xin chào, tôi là ' + this.name);
};
// Tạo các đối tượng từ hàm Person
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);
// Gọi phương thức greet
person1.greet(); // Output: Xin chào, tôi là Alice
person2.greet(); // Output: Xin chào, tôi là Bob
Trong ví dụ trên:
Person
là một hàm (đồng thời là một đối tượng).Person.prototype
là một đối tượng chứa các thuộc tính và phương thức mà tất cả các đối tượng được tạo ra từPerson
sẽ kế thừa.greet
là một phương thức được thêm vàoPerson.prototype
.- Khi chúng ta gọi
person1.greet()
, JavaScript sẽ tìm phương thứcgreet
trongperson1
. Vì không tìm thấy, nó sẽ tìm lênPerson.prototype
và tìm thấy phương thứcgreet
ở đó.
Ứng dụng của Prototype
- Tạo các lớp: Mặc dù JavaScript không có class theo nghĩa truyền thống, nhưng chúng ta có thể sử dụng prototype để mô phỏng các lớp.
- Kế thừa: Prototype là cơ sở cho việc thực hiện kế thừa trong JavaScript.
- Tạo các đối tượng có liên quan: Các đối tượng có cùng prototype sẽ có hành vi tương tự nhau.
Promise là gì và dùng khi nào?
Promise là một đối tượng đại diện cho kết quả (thành công hoặc thất bại) của một hoạt động bất đồng bộ trong tương lai. Nói cách khác, Promise là một “lời hứa” rằng một tác vụ nào đó sẽ được hoàn thành tại một thời điểm nào đó trong tương lai, và khi đó bạn sẽ nhận được kết quả.
Tại sao cần sử dụng Promise?
- Xử lý các tác vụ bất đồng bộ: Các hoạt động như fetch dữ liệu từ server, đọc file, hay xử lý sự kiện thường mất thời gian để hoàn thành. Promise giúp chúng ta quản lý các tác vụ này một cách hiệu quả, tránh bị blocking UI.
- Tránh callback hell: Khi lồng quá nhiều callback vào nhau, code sẽ trở nên khó đọc và bảo trì. Promise giúp chúng ta viết code một cách tuần tự hơn.
- Sử dụng async/await: Promise là nền tảng cho cú pháp async/await, giúp code bất đồng bộ trở nên dễ đọc và gần giống với code đồng bộ.
Cách hoạt động của Promise
Một Promise có thể ở một trong ba trạng thái:
- pending: Tác vụ đang được thực hiện.
- fulfilled: Tác vụ đã hoàn thành thành công.
- rejected: Tác vụ đã thất bại.
Khi tạo một Promise, bạn cung cấp cho nó một function executor. Function này nhận hai tham số:
resolve
vàreject
.- resolve: Gọi khi tác vụ hoàn thành thành công, kèm theo kết quả.
- reject: Gọi khi tác vụ thất bại, kèm theo lý do.
function fetchData(url) {
return new Promise((resolve, reject) => {
// Code để fetch dữ liệu từ server
fetch(url)
.then(response => response.json())
.then(data => resolve(data))
.catch(error => reject(error));
});
}
fetchData('https://api.example.com/data')
.then(data => {
console.log(data); // Xử lý dữ liệu thành công
})
.catch(error => {
console.error(error); // Xử lý lỗi
});
Trong ví dụ trên:
fetchData
là một hàm trả về một Promise.- Bên trong
fetchData
, chúng ta sử dụngfetch
để lấy dữ liệu từ server. - Nếu fetch thành công, chúng ta gọi
resolve
với dữ liệu. - Nếu fetch thất bại, chúng ta gọi
reject
với lỗi.
Ưu điểm của Promise
- Dễ đọc và bảo trì hơn: So với callback hell, Promise giúp code rõ ràng hơn.
- Hỗ trợ chaining: Có thể liên kết nhiều Promise lại với nhau bằng
.then()
và.catch()
. - Sử dụng async/await: Cú pháp async/await giúp code bất đồng bộ trông giống như code đồng bộ.
Khi nào nên sử dụng Promise?
- Xử lý các tác vụ bất đồng bộ: Bất cứ khi nào bạn cần thực hiện một tác vụ mất thời gian và không muốn chặn luồng thực thi chính.
- Quản lý lỗi: Promise cho phép bạn xử lý lỗi một cách rõ ràng và tập trung.
- Tạo ra các API bất đồng bộ: Bạn có thể tạo ra các API của riêng mình dựa trên Promise để cung cấp cho các thành phần khác trong ứng dụng.
Phần 3: Câu hỏi JavaScript nâng cao
Async/Await là gì và hoạt động như thế nào?
Async/await là một cặp từ khóa trong JavaScript được giới thiệu để giúp chúng ta làm việc với các hoạt động bất đồng bộ một cách dễ dàng hơn và trực quan hơn. Nó giúp code bất đồng bộ trông giống như code đồng bộ, làm cho việc đọc và viết code trở nên đơn giản hơn.
Async: Khai báo hàm bất đồng bộ
- Async được đặt trước một hàm để biến hàm đó thành một hàm bất đồng bộ.
- Hàm async luôn trả về một Promise.
async function fetchData() {
// Code bất đồng bộ ở đây
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}
Await: Chờ đợi một Promise
- Await chỉ có thể được sử dụng bên trong một hàm async.
- Khi gặp
await
trước một Promise, JavaScript sẽ tạm dừng việc thực thi hàm async cho đến khi Promise đó được giải quyết (fulfilled) hoặc bị reject. - Kết quả của Promise sẽ được gán cho biến ở bên trái
await
.
async function fetchDataAndLog() {
const data = await fetchData();
console.log(data);
}
Cách hoạt động của Async/Await
- Khi gọi một hàm async, một Promise được tạo ra.
- Khi gặp
await
trong hàm async, JavaScript sẽ chờ Promise đó được giải quyết. - Trong khi chờ đợi, luồng thực thi sẽ chuyển sang các tác vụ khác.
- Khi Promise được giải quyết, giá trị trả về của Promise sẽ được gán cho biến ở bên trái
await
và việc thực thi hàm async sẽ tiếp tục.
Tại sao nên sử dụng Async/Await?
- Code dễ đọc hơn: Code async/await trông giống như code đồng bộ, giúp bạn dễ dàng theo dõi luồng thực thi.
- Tránh callback hell: Async/await giúp bạn viết code một cách tuần tự hơn, tránh lồng quá nhiều callback.
- Sử dụng với Promise: Async/await được xây dựng trên nền tảng của Promise, giúp bạn tận dụng được tất cả các tính năng của Promise.
Ví dụ cụ thể về async & await
async function getUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`);
const user = await response.json();
return user;
}
async function main() {
try {
const user = await getUser(123);
console.log(user.name);
} catch (error) {
console.error('Error:', error);
}
}
main();
Trong ví dụ trên:
getUser
là một hàm async, lấy dữ liệu người dùng từ server.main
là một hàm async, gọigetUser
và xử lý kết quả hoặc lỗi.- Chúng ta sử dụng
try...catch
để bắt lỗi trong trường hợp fetch dữ liệu thất bại.
So sánh với Promise
- Promise: Cung cấp một cách cơ bản để xử lý các hoạt động bất đồng bộ.
- Async/await: Là một cú pháp xây dựng trên Promise, giúp code bất đồng bộ dễ đọc và viết hơn.
Explain Event Loop in JavaScript.
Vòng lặp sự kiện là một cơ chế cốt lõi trong JavaScript, cho phép ngôn ngữ này xử lý các tác vụ bất đồng bộ một cách hiệu quả. Nó là lý do tại sao JavaScript có thể thực hiện nhiều việc cùng một lúc mặc dù nó chỉ có một luồng duy nhất.

Vòng lặp sự kiện hoạt động như thế nào?
Hãy tưởng tượng vòng lặp sự kiện như một người quản lý bận rộn, liên tục kiểm tra danh sách công việc và thực hiện chúng theo một thứ tự nhất định.
- Stack (Ngăn xếp): Đây là nơi các hàm được gọi và thực thi một cách tuần tự. Khi một hàm được gọi, nó được đẩy vào stack. Khi hàm kết thúc, nó được pop ra khỏi stack.
- Queue (Hàng đợi): Đây là nơi các tác vụ bất đồng bộ (như setTimeout, fetch, click events) được đưa vào khi chúng được khởi tạo.
- Vòng lặp sự kiện:
- Liên tục kiểm tra xem stack có rỗng hay không.
- Nếu stack rỗng, vòng lặp sẽ lấy một tác vụ đầu tiên từ queue và đẩy vào stack để thực thi.
- Quá trình này lặp đi lặp lại cho đến khi không còn tác vụ nào trong queue.
console.log('Start');
setTimeout(() => {
console.log('Callback');
}, 0);
console.log('End');
- Start được in ra ngay lập tức vì nó là một lệnh đồng bộ.
- setTimeout là một hàm bất đồng bộ, nên nó được đưa vào queue.
- End được in ra tiếp theo vì stack vẫn còn trống.
- Sau khi stack rỗng, vòng lặp sự kiện lấy callback của setTimeout từ queue và đẩy vào stack để thực thi.
- Callback được in ra cuối cùng.
Tại sao vòng lặp sự kiện lại quan trọng?
- Xử lý các tác vụ bất đồng bộ: Cho phép JavaScript thực hiện các tác vụ không chặn luồng chính, đảm bảo giao diện người dùng luôn đáp ứng.
- Tạo ra các ứng dụng web động: Nhờ vòng lặp sự kiện, chúng ta có thể xây dựng các ứng dụng web phức tạp với nhiều tương tác người dùng.
- Hiệu suất: Giúp tránh tình trạng ứng dụng bị treo khi thực hiện các tác vụ nặng.
Thế nào là Context và Scope trong JavaScript?
Context (Ngữ cảnh) và Scope (Phạm vi) là hai khái niệm quan trọng trong JavaScript, đặc biệt khi làm việc với các hàm và đối tượng. Chúng xác định cách các biến và đối tượng được truy cập và sử dụng trong mã của bạn.
Context (Ngữ cảnh)
- Định nghĩa: Ngữ cảnh xác định giá trị của từ khóa
this
bên trong một hàm. Nó chỉ ra đối tượng màthis
đang tham chiếu đến. - Cách xác định:
- Global context: Khi một hàm được gọi ở cấp độ toàn cục,
this
thường tham chiếu đến đối tượng window. - Object context: Khi một hàm được gọi như một phương thức của một đối tượng,
this
tham chiếu đến đối tượng đó. - Explicit binding: Bạn có thể sử dụng các phương thức như
call
,apply
hoặcbind
để thiết lập ngữ cảnh cho một hàm.
- Global context: Khi một hàm được gọi ở cấp độ toàn cục,
const person = {
name: 'Alice',
greet: function() {
console.log('Hello, my name is ' + this.name);
}
};
person.greet(); // Output: Hello, my name is Alice
Scope (Phạm vi)
- Định nghĩa: Phạm vi xác định khả năng truy cập đến các biến và hàm trong một phần của mã. Nó quyết định biến nào có thể được truy cập tại một vị trí cụ thể.
- Loại scope:
- Global scope: Các biến được khai báo ở cấp độ ngoài cùng có phạm vi toàn cục.
- Function scope: Các biến được khai báo bên trong một hàm chỉ có thể truy cập được bên trong hàm đó.
- Block scope: Từ ES6, các biến khai báo bằng
let
vàconst
có phạm vi khối, tức là chỉ có thể truy cập được bên trong khối lệnh nơi chúng được khai báo.
let x = 10; // Global scope
function myFunction() {
let y = 5; // Function scope
console.log(x); // Có thể truy cập x
console.log(y); // Có thể truy cập y
}
Sự khác biệt giữa Context và Scope
Khái niệm | Định nghĩa | Ví dụ |
---|---|---|
Context | Xác định giá trị của this | this trong một phương thức |
Scope | Xác định khả năng truy cập đến biến | Biến cục bộ, biến toàn cục |
Sự khác biệt giữa call, apply, và bind là gì?
call
và apply
đều gọi hàm với một giá trị this
cụ thể, nhưng call
nhận các đối số riêng lẻ, còn apply
nhận một mảng. bind
trả về một hàm mới với this
cố định mà không thực thi ngay.