+10

JavaScript Nâng Cao - Kỳ 25

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ộ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. Setter và getter trong JavaScript

Output của đoạn code bên dưới là gì?

const config = {
 languages: [],
 set language(lang) {
  return this.languages.push(lang);
 }
};

console.log(config.language);
  • A: function language(lang) { this.languages.push(lang }
  • B: 0
  • C: []
  • D: undefined
Đá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é ❓️

1.1. Phân tích vấn đề

Trong đoạn code trên, chúng ta định nghĩa một object config với một thuộc tính languages (một mảng rỗng) và một setter language. Sau đó, chúng ta cố gắng in ra giá trị của config.language.

1.2. Setter trong JavaScript

Setter là một phương thức đặc biệt được sử dụng để định nghĩa một hành động sẽ được thực hiện khi chúng ta cố gắng gán một giá trị cho thuộc tính. Trong trường hợp này, language là một setter.

set language(lang) {
    return this.languages.push(lang);
}

Khi chúng ta gọi config.language = someValue, setter này sẽ được kích hoạt và thêm someValue vào mảng languages.

1.3. Tại sao kết quả là undefined?

Khi chúng ta gọi console.log(config.language), chúng ta không thực sự gán giá trị nào cho language. Thay vào đó, chúng ta đang cố gắng đọc giá trị của nó.

Tuy nhiên, language là một setter, không phải một getter. Nó chỉ định nghĩa hành động khi gán giá trị, không định nghĩa cách lấy giá trị. Do đó, khi cố gắng đọc config.language, JavaScript trả về undefined.

1.4. Ví dụ minh họa

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

const config = {
    languages: [],
    set language(lang) {
        this.languages.push(lang);
    },
    get language() {
        return this.languages[this.languages.length - 1];
    }
};

config.language = "JavaScript";
console.log(config.language); // Output: "JavaScript"

Trong ví dụ này, chúng ta đã thêm một getter cho language. Khi gọi config.language, getter này sẽ trả về phần tử cuối cùng của mảng languages.

1.5. Tóm lại

Setter trong JavaScript chỉ định nghĩa hành động khi gán giá trị cho một thuộc tính. Khi cố gắng đọc giá trị của một thuộc tính chỉ có setter mà không có getter, JavaScript sẽ trả về undefined.

2. Toán tử phủ định và typeof

Output của đoạn code bên dưới là gì?

const name = "Lydia Hallie";

console.log(!typeof name === "object");
console.log(!typeof name === "string");
  • A: false true
  • B: true false
  • C: false false
  • D: true true
Đá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é ❓️

2.1. Phân tích vấn đề

Trong đoạn code này, chúng ta có một biến name với giá trị là một chuỗi. Sau đó, chúng ta thực hiện hai phép so sánh sử dụng toán tử phủ định !typeof.

2.2. Toán tử typeof

Toán tử typeof trong JavaScript trả về một chuỗi chỉ ra kiểu của toán hạng. Trong trường hợp này:

typeof name // trả về "string"

2.3. Toán tử phủ định

Toán tử ! trong JavaScript là toán tử phủ định logic. Nó chuyển đổi toán hạng của nó thành boolean và sau đó đảo ngược giá trị đó.

!true // false
!"string" // false (vì "string" là truthy)
!false // true

2.4. Phân tích từng dòng

Dòng 1: !typeof name === "object"

  • typeof name trả về "string"
  • !"string" trả về false
  • false === "object" trả về false

Dòng 2: !typeof name === "string"

  • typeof name trả về "string"
  • !"string" trả về false
  • false === "string" trả về false

2.5. Ví dụ minh họa

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

console.log(typeof "hello"); // "string"
console.log(!"hello"); // false
console.log(!true === false); // true
console.log(!false === true); // true

2.6. Tóm lại

Khi sử dụng toán tử phủ định ! với kết quả của typeof, chúng ta luôn nhận được một giá trị boolean (false trong trường hợp này). Sau đó, khi so sánh giá trị boolean này với một chuỗi bằng toán tử ===, kết quả luôn là false vì chúng khác kiểu dữ liệu.

3. Currying trong JavaScript

Output của đoạn code bên dưới là gì?

const add = x => y => z => {
 console.log(x, y, z);
 return x + y + z;
};

add(4)(5)(6);
  • A: 4 5 6
  • B: 6 5 4
  • C: 4 function function
  • D: undefined undefined 6
Đáp án của câu hỏi này là 
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A

Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️

3.1. Phân tích vấn đề

Đoạn code trên sử dụng một kỹ thuật gọi là "currying" trong JavaScript. Currying là quá trình chuyển đổi một hàm có nhiều tham số thành một chuỗi các hàm, mỗi hàm chỉ nhận một tham số.

3.2. Currying là gì?

Currying là một kỹ thuật trong lập trình hàm, trong đó một hàm nhận nhiều đối số được chuyển đổi thành một chuỗi các hàm, mỗi hàm chỉ nhận một đối số. Điều này cho phép chúng ta tạo ra các hàm có thể được tùy chỉnh và tái sử dụng dễ dàng hơn.

3.3. Phân tích hàm add

Hàm add trong ví dụ trên có thể được viết lại như sau để dễ hiểu hơn:

function add(x) {
    return function(y) {
        return function(z) {
            console.log(x, y, z);
            return x + y + z;
        }
    }
}

Khi chúng ta gọi add(4)(5)(6), điều gì xảy ra?

  1. add(4) trả về một hàm nhận tham số y
  2. Hàm trả về này được gọi với đối số 5, trả về một hàm khác nhận tham số z
  3. Hàm cuối cùng này được gọi với đối số 6

3.4. Ví dụ minh họa

Để hiểu rõ hơn về cách currying hoạt động, hãy xem xét ví dụ sau:

const multiply = x => y => x * y;

const double = multiply(2);
const triple = multiply(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Trong ví dụ này, multiply là một hàm được curried. Chúng ta có thể tạo ra các hàm mới như doubletriple bằng cách áp dụng một phần các đối số.

3.5. Tóm lại

Khi gọi add(4)(5)(6), hàm bên trong cùng được thực thi với các giá trị x = 4, y = 5, và z = 6. Do đó, console.log(x, y, z) sẽ in ra 4 5 6, và hàm trả về tổng của chúng, là 15.

Currying là một kỹ thuật mạnh mẽ trong JavaScript, cho phép chúng ta tạo ra các hàm linh hoạt và có thể tái sử dụng. Tuy nhiên, nó cũng có thể làm cho code trở nên khó đọc nếu sử dụng quá mức, vì vậy hãy sử dụng nó một cách thông minh!

4. Async Generator Functions

Output của đoạn code bên dưới là gì?

async function* range(start, end) {
 for (let i = start; i <= end; i++) {
  yield Promise.resolve(i);
 }
}

(async () => {
 const gen = range(1, 3);
 for await (const item of gen) {
  console.log(item);
 }
})();
  • A: Promise {1} Promise {2} Promise {3}
  • B: Promise {<pending>} Promise {<pending>} Promise {<pending>}
  • C: 1 2 3
  • D: undefined undefined undefined
Đá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. Phân tích vấn đề

Đoạn code trên sử dụng một số tính năng nâng cao của JavaScript, bao gồm async generator functions và for-await-of loops. Hãy cùng phân tích từng phần.

4.2. Async Generator Functions

Async generator functions là sự kết hợp của async functions và generator functions. Chúng cho phép chúng ta tạo ra một chuỗi các giá trị bất đồng bộ.

async function* range(start, end) {
 for (let i = start; i <= end; i++) {
  yield Promise.resolve(i);
 }
}

Trong hàm này, chúng ta sử dụng yield để trả về một Promise cho mỗi giá trị từ start đến end.

4.3. For-await-of Loops

For-await-of loops cho phép chúng ta lặp qua các giá trị bất đồng bộ một cách dễ dàng.

for await (const item of gen) {
    console.log(item);
}

Vòng lặp này sẽ đợi mỗi Promise được resolve trước khi tiếp tục với giá trị tiếp theo.

4.4. Phân tích từng bước

  1. Chúng ta tạo một async generator range(1, 3).
  2. Generator này sẽ yield ra 3 Promises, mỗi Promise resolve thành 1, 2, và 3.
  3. Vòng lặp for-await-of sẽ đợi mỗi Promise resolve và in ra giá trị của nó.

4.5. Ví dụ minh họa

Để hiểu rõ hơn về cách async generator functions hoạt động, hãy xem xét ví dụ sau:

async function* asyncGenerator() {
  yield await Promise.resolve(1);
  yield await Promise.resolve(2);
  yield await Promise.resolve(3);
}

(async () => {
  for await (const value of asyncGenerator()) {
    console.log(value);
  }
})();
// Output: 1, 2, 3

Trong ví dụ này, asyncGenerator yield ra các Promises đã được resolve. Vòng lặp for-await-of sẽ đợi mỗi Promise resolve trước khi in ra giá trị của nó.

4.6. Tóm lại

Khi chạy đoạn code gốc, async generator range(1, 3) tạo ra một chuỗi các Promises. Mỗi Promise này được resolve ngay lập tức với các giá trị 1, 2, và 3. Vòng lặp for-await-of đợi mỗi Promise resolve và in ra giá trị của nó.

Do đó, output cuối cùng sẽ là:

1
2
3

Async generator functions và for-await-of loops là những công cụ mạnh mẽ trong JavaScript để xử lý các tác vụ bất đồng bộ một cách tuần tự và dễ đọc. Chúng đặc biệt hữu ích khi làm việc với các streams dữ liệu bất đồng bộ hoặc khi bạn cần tạo ra một chuỗi các giá trị bất đồng bộ.

5. Destructuring trong JavaScript

Output của đoạn code bên dưới là gì?

const myFunc = ({ x, y, z }) => {
 console.log(x, y, z);
};

myFunc(1, 2, 3);
  • A: 1 2 3
  • B: {1: 1} {2: 2} {3: 3}
  • C: { 1: undefined } undefined undefined
  • D: undefined undefined undefined
Đá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é ❓️

5.1. Phân tích vấn đề

Đoạn code trên sử dụng destructuring trong tham số của hàm. Tuy nhiên, cách gọi hàm không phù hợp với cách định nghĩa tham số, dẫn đến kết quả không như mong đợi.

5.2. Destructuring trong JavaScript

Destructuring là một cú pháp cho phép chúng ta "giải nén" các giá trị từ arrays hoặc thuộc tính từ objects vào các biến riêng biệt.

Trong trường hợp này:

const myFunc = ({ x, y, z }) => {
 console.log(x, y, z);
};

Hàm myFunc mong đợi một object có các thuộc tính x, y, và z làm đối số.

5.3. Phân tích lỗi

Khi chúng ta gọi myFunc(1, 2, 3), chúng ta đang truyền vào ba số riêng biệt, không phải một object như hàm mong đợi.

JavaScript sẽ cố gắng destructure đối số đầu tiên (1) như một object, nhưng vì nó không phải là object, nên x, y, và z đều sẽ là undefined.

5.4. Cách sửa lỗi

Để sửa lỗi này, chúng ta cần truyền vào một object có các thuộc tính x, y, và z:

myFunc({ x: 1, y: 2, z: 3 });
// Output: 1 2 3

5.5. Ví dụ minh họa

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

const person = { name: 'John', age: 30, city: 'New York' };

const { name, age } = person;

console.log(name); // 'John'
console.log(age);  // 30

Trong ví dụ này, chúng ta destructure object person để lấy ra các giá trị của nameage.

5.6. Tóm lại

Destructuring là một tính năng mạnh mẽ trong JavaScript, cho phép chúng ta trích xuất dữ liệu từ arrays hoặc objects một cách dễ dàng. Tuy nhiên, khi sử dụng destructuring trong tham số hàm, chúng ta cần đảm bảo rằng đối số được truyền vào phù hợp với cấu trúc mà hàm mong đợi.

Trong trường hợp này, do cách gọi hàm không phù hợp với cách định nghĩa tham số, nên kết quả là ba giá trị undefined.

Kết luận

Qua 5 ví dụ trên, chúng ta đã đi sâu vào một số khía cạnh nâng cao của JavaScript, bao gồm:

  1. Setter và getter trong objects
  2. Toán tử phủ định và typeof
  3. Currying và higher-order functions
  4. Async generator functions và for-await-of loops
  5. Destructuring trong tham số hàm

Mỗi ví dụ đều cho thấy những tính năng độc đáo và mạnh mẽ của JavaScript, đồng thời cũng chỉ ra những cạm bẫy tiềm ẩn mà developers có thể gặp phải.

Việc hiểu rõ những khái niệm này không chỉ giúp bạn viết code JavaScript tốt hơn, mà còn giúp bạn debug hiệu quả hơn khi gặp các vấn đề phức tạp.

Hãy nhớ rằng, practice makes perfect! Đừng ngại thử nghiệm với những ví dụ này và tạo ra các biến thể của chúng để hiểu sâu hơn về cách JavaScript hoạt động.

Hy vọng bài viết này đã mang lại cho bạn những kiến thức bổ ích. Hẹn gặp lại các bạn trong các bài viết 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í