[JS Basics] Giải mã từ khóa this trong JavaScript: Khi nào dùng? Dùng thế nào?

Trong thế giới JavaScript, có một từ khóa nhỏ bé nhưng lại gieo rắc không ít bối rối cho các lập trình viên, từ người mới bắt đầu đến cả những người đã có kinh nghiệm. Nó được gọi là this. Từ khóa this hoạt động không giống như ở các ngôn ngữ lập trình khác, và sự "kỳ diệu" của nó nằm ở chỗ giá trị của nó thay đổi tùy thuộc vào ngữ cảnh mà nó được gọi.

Giải mã từ khóa this trong JavaScript

Bạn đã bao giờ viết một đoạn code và console.log(this) chỉ để nhận về một kết quả không mong muốn? 🤔 Đừng lo, bạn không đơn độc. Bài viết này sẽ giúp bạn giải mã hoàn toàn this, biến nó từ một khái niệm mơ hồ thành một công cụ mạnh mẽ trong tay bạn.

Từ khóa this trong JavaScript là gì?

Nói một cách đơn giản nhất, this là một tham chiếu đến một đối tượng. Nhưng nó tham chiếu đến đối tượng nào? Câu trả lời là: Đối tượng đó chính là ngữ cảnh thực thi (execution context) của hàm hiện tại.

Đây là điểm mấu chốt: giá trị của this không được xác định tại thời điểm hàm được viết, mà là tại thời điểm hàm được gọi. Cách bạn gọi một hàm sẽ quyết định this là gì.

Hiểu rõ this giúp bạn:

  • Viết code hướng đối tượng hiệu quả.
  • Thao tác với các sự kiện trong DOM một cách chính xác.
  • Tận dụng các phương thức nâng cao như call, apply, và bind.
  • Tránh các lỗi tiềm ẩn khó gỡ rối.

Hãy cùng nhau khám phá 4 quy tắc vàng quyết định giá trị của this.

🎯 Top 4 "quy tắc vàng" xác định giá trị của this

1. Quy tắc mặc định: Global Context

Khi một hàm được gọi một cách độc lập (không phải là phương thức của một đối tượng), this sẽ tham chiếu đến đối tượng toàn cục.

  • Trong trình duyệt, đó là đối tượng window.
  • Trong môi trường Node.js, đó là đối tượng global.
function showMeThis() {
  console.log(this)
}

showMeThis() // Trong trình duyệt, this sẽ là đối tượng Window

Lưu ý trong "Strict Mode": Nếu bạn sử dụng 'use strict';, this trong trường hợp này sẽ là undefined để tránh việc vô tình chỉnh sửa đối tượng toàn cục.

2. Quy tắc ngầm định: Object Context

Đây là trường hợp sử dụng phổ biến nhất. Khi một hàm được gọi với tư cách là một phương thức (method) của một đối tượng, this sẽ tham chiếu đến chính đối tượng gọi phương thức đó.

Hãy nhìn vào đối tượng đứng trước dấu chấm .. Đó chính là this!

const person = {
  name: 'John Doe',
  greet: function () {
    console.log(`Hello, my name is ${this.name}.`)
  },
}

person.greet() // "Hello, my name is John Doe."
// `this` ở đây chính là đối tượng `person` vì `person` đã gọi hàm `greet`.

3. Quy tắc tường minh: call, apply, và bind

JavaScript cung cấp cho chúng ta cách để "ra lệnh" cho một hàm rằng this nên là gì, bất kể nó được gọi như thế nào. Đó là lúc call, apply, và bind tỏa sáng. ✨

  • call(thisArg, arg1, arg2, ...): Thực thi hàm ngay lập tức, với this được gán bằng thisArg và các tham số được truyền vào riêng lẻ.

  • apply(thisArg, [arg1, arg2, ...]): Tương tự call, nhưng các tham số được truyền vào dưới dạng một mảng.

  • bind(thisArg): Không thực thi hàm ngay lập tức. Thay vào đó, nó trả về một hàm mới với this đã được "khóa cứng" vào thisArg.

function introduce(city, country) {
  console.log(`I am ${this.name} from ${city}, ${country}.`)
}

const user1 = { name: 'Alice' }
const user2 = { name: 'Bob' }

// Sử dụng call
introduce.call(user1, 'New York', 'USA') // I am Alice from New York, USA.

// Sử dụng apply
introduce.apply(user2, ['Tokyo', 'Japan']) // I am Bob from Tokyo, Japan.

// Sử dụng bind
const introduceAlice = introduce.bind(user1, 'London', 'UK')
introduceAlice() // I am Alice from London, UK.

bind đặc biệt hữu ích khi làm việc với các hàm callback hoặc xử lý sự kiện, nơi ngữ cảnh của this rất dễ bị "mất".

4. Quy tắc new: Constructor Context

