+7

JavaScript Nâng Cao - Kỳ 9

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. Toán tử phủ định kép (!!) và truthy/falsy

Output của đoạn code sau là gì?

!!null;
!!"";
!!1;
  • A: false true false
  • B: false false true
  • C: false true true
  • D: true true false
Đá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. Toán tử phủ định (!)

Trước tiên, chúng ta cần hiểu về toán tử phủ định !. Toán tử này sẽ chuyển đổi giá trị sang kiểu boolean và đảo ngược nó:

  • Nếu giá trị là truthy, ! sẽ trả về false
  • Nếu giá trị là falsy, ! sẽ trả về true

1.2. Toán tử phủ định kép (!!)

Toán tử phủ định kép !! là cách nhanh chóng để chuyển đổi một giá trị sang kiểu boolean mà không làm thay đổi tính truthy hoặc falsy của nó. Nó hoạt động bằng cách áp dụng toán tử ! hai lần:

  • Lần đầu tiên chuyển đổi giá trị sang boolean và đảo ngược nó
  • Lần thứ hai đảo ngược lại kết quả, cho ta giá trị boolean tương ứng với tính truthy/falsy ban đầu

1.3. Phân tích từng trường hợp

1.3.1. !!null

  • null là một giá trị falsy
  • !null sẽ trả về true
  • !!null sẽ trả về false

1.3.2. !!""

  • Chuỗi rỗng "" là một giá trị falsy
  • !"" sẽ trả về true
  • !!"" sẽ trả về false

1.3.3. !!1

  • Số 1 là một giá trị truthy
  • !1 sẽ trả về false
  • !!1 sẽ trả về true

1.4. Tổng kết

Vậy nên, kết quả cuối cùng là false false true, tương ứng với đáp án B.

Hiểu về cách hoạt động của toán tử !! và khái niệm truthy/falsy trong JavaScript là rất quan trọng. Nó giúp chúng ta có thể chuyển đổi các giá trị sang boolean một cách nhanh chóng và hiệu quả trong nhiều tình huống lập trình.

2. Hàm setInterval và giá trị trả về

Hàm setInterval trả về cái gì?

setInterval(() => console.log("Hi"), 1000);
  • A: một id duy nhất
  • B: số lượng milliseconds
  • C: function truyền vào
  • D: undefined
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A

Hãy cùng mình tìm hiểu chi tiết về hàm setInterval và giá trị mà nó trả về nhé.

2.1. Hàm setInterval là gì?

setInterval là một hàm có sẵn 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ó nhận vào hai tham số chính:

  1. Một hàm callback (hoặc một đoạn code) để thực thi
  2. Khoảng thời gian (tính bằng milliseconds) giữa mỗi lần thực thi

2.2. Giá trị trả về của setInterval

Khi được gọi, setInterval trả về một giá trị số nguyên duy nhất, được gọi là interval ID. Giá trị này rất quan trọng vì nó cho phép chúng ta có thể dừng việc thực thi lặp đi lặp lại của setInterval bằng cách sử dụng hàm clearInterval().

2.3. Ví dụ minh họa

Hãy xem xét ví dụ sau:

let count = 0;
const intervalId = setInterval(() => {
  console.log("Count:", count);
  count++;
  if (count > 5) {
    clearInterval(intervalId);
    console.log("Interval stopped");
  }
}, 1000);

console.log("Interval ID:", intervalId);

Trong ví dụ này:

  1. Chúng ta tạo một setInterval để in ra giá trị của count mỗi giây.
  2. setInterval trả về một ID, được lưu vào biến intervalId.
  3. Chúng ta sử dụng intervalId để dừng interval sau khi count vượt quá 5.

2.4. Tại sao việc trả về ID là quan trọng?

  1. Kiểm soát: ID cho phép chúng ta kiểm soát interval, đặc biệt là khi cần dừng nó.
  2. Quản lý tài nguyên: Có thể dừng các interval không cần thiết, tránh lãng phí tài nguyên.
  3. Tránh xung đột: Mỗi interval có một ID riêng, giúp tránh xung đột giữa các interval khác nhau.

2.5. Lưu ý quan trọng

  • Nếu bạn không lưu ID được trả về, bạn sẽ không thể dừng interval sau này.
  • Interval sẽ tiếp tục chạy cho đến khi bạn gọi clearInterval() hoặc đóng trang web.

setInterval giúp chúng ta quản lý hiệu quả các tác vụ lặp đi lặp lại trong JavaScript, đặc biệt trong các ứng dụng phức tạp cần kiểm soát chặt chẽ việc sử dụng tài nguyên.

3. Spread operator với chuỗi

Giá trị trả về là gì?

[..."Lydia"];
  • A: ["L", "y", "d", "i", "a"]
  • B: ["Lydia"]
  • C: [[], "Lydia"]
  • D: [["L", "y", "d", "i", "a"]]
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A

