+9

JavaScript Nâng Cao - Kỳ 7

Có một câu nói 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ột comment 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. Event Loop và Callback Queue

Với đoạn code này:

const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"), 0);
const baz = () => console.log("Third");

bar();
foo();
baz();

Output sẽ là gì?

  • A: First Second Third
  • B: First Third Second
  • C: Second First Third
  • D: Second Third First
Đá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. Event Loop là gì?

Event Loop là một cơ chế trong JavaScript giúp xử lý các tác vụ bất đồng bộ. Nó liên tục kiểm tra Call Stack và Callback Queue để đảm bảo rằng Call Stack trống trước khi đẩy các callback từ Queue vào Stack để thực thi.

1.2. Callback Queue là gì?

Callback Queue là nơi lưu trữ các callback function của các tác vụ bất đồng bộ (như setTimeout, setInterval, Promise, etc.) khi chúng đã sẵn sàng để thực thi.

1.3. Phân tích đoạn code

Bây giờ, hãy phân tích từng bước thực thi của đoạn code trên:

  1. bar() được gọi đầu tiên. Nó chứa một setTimeout với delay 0ms. Callback của setTimeout sẽ được đẩy vào Callback Queue.

  2. foo() được gọi tiếp theo và in ra "First".

  3. baz() được gọi và in ra "Third".

  4. Sau khi Call Stack trống, Event Loop sẽ kiểm tra Callback Queue và đẩy callback của setTimeout (in ra "Second") vào Call Stack để thực thi.

Vì vậy, thứ tự output sẽ là: "First", "Third", "Second".

1.4. Tại sao setTimeout(callback, 0) không chạy ngay lập tức?

Mặc dù chúng ta set delay là 0ms, nhưng setTimeout vẫn là một tác vụ bất đồng bộ. JavaScript sẽ đẩy callback của nó vào Callback Queue và tiếp tục thực thi các câu lệnh tiếp theo. Chỉ khi Call Stack trống và tất cả các câu lệnh đồng bộ đã được thực thi, callback mới được đưa vào Call Stack để chạy.

1.5. Ví dụ minh họa

Để hiểu rõ hơn, hãy tưởng tượng Event Loop như một nhân viên quản lý hàng đợi tại một nhà hàng:

  • Call Stack là khu vực phục vụ chính.
  • Callback Queue là khu vực chờ.
  • Event Loop là nhân viên quản lý.
  • WebAPI/LibuvAPI là quầy làm thủ tục.
  • Task vụ đồng bộ: Khách hàng VIP.
  • Task vụ bất đồng bộ: Khách hàng thông thường.

Nhân viên sẽ phục vụ khách hàng VIP ngay lập tức (được đưa vào Call Stack), nhưng nếu có khách hàng thông thường đến, họ sẽ được đưa vào hàng đợi và phục vụ sau. Được đưa qua quầy làm thủ tục (WebAPI) làm thủ tục xong thì phải vào hàng đợi chờ phục vụ (Callback Queue).

Cho đến khi nào tất cả khách hàng VIP được phục vụ xong, nhân viên mới quay lại hàng đợi (Callback Queue) và đưa tuần tự từng khách hàng thông thường vào sảnh chính (Call Stack) để phục vụ.

SetTimeout với delay 0ms không có nghĩa là chạy ngay lập tức, mà là chờ đợi Call Stack trống trước khi thực thi. SetTimeout với delay 0ms được hiểu đơn giản theo ví dụ trên chính là thời gian làm thủ tục tại quầy làm thủ tục (WebAPI).

Ghi chú: WebAPI là một phần của trình duyệt, cung cấp các hàm bất đồng bộ như setTimeout, fetch, XMLHttpRequest, etc. LibuvAPI là một thư viện trong Node.js giúp thực thi các tác vụ bất đồng bộ giống như WebAPI nhưng trên server-side.

Event Loop

1.6. Bài tập

Vậy đố các bạn đoạn code sau sẽ in ra kết quả gì?

