Bạn truyền counter.read cho một hàm nhận callback, rồi code lỗi dù counter.read() vẫn chạy. Khi giải thích trong phỏng vấn, đừng bắt đầu bằng “callback chạy muộn nên mất this”. Vấn đề có thể xuất hiện ngay trong một lời gọi đồng bộ, không liên quan event loop.
Bài đối chiếu tài liệu JavaScript ngày 29/10/2026 và chạy ví dụ trên Node.js 24.18.1. Dùng strict mode rõ ràng để kết quả không phụ thuộc một môi trường có tự thay this bằng global object hay không. Các snippet không dùng timer, DOM hoặc React.
Bắt đầu từ cách gọi
'use strict';
const counter = {
value: 7,
read() {
return this.value;
},
};
console.log(counter.read());
const detached = counter.read;
try {
console.log(detached());
} catch (error) {
console.log(error.name);
}
Kết quả là 7 rồi TypeError. Gán method vào biến lấy ra cùng function, nhưng lời gọi detached() không còn dạng counter.read(). Trong strict mode, lời gọi độc lập ấy truyền this là undefined; đọc .value mới gây lỗi.
Theo MDN this, regular function nhận this theo cách được gọi. Object chứa property không đóng dấu vĩnh viễn rằng function luôn thuộc object đó.
Callback không tự mang theo receiver
function invoke(callback) {
return callback();
}
try {
console.log(invoke(counter.read));
} catch (error) {
console.log(error.name);
}
Vẫn là TypeError. Hàm invoke đã gọi callback ngay lập tức. Điều này giúp loại giả thuyết rằng chỉ setTimeout hoặc Promise mới gây ra lỗi.
Khi đọc code thật, tìm chính xác lời gọi callback. Nếu thư viện gọi callback.call(something), holder.callback() hoặc truyền this theo hợp đồng khác, receiver có thể khác. Không thể suy ra this chỉ từ việc function được truyền vào tham số.
Hai cách giữ lời gọi có chủ đích
const bound = counter.read.bind(counter);
const wrapped = () => counter.read();
console.log(invoke(bound));
console.log(invoke(wrapped));
console.log(bound === counter.read.bind(counter));
Ba kết quả là 7, 7, false. Bind tạo một function mới giữ receiver đã chỉ định. Nếu API cần gỡ callback bằng đúng tham chiếu đã đăng ký, lưu biến bound rồi dùng lại; bind thêm một lần tạo tham chiếu khác.
Arrow wrapper ở đây không đọc this trực tiếp. Nó gọi lại method qua biến counter bằng cú pháp counter.read(). Cách sửa này thể hiện object được chọn ở trong thân callback, thay vì yêu cầu bên gọi truyền receiver đúng.
Hai cách cũng có khác biệt khi method bị thay. Bound giữ function ban đầu; wrapper tra counter.read mỗi lần được gọi. Bạn nên chọn theo hợp đồng mong muốn, không coi chúng đồng nghĩa trong mọi trường hợp. Function.bind giải thích việc tạo function mới và cố định this cho lời gọi thông thường.
Arrow đặt trong object không tự lấy object làm this
Một đáp án sửa sai thường gặp là đổi mọi method sang arrow. Ví dụ dưới đây cố tình tạo object trong một function strict được gọi độc lập:
function makeCounter() {
'use strict';
return {
value: 7,
read: () => this,
};
}
const wrong = makeCounter();
console.log(wrong.read() === undefined);
console.log(wrong.read.call({ value: 99 }) === undefined);
Cả hai dòng là true. Arrow không có this binding riêng; nó dùng this của scope tạo ra nó. Dấu ngoặc object literal không tạo một receiver scope cho arrow. Gọi .call(...) không thay được this đã được arrow lấy từ bên ngoài.
Arrow functions reference mô tả sự khác biệt này. Arrow làm callback rất hữu ích, nhưng lý do dùng là ngữ nghĩa scope cần có, không phải quy tắc “arrow luôn sửa lỗi this”.
Cách trả lời và mở rộng bài tập
Một câu trả lời đủ rõ có thể đi theo ba bước: chỉ ra receiver của lời gọi ban đầu, chỉ ra receiver sau khi tách function, rồi sửa đúng tại nơi hợp đồng bị thay. Cuối cùng, chứng minh bằng snippet ngắn thay vì dự đoán nội dung global object.
Để thử thêm, gọi counter.read.call({ value: 20 }) và xác nhận regular function trả 20. Sau đó thay method bằng một function trả 100, gọi bound và wrapped để quan sát bound vẫn dùng phần triển khai cũ. Khôi phục fixture hoặc chạy các thử nghiệm trong process riêng để không lẫn trạng thái.
Các ví dụ đồng bộ này chỉ kiểm tra cơ chế gọi. Chúng không nói một framework sẽ bind handler theo cách nào, cũng không đo chi phí cấp phát callback. Khi đi vào React hoặc event listener native, đọc thêm hợp đồng của API đó.
Lỗi thường gặp
- Giải thích bằng thời điểm chạy dù lỗi vẫn xảy ra đồng bộ.
- Nghĩ gán method sang biến đã clone cả object.
- Gọi bind mỗi lần đăng ký/gỡ listener rồi mong hai callback bằng nhau.
- Đổi method trong object literal sang arrow mà không kiểm tra this của scope ngoài.
Theo dõi cách gọi giúp bạn giải thích cả lỗi lẫn cách sửa. Khi receiver cần ổn định, hãy thể hiện nó bằng bind hoặc một lời gọi rõ ràng trong callback, rồi giữ tham chiếu đúng nếu API cần đăng ký và gỡ đăng ký.

