+9

JavaScript Nâng Cao - Kỳ 16

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. Destructuring và Renaming

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

const { name: myName } = { name: "Lydia" };

console.log(name);
  • A: "Lydia"
  • B: "myName"
  • C: undefined
  • D: ReferenceError
Đá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. Destructuring trong JavaScript

Destructuring là một cú pháp cho phép chúng ta "unpack" các giá trị từ arrays hoặc properties từ objects vào các biến riêng biệt. Nó giúp code của chúng ta ngắn gọn và dễ đọc hơn.

1.2. Renaming trong Destructuring

Trong ví dụ trên, chúng ta đang sử dụng destructuring kết hợp với renaming. Cú pháp { name: myName } có nghĩa là:

  • Lấy giá trị của property name từ object bên phải dấu =
  • Gán giá trị đó cho một biến mới có tên là myName

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

const { name: myName } = { name: "Lydia" };

Ở đây, chúng ta đang tạo một biến mới có tên myName và gán cho nó giá trị "Lydia". Chúng ta KHÔNG tạo một biến có tên name.

console.log(name);

Khi chúng ta cố gắng log ra biến name, JavaScript không thể tìm thấy bất kỳ biến nào có tên name trong scope hiện tại. Do đó, nó sẽ throw ra một ReferenceError.

1.4. Tóm lại

Destructuring là một công cụ mạnh mẽ trong JavaScript, nhưng chúng ta cần cẩn thận khi sử dụng nó, đặc biệt là khi kết hợp với renaming. Luôn nhớ rằng khi bạn rename một property, bạn đang tạo ra một biến mới với tên mới, không phải là biến với tên ban đầu của property.

2. Pure Functions

Đây có phải là một pure function không?

function sum(a, b) {
  return a + b;
}
  • A: Yes
  • B: No
Đá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é ❓️

2.1. Pure Function là gì?

Một pure function (hàm thuần khiết) là một hàm mà:

  1. Luôn trả về cùng một kết quả nếu được cung cấp cùng một đầu vào.
  2. Không gây ra bất kỳ side effects nào (không thay đổi bất kỳ trạng thái nào bên ngoài function).

2.2. Phân tích hàm sum

Hãy xem xét hàm sum:

function sum(a, b) {
  return a + b;
}
  1. Tính nhất quán: Với cùng một cặp đầu vào, hàm sum luôn trả về cùng một kết quả. Ví dụ:

    • sum(2, 3) luôn trả về 5
    • sum(10, 5) luôn trả về 15
  2. Không có side effects: Hàm sum không thay đổi bất kỳ trạng thái nào bên ngoài nó. Nó chỉ đơn giản là nhận vào hai tham số, thực hiện phép cộng và trả về kết quả.

2.3. Ví dụ về Non-Pure Function

Để hiểu rõ hơn, hãy xem một ví dụ về hàm không thuần khiết:

let total = 0;

function addToTotal(value) {
  total += value;
  return total;
}

Hàm addToTotal không phải là pure function vì:

  1. Nó thay đổi biến total bên ngoài hàm (side effect).
  2. Kết quả trả về phụ thuộc vào trạng thái của total, không chỉ phụ thuộc vào đầu vào value.

2.4. Lợi ích của Pure Functions

  1. Dễ test: Vì luôn cho cùng kết quả với cùng đầu vào, pure functions rất dễ để viết unit test.
  2. Dễ debug: Không có side effects, nên dễ dàng xác định nguyên nhân lỗi.
  3. Tránh được các lỗi không mong muốn: Không thay đổi trạng thái bên ngoài, giảm thiểu các lỗi liên quan đến shared state.
  4. Có thể cache: Kết quả của pure functions có thể được cache để tối ưu hiệu suất.

2.5. Tóm lại

Hàm sum là một pure function vì nó luôn trả về cùng một kết quả cho cùng một đầu vào và không gây ra bất kỳ side effects nào. Việc sử dụng pure functions khi có thể sẽ giúp code của bạn dễ đọc, dễ test và ít gây ra lỗi hơn.

3. Memoization và Closure

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

const add = () => {
  const cache = {};
  return num => {
    if (num in cache) {
      return `From cache! ${cache[num]}`;
    } else {
      const result = num + 10;
      cache[num] = result;
      return `Calculated! ${result}`;
    }
  };
};

const addFunction = add();
console.log(addFunction(10));
console.log(addFunction(10));
console.log(addFunction(5 * 2));
  • A: Calculated! 20 Calculated! 20 Calculated! 20
  • B: Calculated! 20 From cache! 20 Calculated! 20
  • C: Calculated! 20 From cache! 20 From cache! 20
  • D: Calculated! 20 From cache! 20 Error
Đá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é ❓️

3.1. Closure và Memoization

Đoạn code trên là một ví dụ tuyệt vời về việc sử dụng closure để implement memoization. Hãy cùng phân tích từng phần:

  1. Closure: Closure là khả năng của một hàm có thể "nhớ" và truy cập vào lexical scope của nó, ngay cả khi hàm đó được thực thi bên ngoài lexical scope.

  2. Memoization: Là một kỹ thuật tối ưu hóa, lưu trữ kết quả của các phép tính tốn kém để sử dụng lại trong tương lai mà không cần tính toán lại.

