JavaScript Nâng Cao - Kỳ 30 (Kết)
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é.
Đây là kỳ cuối cùng trong series JavaScript Nâng Cao. Mình hy vọng qua 30 kỳ vừa qua, các bạn đã có thêm nhiều kiến thức bổ ích về JavaScript. Hãy cùng kết thúc series này với những ví dụ thú vị cuối cùng của series này 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. 🤗
1. Optional Chaining (?.) trong JavaScript
Xem xét đoạn code sau:
function getFruit(fruits) {
console.log(fruits?.[1]?.[1])
}
getFruit([['🍊', '🍌'], ['🍍']])
getFruit()
getFruit([['🍍'], ['🍊', '🍌']])
Output là gì?
- A:
null
,undefined
, 🍌 - B:
[]
,null
, 🍌 - C:
[]
,[]
, 🍌 - D:
undefined
,undefined
, 🍌
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: D
1.1 Phân tích vấn đề
Để hiểu được tại sao kết quả lại như vậy, chúng ta cần tìm hiểu về Optional Chaining (?.) trong JavaScript.
1.2 Optional Chaining (?.) là gì?
Optional Chaining (?.) là một tính năng mới trong JavaScript, cho phép chúng ta đọc giá trị của một thuộc tính nằm sâu trong chuỗi các object mà không cần kiểm tra xem mỗi tham chiếu trong chuỗi có hợp lệ hay không.
Khi sử dụng ?.
, nếu object bên trái của ?.
là null
hoặc undefined
, biểu thức sẽ short-circuit và trả về undefined
thay vì ném ra lỗi.
1.3 Phân tích từng trường hợp
-
getFruit([['🍊', '🍌'], ['🍍']])
fruits[1]
tồn tại và là['🍍']
fruits[1][1]
không tồn tại, nên kết quả làundefined
-
getFruit()
fruits
làundefined
fruits?.[1]
trả vềundefined
undefined?.[1]
trả vềundefined
-
getFruit([['🍍'], ['🍊', '🍌']])
fruits[1]
tồn tại và là['🍊', '🍌']
fruits[1][1]
tồn tại và là'🍌'
1.4 Tóm lại
Optional Chaining (?.) giúp chúng ta tránh được các lỗi TypeError khi truy cập các thuộc tính của null
hoặc undefined
. Nó đặc biệt hữu ích khi làm việc với các cấu trúc dữ liệu phức tạp hoặc khi xử lý dữ liệu từ API mà chúng ta không chắc chắn về cấu trúc.
2. Class và Instance trong JavaScript
Xem xét đoạn code sau:
class Calc {
constructor() {
this.count = 0
}
increase() {
this.count++
}
}
const calc = new Calc()
new Calc().increase()
console.log(calc.count)
Output là gì?
- A:
0
- B:
1
- C:
undefined
- D:
ReferenceError
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A
2.1 Phân tích vấn đề
Để hiểu được tại sao kết quả lại là 0
, chúng ta cần nắm rõ về cách hoạt động của class và instance trong JavaScript.
2.2 Class và Instance
Trong JavaScript, class là một "bản thiết kế" cho việc tạo ra các object. Khi chúng ta tạo một instance của class bằng từ khóa new
, chúng ta đang tạo ra một object mới dựa trên bản thiết kế đó.
Mỗi instance là một object độc lập, có các thuộc tính và phương thức riêng của nó.
2.3 Phân tích code
-
Chúng ta tạo một instance
calc
từ classCalc
:const calc = new Calc()
Lúc này,
calc.count
bằng 0. -
Tiếp theo, chúng ta tạo một instance mới và gọi phương thức
increase()
trên nó:new Calc().increase()
Điều này tạo ra một instance mới, tăng
count
của instance đó lên 1, nhưng instance này không được lưu lại ở đâu cả. -
Cuối cùng, chúng ta in ra
calc.count
:console.log(calc.count)
calc
vẫn là instance ban đầu, không bị ảnh hưởng bởi việc tăngcount
của instance thứ hai. Do đó,calc.count
vẫn bằng 0.
2.4 Tóm lại
Mỗi instance của một class là một object độc lập. Các thay đổi trên một instance không ảnh hưởng đến các instance khác. Điều này giúp chúng ta tạo ra nhiều object có cùng cấu trúc nhưng có thể có các giá trị khác nhau.
3. Object.assign() và Spread Operator trong JavaScript
Xem xét đoạn code sau:
const user = {
email: "e@mail.com",
password: "12345"
}
const updateUser = ({ email, password }) => {
if (email) {
Object.assign(user, { email })
}
if (password) {
user.password = password
}
return user
}
const updatedUser = updateUser({ email: "new@email.com" })
console.log(updatedUser === user)
Output là gì?
- A:
false
- B:
true
- C:
TypeError
- D:
ReferenceError
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: B
3.1 Phân tích vấn đề
Để hiểu được tại sao kết quả lại là true
, chúng ta cần tìm hiểu về Object.assign()
và cách JavaScript xử lý tham chiếu object.
3.2 Object.assign()
Object.assign(target, source)
là một phương thức trong JavaScript dùng để sao chép các thuộc tính có thể liệt kê từ một hoặc nhiều object nguồn vào một object đích. Nó trả về object đích đã được sửa đổi.
3.3 Phân tích code
-
Chúng ta có một object
user
ban đầu:const user = { email: "e@mail.com", password: "12345" }
-
Hàm
updateUser
nhận một object có thể chứaemail
vàpassword
:const updateUser = ({ email, password }) => { if (email) { Object.assign(user, { email }) } if (password) { user.password = password } return user }
-
Chúng ta gọi
updateUser
với một email mới:const updatedUser = updateUser({ email: "new@email.com" })
-
Trong hàm
updateUser
,Object.assign(user, { email })
sẽ cập nhật thuộc tínhemail
của objectuser
gốc. -
Hàm trả về chính object
user
đã được cập nhật. -
Khi so sánh
updatedUser === user
, kết quả làtrue
vì cả hai đều tham chiếu đến cùng một object trong bộ nhớ.
3.4 Tóm lại
Object.assign()
và việc trực tiếp gán giá trị cho thuộc tính của object đều thay đổi object gốc. Trong JavaScript, khi chúng ta làm việc với object, chúng ta đang làm việc với tham chiếu đến object đó. Điều này có thể dẫn đến những thay đổi không mong muốn nếu chúng ta không cẩn thận. Để tránh điều này, chúng ta có thể sử dụng spread operator (...) để tạo một bản sao mới của object trước khi thay đổi.
4. Các phương thức xử lý mảng trong JavaScript
Xem xét đoạn code sau:
const fruit = ['🍌', '🍊', '🍎']
fruit.slice(0, 1)
fruit.splice(0, 1)
fruit.unshift('🍇')
console.log(fruit)
Output là gì?
- A:
['🍌', '🍊', '🍎']
- B:
['🍊', '🍎']
- C:
['🍇', '🍊', '🍎']
- D:
['🍇', '🍌', '🍊', '🍎']
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: C
4.1 Phân tích vấn đề
Để hiểu được tại sao kết quả lại là ['🍇', '🍊', '🍎']
, chúng ta cần tìm hiểu về các phương thức xử lý mảng trong JavaScript: slice()
, splice()
, và unshift()
.
4.2 Các phương thức xử lý mảng
slice(start, end)
: Trích xuất một phần của mảng và trả về mảng mới, không thay đổi mảng gốc.splice(start, deleteCount, item1, item2, ...)
: Thay đổi nội dung của mảng bằng cách xóa hoặc thay thế các phần tử hiện có và/hoặc thêm các phần tử mới.unshift(element1, ..., elementN)
: Thêm một hoặc nhiều phần tử vào đầu mảng và trả về độ dài mới của mảng.
4.3 Phân tích code
-
Ban đầu, chúng ta có mảng:
const fruit = ['🍌', '🍊', '🍎']
-
fruit.slice(0, 1)
:- Trích xuất phần tử từ index 0 đến (nhưng không bao gồm) index 1.
- Trả về
['🍌']
, nhưng không thay đổi mảng gốc. - Mảng vẫn là
['🍌', '🍊', '🍎']
-
fruit.splice(0, 1)
:- Xóa 1 phần tử bắt đầu từ index 0.
- Thay đổi mảng gốc.
- Mảng trở thành
['🍊', '🍎']
-
fruit.unshift('🍇')
:- Thêm '🍇' vào đầu mảng.
- Mảng trở thành
['🍇', '🍊', '🍎']
4.4 Tóm lại
Hiểu rõ cách hoạt động của các phương thức xử lý mảng là rất quan trọng trong JavaScript. Một số phương thức như slice()
không thay đổi mảng gốc, trong khi các phương thức khác như splice()
và unshift()
lại thay đổi mảng gốc. Điều này có thể dẫn đến những kết quả không mong muốn nếu chúng ta không cẩn thận.
5. Object keys và type coercion trong JavaScript
Xem xét đoạn code sau:
const animals = {};
let dog = { emoji: '🐶' }
let cat = { emoji: '🐈' }
animals[dog] = { ...dog, name: "Mara" }
animals[cat] = { ...cat, name: "Sara" }
console.log(animals[dog])
Output là gì?
- A:
{ emoji: "🐶", name: "Mara" }
- B:
{ emoji: "🐈", name: "Sara" }
- C:
undefined
- D:
ReferenceError
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: B
5.1 Phân tích vấn đề
Để hiểu được tại sao kết quả lại là { emoji: "🐈", name: "Sara" }
, chúng ta cần tìm hiểu về cách JavaScript xử lý object keys và type coercion.
5.2 Object keys và Type Coercion trong JavaScript
Trong JavaScript, khi chúng ta sử dụng một object làm key cho một object khác, JavaScript sẽ tự động chuyển đổi object key đó thành một chuỗi. Quá trình này gọi là type coercion.
Khi một object được chuyển đổi thành chuỗi, nó sẽ trở thành "[object Object]". Điều này có nghĩa là bất kỳ object nào được sử dụng làm key đều sẽ trở thành cùng một key: "[object Object]".
5.3 Phân tích code
-
Chúng ta tạo một object rỗng
animals
:const animals = {};
-
Chúng ta định nghĩa hai object
dog
vàcat
:let dog = { emoji: '🐶' } let cat = { emoji: '🐈' }
-
Khi chúng ta thực hiện:
animals[dog] = { ...dog, name: "Mara" }
JavaScript chuyển đổi
dog
thành chuỗi "[object Object]". Vì vậy, thực tế chúng ta đang thực hiện:animals["[object Object]"] = { emoji: '🐶', name: "Mara" }
-
Tương tự, khi chúng ta thực hiện:
animals[cat] = { ...cat, name: "Sara" }
Chúng ta đang ghi đè lên cùng một key "[object Object]":
animals["[object Object]"] = { emoji: '🐈', name: "Sara" }
-
Cuối cùng, khi chúng ta log
animals[dog]
, JavaScript lại chuyển đổidog
thành "[object Object]", và trả về giá trị tương ứng với key này, đó là{ emoji: "🐈", name: "Sara" }
.
5.4 Tóm lại
Khi sử dụng objects làm keys cho một object khác, cần phải cẩn thận vì JavaScript sẽ chuyển đổi tất cả các object keys thành cùng một chuỗi. Điều này có thể dẫn đến việc ghi đè dữ liệu một cách không mong muốn.
Để tránh vấn đề này, chúng ta có thể sử dụng Map
object trong JavaScript, nó cho phép sử dụng bất kỳ giá trị nào (bao gồm cả objects) làm keys mà không bị chuyển đổi.
6. Arrow function và this
Output của đoạn code bên dưới là gì:
const user = {
email: "my@email.com",
updateEmail: email => {
this.email = email
}
}
user.updateEmail("new@email.com")
console.log(user.email)
- A:
my@email.com
- B:
new@email.com
- C:
undefined
- D:
ReferenceError
Đá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é ❓️
6.1. Arrow function và this
Trong JavaScript, arrow function có một đặc điểm quan trọng: nó không tạo ra một this
mới của riêng nó. Thay vào đó, nó kế thừa this
từ phạm vi bên ngoài (lexical scope).
6.2. Phân tích đoạn code
Trong đoạn code trên, chúng ta có một object user
với một phương thức updateEmail
được định nghĩa bằng arrow function. Khi chúng ta gọi user.updateEmail("new@email.com")
, this
trong arrow function không trỏ đến user
object như chúng ta mong đợi.
Thay vào đó, this
trong arrow function sẽ trỏ đến this
của phạm vi bên ngoài, trong trường hợp này là global object (hoặc undefined
trong strict mode).
6.3. Kết quả
Vì vậy, khi chúng ta thực hiện this.email = email
trong arrow function, chúng ta không thay đổi email
của user
object, mà đang cố gắng thay đổi một thuộc tính email
của global object (hoặc gây ra lỗi trong strict mode).
Kết quả là, user.email
vẫn giữ nguyên giá trị ban đầu là "my@email.com"
.
6.4. Cách khắc phục
Để khắc phục vấn đề này, chúng ta có thể sử dụng function thông thường thay vì arrow function:
const user = {
email: "my@email.com",
updateEmail: function(email) {
this.email = email
}
}
user.updateEmail("new@email.com")
console.log(user.email) // "new@email.com"
Hoặc sử dụng phương thức ngắn gọn trong ES6:
const user = {
email: "my@email.com",
updateEmail(email) {
this.email = email
}
}
6.5. Tóm lại
Arrow function rất hữu ích trong nhiều trường hợp, nhưng chúng ta cần cẩn thận khi sử dụng chúng làm phương thức của object, đặc biệt khi cần sử dụng this
. Trong những trường hợp như vậy, function thông thường thường là lựa chọn tốt hơn.
7. Promise.all và xử lý lỗi
Output của đoạn code bên dưới là gì:
const promise1 = Promise.resolve('First')
const promise2 = Promise.resolve('Second')
const promise3 = Promise.reject('Third')
const promise4 = Promise.resolve('Fourth')
const runPromises = async () => {
const res1 = await Promise.all([promise1, promise2])
const res2 = await Promise.all([promise3, promise4])
return [res1, res2]
}
runPromises()
.then(res => console.log(res))
.catch(err => console.log(err))
- A:
[['First', 'Second'], ['Fourth']]
- B:
[['First', 'Second'], ['Third', 'Fourth']]
- C:
[['First', 'Second']]
- D:
'Third'
Đá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é ❓️
7.1. Promise.all
Promise.all
là một phương thức trong JavaScript dùng để xử lý nhiều Promise cùng một lúc. Nó nhận vào một mảng các Promise và trả về một Promise mới.
- Nếu tất cả các Promise trong mảng đều resolve,
Promise.all
sẽ resolve với một mảng chứa kết quả của tất cả các Promise theo thứ tự. - Nếu bất kỳ Promise nào trong mảng reject,
Promise.all
sẽ ngay lập tức reject với lý do của Promise đầu tiên bị reject.
7.2. Phân tích đoạn code
Trong đoạn code trên, chúng ta có:
promise1
vàpromise2
đều resolve.promise3
reject với giá trị 'Third'.promise4
resolve.
Trong hàm runPromises
:
res1 = await Promise.all([promise1, promise2])
sẽ thành công và trả về['First', 'Second']
.res2 = await Promise.all([promise3, promise4])
sẽ ngay lập tức reject vìpromise3
reject.
7.3. Kết quả
Khi Promise.all([promise3, promise4])
reject, nó sẽ throw một exception. Vì chúng ta đang sử dụng await
, exception này sẽ được catch bởi try-catch
ngầm định của async function.
Kết quả là, runPromises()
sẽ trả về một rejected Promise với giá trị 'Third'.
Khi chúng ta gọi runPromises()
, .catch
sẽ bắt được lỗi này và log ra 'Third'.
7.4. Tóm lại
Promise.all
là một công cụ mạnh mẽ để xử lý nhiều Promise cùng lúc, nhưng chúng ta cần cẩn thận với cách nó xử lý lỗi. Nếu bất kỳ Promise nào reject, toàn bộ Promise.all
sẽ reject. Trong nhiều trường hợp, chúng ta có thể muốn xử lý lỗi một cách tinh tế hơn, ví dụ bằng cách sử dụng Promise.allSettled
hoặc xử lý try-catch trong async function.
8. Object.fromEntries
Giá trị nào của method
sẽ được trả về với log { name: "Lydia", age: 22 }
?
const keys = ["name", "age"];
const values = ["Lydia", 22];
const method = "?????";
console.log(
Object[method](
keys.map((_, i) => {
return [keys[i], values[i]];
})
)
); // { name: "Lydia", age: 22 }
- A:
entries
- B:
values
- C:
fromEntries
- D:
forEach
Đá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é ❓️
8.1. Object.fromEntries
Object.fromEntries()
là một phương thức được giới thiệu trong ECMAScript 2019 (ES10). Nó nhận vào một danh sách các cặp key-value và trả về một object mới.
8.2. Phân tích đoạn code
Trong đoạn code trên, chúng ta có:
- Một mảng
keys
chứa các key. - Một mảng
values
chứa các value tương ứng. - Một phép map trên mảng
keys
để tạo ra một mảng mới, mỗi phần tử là một mảng con chứa key và value tương ứng.
8.3. Cách hoạt động
-
keys.map((_, i) => { return [keys[i], values[i]] })
tạo ra một mảng mới có dạng:[ ["name", "Lydia"], ["age", 22] ]
-
Object.fromEntries()
nhận vào mảng này và chuyển đổi nó thành một object:{ name: "Lydia", age: 22 }
8.4. Các phương thức khác
Object.entries()
: Ngược lại vớifromEntries()
, nó chuyển đổi một object thành một mảng các cặp key-value.Object.values()
: Trả về một mảng chứa các giá trị của object.Object.keys()
: Trả về một mảng chứa các key của object.
8.5. Ví dụ minh họa
const obj = { name: "Lydia", age: 22 };
console.log(Object.entries(obj));
// [["name", "Lydia"], ["age", 22]]
console.log(Object.fromEntries(Object.entries(obj)));
// { name: "Lydia", age: 22 }
8.6. Tóm lại
Object.fromEntries()
là một phương thức hữu ích khi chúng ta cần chuyển đổi từ một cấu trúc dữ liệu dạng mảng (như kết quả của Map
hoặc Array.prototype.map()
) thành một object. Nó thường được sử dụng kết hợp với các phương thức xử lý mảng để tạo ra các object mới một cách linh hoạt.
9. Default parameters và truthy/falsy values
Output của đoạn code bên dưới là gì?
const createMember = ({ email, address = {}}) => {
const validEmail = /.+\@.+\..+/.test(email)
if (!validEmail) throw new Error("Valid email pls")
return {
email,
address: address ? address : null
}
}
const member = createMember({ email: "my@email.com" })
console.log(member)
- A:
{ email: "my@email.com", address: null }
- B:
{ email: "my@email.com" }
- C:
{ email: "my@email.com", address: {} }
- D:
{ email: "my@email.com", address: 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é ❓️
9.1. Default parameters
Trong ES6+, JavaScript cho phép chúng ta định nghĩa giá trị mặc định cho tham số của hàm. Nếu không có giá trị nào được truyền vào hoặc giá trị truyền vào là undefined
, giá trị mặc định sẽ được sử dụng.
9.2. Phân tích đoạn code
Trong hàm createMember
, chúng ta có:
({ email, address = {} })
Điều này có nghĩa là nếu address
không được truyền vào hoặc là undefined
, nó sẽ được gán giá trị mặc định là một object rỗng {}
.
9.3. Truthy và Falsy values
Trong JavaScript, khi một giá trị được sử dụng trong ngữ cảnh boolean, nó sẽ được chuyển đổi thành true hoặc false.
- Falsy values bao gồm:
false
,0
,''
(chuỗi rỗng),null
,undefined
, vàNaN
. - Tất cả các giá trị khác đều là truthy, bao gồm cả object rỗng
{}
và mảng rỗng[]
.
Trong đoạn code của chúng ta:
address: address ? address : null
Vì address
là một object rỗng {}
(giá trị mặc định), nó là một truthy value. Do đó, biểu thức điều kiện sẽ trả về address
(tức là {}
).
9.4. Kết quả
Khi chúng ta gọi createMember({ email: "my@email.com" })
:
email
được truyền vào là "my@email.com".address
không được truyền vào, nên nó nhận giá trị mặc định là{}
.- Kiểm tra email hợp lệ thành công.
- Hàm trả về một object với
email
là "my@email.com" vàaddress
là{}
.
Vì vậy, kết quả cuối cùng là:
{ email: "my@email.com", address: {} }
9.5. Lưu ý quan trọng
Điều này có thể gây nhầm lẫn vì nhiều người mong đợi null
sẽ được trả về khi address
không được cung cấp. Tuy nhiên, do cách hoạt động của default parameters và truthy/falsy values, chúng ta nhận được một object rỗng thay vì null
.
9.6. Cách để nhận được null
Nếu chúng ta muốn address
là null
khi không được cung cấp, chúng ta có thể sửa đổi hàm như sau:
const createMember = ({ email, address = null }) => {
// ... rest of the function
return {
email,
address
}
}
Hoặc nếu chúng ta muốn phân biệt giữa "không được cung cấp" và "được cung cấp là null", chúng ta có thể sử dụng:
const createMember = ({ email, address }) => {
// ... rest of the function
return {
email,
address: address === undefined ? null : address
}
}
9.7. Tóm lại
Default parameters và truthy/falsy values là những khái niệm quan trọng trong JavaScript. Chúng có thể tạo ra những hành vi không mong đợi nếu không được hiểu đúng. Khi làm việc với các giá trị mặc định và điều kiện, luôn đảm bảo rằng bạn hiểu rõ cách các giá trị sẽ được đánh giá trong các ngữ cảnh boolean khác nhau.
10. Type coercion và phép so sánh
Output của đoạn code bên dưới là gì?
let randomValue = { name: "Lydia" }
randomValue = 23
if (!typeof randomValue === "string") {
console.log("It's not a string!")
} else {
console.log("Yay it's a string!")
}
- A:
It's not a string!
- B:
Yay it's a string!
- C:
TypeError
- D:
undefined
Đá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é ❓️
10.1. Type coercion và toán tử
Trong JavaScript, toán tử !
(phủ định) sẽ chuyển đổi giá trị sang boolean và sau đó đảo ngược nó. Điều này có thể dẫn đến một số kết quả không mong đợi khi kết hợp với các phép so sánh.
10.2. Phân tích đoạn code
Hãy chia nhỏ biểu thức điều kiện:
!typeof randomValue === "string"
typeof randomValue
sẽ trả về"number"
(vìrandomValue
đã được gán giá trị 23).!
sẽ chuyển đổi"number"
thành booleantrue
, sau đó đảo ngược nó thànhfalse
.- Bây giờ chúng ta có:
false === "string"
10.3. Thứ tự ưu tiên của toán tử
Toán tử !
có độ ưu tiên cao hơn toán tử ===
. Điều này có nghĩa là biểu thức sẽ được đánh giá như sau:
(!typeof randomValue) === "string"
chứ không phải:
!(typeof randomValue === "string")
10.4. Kết quả
false === "string"
sẽ trả về false
, vì một boolean không thể bằng một string.
Do đó, điều kiện trong if
là false
, và khối else
sẽ được thực thi, in ra "Yay it's a string!".
10.5. Cách viết đúng
Nếu mục đích là kiểm tra xem randomValue
có phải là string hay không, chúng ta nên viết:
if (typeof randomValue !== "string") {
console.log("It's not a string!")
} else {
console.log("Yay it's a string!")
}
10.6. Lưu ý về type coercion
JavaScript là một ngôn ngữ động và thường thực hiện type coercion một cách ngầm định. Điều này có thể dẫn đến những kết quả không mong đợi nếu không cẩn thận. Một số ví dụ:
console.log(1 == "1") // true
console.log(1 === "1") // false
console.log([] == false) // true
console.log({} == {}) // false
10.7. Tóm lại
Khi làm việc với các phép so sánh và toán tử logic trong JavaScript, cần đặc biệt chú ý đến:
- Thứ tự ưu tiên của các toán tử
- Type coercion
- Sự khác biệt giữa
==
và===
Sử dụng dấu ngoặc đơn để làm rõ ý định của bạn và tránh những kết quả không mong muốn là một thực hành tốt. Ngoài ra, sử dụng ===
thay vì ==
trong hầu hết các trường hợp cũng giúp tránh được nhiều lỗi liên quan đến type coercion.
Kết luận
Qua 30 kỳ của series "JavaScript Nâng Cao", chúng ta đã cùng nhau khám phá nhiều khía cạnh thú vị và đôi khi gây bối rối của JavaScript. Từ những khái niệm cơ bản như biến và hàm, cho đến những tính năng phức tạp hơn như closures, promises, và async/await, even loop,... chúng ta đã thấy được sức mạnh và sự linh hoạt của ngôn ngữ này.
JavaScript có thể gây khó hiểu đôi khi, nhưng đó cũng chính là điều làm cho nó trở nên thú vị. Mỗi thử thách là một cơ hội để học hỏi và phát triển kỹ năng của chúng ta.
Hy vọng rằng series này đã giúp bạn hiểu sâu hơn về JavaScript và trang bị cho bạn những công cụ cần thiết để xử lý những tình huống phức tạp trong lập trình. Hãy tiếp tục thực hành, thử nghiệm, và không ngừng học hỏi. Chúc các bạn thành công trong hành trình khám phá JavaScript!
Cảm ơn các bạn đã theo dõi series "JavaScript Nâng Cao". Nếu bạn có bất kỳ câu hỏi hoặc góp ý nào, đừng ngại chia sẻ trong phần bình luận nhé. Chúc các bạn có một ngày tốt lành 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ộ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