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().

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ốnthistrở 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() và 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àthissẽ đượ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ôn là person, 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 thi | Gọi hàm ngay lập tức ✅ | Gọi hàm ngay lập tức ✅ | Khô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ốc | Giá trị trả về của hàm gốc | Mộ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ặcapply()khi bạn muốn thực thi một hàm ngay lập tức với một ngữ cảnhthiskhá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ượngargumentsthà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.maxhoặcMath.min. Ví dụ:Math.max.apply(null, [1, 5, 2, 9]).
- 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
-
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ảnhthiscủ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)),thissẽ 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)) - 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ụ:
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!
![[JS Basics] Câu lệnh điều kiện trong JavaScript: Ví dụ & cách dùng hiệu quả](/images/blog/conditional-statements-in-javascript.webp)
![[JS Basics] Vòng lặp trong JavaScript: Hướng dẫn chi tiết cho người mới](/images/blog/loops-in-javascript.webp)
![[JS Basics] Giải mã từ khóa this trong JavaScript: Khi nào dùng? Dùng thế nào?](/images/blog/this-keyword-in-javascript.webp)
![[JS Basics] Đối tượng (Objects) trong JavaScript: Khái niệm & Cách sử dụng hiệu quả](/images/blog/objects-in-javascript.webp)