+11

JavaScript Nâng Cao - Kỳ 23

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. Temporal Dead Zone và Hoisting

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

let name = 'Lydia'

function getName() {
  console.log(name)
  let name = 'Sarah'
}

getName()
  • A: Lydia
  • B: Sarah
  • 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. Hoisting và Temporal Dead Zone

Để hiểu được vấn đề này, chúng ta cần nắm rõ hai khái niệm quan trọng trong JavaScript: Hoisting và Temporal Dead Zone (TDZ).

Hoisting

Hoisting là một hành vi trong JavaScript, nơi các khai báo biến và hàm được di chuyển lên đầu scope của chúng trước khi code được thực thi. Tuy nhiên, chỉ có khai báo được hoisted, không phải là khởi tạo.

Temporal Dead Zone (TDZ)

TDZ là khoảng thời gian từ khi biến được hoisted đến khi nó được khởi tạo. Trong khoảng thời gian này, nếu ta cố gắng truy cập biến, JavaScript sẽ throw ra ReferenceError.

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

Bây giờ, hãy phân tích đoạn code của chúng ta:

let name = 'Lydia'

function getName() {
  console.log(name)
  let name = 'Sarah'
}

getName()

Khi hàm getName() được gọi, JavaScript sẽ tạo ra một scope mới cho hàm này. Trong scope này, biến name được khai báo bằng let. Do hoisting, khai báo này sẽ được đưa lên đầu scope của hàm:

function getName() {
  let name; // Hoisted
  console.log(name)
  name = 'Sarah'
}

Tuy nhiên, let (và const) không được khởi tạo trong quá trình hoisting như var. Thay vào đó, chúng được đặt vào TDZ. Khi ta cố gắng truy cập name trước khi nó được khởi tạo, JavaScript sẽ throw ra ReferenceError.

1.3. Ví dụ minh họa

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

console.log(x); // Throws ReferenceError
let x = 5;

console.log(y); // Outputs: undefined
var y = 10;

Trong ví dụ này, x được khai báo bằng let nên nó nằm trong TDZ cho đến khi được khởi tạo. Còn y được khai báo bằng var, nên nó được hoisted và khởi tạo với giá trị undefined.

1.4. Tóm lại

Quay lại với câu hỏi ban đầu, khi gọi getName(), ta đang cố gắng truy cập biến name trong khi nó đang ở trong TDZ. Điều này dẫn đến ReferenceError.

Hiểu về hoisting và TDZ là rất quan trọng trong JavaScript. Nó giúp chúng ta tránh được những lỗi không mong muốn và viết code một cách rõ ràng, dễ đọc hơn.

2. Generator Functions và yield

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

function* generatorOne() {
  yield ['a', 'b', 'c'];
}

function* generatorTwo() {
  yield* ['a', 'b', 'c'];
}

const one = generatorOne()
const two = generatorTwo()

console.log(one.next().value)
console.log(two.next().value)
  • A: aa
  • B: aundefined
  • C: ['a', 'b', 'c']a
  • D: a['a', 'b', 'c']
Đá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. Generator Functions

Generator functions là một tính năng mạnh mẽ trong JavaScript, cho phép chúng ta tạo ra các iterator một cách dễ dàng. Chúng được định nghĩa bằng cách sử dụng dấu sao (*) sau từ khóa function.

2.2. Từ khóa yieldyield*

Trong generator functions, chúng ta sử dụng từ khóa yield để trả về giá trị. Mỗi lần gọi phương thức next(), generator sẽ thực thi cho đến khi gặp từ khóa yield tiếp theo.

  • yield: Trả về một giá trị đơn lẻ.
  • yield*: Ủy quyền cho một generator hoặc iterable object khác.

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

Hãy phân tích từng generator function:

function* generatorOne() {
  yield ['a', 'b', 'c'];
}

generatorOne sử dụng yield để trả về toàn bộ mảng ['a', 'b', 'c'] như một giá trị duy nhất.

function* generatorTwo() {
  yield* ['a', 'b', 'c'];
}

generatorTwo sử dụng yield* để ủy quyền cho mảng ['a', 'b', 'c']. Điều này có nghĩa là nó sẽ lặp qua từng phần tử của mảng.

2.4. Kết quả

Khi chúng ta gọi one.next().value, nó trả về toàn bộ mảng ['a', 'b', 'c'].

Khi gọi two.next().value, nó trả về phần tử đầu tiên của mảng, tức là 'a'.

2.5. Ví dụ minh họa

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

const one = generatorOne();
console.log(one.next().value); // ['a', 'b', 'c']
console.log(one.next().value); // undefined

const two = generatorTwo();
console.log(two.next().value); // 'a'
console.log(two.next().value); // 'b'
console.log(two.next().value); // 'c'
console.log(two.next().value); // undefined