3.2. Phân tích code

const add = () => {
  const cache = {};
  return num => {
    if (num in cache) {
      return `From cache! ${cache[num]}`;
    } else {
      const result = num + 10;
      cache[num] = result;
      return `Calculated! ${result}`;
    }
  };
};
  • Hàm add trả về một hàm khác. Hàm được trả về này có quyền truy cập vào biến cache nhờ closure.
  • cache là một object được sử dụng để lưu trữ các kết quả đã tính toán trước đó.

3.3. Thực thi code

const addFunction = add();
  • addFunction giờ đây là hàm được trả về từ add(). Nó "nhớ" cache object.
console.log(addFunction(10));
  • Đây là lần đầu tiên chúng ta gọi với đối số 10.
  • 10 chưa có trong cache, nên nó sẽ tính 10 + 10 = 20, lưu vào cache, và trả về "Calculated! 20".
console.log(addFunction(10));
  • Lần thứ hai gọi với đối số 10.
  • 10 đã có trong cache, nên nó sẽ trả về giá trị đã lưu: "From cache! 20".
console.log(addFunction(5 * 2));
  • 5 * 2 được tính toán trước khi truyền vào hàm, nên nó tương đương với addFunction(10).
  • 10 đã có trong cache, nên kết quả vẫn là "From cache! 20".

3.4. Lợi ích của Memoization

  1. Tối ưu hiệu suất: Tránh tính toán lại các giá trị đã biết.
  2. Tiết kiệm tài nguyên: Đặc biệt hữu ích cho các phép tính phức tạp hoặc tốn kém.

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

  1. Bộ nhớ: Cần cân nhắc giữa tốc độ và bộ nhớ sử dụng.
  2. Side effects: Cẩn thận với các hàm có side effects, vì memoization có thể làm thay đổi hành vi của chúng.

3.6. Tóm lại

Đoạn code trên là một ví dụ tuyệt vời về cách sử dụng closure để implement memoization. Nó cho thấy cách JavaScript có thể "nhớ" các giá trị qua nhiều lần gọi hàm, giúp tối ưu hiệu suất bằng cách tránh tính toán lại các giá trị đã biết.

4. for...in và for...of loops

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

const myLifeSummedUp = ["☕", "💻", "🍷", "🍫"]

for (let item in myLifeSummedUp) {
  console.log(item)
}

for (let item of myLifeSummedUp) {
  console.log(item)
}
  • A: 0 1 2 3"☕" "💻" "🍷" "🍫"
  • B: "☕" "💻" "🍷" "🍫""☕" "💻" "🍷" "🍫"
  • C: "☕" "💻" "🍷" "🍫"0 1 2 3
  • D: 0 1 2 3{0: "☕", 1: "💻", 2: "🍷", 3: "🍫"}
Đá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é ❓️

4.1. for...in loop

for...in loop được sử dụng để lặp qua các thuộc tính enumerable của một object. Khi sử dụng với mảng, nó sẽ lặp qua các chỉ số (index) của mảng.

for (let item in myLifeSummedUp) {
  console.log(item)
}

Kết quả: 0 1 2 3

Điều này xảy ra vì for...in đang lặp qua các chỉ số của mảng, không phải các giá trị.

4.2. for...of loop

for...of loop được sử dụng để lặp qua các giá trị của các đối tượng có thể lặp lại (iterable objects), bao gồm cả Array, Map, Set, String, v.v.

for (let item of myLifeSummedUp) {
  console.log(item)
}

Kết quả: "☕" "💻" "🍷" "🍫"

Ở đây, for...of đang lặp qua các giá trị của mảng.

4.3. So sánh for...in và for...of

  1. for...in:

    • Lặp qua các thuộc tính enumerable của object.
    • Với mảng, nó lặp qua các chỉ số (dưới dạng string).
    • Có thể lặp qua các thuộc tính được thêm vào prototype.
  2. for...of:

    • Lặp qua các giá trị của các đối tượng iterable.
    • Với mảng, nó lặp qua các giá trị trực tiếp.
    • Không thể sử dụng với các object thông thường (non-iterable).

4.4. Ví dụ mở rộng

Hãy xem xét ví dụ sau để hiểu rõ hơn sự khác biệt:

const arr = ['a', 'b', 'c'];
arr.customProperty = 'custom';

for (let item in arr) {
  console.log(item);
}
// Output: "0", "1", "2", "customProperty"

for (let item of arr) {
  console.log(item);
}
// Output: "a", "b", "c"

Trong ví dụ này, for...in lặp qua cả thuộc tính tùy chỉnh customProperty, trong khi for...of chỉ lặp qua các giá trị của mảng.

