+9

JavaScript Nâng Cao - Kỳ 6

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. Ngữ cảnh thực thi global của JavaScript

Ngữ cảnh thực thi global của JavaScript tạo ra 2 thứ cho chúng ta: global object, và từ khóa "this".

  • A: đúng
  • B: sai
  • C: còn tùy
Đá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é ❓️

1.1. Ngữ cảnh thực thi global

Trong JavaScript, ngữ cảnh thực thi cơ bản chính là ngữ cảnh global. Đây là ngữ cảnh mà chúng ta có thể truy cập được ở bất cứ đâu trong code.

Khi một đoạn mã JavaScript được thực thi, nó luôn chạy trong một ngữ cảnh nào đó. Ngữ cảnh global chính là ngữ cảnh mặc định, nơi mà các biến và hàm được khai báo nếu không nằm trong bất kỳ ngữ cảnh nào khác.

1.2. Global object và từ khóa "this"

Trong ngữ cảnh global, JavaScript tạo ra hai thứ cho chúng ta:

  1. Global object: Đây là một đối tượng đặc biệt chứa tất cả các biến và hàm toàn cục. Trong trình duyệt web, global object chính là window. Trong Node.js, nó là global.

  2. Từ khóa "this": Trong ngữ cảnh global, this trỏ đến chính global object.

Ví dụ:

var message = "Hello";
console.log(window.message); // "Hello"
console.log(this.message); // "Hello"

Trong ví dụ trên, biến message được khai báo trong ngữ cảnh global. Do đó, nó trở thành một thuộc tính của global object (window trong trình duyệt). Chúng ta có thể truy cập biến này thông qua window.message hoặc this.message.

1.3. Tóm lại

Ngữ cảnh thực thi global của JavaScript tạo ra global object và từ khóa "this". Điều này cho phép chúng ta truy cập các biến và hàm toàn cục từ bất kỳ đâu trong code. Tuy nhiên, cần lưu ý rằng việc lạm dụng biến toàn cục có thể dẫn đến xung đột tên và khó bảo trì code. Vì vậy, nên hạn chế sử dụng biến toàn cục và ưu tiên sử dụng các phạm vi hẹp hơn như hàm hoặc block.

2. Lệnh continue trong vòng lặp

Output là gì?

for (let i = 1; i < 5; i++) {
  if (i === 3) continue;
  console.log(i);
}
  • A: 1 2
  • B: 1 2 3
  • C: 1 2 4
  • D: 1 3 4
Đá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. Lệnh continue

Trong JavaScript, lệnh continue được sử dụng để bỏ qua một vòng lặp hiện tại và tiếp tục với vòng lặp tiếp theo nếu điều kiện của nó là true.

Khi lệnh continue được thực thi, nó sẽ dừng thực hiện các câu lệnh còn lại trong vòng lặp hiện tại và chuyển sang vòng lặp tiếp theo.

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

Hãy xem xét đoạn code sau:

for (let i = 1; i < 5; i++) {
  if (i === 3) continue;
  console.log(i);
}

Đoạn code trên sử dụng vòng lặp for để duyệt qua các giá trị từ 1 đến 4. Tuy nhiên, có một điều kiện đặc biệt ở đây:

if (i === 3) continue;

Điều kiện này kiểm tra xem giá trị của biến i có bằng 3 hay không. Nếu đúng, lệnh continue sẽ được thực thi, và vòng lặp sẽ bỏ qua việc in giá trị 3 ra console.

Vì vậy, kết quả của đoạn code trên sẽ là:

1
2
4

Giá trị 3 bị bỏ qua do lệnh continue.

2.3. Tóm lại

Lệnh continue trong JavaScript cho phép chúng ta bỏ qua một vòng lặp hiện tại và tiếp tục với vòng lặp tiếp theo nếu điều kiện của nó là true. Điều này giúp chúng ta kiểm soát luồng thực thi của vòng lặp và bỏ qua các giá trị không mong muốn.

Tuy nhiên, cần sử dụng lệnh continue một cách cẩn thận và chỉ khi thực sự cần thiết, vì nó có thể làm cho code trở nên khó đọc và khó bảo trì nếu lạm dụng.

3. Thêm phương thức vào prototype của String

Output là gì?

