+15

JavaScript Nâng Cao - Kỳ 17

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

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. Tham số mặc định trong hàm

Output của đoạn code bên dưới là gì?

function sayHi(name) {
  return `Hi there, ${name}`
}

console.log(sayHi())
  • A: Hi there,
  • B: Hi there, undefined
  • C: Hi there, null
  • 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é ❓️

1.1. Phân tích vấn đề

Trong JavaScript, khi chúng ta định nghĩa một hàm với tham số nhưng không truyền giá trị cho tham số đó khi gọi hàm, JavaScript sẽ tự động gán giá trị undefined cho tham số đó.

Trong trường hợp này, hàm sayHi được định nghĩa với một tham số name, nhưng khi gọi hàm sayHi(), chúng ta không truyền bất kỳ đối số nào. Do đó, name sẽ nhận giá trị undefined.

1.2. Template literals và undefined

Khi chúng ta sử dụng template literals (chuỗi được bao quanh bởi backticks ``), JavaScript sẽ tự động chuyển đổi các giá trị không phải chuỗi thành chuỗi. Trong trường hợp này, undefined sẽ được chuyển thành chuỗi "undefined".

Vì vậy, kết quả của Hi there, ${name} sẽ là "Hi there, undefined".

1.3. Giá trị mặc định cho tham số

Để tránh tình huống này, chúng ta có thể sử dụng giá trị mặc định cho tham số. Ví dụ:

function sayHi(name = "Guest") {
  return `Hi there, ${name}`
}

console.log(sayHi()) // "Hi there, Guest"
console.log(sayHi("Lydia")) // "Hi there, Lydia"

Trong ví dụ này, nếu không có giá trị nào được truyền vào cho name, nó sẽ sử dụng giá trị mặc định "Guest".

1.4. Tóm lại

Hiểu về cách JavaScript xử lý các tham số không được truyền giá trị là rất quan trọng để tránh các lỗi không mong muốn trong code của bạn. Luôn nhớ rằng:

  1. Tham số không được truyền giá trị sẽ có giá trị undefined.
  2. Sử dụng giá trị mặc định cho tham số là một cách tốt để xử lý trường hợp tham số không được truyền giá trị.

1.5 Ví dụ thêm

Đây là một ví dụ khác về việc sử dụng giá trị mặc định cho tham số:

const readOnlyClient = {
  query: () => "query from read only client"
}
const writeClient = {
  query: () => "query from write client"
}

async function getData(query, client = readOnlyClient) {
  return client.query(query)
}

await getData("SELECT * FROM users") // "query" with readOnlyClient
await getData("DELETE * FROM users", writeClient) // "query" with writeClient

Trong ví dụ này, hàm getData sẽ trả về kết quả từ client.query(query). Nếu không có client nào được truyền vào, nó sẽ sử dụng readOnlyClient mặc định. Điều này giúp chúng ta dễ dàng thay đổi client mà không cần thay đổi code của hàm getData.

2. This trong các phương thức và hàm

Output của đoạn code bên dưới là gì?

var status = "😎"

setTimeout(() => {
  const status = "😍"

  const data = {
    status: "🥑",
    getStatus() {
      return this.status
    }
  }

  console.log(data.getStatus())
  console.log(data.getStatus.call(this))
}, 0)
  • A: "🥑" and "😍"
  • B: "🥑" and "😎"
  • C: "😍" and "😎"
  • D: "😎" and "😎"
Đá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é ❓️

2.1. Phân tích vấn đề

Đoạn code này liên quan đến một số khái niệm quan trọng trong JavaScript:

  • Phạm vi của biến (variable scope)
  • Từ khóa this
  • Phương thức call()

2.2. Phạm vi của biến

Trong đoạn code, chúng ta có ba biến status:

  1. Biến global var status = "😎"
  2. Biến local const status = "😍" trong callback của setTimeout
  3. Thuộc tính status: "🥑" của object data

2.3. Từ khóa this trong phương thức

Trong phương thức getStatus của object data, this sẽ trỏ đến chính object data. Do đó, this.status sẽ trả về "🥑".

console.log(data.getStatus()) // "🥑"

2.4. Phương thức call()

Phương thức call() cho phép chúng ta gọi một hàm với một giá trị this được chỉ định. Trong trường hợp này:

console.log(data.getStatus.call(this))

this ở đây là this của arrow function trong setTimeout. Arrow function không có this riêng, nó sẽ sử dụng this của context bên ngoài, trong trường hợp này là global context. Trong global context, this trỏ đến global object (trong trình duyệt là window), và status global là "😎".

