[Advanced JS] Call, Apply, Bind trong JavaScript: Ví dụ dễ hiểu và Áp dụng thực tế

Trong thế giới JavaScript, từ khóa this vừa là một người bạn đồng hành mạnh mẽ, vừa có thể trở thành một "kẻ phản bội" khó lường. Cách mà this được xác định trong mỗi ngữ cảnh khác nhau là một trong những khái niệm dễ gây nhầm lẫn nhất cho các lập trình viên. May mắn thay, JavaScript cung cấp cho chúng ta một bộ ba "siêu anh hùng" để chế ngự this, đó chính là call(), apply(), và bind().

Call, Apply, Bind trong JavaScript

Việc hiểu và sử dụng thành thạo ba phương thức này không chỉ giúp bạn giải quyết các vấn đề phức tạp liên quan đến this, mà còn mở ra những cách viết mã linh hoạt, hiệu quả và dễ tái sử dụng hơn. Hãy cùng nhau khám phá sâu hơn trong bài viết này nhé! 🚀

Bối cảnh vấn đề: Khi "this" nổi loạn

Trước khi đi vào chi tiết, hãy xem xét một ví dụ kinh điển để thấy tại sao chúng ta cần đến call, apply, và bind.

const person = {
  name: 'John Doe',
  greet: function () {
    console.log(`Xin chào, tôi là ${this.name}`)
  },
}

const greetFunc = person.greet

greetFunc() // Kết quả: "Xin chào, tôi là undefined"

Tại sao lại là undefined? Bởi vì khi chúng ta gán person.greet cho greetFunc và gọi nó một cách độc lập, this bên trong greetFunc không còn trỏ đến đối tượng person nữa. Thay vào đó, nó trỏ về đối tượng toàn cục (window trong trình duyệt) ở chế độ non-strict mode, hoặc là undefined trong strict mode. Đây chính là lúc call, apply, và bind tỏa sáng.

Call: Vay mượn phương thức một cách trực tiếp

Phương thức call() cho phép bạn gọi một hàm và chủ động thiết lập giá trị cho this bên trong hàm đó. Ngoài ra, bạn có thể truyền các đối số cho hàm một cách riêng lẻ, ngăn cách bởi dấu phẩy.

Cú pháp

function.call(thisArg, arg1, arg2, ...);
  • thisArg: Giá trị mà bạn muốn this trở thành bên trong hàm.
  • arg1, arg2, ...: Các đối số được truyền vào hàm.

Ví dụ thực tế

Hãy quay lại vấn đề ban đầu và sửa nó bằng call():

const person = {
  name: 'John Doe',
  greet: function () {
    console.log(`Xin chào, tôi là ${this.name}`)
  },
}

const anotherPerson = {
  name: 'Jane Smith',
}

person.greet.call(anotherPerson) // Kết quả: "Xin chào, tôi là Jane Smith"

Ở đây, chúng ta đã "mượn" phương thức greet của person và thực thi nó trong ngữ cảnh của anotherPerson. this bên trong greet lúc này đã được call() chỉ định là anotherPerson, vì vậy this.name trả về "Jane Smith".

Apply: Người anh em song sinh của Call

apply() hoạt động gần như y hệt call(): nó cũng thực thi một hàm với một giá trị this được chỉ định. Điểm khác biệt duy nhất và cốt lõi nằm ở cách truyền đối số. Thay vì truyền từng đối số riêng lẻ, apply() nhận một mảng (hoặc một đối tượng giống mảng) chứa các đối số.

Cú pháp

function.apply(thisArg, [argsArray]);
  • thisArg: Tương tự như call().
  • [argsArray]: Một mảng hoặc đối tượng giống mảng chứa các đối số cho hàm.

Ví dụ thực tế

Hãy xem xét một hàm cần nhiều đối số:

function introduce(profession, city) {
  console.log(`Tôi là ${this.name}, một ${profession} đến từ ${city}.`)
}

const person = {
  name: 'Peter Pan',
}

const details = ['kỹ sư phần mềm', 'thành phố Neverland']

introduce.apply(person, details)
// Kết quả: "Tôi là Peter Pan, một kỹ sư phần mềm đến từ thành phố Neverland."

apply() đặc biệt hữu ích khi bạn có một mảng dữ liệu và muốn truyền các phần tử của mảng đó làm đối số cho một hàm.

Bind: Tạo ra một bản sao được "ràng buộc"

Khác với call()apply() là thực thi hàm ngay lập tức, bind() không gọi hàm. Thay vào đó, nó tạo ra một hàm mới với this đã được "ràng buộc" (bound) vĩnh viễn với giá trị mà bạn chỉ định.

Cú pháp