Như bạn có thể thấy, generatorOne chỉ yield một lần, trong khi generatorTwo yield ba lần, mỗi lần một phần tử của mảng.

2.6. Tóm lại

Hiểu về cách hoạt động của generator functions và sự khác biệt giữa yieldyield* là rất quan trọng khi làm việc với các iterator trong JavaScript. Nó cho phép chúng ta tạo ra các sequence phức tạp một cách dễ dàng và hiệu quả.

3. Template Literals và Arrow Functions

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

console.log(`${(x => x)('I love')} to program`)
  • A: I love to program
  • B: undefined to program
  • C: ${(x => x)('I love') to program
  • D: TypeError
Đá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. Template Literals

Template literals là một tính năng trong JavaScript cho phép chúng ta tạo ra các chuỗi đa dòng và nhúng các biểu thức JavaScript vào trong chuỗi. Chúng được bao quanh bởi dấu backtick (`) thay vì dấu nháy đơn hoặc nháy kép.

3.2. Arrow Functions

Arrow functions là một cú pháp ngắn gọn để viết function expressions. Chúng có cú pháp ngắn hơn so với function expressions thông thường và không bind this của riêng chúng.

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

Hãy chia nhỏ đoạn code của chúng ta:

`${(x => x)('I love')} to program`
  1. (x => x) là một arrow function nhận vào một tham số x và trả về chính x.
  2. ('I love') là đối số được truyền vào arrow function.
  3. Kết quả của việc gọi arrow function được nhúng vào template literal bằng cách sử dụng ${}.

3.4. Cách thức hoạt động

  1. Arrow function (x => x) được gọi với đối số 'I love'.
  2. Function này đơn giản chỉ trả về đối số được truyền vào, tức là 'I love'.
  3. Kết quả này được nhúng vào template literal.
  4. Phần còn lại của chuỗi to program được thêm vào.

3.5. Ví dụ minh họa

Để hiểu rõ hơn, hãy xem xét một vài ví dụ tương tự:

console.log(`${(x => x * 2)(5)} is ten`); // "10 is ten"
console.log(`Hello, ${(name => `Mr. ${name}`)('John')}`); // "Hello, Mr. John"

Trong ví dụ đầu tiên, arrow function nhân đối số với 2. Trong ví dụ thứ hai, arrow function thêm "Mr." vào trước tên.

3.6. Tóm lại

Sự kết hợp giữa template literals và arrow functions cho phép chúng ta tạo ra các chuỗi động một cách linh hoạt và ngắn gọn. Điều này đặc biệt hữu ích khi chúng ta cần thực hiện các phép tính hoặc biến đổi đơn giản ngay trong chuỗi.

4. setInterval và Garbage Collection

Điều gì sẽ xảy ra với đoạn code sau?

let config = {
  alert: setInterval(() => {
    console.log('Alert!')
  }, 1000)
}

config = null
  • A: Callback setInterval sẽ không được gọi
  • B: Callback setInterval sẽ được gọi một lần duy nhất
  • C: Callback setInterval vẫn sẽ được gọi mỗi giây một lần
  • D: config.alert() không bao giờ được gọi bởi config là null
Đá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. setInterval

setInterval là một hàm 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ó trả về một ID duy nhất, có thể được sử dụng để hủy interval bằng clearInterval.

4.2. Garbage Collection

Garbage Collection là quá trình tự động giải phóng bộ nhớ không còn được sử dụng trong chương trình. Trong JavaScript, garbage collector sẽ tự động xóa các object không còn được tham chiếu đến.

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

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

let config = {
  alert: setInterval(() => {
    console.log('Alert!')
  }, 1000)
}
  1. Chúng ta tạo một object config với một thuộc tính alert.
  2. Giá trị của alert là kết quả trả về từ setInterval, là một ID duy nhất.
  3. setInterval được thiết lập để gọi một arrow function mỗi 1000ms (1 giây).
config = null

Dòng này gán null cho biến config, nhưng điều này không ảnh hưởng đến interval đã được thiết lập.

4.4. Tại sao interval vẫn tiếp tục chạy?

Mặc dù config đã được gán null, interval vẫn tiếp tục chạy vì:

  1. JavaScript engine vẫn giữ một tham chiếu đến hàm callback trong setInterval.
  2. ID trả về bởi setInterval vẫn được JavaScript runtime lưu giữ.
  3. Arrow function trong setInterval không tạo ra một this mới, nên nó không bị ràng buộc với object config.

4.5. Ví dụ minh họa

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

let intervalId = setInterval(() => {
  console.log('Still running!');
}, 1000);

// Interval vẫn chạy ngay cả khi ta gán null
intervalId = null;

// Cách đúng để dừng interval
clearInterval(intervalId);

Trong ví dụ này, gán null cho intervalId không dừng interval. Để dừng interval, chúng ta cần sử dụng clearInterval.

4.6. Tóm lại

Hiểu về cách setInterval hoạt động và mối quan hệ của nó với garbage collection là rất quan trọng trong JavaScript. Nó giúp chúng ta tránh được các lỗi liên quan đến memory leak và quản lý tài nguyên hiệu quả hơn.

Để dừng một interval, luôn nhớ sử dụng clearInterval với ID mà setInterval trả về, thay vì chỉ đơn giản gán null cho biến chứa ID đó.

5. Map và Function References

Những hàm nào sẽ trả về 'Hello world!'?

const myMap = new Map()
const myFunc = () => 'greeting'

myMap.set(myFunc, 'Hello world!')

//1
myMap.get('greeting')
//2
myMap.get(myFunc)
//3
myMap.get(() => 'greeting')
  • A: 1
  • B: 2
  • C: 2 và 3
  • D: Tất cả
Đá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é ❓️

5.1. Map trong JavaScript

Map là một cấu trúc dữ liệu trong JavaScript cho phép lưu trữ các cặp key-value, trong đó cả key và value có thể là bất kỳ kiểu dữ liệu nào, kể cả object hoặc function.

5.2. Function References

Trong JavaScript, functions là first-class citizens, có nghĩa là chúng có thể được gán cho biến, truyền vào như tham số, và trả về từ các hàm khác.

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 myMap = new Map()
const myFunc = () => 'greeting'

myMap.set(myFunc, 'Hello world!')
  1. Chúng ta tạo một Map mới.
  2. Định nghĩa một arrow function myFunc.
  3. Thêm một cặp key-value vào Map, trong đó key là myFunc và value là 'Hello world!'.

5.4. Các trường hợp gọi get

  1. myMap.get('greeting'):

    • Trả về undefined vì không có key nào là chuỗi 'greeting' trong Map.
  2. myMap.get(myFunc):

    • Trả về 'Hello world!'myFunc chính là key đã được set.
  3. myMap.get(() => 'greeting'):

    • Trả về undefined vì đây là một arrow function mới, khác với myFunc.

5.5. Tại sao chỉ có trường hợp 2 trả về đúng?

  • Trong JavaScript, hai function có cùng định nghĩa không được coi là bằng nhau.
  • Khi sử dụng Map, việc so sánh key được thực hiện bằng thuật toán "same-value equality" (tương tự ===).
  • myFunc() => 'greeting' là hai function khác nhau trong bộ nhớ, mặc dù chúng có cùng định nghĩa.

5.6. Ví dụ minh họa

Để hiểu rõ hơn về cách JavaScript so sánh functions, hãy xem xét ví dụ sau:

const func1 = () => 'hello';
const func2 = () => 'hello';

console.log(func1 === func2); // false
console.log(func1 === func1); // true

const myMap = new Map();
myMap.set(func1, 'Value 1');
console.log(myMap.get(func2)); // undefined
console.log(myMap.get(func1)); // 'Value 1'

5.7. Tóm lại

Hiểu về cách Map hoạt động với function references là rất quan trọng trong JavaScript. Nó giúp chúng ta tránh được các lỗi không mong muốn khi làm việc với Map và functions.

Khi sử dụng functions làm key trong Map, luôn nhớ rằng chỉ có chính xác function reference đó mới có thể được sử dụng để truy xuất giá trị, không phải một function khác có cùng định nghĩa.

Kết luận

Qua bài viết này, chúng ta đã đi sâu vào một số khía cạnh nâng cao của JavaScript như Temporal Dead Zone, Generator Functions, Template Literals kết hợp với Arrow Functions, setInterval và Garbage Collection, cũng như cách Map hoạt động với Function References.

Những kiến thức này không chỉ giúp chúng ta hiểu sâu hơn về cách JavaScript hoạt động, mà còn giúp chúng ta viết code hiệu quả và tránh được những lỗi tiềm ẩn. Hy vọng rằng qua những ví dụ và phân tích chi tiết, các bạn đã có thể nắm bắt được những khái niệm này một cách rõ ràng hơn.

Hãy nhớ rằng, việc học JavaScript là một hành trình dài, và những khái niệm nâng cao này đòi hỏi thời gian và thực hành để thực sự thành thạo. Đừng ngại thử nghiệm với code và đặt câu hỏi khi bạn gặp khó khăn.

Trong các bài viết tiếp theo, chúng ta sẽ tiếp tục khám phá những khía cạnh thú vị khác của JavaScript. Hãy tiếp tục theo dõi series này nhé!

Chúc các bạn học tập tốt và luôn giữ được niềm đam mê với lập trì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
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í