2.5. Tóm lại

  1. data.getStatus() trả về "🥑"this trong phương thức trỏ đến object data.
  2. data.getStatus.call(this) trả về "😎"this được chỉ định là this của context global.

Hiểu về cách this hoạt động trong các ngữ cảnh khác nhau là rất quan trọng trong JavaScript. Nó giúp chúng ta kiểm soát được context của hàm và tránh các lỗi không mong muốn.

Chú ý: Trong JavaScript, this là một trong những khái niệm hơi phức tạp. Tuy nhiên bạn chỉ cần nhớ 2 quy tắc sau:

  1. Arrow function không có this riêng, nó sẽ sử dụng this của context bên ngoài.

  2. Trong phương thức của một object, this sẽ trỏ đến object gọi phương thức đó.

3. Tham chiếu và gán giá trị

Output của đoạn code bên dưới là gì?

const person = {
  name: "Lydia",
  age: 21
}

let city = person.city
city = "Amsterdam"

console.log(person)
  • A: { name: "Lydia", age: 21 }
  • B: { name: "Lydia", age: 21, city: "Amsterdam" }
  • C: { name: "Lydia", age: 21, city: undefined }
  • D: "Amsterdam"
Đá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. Phân tích vấn đề

Đoạn code này liên quan đến cách JavaScript xử lý việc gán giá trị và tham chiếu đối với objects.

3.2. Gán giá trị từ thuộc tính không tồn tại

Khi chúng ta thực hiện:

let city = person.city

Chúng ta đang cố gắng truy cập thuộc tính city của object person. Tuy nhiên, person không có thuộc tính city. Trong JavaScript, khi chúng ta truy cập một thuộc tính không tồn tại của một object, kết quả sẽ là undefined.

Vì vậy, sau dòng code này, city sẽ có giá trị undefined.

3.3. Gán giá trị mới cho biến

Tiếp theo, chúng ta có:

city = "Amsterdam"

Ở đây, chúng ta đang gán một giá trị mới cho biến city. Điều này không ảnh hưởng gì đến object person ban đầu. Chúng ta chỉ đang thay đổi giá trị của một biến độc lập.

3.4. Không có sự thay đổi đối với object ban đầu

Quan trọng là phải hiểu rằng không có bất kỳ thao tác nào trong đoạn code này thực sự thay đổi object person. Chúng ta không thêm thuộc tính mới hay sửa đổi bất kỳ thuộc tính nào của person.

3.5. Tóm lại

  1. Việc truy cập person.city trả về undefined vì thuộc tính này không tồn tại.
  2. Gán "Amsterdam" cho city chỉ thay đổi giá trị của biến city, không ảnh hưởng đến person.
  3. Object person vẫn giữ nguyên như ban đầu.

Hiểu về cách JavaScript xử lý objects và tham chiếu là rất quan trọng. Trong trường hợp này, chúng ta thấy rằng việc gán giá trị cho một biến độc lập không ảnh hưởng đến object gốc, ngay cả khi giá trị ban đầu của biến đó được lấy từ object.

4. Block scope và biến

Output của đoạn code bên dưới là gì?

function checkAge(age) {
  if (age < 18) {
    const message = "Sorry, you're too young."
  } else {
    const message = "Yay! You're old enough!"
  }

  return message
}

console.log(checkAge(21))
  • A: "Sorry, you're too young."
  • B: "Yay! You're old enough!"
  • C: ReferenceError
  • 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. Phân tích vấn đề

Đoạn code này liên quan đến khái niệm quan trọng trong JavaScript: block scope (phạm vi khối).

4.2. Block scope trong JavaScript

Trong JavaScript, các biến được khai báo bằng letconst có phạm vi block. Điều này có nghĩa là chúng chỉ tồn tại trong block (khối) mà chúng được khai báo, bao gồm cả các block con.

Một block trong JavaScript được xác định bởi cặp dấu ngoặc nhọn {}. Ví dụ, mỗi câu lệnh ifelse tạo ra một block riêng.

4.3. Vấn đề với biến message

Trong hàm checkAge, biến message được khai báo bên trong các block ifelse:

if (age < 18) {
  const message = "Sorry, you're too young."
} else {
  const message = "Yay! You're old enough!"
}

Điều này có nghĩa là biến message chỉ tồn tại bên trong các block này. Khi chúng ta cố gắng truy cập message bên ngoài các block (trong câu lệnh return), JavaScript không thể tìm thấy biến này.

4.4. ReferenceError

