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ộtcomment 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:
-
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
. -
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:
20
và62.83185307179586
- B:
20
vàNaN
- C:
20
và63
- D:
NaN
và63
Đá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 shape
vì diameter
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ả window
và global
đề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.