• Home
  • Lập trình
  • Một số câu hỏi phỏng vấn JavaScript từ cơ bản đến nâng cao
Câu hỏi phỏng vấn JS

Một số câu hỏi phỏng vấn JavaScript từ cơ bản đến nâng cao

Danh mục bài viết

Phầ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ống let 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àm myFunction, 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 block if cũng ảnh hưởng đến giá trị của x 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 block if là một biến riêng biệt, không ảnh hưởng đến biến x 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ếu x 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, letconst đều được hoisting, nhưng cách chúng được khởi tạo là khác nhau.
  • Temporal Dead Zone (TDZ): Biến letconst 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 loopcallback 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:
    1. console.log('Start'); được thực thi ngay lập tức và in ra “Start”.
    2. setTimeout() tạo ra một callback và đưa nó vào Callback Queue, nhưng với thời gian chờ là 0ms.
    3. console.log('End'); được thực thi tiếp theo, in ra “End”.
    4. 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?

  • 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ó argumentsconstructor.

Để 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óa return. 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óa return để chỉ rõ giá trị trả về.

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ằng var, 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áo x, nhưng do hoisting, biến x đã đượ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 letconst, 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ỗi ReferenceError.

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ố: namecallback.
    • sayGoodbye là một callback được truyền vào hàm greet.
    • 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àm callback (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()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ứcMục đíchTrả về
map()Tạo mảng mới với các phần tử được biến đổiMả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ệnMả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ấtMộ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ào Person.prototype.
    • Khi chúng ta gọi person1.greet(), JavaScript sẽ tìm phương thức greet trong person1. Vì không tìm thấy, nó sẽ tìm lên Person.prototype và tìm thấy phương thức greet ở đó.

Ứ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ố: resolvereject.

    • 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ụng fetch để 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().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ọi getUser 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.

Explain Event Loop in JavaScript.
Explain Event Loop in JavaScript.

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.

  1. 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.
  2. 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.
  3. 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)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ặc bind để thiết lập ngữ cảnh cho một hàm.
				
					const person = {
    name: 'Alice',
    greet: function() {
        console.log('Hello, my name is ' + this.name);
    }
};

person.greet(); // Output: Hello, my name is Alice
				
			
Trong ví dụ trên, khi gọi person.greet(), this bên trong hàm greet sẽ tham chiếu đến đối tượng person.

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 letconst 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ĩaVí dụ
ContextXác định giá trị của thisthis trong một phương thức
ScopeXác định khả năng truy cập đến biếnBiến cục bộ, biến toàn cục

Sự khác biệt giữa call, apply, và bind là gì?

callapply đề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.

 

Bài viết liên quan

Công Nghệ Đám Mây: Giải Pháp Lưu Trữ Và Xử Lý Dữ Liệu Tối Ưu

Công Nghệ Đám Mây: Giải Pháp Lưu Trữ Và Xử Lý Dữ Liệu Tối Ưu Công nghệ đám…

ByByTrường SơnMar 19, 2025

Thực Tế Ảo (VR) và Thực Tế Tăng Cường (AR): Cánh Cửa Đến Thế Giới Mới

That’s a good title! It’s concise and accurately reflects the transformative potential of VR and AR. However, depending…

ByByTrường SơnMar 19, 2025

Lập Trình 4.0: Học Tập Và Thực Hành Để Thành Công

Lập Trình 4.0: Học Tập Và Thực Hành Để Thành Công Lập trình 4.0 không chỉ đơn thuần…

ByByTrường SơnMar 19, 2025

5 Công Nghệ Đột Phá Thay Đổi Cách Chúng Ta Sống

Năm công nghệ đột phá đang thay đổi cách chúng ta sống bao gồm: Trí tuệ nhân tạo…

ByByTrường SơnMar 19, 2025

Subscribe
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bình chọn nhiều nhất
Phản hồi nội tuyến
Xem tất cả bình luận

0
Rất mong nhận được suy nghĩ của bạn, vui lòng bình luận.x
()
x
0869224813
Liên hệ