this được quyết định lúc gọi hàm
Phần lớn nhầm lẫn về this đến từ một giả định sai: rằng this phụ thuộc vào nơi hàm được định nghĩa. Không phải vậy. this phụ thuộc vào cách hàm được gọi.
Hãy xem cùng một hàm được gọi theo hai cách:
Cùng một hàm, nhưng this khác nhau. Lời gọi đầu tiên có user ở bên trái dấu chấm, nên this là user. Lời gọi thứ hai không có gì bên trái, nên this là undefined. Bản thân hàm không đổi: chỗ gọi mới là thứ thay đổi.
Hãy ghi nhớ ý này: muốn biết this là gì, hãy nhìn vào lời gọi, không phải phần định nghĩa.
Bốn quy tắc binding
Có bốn cách để this được gán giá trị. Gần như mọi thắc mắc liên quan đến this đều được giải đáp khi bạn xác định được quy tắc nào đang áp dụng.
1. Gọi phương thức: obj.fn()
Khi một hàm được gọi như một thuộc tính của đối tượng, this chính là đối tượng đó:
Thứ nằm bên trái dấu chấm trở thành this. Đơn giản vậy thôi.
2. Gọi thông thường: fn()
Khi được gọi mà không có đối tượng phía trước, this là undefined ở strict mode (module và lớp tự động bật chế độ này) hoặc là đối tượng toàn cục ở sloppy mode:
Đây chính là nguồn gốc của các lỗi "this is undefined". Tách một phương thức khỏi đối tượng của nó sẽ biến lời gọi phương thức thành lời gọi thông thường:
Không có counter. trước lời gọi thì không có binding. Hàm không nhớ đối tượng mà nó được lấy ra.
3. Binding tường minh: .call(), .apply(), .bind()
Bạn có thể ép this thành bất cứ thứ gì bạn muốn:
.call và .apply gọi hàm ngay lập tức; chúng chỉ khác nhau ở cách truyền đối số. .bind trả về một hàm mới với this được khóa cố định, rất hữu ích cho callback.
4. Gọi bằng new: new Fn()
Gọi một hàm bằng new sẽ tạo một đối tượng mới và gán nó cho this:
Lớp (class) dùng chính cơ chế này ở bên dưới. Chúng ta sẽ gặp lớp ở một chương sau.
Arrow function không có this riêng
Arrow function cố ý phá vỡ các quy tắc trên. Nó hoàn toàn không gán this: nó kế thừa this từ phạm vi bao quanh, cố định tại thời điểm arrow function được định nghĩa:
Arrow function ở cấp cao nhất của một module giữ lấy this của module, tức là undefined. Dù được gọi dưới dạng user.arrow(), arrow function vẫn không chịu gán lại.
Nghe như một bug, nhưng đó chính là mục đích. Arrow function phát huy tác dụng bên trong phương thức, khi bạn muốn giữ this của bên ngoài:
Arrow function bên trong setInterval kế thừa this từ start, vốn được gọi dưới dạng timer.start(). Vì thế this.seconds hoạt động. Nếu dùng một function thông thường ở đó, nó sẽ có this riêng (bất cứ thứ gì setInterval truyền vào) và code sẽ hỏng.
Quy tắc chung: dùng arrow function cho callback bên trong phương thức. Dùng hàm thông thường cho chính các phương thức.
Cái bẫy callback kinh điển
Đây là cách phổ biến nhất khiến this đi chệch hướng. Bạn truyền một phương thức làm callback và nó mất binding:
setTimeout gọi hàm như một lời gọi thông thường, không phải c.increment(). Có ba cách sửa:
Cả ba đều hoạt động. Bọc trong arrow function thường là cách rõ ràng nhất.
this ở cấp cao nhất
Giá trị this ở cấp cao nhất phụ thuộc vào nơi code của bạn chạy:
- Script trên trình duyệt (không phải module):
thislàwindow. - ES module (gồm hầu hết code hiện đại đã qua bundler):
thislàundefined. - Module CommonJS của Node.js:
thislàmodule.exports.
Để tham chiếu đáng tin cậy tới đối tượng toàn cục ở mọi môi trường, hãy dùng globalThis:
Trên thực tế, bạn không nên dựa vào this ở cấp cao nhất. Hãy dùng globalThis khi thật sự cần đối tượng toàn cục, còn lại thì truyền giá trị một cách tường minh.
Cây quyết định nhanh
Khi gặp this mà không chắc nó sẽ là gì, hãy lần lượt đi qua danh sách sau:
- Hàm có phải là arrow function không? Nếu có,
thislà giá trị của nó trong phạm vi bao quanh. Bỏ qua hoàn toàn chỗ gọi. - Hàm có được gọi bằng
newkhông? Nếu có,thislà đối tượng mới. - Hàm có được gọi bằng
.call,.applyhoặc là một hàm đã bind không? Nếu có,thislà thứ được truyền vào. - Hàm có được gọi dưới dạng
obj.method()không? Nếu có,thislàobj. - Hàm có được gọi dưới dạng
fn()thông thường không? Nếu có,thislàundefinedở strict mode.
Chiếc thang đó, theo đúng thứ tự đó, quyết định mọi trường hợp.
Tiếp theo: hàm bậc cao
Giờ this không còn là bí ẩn, bạn đã sẵn sàng cho mẫu hình khiến JavaScript giàu sức biểu đạt đến vậy: truyền hàm đi như những giá trị. Tiếp theo, chúng ta sẽ tìm hiểu hàm bậc cao (higher-order function), tức là những hàm nhận vào hoặc trả về hàm khác, và cách chúng làm nên các phương thức mảng, trình xử lý sự kiện và phần lớn code JavaScript trong thực tế.
Câu hỏi thường gặp
this trong JavaScript trỏ tới đâu?
this trỏ tới đối tượng mà hàm được gọi trên đó, không phải nơi hàm được định nghĩa. Trong user.greet(), this là user. Trong một lời gọi greet() thông thường, this là undefined ở strict mode (hoặc là đối tượng toàn cục ở sloppy mode). Quy tắc nằm ở chỗ gọi hàm, không phải chỗ định nghĩa.
Vì sao this lại là undefined trong hàm của tôi?
Có lẽ bạn đã tách một phương thức khỏi đối tượng của nó rồi gọi riêng, hoặc truyền nó làm callback. const fn = user.greet; fn(); làm mất binding: ở chỗ gọi không có đối tượng nào bên trái dấu chấm. Hãy sửa bằng .bind(user), bọc trong một arrow function, hoặc gọi nó dưới dạng user.greet().
this trong arrow function khác thế nào?
Arrow function không có this riêng. Nó kế thừa this từ phạm vi bao quanh tại thời điểm được định nghĩa. Vì vậy arrow function rất hợp làm callback bên trong phương thức, khi bạn muốn giữ this của bên ngoài. Điều đó cũng có nghĩa là .call(), .apply() và .bind() không thể đổi this của một arrow function.
this ở cấp cao nhất của một script là gì?
Trong một script thông thường trên trình duyệt, this ở cấp cao nhất là đối tượng window. Trong một ES module, nó là undefined. Trong module CommonJS của Node.js, nó là module.exports. Cách tham chiếu dùng được ở mọi môi trường là globalThis, luôn trỏ tới đối tượng toàn cục.