Khi JavaScript không thể tìm thấy một biến đã được tham chiếu, nó sẽ throw một ReferenceError. Đó chính xác là những gì xảy ra trong trường hợp này.

4.5. Cách khắc phục

Để khắc phục vấn đề này, chúng ta có thể khai báo biến message bên ngoài các block if/else:

function checkAge(age) {
  let message;
  if (age < 18) {
    message = "Sorry, you're too young."
  } else {
    message = "Yay! You're old enough!"
  }

  return message
}

console.log(checkAge(21)) // "Yay! You're old enough!"

Bằng cách này, message sẽ tồn tại trong phạm vi của toàn bộ hàm checkAge, và chúng ta có thể truy cập nó trong câu lệnh return.

4.6. Tóm lại

  1. Biến được khai báo với letconst có phạm vi block.
  2. Cố gắng truy cập biến ngoài phạm vi của nó sẽ dẫn đến ReferenceError.
  3. Để sử dụng một biến trong toàn bộ hàm, hãy khai báo nó ở đầu hàm, ngoài các block con.

Hiểu về phạm vi của biến là rất quan trọng trong JavaScript. Nó giúp chúng ta tránh được nhiều lỗi không mong muốn và viết code chặt chẽ, an toàn hơn.

5. Promises và Fetch API

Những thông tin nào sẽ được ghi ra với đoạn code sau?

fetch('https://www.website.com/api/user/1')
  .then(res => res.json())
  .then(res => console.log(res))
  • A: Kết quả của phương thức fetch.
  • B: Kết quả của lần gọi thứ hai đến phương thức fetch.
  • C: Kết quả của callback trong .then() trước đó.
  • D: Luôn là 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é ❓️

5.1. Phân tích vấn đề

Đoạn code này liên quan đến Fetch API và Promises trong JavaScript, hai công cụ mạnh mẽ để xử lý các yêu cầu bất đồng bộ.

5.2. Fetch API

fetch() là một phương thức modern của JavaScript để thực hiện các yêu cầu HTTP. Nó trả về một Promise mà resolve với một đối tượng Response.

5.3. Chuỗi Promise

Trong đoạn code, chúng ta thấy một chuỗi các phương thức .then(). Mỗi .then() nhận kết quả từ Promise trước đó và trả về một Promise mới.

5.4. Phân tích từng bước

  1. fetch('https://www.website.com/api/user/1'):

    • Gửi một yêu cầu GET đến URL được chỉ định.
    • Trả về một Promise resolve với đối tượng Response.
  2. .then(res => res.json()):

    • Nhận đối tượng Response từ fetch.
    • Gọi phương thức json() trên Response, trả về một Promise mới.
    • Promise này resolve với dữ liệu JSON đã được parse.
  3. .then(res => console.log(res)):

    • Nhận dữ liệu JSON đã được parse từ Promise trước đó.
    • Log dữ liệu này ra console.

5.5. Kết quả

Kết quả được log ra console sẽ là dữ liệu JSON đã được parse từ response của API. Đây chính là "kết quả của callback trong .then() trước đó".

5.6. Tóm lại

  1. Fetch API trả về một Promise với đối tượng Response.
  2. Phương thức json() parse body của Response thành JSON.
  3. Chuỗi .then() cho phép chúng ta xử lý dữ liệu qua nhiều bước.
  4. Kết quả cuối cùng là dữ liệu JSON đã được parse.

Hiểu về cách làm việc với Promises và Fetch API là rất quan trọng trong phát triển web hiện đại. Nó cho phép chúng ta xử lý các tác vụ bất đồng bộ một cách dễ dàng và hiệu quả.

Kết luận

Qua 5 ví dụ trên, chúng ta đã đi sâu vào một số khía cạnh quan trọng của JavaScript:

  1. Tham số mặc định trong hàm
  2. Cách this hoạt động trong các ngữ cảnh khác nhau
  3. Tham chiếu và gán giá trị cho objects
  4. Block scope và phạm vi của biến
  5. Làm việc với Promises và Fetch API

Mỗi khía cạnh này đều có những nét đặc trưng riêng và đóng vai trò quan trọng trong việc viết code JavaScript hiệu quả và tránh bugs.

Hãy nhớ rằng, JavaScript là một ngôn ngữ rất linh hoạt và đôi khi có những hành vi không mong đợi. Việc hiểu rõ các khái niệm cơ bản và thực hành thường xuyên sẽ giúp bạn trở thành một lập trình viên JavaScript giỏi hơn.

Hy vọng bài viết này đã giúp bạn hiểu rõ hơn về JavaScript. Hẹn gặp lại các bạn trong các 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ộ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
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í