String.prototype.giveLydiaPizza = () => {
  return "Just give Lydia pizza already!";
};

const name = "Lydia";

console.log(name.giveLydiaPizza())
  • A: "Just give Lydia pizza already!"
  • B: TypeError: not a function
  • C: SyntaxError
  • D: undefined
Đá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. Prototype trong JavaScript

Trong JavaScript, mỗi hàm đều có một thuộc tính gọi là prototype. prototype là một đối tượng chứa các thuộc tính và phương thức mà tất cả các instance của hàm đó có thể truy cập và sử dụng.

Khi chúng ta tạo một đối tượng từ một hàm constructor, đối tượng đó sẽ kế thừa tất cả các thuộc tính và phương thức được định nghĩa trong prototype của hàm constructor đó.

3.2. Thêm phương thức vào prototype của String

Trong đoạn code trên, chúng ta đang thêm một phương thức mới vào prototype của hàm constructor String:

String.prototype.giveLydiaPizza = () => {
  return "Just give Lydia pizza already!";
};

Điều này có nghĩa là tất cả các đối tượng string (instance của String) sẽ có quyền truy cập vào phương thức giveLydiaPizza().

3.3. Sử dụng phương thức đã thêm

Sau khi thêm phương thức vào prototype của String, chúng ta có thể gọi phương thức đó trên bất kỳ đối tượng string nào.

Trong đoạn code trên, chúng ta có một biến name với giá trị là một chuỗi "Lydia". Khi chúng ta gọi phương thức giveLydiaPizza() trên biến name, nó sẽ trả về chuỗi "Just give Lydia pizza already!".

Vì vậy, kết quả của console.log(name.giveLydiaPizza()) sẽ là:

"Just give Lydia pizza already!"

3.4. Tóm lại

Trong JavaScript, chúng ta có thể thêm các phương thức vào prototype của các hàm constructor như String, Array, Object, vv. Điều này cho phép tất cả các đối tượng được tạo từ các hàm constructor đó có quyền truy cập và sử dụng các phương thức đã thêm.

Tuy nhiên, cần lưu ý rằng việc thêm phương thức vào prototype của các hàm constructor có sẵn như String có thể gây ra xung đột nếu các thư viện hoặc framework khác cũng thêm các phương thức với tên tương tự. Vì vậy, hãy cẩn thận khi mở rộng các prototype có sẵn và chỉ làm điều đó khi thực sự cần thiết.

4. Truy xuất thuộc tính của object

Output là gì?

const a = {};
const b = { key: "b" };
const c = { key: "c" };

a[b] = 123;
a[c] = 456;

console.log(a[b]);
  • A: 123
  • B: 456
  • C: undefined
  • D: ReferenceError
Đá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é ❓️

4.1. Chuyển đổi object thành string

Trong JavaScript, khi sử dụng một object như một key cho một object khác, object đó sẽ tự động được chuyển đổi thành một string. Quá trình chuyển đổi này sử dụng phương thức toString() của object.

Ví dụ:

const obj = { key: "value" };
console.log(obj.toString()); // "[object Object]"

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

Hãy xem xét đoạn code sau:

const a = {};
const b = { key: "b" };
const c = { key: "c" };

a[b] = 123;
a[c] = 456;

console.log(a[b]);

Khi chúng ta gán a[b] = 123, thực chất JavaScript sẽ chuyển đổi b thành một string bằng cách gọi b.toString(). Kết quả là "[object Object]". Vì vậy, a["[object Object]"] = 123.

Tương tự, khi gán a[c] = 456, c cũng được chuyển đổi thành "[object Object]". Lúc này, a["[object Object]"] = 456.

Cuối cùng, khi gọi console.log(a[b]), b một lần nữa được chuyển đổi thành "[object Object]". Do đó, a["[object Object]"] trả về giá trị 456.

4.3. Tóm lại

Trong JavaScript, khi sử dụng một object làm key cho một object khác, object đó sẽ được tự động chuyển đổi thành một string bằng cách gọi phương thức toString(). Điều này có thể dẫn đến kết quả không mong muốn nếu chúng ta không cẩn thận.

Để tránh vấn đề này, tốt nhất nên sử dụng các giá trị nguyên thủy (như string, number) làm key cho object thay vì sử dụng object.

