Câu 1Sau const calc = cart.total;, khi gọi calc() thì this bên trong total là gì?
this binding — this thay đổi theo cách gọi và cách cố định nó
this thay đổi khi bạn tách phương thức ra biến hay truyền nó làm callback. Kèm TypeError, bind, arrow function field và setTimeout.
Đôi khi bạn truyền phương thức tính tổng tiền giỏ hàng cho một đoạn code khác để gọi sau. Một phương thức chạy tốt khi gọi cart.total() có thể báo lỗi khi bạn gán nó vào biến hoặc truyền cho setTimeout, vì this đã thay đổi.
Bài này nói về this binding — giá trị của this phụ thuộc vào cách gọi hàm — và cách cố định this bằng bind hoặc arrow function.
Tách phương thức ra thì hỏng — this trong phương thức
Giả sử bạn muốn dùng lại cart.total() cho ô tổng tiền trên trang, nên gán nó vào biến bằng const calcTotal = cart.total;. Nhưng khi gọi calcTotal(), code báo lỗi trước cả khi kịp cộng gì, dù giỏ hàng vẫn như cũ.
Giá trị của this bên trong phương thức do dòng gọi quyết định, chứ không phụ thuộc vào nơi phương thức được viết. Điều này gọi là this binding (this được gắn với một giá trị). Nếu bạn đọc cart.total mà không có (), bạn chỉ nhận được hàm; cart không đi kèm theo.
Khi gọi calcTotal() mà không có gì trước dấu chấm, this bên trong class là undefined.
class Cart {
constructor(items) {
this.items = items; // Tạm tính của từng sản phẩm
}
total() {
return this.items.reduce((sum, price) => sum + price, 0);
}
}
const cart = new Cart([1980, 3000]);
// Gọi với cart đứng trước dấu chấm
console.log(cart.total()); // 4980
// Gán vào biến / tách ra bằng destructuring
const calcTotal = cart.total;
const { total } = cart;
console.log(typeof calcTotal); // function
// calcTotal(); // TypeError: Cannot read properties of undefined (reading 'items')
// total(); // TypeError (cùng lý do)
Cột giữa và cột phải đều báo lỗi TypeError: Cannot read properties of undefined (reading 'items'). Destructuring bằng const { total } = cart; cũng chỉ sao chép hàm vào một biến, nên total() lỗi vì cùng lý do với calcTotal().
Giữ this khi gán vào biến — phương thức bind
Giả sử bạn muốn truyền phép tính tổng tiền dưới dạng một hàm cho đoạn code hiển thị tổng. Như phần trước đã cho thấy, gán cart.total vào biến thì cart bị bỏ lại, nên khi đoạn code nhận hàm gọi nó, bạn sẽ gặp TypeError.
Khi gọi bind (phương thức mà mọi hàm đều có, trả về một hàm mới với this được cố định vào giá trị bạn truyền vào) dưới dạng cart.total.bind(cart), bạn nhận được một hàm luôn giữ this là cart, kể cả khi gọi mà không có gì trước dấu chấm.
Chỉ hàm được trả về mới được cố định, nên khi gọi hoặc truyền đi, hãy dùng chính hàm đó.
class Cart {
constructor(items) { this.items = items; } // Tạm tính của từng sản phẩm
total() { return this.items.reduce((sum, price) => sum + price, 0); }
}
const cart = new Cart([1980, 3000]);
// Hàm vừa tách ra sẽ có this là undefined khi được gọi
const unbound = cart.total;
// unbound(); // TypeError
// bind trả về một hàm mới có this là cart
const boundTotal = cart.total.bind(cart);
console.log(boundTotal()); // 4980
console.log(boundTotal === cart.total); // false
// bind cố định chính object cart, nên hàm đọc được cả khoản thêm vào sau
cart.items.push(500);
console.log(boundTotal()); // 5480
total không nhận đối số, nhưng khi bạn cố định một phương thức có nhận đối số, mọi đối số bạn truyền cho hàm do bind trả về sẽ được chuyển nguyên tới phương thức gốc. Chỉ this bị cố định, nên mỗi lần gọi bạn có thể truyền đối số khác nhau.
Viết phương thức vẫn chạy khi bị tách ra — arrow function
Giả sử bạn gắn cùng một phương thức làm hàm xử lý click cho nhiều nút trên trang, chẳng hạn các nút thích dưới mỗi bài viết. Nếu lần nào cũng phải thêm bind, chỉ cần quên một lần là nút đó sẽ báo lỗi TypeError.
Arrow function field (class field có giá trị khởi tạo là một arrow function) được tạo riêng cho từng instance mỗi lần bạn gọi new. Trong biểu thức khởi tạo field, this là instance mới, giống như trong constructor, và arrow function vốn không có this riêng nên sẽ dùng chính this đó.
Vì vậy bạn có thể tách nó ra và gọi mà không cần bind.
class LikeButton {
likes = 0;
label = `Lượt thích: ${this.likes}`; // this trong biểu thức khởi tạo là button đang được tạo
// Phương thức thường: this do dòng gọi quyết định
addLikeMethod() { this.likes += 1; return this.likes; }
// Arrow function field: dùng cùng this với biểu thức khởi tạo
addLike = () => { this.likes += 1; return this.likes; };
}
const button = new LikeButton();
console.log(button.label); // Lượt thích: 0
// Tách ra và gọi mà không cần bind
const onPress = button.addLike;
console.log(onPress()); // 1
console.log(onPress()); // 2
console.log(Object.keys(button).join(", ")); // likes, label, addLike
// Tách phương thức thường ra thì hỏng
const onPressMethod = button.addLikeMethod;
// onPressMethod(); // TypeError
likes = 0— nằm trênbuttonthistrong biểu thức khởi tạolabel— làbuttonđang được tạo (giống trong constructor)
- Không có
thisriêng this.likes— đọc giá trị trênbuttontừ scope bao ngoài- Kể cả khi gọi
onPress(),thisvẫn làbutton
this— object đứng trước dấu chấm ở dòng gọi- Là
buttonkhi gọibutton.addLikeMethod() - Là
undefinednếu tách ra rồi gọi
addLike xuất hiện trong Object.keys(button) vì arrow function field nằm ngay trên instance, giống như likes; còn addLikeMethod thì không. Điều đó cũng có nghĩa là tạo 100 instance thì sẽ có 100 hàm addLike được tạo ra.
Arrow function trong object literal không nhận object đó làm this
Nếu bạn viết const counter = { add: () => { this.likes += 1 } };, thì this bên trong counter.add() không phải là counter. Object literal không tạo ra this riêng, nên arrow function dùng this ở bên ngoài. Hãy viết thành add() { ... }.
Truyền phương thức cho code sẽ gọi nó sau — setTimeout
Giả sử bạn muốn hiển thị lời cảm ơn kèm tên khách hàng 100ms sau khi đơn hàng được đặt. Nếu viết setTimeout(this.showMessage, 100) bên trong một phương thức, 100ms sau lời nhắn vẫn hiện ra, nhưng tên khách hàng lại là undefined.
setTimeout tự gọi hàm sau 100ms, và trong trình duyệt, nó đặt this của lần gọi đó là window (window là object toàn cục mà trình duyệt tạo sẵn cho mỗi trang). Nếu bạn bọc nó trong một arrow function, this trong () => this.showMessage() chính là this của phương thức bao ngoài, nên phương thức được gọi trên instance.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
class OrderNotice {
constructor(customerName) { this.customerName = customerName; }
showMessage() { console.log(`Cảm ơn ${this.customerName} đã đặt hàng`); }
// Truyền nguyên phương thức. setTimeout sẽ gọi nó sau 100ms
notifyDirect() { setTimeout(this.showMessage, 100); }
// Bọc trong arrow function / cố định bằng bind rồi mới truyền
notifyWithArrow() { setTimeout(() => this.showMessage(), 100); }
notifyWithBind() { setTimeout(this.showMessage.bind(this), 100); }
}
const notice = new OrderNotice("Minh");
notice.notifyDirect(); // Cảm ơn undefined đã đặt hàng
notice.notifyWithArrow(); // Cảm ơn Minh đã đặt hàng
notice.notifyWithBind(); // Cảm ơn Minh đã đặt hàng
await delay(300); // Trên trang này, đợi đến khi mọi output đã hiện ra
Hàng trên không báo lỗi TypeError: khác với calcTotal(), this ở đây không phải undefined mà là window. Vì window không có customerName nên undefined được hiển thị. Bảng dưới đây liệt kê giá trị của this với từng cách gọi đã gặp trong bài.
| Cách gọi | Giá trị của this | Kết quả ở đây |
|---|---|---|
| Gọi cart.total() | cart (trước dấu chấm) | Trả về 4980 |
| Tách ra rồi gọi calcTotal() | undefined | Báo lỗi TypeError |
| Hàm do cart.total.bind(cart) trả về | cart (truyền cho bind) | Trả về 4980 |
| Tách arrow field, gọi onPress() | button (chứa field) | Lượt thích tăng lên |
| setTimeout(this.showMessage, 100) | window (trên trình duyệt) | Cảm ơn undefined đã đặt hàng |
| setTimeout(() => this.showMessage(), 100) | this của phương thức ngoài | Cảm ơn Minh đã đặt hàng |
Bọc trong function không giữ được this
Nếu bạn bọc thành setTimeout(function () { this.showMessage(); }, 100), this bên trong là window, nên code báo lỗi TypeError. Console trên trang này hoàn toàn không hiển thị lỗi đó; bạn chỉ thấy Uncaught TypeError trong developer tools của trình duyệt. Hãy bọc bằng arrow function.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Sau khi chỉ viết cart.total.bind(cart);, điều gì xảy ra nếu bạn gán cart.total vào biến rồi gọi?
Câu 3Khi truyền một phương thức cho setTimeout từ bên trong một phương thức khác, cách nào KHÔNG giữ this là instance?