setTimeout(() => {
console.log('------------------ timeout -------------------');
}, 10);
for(let i = 0; i < 1000000; i++){
 if (i % 10 === 0) {
     console.log(i);
    }
}

2. Event Bubbling và Event Target

Xét đoạn HTML sau:

<div onclick="console.log('first div')">
  <div onclick="console.log('second div')">
    <button onclick="console.log('button')">
      Click!
    </button>
  </div>
</div>

Khi click vào button, giá trị của event.target là gì?

  • A: Outer div
  • B: Inner div
  • C: button
  • D: Một mảng với toàn bộ các phần tử lồng nhau.
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: C

2.1. Event Target là gì?

event.target là phần tử DOM mà sự kiện được kích hoạt trực tiếp trên đó. Trong trường hợp này, khi chúng ta click vào button, event.target sẽ là chính phần tử button đó.

Event Target

2.2. Event Bubbling là gì?

Event Bubbling là quá trình mà một sự kiện, sau khi được kích hoạt trên một phần tử con, sẽ "nổi bọt" (bubble up) lên các phần tử cha của nó trong cây DOM.

2.3. Phân tích đoạn code

Khi chúng ta click vào button:

  1. Sự kiện click được kích hoạt trên button, in ra "button".
  2. Sự kiện "nổi bọt" lên div bên trong, in ra "second div".
  3. Cuối cùng, sự kiện "nổi bọt" lên div bên ngoài cùng, in ra "first div".

Mặc dù sự kiện "nổi bọt" qua tất cả các phần tử cha, event.target vẫn luôn trỏ đến phần tử gốc mà sự kiện được kích hoạt - trong trường hợp này là button.

2.4. Làm thế nào để ngăn chặn Event Bubbling?

Nếu bạn muốn ngăn sự kiện "nổi bọt" lên các phần tử cha, bạn có thể sử dụng phương thức event.stopPropagation(). Ví dụ:

<div onclick="console.log('first div')">
  <div onclick="console.log('second div')">
    <button onclick="event.stopPropagation(); console.log('button')">
      Click!
    </button>
  </div>
</div>

Trong trường hợp này, khi click vào button, chỉ có "button" được in ra console.

2.5. Ví dụ minh họa

Hãy tưởng tượng Event Bubbling như việc ném một hòn đá vào một cái hồ:

  • Hòn đá (sự kiện) được ném vào một điểm cụ thể trên mặt hồ (phần tử DOM được click).
  • Các gợn sóng (sự kiện) lan tỏa từ điểm đó ra xung quanh (lên các phần tử cha).
  • Sau đó chạm vào bờ hồ (phần tử gốc) và phản xạ lại từ từ hội tụ về điểm xuất phát (phần tử gốc).
  • Mặc dù sóng lan rộng, điểm mà hòn đá chạm vào mặt nước (event.target) vẫn không thay đổi.

Hiểu được cơ chế Event Bubbling và cách sử dụng event.target sẽ giúp bạn xử lý sự kiện trong JavaScript một cách hiệu quả hơn, đặc biệt khi làm việc với các phần tử DOM phức tạp.

3. Call và Bind trong JavaScript

Xét đoạn code sau:

const person = { name: "Lydia" };

function sayHi(age) {
  console.log(`${this.name} is ${age}`);
}

sayHi.call(person, 21);
sayHi.bind(person, 21);

Output là gì?

  • A: undefined is 21 Lydia is 21
  • B: function function
  • C: Lydia is 21 Lydia is 21
  • D: Lydia is 21 function
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: D

3.1. Phương thức call()

Phương thức call() gọi một hàm với một giá trị this được chỉ định và các đối số được truyền riêng lẻ.

Cú pháp: function.call(thisArg, arg1, arg2, ...)

Trong ví dụ trên, sayHi.call(person, 21) sẽ gọi hàm sayHi ngay lập tức, với this được set là person và đối số age là 21. Do đó, nó sẽ in ra "Lydia is 21".

3.2. Phương thức bind()

Phương thức bind() tạo ra một hàm mới với this được gán cố định, và các đối số ban đầu được thiết lập sẵn nếu được cung cấp.

Cú pháp: function.bind(thisArg[, arg1[, arg2[, ...]]])

Trong ví dụ trên, sayHi.bind(person, 21) sẽ tạo ra một hàm mới, với this được gán là person và đối số age là 21. Tuy nhiên, hàm này chưa được gọi ngay lập tức, mà chỉ được trả về.

3.3. So sánh call()bind()

Sự khác biệt chính giữa call()bind() là:

  • call() thực thi hàm ngay lập tức.
  • bind() trả về một hàm mới mà có thể được thực thi sau.

Vì vậy, trong ví dụ của chúng ta:

sayHi.call(person, 21); // Thực thi ngay và in ra "Lydia is 21"
sayHi.bind(person, 21); // Trả về một hàm mới, không thực thi ngay

3.4. Ví dụ minh họa

Để hiểu rõ hơn, hãy xem xét ví dụ sau:

const person = {
  name: "John",
  greet: function(greeting) {
    console.log(`${greeting}, ${this.name}!`);
  }
};

const anotherPerson = {
  name: "Jane"
};

// Sử dụng call
person.greet.call(anotherPerson, "Hello"); // Output: Hello, Jane!

// Sử dụng bind
const greetJane = person.greet.bind(anotherPerson);
greetJane("Hi"); // Output: Hi, Jane!

Trong ví dụ này:

  • call() được sử dụng để gọi hàm greet ngay lập tức với thisanotherPerson.
  • bind() tạo ra một hàm mới greetJane với this được gán cố định là anotherPerson. Hàm này có thể được gọi sau đó.

3.5. Lưu ý khi sử dụng

  • call()apply() rất giống nhau, chỉ khác ở cách truyền đối số. call() nhận các đối số riêng lẻ, trong khi apply() nhận một mảng đối số.
  • bind() rất hữu ích khi bạn muốn tạo ra một phiên bản của hàm với this cố định, thường được sử dụng trong các callback hoặc event handler.

3.6. Tóm lại

Hiểu và sử dụng đúng call(), apply(), và bind() là rất quan trọng trong JavaScript. Chúng cho phép bạn kiểm soát giá trị của this và cách truyền đối số vào hàm, giúp code của bạn linh hoạt và mạnh mẽ hơn.

4. Immediately Invoked Function Expression (IIFE)

Xét đoạn code sau:

function sayHi() {
  return (() => 0)();
}

typeof sayHi();

Output là gì?

  • A: "object"
  • B: "number"
  • C: "function"
  • D: "undefined"
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: B

4.1. IIFE là gì?

IIFE (Immediately Invoked Function Expression) là một hàm được định nghĩa và thực thi ngay lập tức. Nó thường được sử dụng để tạo một phạm vi riêng biệt, tránh ô nhiễm biến toàn cục.

4.2. Phân tích đoạn code

Trong đoạn code trên, chúng ta có:

  1. Hàm sayHi() được định nghĩa.
  2. Bên trong sayHi(), chúng ta có một IIFE: (() => 0)()
    • Đây là một arrow function trả về giá trị 0
    • Nó được bao bọc trong dấu ngoặc đơn và được gọi ngay lập tức ()
  3. Kết quả của IIFE (giá trị 0) được trả về từ hàm sayHi()
  4. typeof sayHi() sẽ trả về kiểu dữ liệu của giá trị mà sayHi() trả về

4.3. Giải thích kết quả

Vì IIFE trong sayHi() trả về giá trị 0, và 0 là một số (number), nên typeof sayHi() sẽ trả về "number".

4.4. Ví dụ minh họa

Để hiểu rõ hơn về IIFE, hãy xem xét ví dụ sau:

// IIFE thông thường
(function() {
  var privateVar = "I'm private";
  console.log(privateVar);
})();