Hãy cùng mình tìm hiểu về cách hoạt động của spread operator với chuỗi trong JavaScript nhé.

3.1. Spread operator là gì?

Spread operator (...) là một tính năng được giới thiệu trong ES6 (ECMAScript 2015). Nó cho phép một iterable (như một mảng hoặc chuỗi) được "mở rộng" thành các phần tử riêng lẻ.

3.2. Chuỗi là iterable

Trong JavaScript, chuỗi (string) là một iterable. Điều này có nghĩa là chúng ta có thể lặp qua từng ký tự trong chuỗi.

3.3. Spread operator với chuỗi

Khi sử dụng spread operator với một chuỗi, mỗi ký tự trong chuỗi sẽ được tách ra thành một phần tử riêng biệt.

Trong ví dụ [..."Lydia"]:

  1. Spread operator ... được áp dụng cho chuỗi "Lydia".
  2. Mỗi ký tự trong chuỗi được tách ra.
  3. Các ký tự này được đặt vào một mảng mới.

Kết quả là một mảng mới chứa các ký tự riêng lẻ: ["L", "y", "d", "i", "a"].

3.4. Ví dụ minh họa

Hãy xem xét một vài ví dụ khác để hiểu rõ hơn:

console.log([..."Hello"]); // ["H", "e", "l", "l", "o"]
console.log([..."World"]); // ["W", "o", "r", "l", "d"]
console.log([..."123"]); // ["1", "2", "3"]

3.5. Ứng dụng thực tế

Spread operator với chuỗi có nhiều ứng dụng hữu ích trong thực tế:

  1. Chuyển đổi chuỗi thành mảng:

    const str = "Hello";
    const arr = [...str];
    console.log(arr); // ["H", "e", "l", "l", "o"]
    
  2. Đếm số ký tự duy nhất trong chuỗi:

    const uniqueChars = new Set([..."Mississippi"]);
    console.log(uniqueChars.size); // 4
    
  3. Kết hợp với các phương thức mảng:

    const reversed = [..."Hello"].reverse().join("");
    console.log(reversed); // "olleH"
    

3.6. Lưu ý quan trọng

  • Spread operator chỉ hoạt động với các iterable. Nếu bạn cố gắng sử dụng nó với một non-iterable (như số), bạn sẽ gặp lỗi.
  • Khi sử dụng spread operator với chuỗi Unicode, hãy cẩn thận vì một số ký tự Unicode có thể được biểu diễn bằng nhiều code unit.

3.7. Tổng kết

Spread operator là một công cụ mạnh mẽ trong JavaScript, đặc biệt khi làm việc với chuỗi. Nó cho phép chúng ta dễ dàng chuyển đổi chuỗi thành mảng các ký tự, mở ra nhiều khả năng xử lý chuỗi một cách linh hoạt và hiệu quả.

4. Generator functions và yield

Output là gì?

function* generator(i) {
  yield i;
  yield i * 2;
}

const gen = generator(10);

console.log(gen.next().value);
console.log(gen.next().value);
  • A: [0, 10], [10, 20]
  • B: 20, 20
  • C: 10, 20
  • D: 0, 10 and 10, 20
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: C

Để hiểu được kết quả này, chúng ta cần tìm hiểu về generator functions và từ khóa yield trong JavaScript.

4.1. Generator functions là gì?

Generator functions là một loại hàm đặc biệt trong JavaScript, được định nghĩa bằng cách thêm dấu sao (*) sau từ khóa function. Khi được gọi, generator function không thực thi ngay mà trả về một iterator object.

4.2. Từ khóa yield

Từ khóa yield được sử dụng trong generator functions để tạm dừng và tiếp tục thực thi function. Mỗi lần yield được gọi, nó sẽ trả về một giá trị và lưu trạng thái hiện tại của function.

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

Hãy phân tích từng dòng của đoạn code:

function* generator(i) {
  yield i;
  yield i * 2;
}

Đây là một generator function nhận vào một tham số i. Nó có hai câu lệnh yield:

  • Câu lệnh đầu tiên trả về giá trị i
  • Câu lệnh thứ hai trả về giá trị i * 2
const gen = generator(10);

Ở đây, chúng ta khởi tạo generator với giá trị i là 10.

console.log(gen.next().value);
console.log(gen.next().value);

Phương thức next() được gọi trên generator object. Mỗi lần gọi next(), generator sẽ thực thi cho đến khi gặp từ khóa yield tiếp theo.

4.4. Kết quả chi tiết

  1. Lần gọi next() đầu tiên:

    • Generator thực thi đến yield i
    • Trả về giá trị của i, tức là 10
  2. Lần gọi next() thứ hai:

    • Generator tiếp tục từ vị trí dừng trước đó
    • Thực thi đến yield i * 2
    • Trả về giá trị i * 2, tức là 10 * 2 = 20

