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ốnthis
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()
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à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ô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ảnhthis
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ượngarguments
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ặ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ảnhthis
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))
- 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!