4.5. Khi nào sử dụng for...in và for...of?

  • Sử dụng for...in khi bạn cần lặp qua các thuộc tính của một object.
  • Sử dụng for...of khi bạn muốn lặp qua các giá trị của một iterable như mảng hoặc string.

4.6. Tóm lại

Hiểu rõ sự khác biệt giữa for...infor...of là rất quan trọng trong JavaScript. for...in thường được sử dụng với objects để lặp qua các key, trong khi for...of được sử dụng với các iterable để lặp qua các giá trị. Việc chọn đúng loại vòng lặp sẽ giúp code của bạn rõ ràng và hiệu quả hơn.

5. Tính toán trong mảng

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

const list = [1 + 2, 1 * 2, 1 / 2]
console.log(list)
  • A: ["1 + 2", "1 * 2", "1 / 2"]
  • B: ["12", 2, 0.5]
  • C: [3, 2, 0.5]
  • D: [1, 1, 1]
Đá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é ❓️

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

Trong đoạn code này, chúng ta đang tạo một mảng list với ba phần tử. Mỗi phần tử là một biểu thức toán học:

  1. 1 + 2
  2. 1 * 2
  3. 1 / 2

5.2. Cách JavaScript xử lý biểu thức trong mảng

Khi JavaScript gặp các biểu thức trong khai báo mảng, nó sẽ tính toán giá trị của các biểu thức đó trước khi đưa vào mảng. Điều này có nghĩa là:

  • 1 + 2 sẽ được tính thành 3
  • 1 * 2 sẽ được tính thành 2
  • 1 / 2 sẽ được tính thành 0.5

5.3. Kết quả

Vì vậy, khi chúng ta console.log(list), kết quả sẽ là:

[3, 2, 0.5]

5.4. Các trường hợp khác

Để hiểu rõ hơn, hãy xem xét một số ví dụ khác:

const list1 = [1 + 2, "1 + 2", `${1 + 2}`];
console.log(list1); // [3, "1 + 2", "3"]

const list2 = [Math.random(), new Date(), () => "Hello"];
console.log(list2); // [0.123456789, Date object, function]

Trong list1:

  • Phần tử đầu tiên là kết quả của phép tính 1 + 2.
  • Phần tử thứ hai là một chuỗi, không được tính toán.
  • Phần tử thứ ba sử dụng template literal, nên 1 + 2 được tính toán trước khi chuyển thành chuỗi.

Trong list2:

  • Phần tử đầu tiên là kết quả của Math.random().
  • Phần tử thứ hai là một đối tượng Date.
  • Phần tử thứ ba là một hàm.

5.5. Lưu ý quan trọng

  1. Tính toán ngay lập tức: Các biểu thức trong mảng được tính toán ngay khi mảng được tạo, không phải khi truy cập.

  2. Không thay đổi sau khi tạo: Kết quả của các phép tính này sẽ không thay đổi sau khi mảng đã được tạo.

  3. Cẩn thận với side effects: Nếu bạn đặt các hàm có side effects trong mảng, chúng sẽ được thực thi ngay khi mảng được tạo.

5.6. Tóm lại

Khi làm việc với mảng trong JavaScript, điều quan trọng là phải hiểu rằng các biểu thức sẽ được tính toán ngay lập tức khi mảng được tạo. Điều này có thể rất hữu ích cho việc tạo các mảng động, nhưng cũng cần cẩn thận để tránh các side effects không mong muốn hoặc tính toán không cần thiết.

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 thú vị của JavaScript:

  1. Destructuring và Renaming: Cách JavaScript xử lý việc gán giá trị và đổi tên biến trong quá trình destructuring.
  2. Pure Functions: Khái niệm về hàm thuần khiết và tầm quan trọng của nó trong lập trình.
  3. Memoization và Closure: Cách sử dụng closure để implement memoization, một kỹ thuật tối ưu hóa quan trọng.
  4. for...in và for...of loops: Sự khác biệt giữa hai loại vòng lặp này và khi nào nên sử dụng chúng.
  5. Tính toán trong mảng: Cách JavaScript xử lý các biểu thức khi khai báo mảng.

Mỗi ví dụ đều cho thấy những đặc điểm độc đáo của JavaScript, từ cách nó xử lý biến và scope, đến cách nó thực hiện các phép tính và lặp qua các cấu trúc dữ liệu.

Hiểu rõ những khía cạnh này không chỉ giúp bạn viết code JavaScript tốt hơn, mà còn giúp bạn tránh được những lỗi phổ biến và tối ưu hóa hiệu suất của ứng dụng.

Hãy nhớ rằng, trong JavaScript, thực hành là chìa khóa để nắm vững. Đừng ngại thử nghiệm với các ví dụ này và tạo ra các biến thể của chúng để hiểu sâu hơn cách ngôn ngữ hoạt động.

Hy vọng bài viết này đã mang lại cho bạn những hiểu biết mới về JavaScript. Hãy tiếp tục học hỏi và khám phá, vì JavaScript là một ngôn ngữ rộng lớn và luôn phát triển!

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" nhé!

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
Let's register a Viblo Account to get more interesting posts.