4.5. Ứng dụng thực tế

Generator functions có nhiều ứng dụng hữu ích:

  1. Tạo dãy số vô hạn:

    function* infiniteSequence() {
      let i = 0;
      while(true) {
        yield i++;
      }
    }
    
  2. Xử lý dữ liệu lớn: Generator cho phép xử lý từng phần của dữ liệu lớn, giúp tiết kiệm bộ nhớ.

  3. Quản lý trạng thái phức tạp: Có thể sử dụng generator để quản lý các trạng thái phức tạp trong ứng dụng.

4.6. Tổng kết

Generator functions là một công cụ mạnh mẽ trong JavaScript, cho phép chúng ta tạo ra các iterator một cách dễ dàng và kiểm soát luồng thực thi của chương trình. Hiểu rõ cách hoạt động của generator và yield sẽ giúp bạn viết code hiệu quả hơn, đặc biệt khi làm việc với các tác vụ bất đồng bộ hoặc xử lý dữ liệu lớn.

5. Promise.race

Giá trị trả về là gì?

const firstPromise = new Promise((res, rej) => {
  setTimeout(res, 500, "one");
});

const secondPromise = new Promise((res, rej) => {
  setTimeout(res, 100, "two");
});

Promise.race([firstPromise, secondPromise]).then(res => console.log(res));
  • A: "one"
  • B: "two"
  • C: "two" "one"
  • D: "one" "two"
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: B

Để hiểu được kết quả này, chúng ta cần tìm hiểu về Promise.race() và cách nó hoạt động.

5.1. Promise.race() là gì?

Promise.race() là một phương thức tĩnh của đối tượng Promise trong JavaScript. Nó nhận vào một mảng các Promise và trả về một Promise mới. Promise mới này sẽ được resolve hoặc reject ngay khi một trong các Promise trong mảng được resolve hoặc reject, với giá trị hoặc lý do của Promise đó.

5.2. Cách hoạt động của Promise.race()

  • Promise.race() sẽ "chạy đua" giữa các Promise được truyền vào.
  • Promise nào hoàn thành trước (dù là resolve hay reject) sẽ quyết định kết quả của Promise.race().
  • Các Promise khác vẫn tiếp tục thực thi, nhưng kết quả của chúng sẽ bị bỏ qua.

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 firstPromise = new Promise((res, rej) => {
  setTimeout(res, 500, "one");
});

Promise này sẽ resolve sau 500ms với giá trị "one".

const secondPromise = new Promise((res, rej) => {
  setTimeout(res, 100, "two");
});

Promise này sẽ resolve sau 100ms với giá trị "two".

Promise.race([firstPromise, secondPromise]).then(res => console.log(res));

Chúng ta sử dụng Promise.race() với hai Promise trên.

5.4. Kết quả chi tiết

  • secondPromise sẽ resolve sau 100ms, trong khi firstPromise cần 500ms.
  • Do secondPromise hoàn thành trước, Promise.race() sẽ resolve với giá trị của secondPromise, tức là "two".
  • Mặc dù firstPromise vẫn tiếp tục thực thi và resolve sau 500ms, kết quả của nó sẽ bị bỏ qua.

5.5. Ứng dụng thực tế của Promise.race()

  1. Timeout cho các yêu cầu:

    const timeout = new Promise((_, reject) => 
      setTimeout(() => reject(new Error('Request timed out')), 5000)
    );
    const fetchData = fetch('https://api.example.com/data');
    
    Promise.race([fetchData, timeout])
      .then(response => response.json())
      .catch(error => console.error(error));
    
  2. Chọn nguồn dữ liệu nhanh nhất: Khi có nhiều nguồn dữ liệu, bạn có thể sử dụng Promise.race() để lấy dữ liệu từ nguồn nhanh nhất.

  3. Xử lý tương tác người dùng: Có thể sử dụng để xử lý các tình huống như "người dùng click nút hoặc tự động chuyển sau 5 giây".

5.6. Lưu ý quan trọng

  • Nếu mảng truyền vào Promise.race() rỗng, Promise trả về sẽ mãi mãi ở trạng thái pending.
  • Nếu mảng chứa một hoặc nhiều giá trị không phải Promise, những giá trị này sẽ được chuyển đổi thành Promise bằng Promise.resolve().

5.7. Tổng kết

Promise.race() là một công cụ mạnh mẽ trong JavaScript để xử lý các tình huống cần phản hồi nhanh từ nhiều nguồn bất đồng bộ. Nó sẽ giúp bạn xử lý hiệu quả các tác vụ có yêu cầu về thời gian hoặc cần phản hồi từ nguồn nhanh nhất.


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í