function.bind(thisArg, arg1, arg2, ...);
  • thisArg: Giá trị mà this sẽ được ràng buộc vĩnh viễn trong hàm mới.
  • arg1, arg2, ...: (Tùy chọn) Các đối số được "áp dụng trước" (partially applied).

Ví dụ thực tế

bind() là giải pháp hoàn hảo cho vấn đề ban đầu của chúng ta, đặc biệt trong các tình huống như xử lý sự kiện hoặc callback.

const person = {
  name: 'Alice Wonderland',
  greet: function () {
    console.log(`Xin chào, tôi là ${this.name}`)
  },
}

const boundGreet = person.greet.bind(person)

boundGreet() // Kết quả: "Xin chào, tôi là Alice Wonderland"

// Thậm chí khi dùng trong một callback, `this` vẫn được giữ nguyên
setTimeout(boundGreet, 1000) // Sau 1 giây: "Xin chào, tôi là Alice Wonderland"

Hàm boundGreet là một phiên bản mới của person.greet, nơi this sẽ luôn luônperson, bất kể nó được gọi ở đâu và như thế nào.

So sánh nhanh: Call, Apply và Bind

Bảng tổng hợp nhanh kiến thức về Call, Apply và Bind theo các tiêu chí quan trọng:

Tiêu chícall()apply()bind()
Thực thiGọi hàm ngay lập tứcGọi hàm ngay lập tứcKhông gọi hàm, trả về một hàm mới ❌
Truyền đối sốTruyền riêng lẻ (arg1, arg2, ...)Truyền qua một mảng ([arg1, arg2])Truyền riêng lẻ, có thể áp dụng trước
Giá trị trả vềGiá trị trả về của hàm gốcGiá trị trả về của hàm gốcMột hàm mới đã được ràng buộc this

Mẹo ghi nhớ:

  • Call: Comma-separated arguments (Đối số ngăn cách bằng dấu phẩy).
  • Apply: Array of arguments (Mảng đối số).
  • Bind: Bound function (Hàm được ràng buộc).

Kết luận: Khi nào nên dùng cái nào?

Những trường hợp thực tế phổ biến để bạn có thể áp dụng call(), apply(), và bind() một cách hiệu quả:

  • Sử dụng call() hoặc apply() khi bạn muốn thực thi một hàm ngay lập tức với một ngữ cảnh this khác.

    • Vay mượn phương thức: Mượn các phương thức từ đối tượng khác, ví dụ như dùng Array.prototype.slice.call(arguments) để chuyển đối tượng arguments thành một mảng thực sự.
    • Gọi hàm với mảng đối số động: Dùng apply() để truyền một mảng giá trị làm tham số cho các hàm như Math.max hoặc Math.min. Ví dụ: Math.max.apply(null, [1, 5, 2, 9]).
  • Sử dụng bind() khi bạn cần một hàm để sử dụng sau này, nhưng muốn chắc chắn rằng ngữ cảnh this của nó không bị thay đổi.

    • Event Listeners và Callbacks: Đây là trường hợp phổ biến nhất. Khi bạn truyền một phương thức của đối tượng làm trình xử lý sự kiện (ví dụ: element.addEventListener('click', myObject.myMethod)), this sẽ bị mất. bind() là giải pháp hoàn hảo: element.addEventListener('click', myObject.myMethod.bind(myObject)).
    • Partial Application (Áp dụng hàm từng phần): bind() cho phép bạn tạo ra các hàm mới với một vài đối số đã được thiết lập sẵn.
    function multiply(a, b) {
      return a * b
    }
    
    const double = multiply.bind(null, 2) // this không quan trọng, gán là null
    console.log(double(5)) // Kết quả: 10 (tương đương multiply(2, 5))
    

Nhìn chung, call(), apply(), và bind() là những công cụ vô cùng mạnh mẽ trong kho vũ khí của một lập trình viên JavaScript. Chúng không chỉ giúp bạn kiểm soát hoàn toàn từ khóa this mà còn thúc đẩy việc viết mã theo hướng module hóa và tái sử dụng. Bằng cách hiểu rõ sự khác biệt tinh tế và các trường hợp sử dụng lý tưởng của từng phương thức, bạn có thể viết ra những đoạn mã sạch sẽ, dễ đoán và ít lỗi hơn.

Lần tới khi bạn đối mặt với một this "lạc lối", hãy nhớ đến "bộ ba quyền lực" này!

Bài viết liên quan

[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] Giải mã từ khóa this trong JavaScript: Khi nào dùng? Dùng thế nào?

Từ khóa this trong JavaScript là một trong những khái niệm quan trọng nhưng cũng gây nhiều nhầm lẫn. Bài viết này sẽ giúp bạn làm chủ this thông qua các ví dụ thực tế và giải thích dễ hiểu, ngay cả khi bạn là người mới.

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