Mục lục
Xem mục lục
this được xác định thế nào
Khác với nhiều ngôn ngữ khác, giá trị của this trong JavaScript không phụ thuộc vào nơi function được khai báo, mà phụ thuộc vào cách function đó được gọi tại thời điểm runtime. Cùng một function, gọi theo cách khác nhau sẽ cho this khác nhau.
Có 4 rule xác định this, theo thứ tự ưu tiên tăng dần: Default Binding, Implicit Binding, Explicit Binding, new Binding.
Default Binding
Khi một function được gọi độc lập, không thông qua object nào cả:
function show() {
console.log(this);
}
show();
Ở strict mode, this là undefined. Ở non-strict mode, this trỏ tới global object (window trên browser, global trên Node.js). Đây là rule mặc định, áp dụng khi không có rule nào khác can thiệp.
Implicit Binding
Khi function được gọi như một phương thức của object, this trỏ tới object đứng ngay trước dấu chấm lúc gọi:
const user = {
name: "An",
greet() {
console.log(this.name);
},
};
user.greet(); // "An", this trỏ tới user
Điểm cần lưu ý: this được xác định bởi cú pháp gọi, không phải bởi nơi greet được định nghĩa. Nếu user.greet được gán cho một biến khác rồi gọi độc lập, this sẽ không còn trỏ tới user nữa, như mục tiếp theo trình bày.
Cạm bẫy thường gặp: mất Implicit Binding
const user = {
name: "An",
greet() {
console.log(this.name);
},
};
const fn = user.greet;
fn(); // undefined (hoặc lỗi ở strict mode nếu truy cập this.name)
Khi user.greet được gán cho fn, phần liên kết với user bị mất. Lúc gọi fn(), cú pháp gọi không còn object đứng trước dấu chấm, nên rơi về Default Binding.
Tình huống này thường gặp khi truyền method làm callback:
setTimeout(user.greet, 1000); // this không còn trỏ tới user
element.addEventListener("click", user.greet); // this trỏ tới element, không phải user
Trong addEventListener, this bên trong hàm callback trỏ tới phần tử DOM đang lắng nghe sự kiện, vì trình duyệt gọi hàm theo kiểu element.handler(), tương ứng với Implicit Binding trên element, không phải undefined như hai ví dụ trên.
Cách khắc phục: dùng bind để ép cứng this, hoặc dùng arrow function để bọc lại lệnh gọi.
Explicit Binding
call, apply, bind cho phép chủ động chỉ định this, thay vì phụ thuộc cách gọi:
function greet() {
console.log(this.name);
}
const user = { name: "An" };
greet.call(user); // gọi ngay, tham số truyền rời: fn.call(thisArg, arg1, arg2)
greet.apply(user); // gọi ngay, tham số truyền dạng mảng: fn.apply(thisArg, [arg1, arg2])
const boundGreet = greet.bind(user); // không gọi ngay, trả về function mới đã gán cứng this
boundGreet();
Khác biệt giữa call/apply và bind: call và apply gọi function ngay lập tức, chỉ khác cách truyền tham số. bind không gọi ngay, mà trả về một function mới với this đã được gán cố định, dùng để truyền làm callback mà không lo mất binding như cạm bẫy ở mục trước:
setTimeout(user.greet.bind(user), 1000); // this luôn trỏ tới user
new Binding
Khi một function được gọi với từ khóa new, JavaScript tạo ra một object mới, và this bên trong function trỏ tới object đó:
function User(name) {
this.name = name;
}
const u = new User("An");
console.log(u.name); // "An"
new thực hiện bốn bước: tạo object mới, gán prototype cho nó, gọi function với this trỏ tới object mới đó, rồi trả về object (trừ khi function tự return một object khác).
Arrow Function không có this riêng
Arrow function không có this của riêng nó. Nó lấy this theo lexical scope tại nơi được khai báo, giống cách closure lấy biến từ outer scope, không phụ thuộc cách gọi như 4 rule ở trên.
const user = {
name: "An",
greet() {
setTimeout(() => {
console.log(this.name);
}, 1000);
},
};
user.greet(); // "An"
Arrow function bên trong greet không tạo this riêng, nên nó lấy this từ greet, lúc đó đang trỏ tới user nhờ Implicit Binding. Đây là cách phổ biến nhất để khắc phục cạm bẫy mất this khi truyền callback, thay vì phải gọi bind thủ công.
Vì không có this riêng, call, apply, bind không có tác dụng thay đổi this của arrow function.
Thứ tự ưu tiên khi nhiều rule cùng áp dụng
Khi nhiều rule cùng có thể áp dụng cho một lệnh gọi, thứ tự ưu tiên từ cao xuống thấp là:
new Binding > Explicit Binding > Implicit Binding > Default Binding
Explicit Binding thắng Implicit Binding:
const user = {
name: "An",
greet() {
console.log(this.name);
},
};
const admin = { name: "Root" };
user.greet.call(admin); // "Root", không phải "An"
user.greet() gọi qua object nên đáng lẽ áp dụng Implicit Binding, trỏ tới user. Nhưng call(admin) ép this một cách tường minh, nên this trỏ tới admin thay vì user.
new Binding thắng Explicit Binding:
function User(name) {
this.name = name;
}
const BoundUser = User.bind({ name: "Bị ghi đè" });
const u = new BoundUser("An");
console.log(u.name); // "An", không phải "Bị ghi đè"
BoundUser đã được bind cứng this vào một object khác. Nhưng khi BoundUser được gọi bằng new, JavaScript vẫn tạo object mới và ép this trỏ tới object đó, bỏ qua hoàn toàn this đã gán bằng bind trước đó, vì new Binding có độ ưu tiên cao nhất.
Tổng kết
thisđược xác định bởi cách function được gọi, không phải nơi nó được khai báo.- Default Binding: gọi độc lập,
thislàundefined(strict mode) hoặc global object. - Implicit Binding: gọi qua object (
obj.method()),thistrỏ tới object đó, nhưng dễ mất khi method được gán ra biến riêng hoặc truyền làm callback. - Explicit Binding:
call/applygọi ngay vớithischỉ định,bindtrả về function mới đã gán cứngthis. newBinding:thistrỏ tới object mới đượcnewtạo ra.- Arrow function không có
thisriêng, lấythistheo lexical scope, không theo 4 rule trên, thường dùng để tránh mất binding trong callback. - Thứ tự ưu tiên:
new> Explicit > Implicit > Default.