JavaScript Nâng Cao - Kỳ 20
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é.
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. Gán lại giá trị cho class
class Person {
constructor() {
this.name = "Lydia"
}
}
Person = class AnotherPerson {
constructor() {
this.name = "Sarah"
}
}
const member = new Person()
console.log(member.name)
- A:
"Lydia"
- B:
"Sarah"
- C:
Error: cannot redeclare Person
- D:
SyntaxError
Đá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é ❓️
1.1. Class trong JavaScript
Trước hết, chúng ta cần hiểu rằng trong JavaScript, class thực chất chỉ là "syntactic sugar" cho constructor function. Khi chúng ta khai báo một class, JavaScript engine sẽ tạo ra một function constructor tương ứng.
1.2. Gán lại giá trị cho class
Trong JavaScript, chúng ta có thể gán lại giá trị cho một biến, kể cả khi biến đó đang giữ một class. Điều này có nghĩa là chúng ta có thể "thay đổi" định nghĩa của một class sau khi nó đã được khai báo.
1.3. Phân tích đoạn code
Trong đoạn code trên, chúng ta làm hai việc:
- Khai báo class
Person
với constructor setthis.name = "Lydia"
. - Gán lại
Person
bằng một class mớiAnotherPerson
với constructor setthis.name = "Sarah"
.
Khi chúng ta tạo một instance mới của Person
bằng new Person()
, JavaScript sẽ sử dụng định nghĩa mới nhất của Person
, tức là AnotherPerson
.
1.4. Ví dụ minh họa
Để hiểu rõ hơn, hãy tưởng tượng bạn đang xây dựng một ngôi nhà. Ban đầu, bạn có bản thiết kế A (class Person
ban đầu). Nhưng sau đó, bạn quyết định thay đổi hoàn toàn thiết kế và sử dụng bản thiết kế B (class AnotherPerson
). Khi bạn bắt đầu xây nhà (tạo instance), bạn sẽ sử dụng bản thiết kế mới nhất, tức là B.
1.5. Lưu ý quan trọng
Điều quan trọng cần nhớ là việc gán lại giá trị cho class như thế này có thể gây ra những hậu quả không mong muốn trong code của bạn. Nó có thể làm cho code trở nên khó đọc và khó bảo trì. Trong thực tế, bạn nên tránh việc gán lại giá trị cho class như vậy để đảm bảo tính nhất quán của code.
2. Symbol và Object.keys()
const info = {
[Symbol('a')]: 'b'
}
console.log(info)
console.log(Object.keys(info))
- A:
{Symbol('a'): 'b'}
và["{Symbol('a')"]
- B:
{}
và[]
- C:
{ a: "b" }
và["a"]
- D:
{Symbol('a'): 'b'}
và[]
Đá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é ❓️
2.1. Symbol trong JavaScript
Symbol là một kiểu dữ liệu nguyên thủy mới được giới thiệu trong ECMAScript 2015 (ES6). Mỗi Symbol là duy nhất và không thể thay đổi (immutable).
2.2. Object với Symbol key
Khi chúng ta sử dụng Symbol làm key cho một object, nó sẽ không hiển thị như một key thông thường. Đây là một trong những đặc điểm quan trọng của Symbol - nó có thể được sử dụng để tạo ra các thuộc tính "ẩn" trong object.
2.3. Object.keys() và Symbol
Object.keys()
là một phương thức trả về một mảng chứa tất cả các key có thể đếm được (enumerable) của một object. Tuy nhiên, Symbol keys không được coi là enumerable theo mặc định.
2.4. Phân tích đoạn code
Trong đoạn code trên:
-
console.log(info)
sẽ hiển thị toàn bộ object, bao gồm cả Symbol key. Kết quả là{Symbol('a'): 'b'}
. -
console.log(Object.keys(info))
sẽ trả về một mảng rỗng[]
vì không có key nào là enumerable trong object này.
2.5. Ví dụ minh họa
Hãy tưởng tượng bạn có một chiếc hộp (object) với một chiếc chìa khóa đặc biệt (Symbol). Khi bạn nhìn vào hộp, bạn có thể thấy chiếc chìa khóa đó. Nhưng khi bạn cố gắng liệt kê tất cả các chìa khóa thông thường, chiếc chìa khóa đặc biệt này sẽ không xuất hiện trong danh sách.
2.6. Lưu ý quan trọng
Mặc dù Symbol keys không xuất hiện trong Object.keys()
, chúng vẫn có thể được truy cập bằng các phương thức khác như Object.getOwnPropertySymbols()
hoặc Reflect.ownKeys()
.
Ví dụ:
console.log(Object.getOwnPropertySymbols(info)); // [Symbol(a)]
console.log(Reflect.ownKeys(info)); // [Symbol(a)]
Symbol là một công cụ mạnh mẽ để tạo ra các thuộc tính "riêng tư" trong JavaScript, nhưng cần sử dụng cẩn thận để tránh gây nhầm lẫn cho các lập trình viên khác khi đọc code của bạn.
3. Destructuring và Arrow Function
const getList = ([x, ...y]) => [x, y]
const getUser = user => { name: user.name, age: user.age }
const list = [1, 2, 3, 4]
const user = { name: "Lydia", age: 21 }
console.log(getList(list))
console.log(getUser(user))
- A:
[1, [2, 3, 4]]
vàundefined
- B:
[1, [2, 3, 4]]
và{ name: "Lydia", age: 21 }
- C:
[1, 2, 3, 4]
và{ name: "Lydia", age: 21 }
- D:
Error
và{ name: "Lydia", age: 21 }
Đá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. 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ừ mảng hoặc thuộc tính từ objects vào các biến riêng biệt.
3.2. Rest parameter
Rest parameter là một cú pháp cho phép chúng ta đại diện cho một số lượng không xác định các đối số dưới dạng một mảng.
3.3. Arrow function
Arrow function là một cú pháp ngắn gọn để viết function expression. Tuy nhiên, nó có một số khác biệt so với function thông thường, đặc biệt là cách xử lý return
và this
.
3.4. Phân tích hàm getList
const getList = ([x, ...y]) => [x, y]
Hàm này sử dụng destructuring và rest parameter. Khi gọi getList(list)
:
x
sẽ lấy phần tử đầu tiên củalist
(1)...y
sẽ lấy tất cả các phần tử còn lại và đưa vào một mảng mới ([2, 3, 4])
Kết quả trả về là một mảng chứa x
và y
: [1, [2, 3, 4]]
3.5. Phân tích hàm getUser
const getUser = user => { name: user.name, age: user.age }
Đây là nơi xảy ra lỗi. Trong arrow function, khi chúng ta muốn trả về một object ngay lập tức, chúng ta cần bọc object đó trong dấu ngoặc tròn. Nếu không, JavaScript sẽ coi {
là bắt đầu của function body, không phải là một object literal.
Cách sửa:
const getUser = user => ({ name: user.name, age: user.age })
Vì không có dấu ngoặc tròn, JavaScript coi đây là function body, nhưng không có return
statement, nên hàm trả về undefined
.
3.6. Ví dụ minh họa
Hãy tưởng tượng bạn có một hộp đồ chơi (mảng) và bạn muốn lấy ra đồ chơi đầu tiên và đặt tất cả đồ chơi còn lại vào một hộp khác. Đó chính là những gì getList
đang làm.
Với getUser
, tưởng tượng bạn đang cố gắng đóng gói thông tin của một người vào một hộp, nhưng bạn quên đóng nắp hộp (thiếu dấu ngoặc tròn), nên khi bạn mở hộp ra, không có gì bên trong (undefined).
3.7. Lưu ý quan trọng
Khi sử dụng arrow function để trả về object literal ngay lập tức, luôn nhớ bọc object trong dấu ngoặc tròn để tránh nhầm lẫn với function body.
4. Gọi một biến như một hàm
const name = "Lydia"
console.log(name())
- A:
SyntaxError
- B:
ReferenceError
- C:
TypeError
- D:
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. Kiểu dữ liệu trong JavaScript
JavaScript có các kiểu dữ liệu nguyên thủy (primitive types) như string, number, boolean, null, undefined, symbol, và một kiểu dữ liệu phức tạp là object (bao gồm cả function, vì trong JavaScript, function cũng là object).
4.2. Gọi hàm trong JavaScript
Trong JavaScript, chúng ta có thể gọi một hàm bằng cách thêm dấu ngoặc đơn ()
sau tên hàm. Tuy nhiên, điều này chỉ hoạt động khi biến đó thực sự chứa một hàm.
4.3. Phân tích đoạn code
Trong đoạn code trên:
- Chúng ta khai báo một biến
name
và gán cho nó giá trị là chuỗi "Lydia". - Sau đó, chúng ta cố gắng gọi
name
như một hàm bằng cách sử dụngname()
.
4.4. TypeError
Khi chúng ta cố gắng sử dụng một giá trị không phải là hàm như một hàm, JavaScript sẽ throw ra một TypeError
. Cụ thể, lỗi sẽ là TypeError: name is not a function
.
4.5. Các loại lỗi khác trong JavaScript
SyntaxError
: Xảy ra khi có lỗi cú pháp trong code.ReferenceError
: Xảy ra khi chúng ta cố gắng sử dụng một biến chưa được khai báo.TypeError
: Xảy ra khi một giá trị không phải là kiểu dữ liệu mong đợi, như trong trường hợp này.
4.7. Lưu ý quan trọng
Luôn đảm bảo rằng bạn đang gọi một hàm, không phải một kiểu dữ liệu khác. Trong JavaScript, bạn có thể kiểm tra xem một biến có phải là hàm hay không bằng cách sử dụng toán tử typeof
:
const name = "Lydia";
console.log(typeof name); // "string"
console.log(typeof name === "function"); // false
5. Template Literals và Short-circuit Evaluation
const output = `${[] && 'Im'}possible!
You should${'' && `n't`} see a therapist after so much JavaScript lol`
- A:
possible! You should see a therapist after so much JavaScript lol
- B:
Impossible! You should see a therapist after so much JavaScript lol
- C:
possible! You shouldn't see a therapist after so much JavaScript lol
- D:
Impossible! You shouldn't see a therapist after so much JavaScript lol
Đá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. Template Literals
Template literals là một cách để tạo chuỗi trong JavaScript, cho phép chúng ta nhúng biểu thức vào trong chuỗi bằng cách sử dụng cú pháp ${}
.
5.2. Short-circuit Evaluation
Short-circuit evaluation là một kỹ thuật trong JavaScript (và nhiều ngôn ngữ khác) nơi toán tử logic (&&
và ||
) được sử dụng để đánh giá biểu thức từ trái sang phải, dừng lại ngay khi kết quả được xác định.
5.3. Phân tích đoạn code
Hãy chia nhỏ biểu thức template literal:
-
${[] && 'Im'}
:[]
là một mảng rỗng, được coi là truthy trong JavaScript.- Do đó,
[] && 'Im'
sẽ trả về'Im'
.
-
${'' &&
n't}
:''
là một chuỗi rỗng, được coi là falsy trong JavaScript.- Do đó,
'' &&
n't`` sẽ trả về''
(chuỗi rỗng).
5.4. Kết quả cuối cùng
Khi ghép tất cả lại, chúng ta có:
Impossible!
You should see a therapist after so much JavaScript lol
5.5. Lưu ý quan trọng
Short-circuit evaluation là một công cụ mạnh mẽ trong JavaScript, nhưng nó cũng có thể làm cho code trở nên khó đọc nếu sử dụng quá nhiều. Trong thực tế, bạn nên cân nhắc giữa việc sử dụng short-circuit evaluation và viết code rõ ràng, dễ đọc.
Kết luận
Qua 5 ví dụ trên, chúng ta đã đi sâu vào một số câu hỏi thú vị trong JavaScript:
- Cách class hoạt động và có thể được gán lại.
- Symbol và cách nó ảnh hưởng đến các phương thức như
Object.keys()
. - Destructuring, rest parameters và cách viết arrow function trả về object.
- Sự khác biệt giữa các loại lỗi trong JavaScript.
- Template literals và short-circuit evaluation.
Hy vọng qua bài viết này, các bạn đã có thêm những kiến thức bổ ích về JavaScript.
Hẹn gặp lại các bạn trong những 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ộ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. 🤗
All rights reserved