Khi truyền các phương thức đối tượng dưới dạng callbacks tới setTimeout, có một vấn đề mà có thể bạn đã biết: this trả về không đúng (undefined, null, …).
Bài viết hôm nay sẽ hướng dẫn bạn khắc phục điều này.
Chúng ta đã thấy những ví dụ về việc mất “this”. Sau khi một phương thức được chuyển đến một nơi nào đó tách biệt với đối tượng – this
sẽ bị mất.
Ví dụ:
let user = { firstName: "Thái", demo() { alert(`Hello, ${this.firstName}!`); } }; setTimeout(user.demo, 1000); // Trả về Hello, undefined!
Như đã thấy, kết quả trả về ở đây không phải là – Hello, Thái! mà là Hello, undefined!. Nguyễn nhân là do setTimeout tách function user.demo ra khỏi đối tượng user. Vậy nên đoạn code cuối cũng có thể được hiểu như sau:
let demoNext = user.demo; setTimeout(demoNext , 1000); // Trả về lost user context
Phương thức setTimeout trong trình duyệt hơi đặc biệt: nó đặt this=window cho lệnh gọi hàm. Vì vậy, this.firstName nó cố gắng để trả về window.firstName, mà không tồn tại. Trong các trường hợp tương tự khác, thường this chỉ trở thành undefined.
Cách để giải quyết vấn đề này như sau:
Cách đơn giản nhất là gọi lại thông qua 1 function khác:
Ví dụ:
let user = { firstName: "Thái", demo() { alert(`Hello, ${this.firstName}!`); } }; setTimeout(function() { user.demo(); // Hello, Thái! }, 1000);
Bây giờ nó hoạt động, bởi vì nó nhận user
từ môi trường bên ngoài, và sau đó gọi phương thức một cách bình thường.
Hoặc bạn có thể dùng arrow function như sau:
setTimeout(() => user.demo(), 1000); // Hello, Thái!
Mặc dù làm như này thì code vẫn chạy được. Nhưng có 1 vấn đề như sau:
Điều gì sẽ xảy ra nếu trước khi setTimeout
kích hoạt (sau 1 giây) user
Thay đổi giá trị? Sau đó, nó sẽ gọi nhầm đối tượng!
let user = { firstName: "Thái", demo() { alert(`Hello, ${this.firstName}!`); } }; setTimeout(() => user.demo(), 1000); // Giá trị của user sẽ bị thay đổi sau 1 giây user = { demo() { alert("Thay đổi giá trị!"); } }; // Thay đổi giá trị!
Giải pháp thứ 2 sẽ giải quyết vấn đề này.
Cú pháp cơ bản là:
let boundFunc = func.bind(context);
Kết quả của func.bind(context)
là một “đối tượng kỳ lạ” giống hàm đặc biệt, có thể gọi là hàm và chuyển lệnh gọi đến func
cài đặt một cách minh bạch this=context
.
Nói cách khác, gọi boundFunc
giống như func
với cố định this
.
Ví dụ: ở đây funcUser
chuyển một cuộc gọi tới func
với this=user
:
let user = { firstName: "Thái" }; function func() { alert(this.firstName); } let funcUser = func.bind(user); funcUser(); // Thái
Đây func.bind(user)
là một “biến thể ràng buộc” của func
, với cố định this=user
.
Tất cả các đối số được chuyển đến “nguyên trạng func
” ban đầu, ví dụ:
let user = { firstName: "Thái" }; function func(phrase) { alert(phrase + ', ' + this.firstName); } // bind this thành user let funcUser = func.bind(user); funcUser("Hello"); // Hello, Thái (tham số 'Hello' đã được truyền như bình thường và this=user)
Bây giờ hãy thử với một object method:
let user = { firstName: "Thái", demo() { alert(`Hello, ${this.firstName}!`); } }; let demo = user.demo.bind(user); // (*) // có thể chạy nó mà không có object demo(); // Hello, Thái! setTimeout(demo, 1000); // Hello, Thái! // kể cả giá trị có thay đổi sau 1 giây // demo sử dụng giá trị ràng buộc trước tham chiếu đến object user cũ user = { demo() { alert("Thay đổi giá trị!"); } };
Trong dòng, (*)
chúng tôi lấy phương thức user.demo
và bind nó với user
. Đây demo
là một hàm “bound”, có thể được gọi một mình hoặc được chuyển đến setTimeout
– không quan trọng, context sẽ phù hợp.
Ở đây chúng ta có thể thấy rằng các đối số được truyền “nguyên trạng”, chỉ this
được sửa bởi bind
:
let user = { firstName: "Thái", say(phrase) { alert(`${phrase}, ${this.firstName}!`); } }; let say = user.say.bind(user); say("Hello"); // Hello, Thái! ("Hello" đối số được thông qua say) say("Bye"); // Bye, Thái! ("Bye" được chuyển cho say)
Và có 1 lưu ý là:
bind
không làm thay đổi được this
của 1 arrow function. Nên các bạn nãy lưu ý điều này.
Bình luận: