Khi truyền các phương thức đối tượng dưới dạng gọi lại, chẳng hạn setTimeout
, có một vấn đề đã biết: “mất this
“.
Trong chương này chúng ta sẽ xem các cách khắc phục.
Nội dung chính
1. Mất đi “this”
Chúng ta đã thấy những ví dụ về sự mất this
. Khi một phương thức được truyền đi đâu đó tách biệt khỏi đối tượng – this
bị mất.
Đây là cách nó có thể xảy ra với setTimeout
:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
Như chúng ta có thể thấy, kết quả đầu ra không phải là John, this.firstName
, nhưng undefined
!
Đó là bởi vì setTimeout
có hàm user.sayHi
, tách biệt với đối tượng. Dòng cuối cùng có thể được viết lại thành:
let f = user.sayHi;
setTimeout(f, 1000); // 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 (đối với Node.js, this
trở thành đối tượng hẹn giờ, nhưng không thực sự quan trọng ở đây). Vì vậy, cho this.firstName
nó cố gắng để có được 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
.
Nhiệm vụ này khá điển hình – chúng ta muốn truyền một phương thức đối tượng ở một nơi khác (ở đây – cho bộ lập lịch) nơi nó sẽ được gọi. Làm thế nào để đảm bảo rằng nó sẽ được gọi trong đúng ngữ cảnh?
2. Giải pháp 1: một trình bao bọc
Giải pháp đơn giản nhất là sử dụng hàm gói:
et user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Hello, John!
}, 1000);
Bây giờ nó hoạt động, bởi vì nó nhận được user
từ môi trường từ vựng bên ngoài, và sau đó gọi phương thức bình thường.
Giống nhau, nhưng ngắn hơn:
setTimeout(() => user.sayHi(), 1000); // Hello, John!
Có vẻ tốt, nhưng một lỗ hổng nhỏ xuất hiện trong cấu trúc mã của chúng ta.
Điều gì xảy ra nếu trước khi setTimeout
kích hoạt (có độ trễ một giây!) user
Thay đổi giá trị? Sau đó, đột nhiên, nó sẽ gọi sai đối tượng!
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// ...the value of user changes within 1 second
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
// Another user in setTimeout!
Giải pháp tiếp theo đảm bảo rằng điều đó sẽ không xảy ra.
3. Giải pháp 2: liên kết
Các hàm cung cấp một liên kết để cho phép sửa chữa this
.
Cú pháp cơ bản là:
// more complex syntax will come a little later
let boundFunc = func.bind(context);
Kết quả func.bind(context)
là một đối tượng kỳ lạ giống như Chức năng đặc biệt, có thể gọi là hàm và chuyển trong suốt cuộc gọi đến func
cài đặt this=context
.
Nói cách khác, gọi boundFunc
là giống như func
với cố định this
.
Chẳng hạn, ở đây funcUser
chuyển một cuộc gọi đến func
với this=user
:
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
Đây func.bind(user)
là một biến thể ràng buộc, func
, với sự cố định this=user
.
func
Ví dụ, tất cả các đối số được truyền cho bản gốc , ví dụ như:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
// bind this to user
let funcUser = func.bind(user);
funcUser("Hello"); // Hello, John (argument "Hello" is passed, and this=user)
Bây giờ hãy thử với một phương thức đối tượng:
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
// can run it without an object
sayHi(); // Hello, John!
setTimeout(sayHi, 1000); // Hello, John!
// even if the value of user changes within 1 second
// sayHi uses the pre-bound value
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
Trong dòng (*)
chúng ta lấy phương thức user.sayHi
và liên kết nó với user
. Đây sayHi
là một chức năng ràng buộc, có thể được gọi một mình hoặc được chuyển đến setTimeout
– không thành vấn đề, bối cảnh sẽ đúng.
Ở đây chúng ta có thể thấy rằng các đối số được truyền qua như là một, chỉ this
được sửa bởi bind
:
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Hello"); // Hello, John ("Hello" argument is passed to say)
say("Bye"); // Bye, John ("Bye" is passed to say)
Phương pháp thuận tiện: bindAll
Nếu một đối tượng có nhiều phương thức và chúng tôi dự định chủ động vượt qua nó, thì chúng tôi có thể liên kết tất cả chúng trong một vòng lặp:
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
Các thư viện JavaScript cũng cung cấp các hàm để liên kết hàng loạt thuận tiện, ví dụ: _.bindAll(obj) trong lodash.
4. Hàm từng phần
Cho đến bây giờ chúng ta chỉ nói về ràng buộc this
. Hãy tiến thêm một bước nữa.
Chúng ta có thể ràng buộc không chỉ this
, mà còn đối số. Điều đó hiếm khi được thực hiện, nhưng đôi khi có thể có ích.
Cú pháp đầy đủ của bind
:
let bound = func.bind(context, [arg1], [arg2], ...);
Nó cho phép liên kết bối cảnh như this
và bắt đầu các đối số của hàm.
Chẳng hạn, chúng ta có hàm nhân mul(a, b)
:
function mul(a, b) {
return a * b;
}
Chúng ta hãy sử dụng bind
để tạo một hàm double
trên cơ sở của nó:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
function mul(a, b) {
return a * b;
}
let double = mul.bind(null, 2);
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10
Cuộc gọi để mul.bind(null, 2)
tạo một hàm mới double
chuyển các cuộc gọi đến mul
, sửa chữa null
làm bối cảnh và 2
làm đối số đầu tiên. Đối số tiếp theo được thông qua là như thế.
Đó gọi là ứng dụng hàm một phần – chúng ta tạo một hàm mới bằng cách sửa một số tham số của hàm hiện có.
Xin lưu ý rằng ở đây chúng ta thực sự không sử dụng this
ở đây. Nhưng bind
đòi hỏi nó, vì vậy chúng ta phải đưa vào một cái gì đó như null
.
Hàm triple
trong mã dưới đây nhân ba giá trị:
function mul(a, b) {
return a * b;
}
let triple = mul.bind(null, 3);
alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15
Tại sao chúng ta thường làm một phần hàm?
Lợi ích là chúng ta có thể tạo một hàm độc lập với tên dễ đọc ( double
, triple
). Chúng ta có thể sử dụng nó và không cung cấp đối số đầu tiên mỗi khi nó cố định bind
.
Trong các trường hợp khác, ứng dụng một phần rất hữu ích khi chúng ta có một hàm rất chung chung và muốn một biến thể ít phổ biến hơn của nó để thuận tiện.
Ví dụ, chúng ta có một hàm send(from, to, text)
. Sau đó, bên trong một user
đối tượng chúng ta có thể muốn sử dụng một biến thể một phần của nó: sendTo(to, text)
gửi từ người dùng hiện tại.
5. Từng phần mà không có context
Điều gì xảy ra nếu chúng ta muốn sửa một số đối số, nhưng không phải bối cảnh this
? Ví dụ, đối với một phương thức đối tượng.
Người bản địa bind
không cho phép điều đó. Chúng ta không thể bỏ qua bối cảnh(context) và chuyển sang đối số.
May mắn thay, một hàm partial
để ràng buộc chỉ các đối số có thể được thực hiện dễ dàng.
Như thế này:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
// Usage:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// add a partial method with fixed time
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// Something like:
// [10:00] John: Hello!
Kết quả của cuộc gọi partial(func[, arg1, arg2...])
là một trình bao bọc (*)
gọi func
với:
- Tương tự
this
như nó được gọi (đểuser.sayNow
gọi nó từuser
) - Sau đó cung cấp cho nó
...argsBound
– đối số từpartial
cuộc gọi ("10:00"
) - Sau đó cung cấp cho nó
...args
– các đối số được cung cấp cho trình bao bọc ("Hello"
)
Thật dễ dàng để làm điều đó với cú pháp lây lan, phải không?
Ngoài ra, có một triển khai _.partial sẵn sàng từ thư viện lodash.
6. Tóm lược
Phương thức func.bind(context, ...args)
trả về một biến thể ràng buộc khác của hàm có chức năng func
sửa lỗi bối cảnh this
và các đối số đầu tiên nếu được đưa ra.
Thông thường chúng ta áp dụng bind
để sửa chữa this
cho một phương thức đối tượng, để chúng ta có thể vượt qua nó ở đâu đó. Ví dụ, để setTimeout
.
Khi chúng tôi sửa một số đối số của hàm hiện có, hàm kết quả (ít phổ biến hơn) được gọi là một phần .
Các thuận tiện khi chúng ta không muốn lặp đi lặp lại cùng một lập luận. Giống như nếu chúng ta có một hàm send(from, to)
, và from
phải luôn giống nhau cho nhiệm vụ của chúng ta, chúng ta có thể có được một phần và tiếp tục với nó.
Full series tự học Javascript từ cơ bản tới nâng cao tại đây nha.
Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:
Chào thân ái và quyết thắng!