Khi một hàm được gọi với từ khóa new ở phía trước (để tạo một instance của một đối tượng), những điều sau sẽ xảy ra:

  1. Một đối tượng trống hoàn toàn mới được tạo ra.
  2. Đối tượng trống này được gán làm giá trị cho this.
  3. Hàm được thực thi.
  4. Đối tượng mới này được trả về (trừ khi hàm trả về một đối tượng khác một cách tường minh).
function Car(make, model) {
  this.make = make
  this.model = model
  this.info = function () {
    return `${this.make} ${this.model}`
  }
}

const myCar = new Car('Toyota', 'Camry')
console.log(myCar.info()) // "Toyota Camry"
// `this` bên trong `Car` tham chiếu đến đối tượng `myCar` vừa được tạo.

💡 Trường hợp đặc biệt: Arrow Functions

Hàm mũi tên (Arrow Function), được giới thiệu trong ES6, có một cách xử lý this hoàn toàn khác biệt và là một "cứu tinh" trong nhiều trường hợp.

Hàm mũi tên không có this của riêng nó. Thay vào đó, nó "vay mượn" this từ ngữ cảnh bao bọc (lexical scope) gần nhất nơi nó được định nghĩa.

Hãy xem ví dụ kinh điển về setTimeout:

const counter = {
  count: 0,
  start: function () {
    // Cách cũ hay gây lỗi
    setTimeout(function () {
      // `this` ở đây là `window`, không phải `counter`!
      console.log(`Lỗi: ${this.count}`) // NaN hoặc undefined
    }, 1000)

    // Cách giải quyết với hàm mũi tên
    setTimeout(() => {
      // `this` ở đây được "thừa hưởng" từ hàm `start`.
      // Mà `this` trong `start` chính là `counter`.
      this.count++
      console.log(`Chính xác: ${this.count}`) // 1
    }, 1000)
  },
}

counter.start()

Vì sự tiện lợi này, hàm mũi tên là lựa chọn tuyệt vời cho các hàm callback và trình xử lý sự kiện. Tuy nhiên, không nên sử dụng hàm mũi tên cho các phương thức của đối tượng hoặc hàm tạo, vì khi đó chúng sẽ không nhận this là đối tượng như mong đợi.

Tổng kết: Thứ tự ưu tiên của this

Vậy khi nhiều quy tắc có thể được áp dụng cùng lúc, quy tắc nào sẽ thắng?

  1. new: Nếu hàm được gọi với new, this là đối tượng mới được tạo.
  2. call, apply, bind: Nếu hàm được gọi với các phương thức này, this là đối tượng được chỉ định tường minh.
  3. Object Context: Nếu hàm được gọi như một phương thức, this là đối tượng chứa nó.
  4. Mặc định: Nếu không thuộc các trường hợp trên, this là đối tượng toàn cục (hoặc undefined trong strict mode).

Từ khóa this có thể phức tạp, nhưng nó tuân theo một bộ quy tắc logic và nhất quán. Chìa khóa để làm chủ nó là luôn tự hỏi: "Hàm này được GỌI như thế nào?".

Bằng cách hiểu rõ 4 quy tắc vàng và sự khác biệt của hàm mũi tên, bạn không chỉ giải quyết được một trong những vấn đề hóc búa nhất của JavaScript mà còn mở ra cánh cửa để viết code sạch hơn, hiệu quả hơn và dễ bảo trì hơn.

Chúc bạn thành công trên hành trình chinh phục JavaScript!

Bài viết liên quan

[JS Basics] Đối tượng (Objects) trong JavaScript: Khái niệm & Cách sử dụng hiệu quả

Tìm hiểu sâu về cách hoạt động của Objects trong JavaScript. Hướng dẫn chi tiết về các thuộc tính, phương thức, và cách sử dụng Objects để viết code sạch và hiệu quả.

[JS Basics] Câu lệnh điều kiện trong JavaScript: Ví dụ & cách dùng hiệu quả

Nắm vững các câu lệnh điều kiện phổ biến trong JavaScript như if, if-else, switch. Hướng dẫn chi tiết từ cú pháp đến cách áp dụng trong các dự án thực tế.

[JS Basics] Vòng lặp trong JavaScript: Hướng dẫn chi tiết cho người mới

Vòng lặp JavaScript là công cụ không thể thiếu. Khám phá cách sử dụng hiệu quả for, while, và các vòng lặp khác để tối ưu hóa code. Kèm theo ví dụ thực tế và mẹo nhỏ từ chuyên gia.

[JS Basics] Toán tử trong JavaScript: Tổng hợp và ví dụ thực hành dễ hiểu

Bạn muốn làm chủ JavaScript? Hãy bắt đầu bằng cách nắm vững các toán tử cơ bản. Hướng dẫn chi tiết này sẽ giúp bạn làm quen với các loại toán tử phổ biến nhất, từ số học đến logic, với các ví dụ thực tế.