+11

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ộ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. 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 ?.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

  1. 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
  2. getFruit()

    • fruitsundefined
    • fruits?.[1] trả về undefined
    • undefined?.[1] trả về undefined
  3. 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

  1. Chúng ta tạo một instance calc từ class Calc:

    const calc = new Calc()
    

    Lúc này, calc.count bằng 0.

  2. 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ả.

  3. 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ăng count 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

  1. Chúng ta có một object user ban đầu:

    const user = {
      email: "e@mail.com",
      password: "12345"
    }
    
  2. Hàm updateUser nhận một object có thể chứa emailpassword:

    const updateUser = ({ email, password }) => {
      if (email) {
        Object.assign(user, { email })
      }
    
      if (password) {
        user.password = password
      }
    
      return user
    }
    
  3. Chúng ta gọi updateUser với một email mới:

    const updatedUser = updateUser({ email: "new@email.com" })
    
  4. Trong hàm updateUser, Object.assign(user, { email }) sẽ cập nhật thuộc tính email của object user gốc.

  5. Hàm trả về chính object user đã được cập nhật.

  6. 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

  1. 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.
  2. 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.
  3. 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

  1. Ban đầu, chúng ta có mảng:

    const fruit = ['🍌', '🍊', '🍎']
    
  2. 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à ['🍌', '🍊', '🍎']
  3. 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 ['🍊', '🍎']
  4. 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()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

  1. Chúng ta tạo một object rỗng animals:

    const animals = {};
    
  2. Chúng ta định nghĩa hai object dogcat:

    let dog = { emoji: '🐶' }
    let cat = { emoji: '🐈' }
    
  3. 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" }
    
  4. 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" }
    
  5. Cuối cùng, khi chúng ta log animals[dog], JavaScript lại chuyển đổi dog 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ó:

  1. promise1promise2 đều resolve.
  2. promise3 reject với giá trị 'Third'.
  3. promise4 resolve.

Trong hàm runPromises:

  1. res1 = await Promise.all([promise1, promise2]) sẽ thành công và trả về ['First', 'Second'].
  2. 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ó:

  1. Một mảng keys chứa các key.
  2. Một mảng values chứa các value tương ứng.
  3. 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

  1. keys.map((_, i) => { return [keys[i], values[i]] }) tạo ra một mảng mới có dạng:

    [
      ["name", "Lydia"],
      ["age", 22]
    ]
    
  2. 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ới fromEntries(), 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

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" }):

  1. email được truyền vào là "my@email.com".
  2. address không được truyền vào, nên nó nhận giá trị mặc định là {}.
  3. Kiểm tra email hợp lệ thành công.
  4. Hàm trả về một object với email là "my@email.com" và address{}.

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 addressnull 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"
  1. typeof randomValue sẽ trả về "number" (vì randomValue đã được gán giá trị 23).
  2. ! sẽ chuyển đổi "number" thành boolean true, sau đó đảo ngược nó thành false.
  3. 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 iffalse, 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:

  1. Thứ tự ưu tiên của các toán tử
  2. Type coercion
  3. Sự khác biệt giữa =====

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ộ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
Let's register a Viblo Account to get more interesting posts.