5. Thứ tự thực thi hàm bất đồng bộ

Output là gì?

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

bar();
foo();
baz();
  • 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é ❓️

5.1. Hàm setTimeout

Đầu tiên, chúng ta cần hiểu về hàm setTimeout. Hàm này cho phép chúng ta thực thi một đoạn code sau một khoảng thời gian nhất định. Tuy nhiên, nó không chặn việc thực thi các đoạn code khác.

Khi gọi setTimeout, JavaScript sẽ đặt hàm callback vào một hàng đợi (queue) và tiếp tục thực thi các đoạn code tiếp theo. Sau khi thời gian chờ kết thúc, hàm callback sẽ được đưa vào call stack và thực thi.

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

Hãy xem xét đoạn code sau:

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

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

Khi chúng ta gọi bar(), hàm setTimeout được thực thi vì thời gian chờ đợi là 0ms. Nó đặt hàm callback () => console.log("Second") vào hàng đợi và tiếp tục thực thi các đoạn code tiếp theo mà không chờ đợi.

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

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

Cuối cùng, sau khi thời gian chờ của setTimeout kết thúc, hàm callback () => console.log("Second") được đưa vào call stack và thực thi, in ra "Second".

Vì vậy, kết quả cuối cùng sẽ là:

First
Third
Second

5.3. Tóm lại

Trong JavaScript, các hàm bất đồng bộ như setTimeout không chặn việc thực thi các đoạn code khác. Thay vào đó, chúng đặt hàm callback vào một hàng đợi và tiếp tục thực thi các đoạn code tiếp theo. Sau khi thời gian chờ kết thúc, hàm callback sẽ được đưa vào call stack và thực thi.

Điều này cho phép JavaScript xử lý các tác vụ bất đồng bộ mà không làm chậm việc thực thi các đoạn code khác, giúp tăng hiệu suất và trải nghiệm người dùng. Chi tiết hề CallBack Queue, Call Stack và Event Loop mình sẽ giải thích ở các bài sau nhé. (Bạn nào quan tâm về nó có thể để lại comment, nếu có nhiều người quan tâm mình sẽ viết bài về nó).

6. Giá trị của this trong arrow function

Giá trị của this bên trong arrow function là gì?

const shape = {
  radius: 10,
  diameter() {
    return this.radius * 2;
  },
  perimeter: () => 2 * Math.PI * this.radius,
};

console.log(shape.diameter());
console.log(shape.perimeter());
  • A: 2062.83185307179586
  • B: 20NaN
  • C: 2063
  • D: NaN63
Đá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é ❓️

6.1. Giá trị của this trong phương thức thông thường

Trong phương thức diameter, this trỏ đến đối tượng shapediameter là một phương thức thông thường và được gọi với cú pháp shape.diameter().

Khi gọi shape.diameter(), this.radius trả về giá trị của thuộc tính radius của đối tượng shape, là 10. Vì vậy, phương thức diameter trả về 20.

6.2. Giá trị của this trong arrow function

Tuy nhiên, phương thức perimeter là một arrow function. Trong arrow function, this không trỏ đến đối tượng gọi phương thức, mà thay vào đó, nó kế thừa giá trị this từ phạm vi bên ngoài.

Trong trường hợp này, phạm vi bên ngoài của perimeter là phạm vi toàn cục (global scope), nơi this trỏ đến đối tượng window (trong trình duyệt) hoặc global (trong Node.js). Cả windowglobal đều không có thuộc tính radius, vì vậy this.radius trả về undefined.

Khi thực hiện phép tính 2 * Math.PI * undefined, kết quả sẽ là NaN.

6.3. Tóm lại

Trong JavaScript, giá trị của this trong arrow function không phụ thuộc vào cách gọi phương thức, mà thay vào đó, nó kế thừa giá trị this từ phạm vi bên ngoài.

Điều này khác với phương thức thông thường, nơi giá trị của this phụ thuộc vào cách gọi phương thức. Arrow function thường được sử dụng khi chúng ta muốn giữ nguyên giá trị của this từ phạm vi bên ngoài, hoặc khi chúng ta muốn tránh việc ghi đè giá trị của this trong các phương thức thông thường.


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í