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.

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 – thisbị 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ì setTimeoutcó 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 setTimeouttrong trình duyệt hơi đặc biệt: nó đặt this=windowcho lệnh gọi hàm (đối với Node.js, thistrở thành đối tượng hẹn giờ, nhưng không thực sự quan trọng ở đây). Vì vậy, cho this.firstNamenó 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 thischỉ 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 usertừ 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 setTimeoutkích hoạt (có độ trễ một giây!) userThay đổ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 funccài đặt this=context.

Nói cách khác, gọi boundFunclà giống như funcvới cố định this.

Chẳng hạn, ở đây funcUserchuyển một cuộc gọi đến funcvớ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.

funcVí 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.sayHivà liên kết nó với user. Đây sayHilà 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ư thisvà 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 doubletrê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 doublechuyển các cuộc gọi đến mul, sửa chữa nulllàm bối cảnh và 2là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 tripletrong 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 bindkhô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 funcvới:

  • Tương tự thisnhư nó được gọi (để user.sayNowgọi nó từuser)
  • Sau đó cung cấp cho nó ...argsBound– đối số từ partialcuộ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 funcsửa lỗi bối cảnh thisvà 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 thischo 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à fromphả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!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!