// IIFE với arrow function
(() => {
  const privateVar = "I'm also private";
  console.log(privateVar);
})();

// Cố gắng truy cập privateVar sẽ gây lỗi
console.log(privateVar); // ReferenceError: privateVar is not defined

Trong ví dụ này, IIFE tạo ra một phạm vi riêng biệt, giúp bảo vệ các biến bên trong nó khỏi sự truy cập từ bên ngoài.

4.5. Lưu ý khi sử dụng IIFE

  • IIFE thường được sử dụng để tạo một phạm vi riêng, tránh ô nhiễm biến toàn cục.
  • Nó cũng hữu ích trong việc tạo ra các module private trong JavaScript.
  • IIFE có thể nhận tham số như một hàm bình thường.

4.6. Tóm lại

IIFE là một kỹ thuật quan trọng trong JavaScript, giúp tạo ra các phạm vi riêng biệt và bảo vệ biến. Hiểu về IIFE sẽ giúp bạn viết code an toàn và module hóa hơn trong JavaScript.

5. Giá trị falsy trong JavaScript

Xét các giá trị sau:

0;
new Number(0);
("");
(" ");
new Boolean(false);
undefined;

Giá trị nào trong các giá trị trên là falsy?

  • A: 0, '', undefined
  • B: 0, new Number(0), '', new Boolean(false), undefined
  • C: 0, '', new Boolean(false), undefined
  • D: Tất cả đều là falsy
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A

5.1. Giá trị falsy là gì?

Trong JavaScript, một giá trị falsy là giá trị được coi là false khi chuyển đổi sang boolean. Khi sử dụng trong các câu lệnh điều kiện, các giá trị falsy sẽ được xem như là false.

5.2. Các giá trị falsy trong JavaScript

JavaScript có 6 giá trị falsy:

  1. false
  2. 0 (số không)
  3. '' (chuỗi rỗng)
  4. null
  5. undefined
  6. NaN (Not a Number)

5.3. Phân tích từng giá trị

  1. 0: Là giá trị falsy.
  2. new Number(0): Là một object, nên là truthy.
  3. '': Chuỗi rỗng, là giá trị falsy.
  4. ' ': Chuỗi chứa một khoảng trắng, không phải chuỗi rỗng, nên là truthy.
  5. new Boolean(false): Là một object, nên là truthy, mặc dù giá trị bên trong là false.
  6. undefined: Là giá trị falsy.

5.4. Ví dụ minh họa

console.log(Boolean(0));               // false
console.log(Boolean(new Number(0)));   // true
console.log(Boolean(''));              // false
console.log(Boolean(' '));             // true
console.log(Boolean(new Boolean(false))); // true
console.log(Boolean(undefined));       // false

if (new Boolean(false)) {
  console.log("This will be executed!");
}

5.5. Lưu ý khi làm việc với giá trị falsy

  • Cẩn thận khi sử dụng các constructor như new Number(), new Boolean(). Chúng tạo ra các object, mà object luôn là truthy.
  • Chuỗi chỉ falsy khi nó hoàn toàn rỗng (''). Chuỗi chứa khoảng trắng (' ') là truthy.
  • Khi so sánh, nên sử dụng === thay vì == để tránh các chuyển đổi kiểu dữ liệu không mong muốn.

5.6. Tóm lại

Hiểu rõ về các giá trị falsy trong JavaScript là rất quan trọng, đặc biệt khi làm việc với các câu lệnh điều kiện. Điều này giúp bạn tránh được các lỗi logic không mong muốn trong code của mình.

Hy vọng rằng bài viết này đã giúp các bạn hiểu rõ hơn về các khái niệm quan trọng trong JavaScript như Event Loop, Event Bubbling, call()bind(), IIFE, và giá trị falsy. Những kiến thức này sẽ giúp bạn viết code JavaScript chính xác và hiệu quả hơn.

Hẹn gặp lại các bạn ở các kỳ 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ột comment 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí