Function binding trong JavaScript

Cập nhật ngày 02/01/2022

Khi sử dụng phương thức của object làm hàm callback, ví dụ sử dụng với hàm setTimeout, thường xảy ra vấn đề là: "mất giá trị this".

Sau đây, mình sẽ cùng tìm hiểu cách sử dụng function binding để khắc phục tình trạng này.

Vấn đề mất this

Ví dụ sử dụng hàm setTimeout như sau:

let user = {
  firstName: "Alex",
  sayHi() {
    console.log(`Hello, ${this.firstName}!`);
  },
};

setTimeout(user.sayHi, 1000); // Hello, undefined! (*)

Kết quả hiển thị sau 1000ms là Hello, undefined!.

Đó là bởi vì hàm setTimeout sử dụng hàm sayHi một cách độc lập với đối tượng user. Dòng (*) có thể viết lại tương đương là:

let func = user.sayHi;
setTimeout(func, 1000); // mất context của `this`

Đối với trình duyệt, hàm setTimeout gán giá trị this=window khi gọi hàm callback. Vì vậy, this.firstName trở thành window.firstName. Mà đối tượng window không chứa thuộc tính firstName, dẫn đến kết quả là undefined.

Vậy câu hỏi đặt ra là: làm sao truyền phương thức của object vào phương thức khác mà vẫn giữ nguyên context this?

Giải pháp sử dụng hàm wrapper

Cách đơn giản nhất để tránh mất this là sử dụng hàm wrapper:

let user = {
  firstName: "Alex",
  sayHi() {
    console.log(`Hello, ${this.firstName}!`);
  },
};

setTimeout(function () {
  user.sayHi(); // Hello, Alex!
}, 1000);

Kết quả hiển thị đã chính xác như mong muốn. Bởi vì khi setTimeout gọi hàm callback, giá trị của user được lấy từ phạm vi bên ngoài, rồi gọi phương thức sayHi của user như bình thường.

Ngoài ra, bạn có thể sử dụng arrow function cho ngắn gọn:

setTimeout(() => user.sayHi(), 1000); // Hello, Alex!

Nhìn chung, cách sử dụng hàm wrapper này khá đơn giản và dễ hiểu. Tuy nhiên, cách này lại tiểm ẩn khả năng xảy ra lỗi.

Giả sử, trước khi hàm setTimeout gọi hàm callback mà giá trị của user lại bị thay đổi thì kết quả hiển thị sẽ không đúng như mong đợi:

let user = {
  firstName: "Alex",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  },
};

// delay 1 giây
setTimeout(() => user.sayHi(), 1000);

// trong lúc đó, giá trị của `user` thay đổi
user = {
  sayHi() {
    console.log("User khác được dùng trong setTimeout!");
  },
};

// User khác được dùng trong setTimeout!

Cách thứ hai sau đây sẽ đảm bảo tình trạng này không xảy ra.

Sử dụng function binding trong JavaScript

Function trong JavaScript có sẵn một phương thức là bind cho phép không làm thay đổi giá trị của this, với cú pháp cơ bản như sau:

let boundFunc = func.bind(context);

Kết quả của func.bind(context) là một đối tượng đặc biệt. Đối tượng này có thể được gọi như hàm và luôn cố định giá trị this=context, ví dụ:

let user = {
  firstName: "Alex",
};

function func() {
  console.log(this.firstName);
}

let funcUser = func.bind(user);
funcUser(); // Alex

Trong ví dụ trên, func.bind(user) cố định giá trị của this=user.

Ngoài ra, func.bind có thể sử dụng với hàm có tham số như sau:

let user = {
  firstName: "Alex",
};

function func(message) {
  console.log(message + ", " + this.firstName);
}

// bind `this` với user
let funcUser = func.bind(user);

funcUser("Hello"); // Hello, Alex (message="Hello" và this=user)

Bây giờ, hãy thử binding function với phương thức của object:

let user = {
  firstName: "Alex",
  sayHi() {
    console.log(`Hello, ${this.firstName}!`);
  },
};

let sayHi = user.sayHi.bind(user); // (*)
// có thể gọi hàm mà không cần object
sayHi(); // Hello, Alex!

setTimeout(sayHi, 1000); // Hello, Alex!

// thậm chí là khi giá trị của user thay đổi
// sayHi vẫn tham chiếu tới giá trị user lúc được bind
user = {
  sayHi() {
    console.log("User đã thay đổi!");
  },
};

Tại (*), phương thức user.sayHi đã bind giá trị this=user. Kết quả trả về là hàm sayHi - có thể gọi độc lập và giá trị this luôn bằng user tại thời điểm bind.

Partial function

Mình mới đề cập tới vấn đề binding function với this. Thực tế, bạn có thể thực hiện binding cả tham số. Cú pháp đầy đủ của binding function là:

let bound = func.bind(context, [arg1], [arg2],...);

Phương thức bind cho phép bind giá trị this=context và sau đó là các tham số của hàm.

Ví dụ mình có hàm tính tích hai số như sau:

function mul(a, b) {
  return a * b;
}

Bây giờ, mình có thể sử dụng bind để tạo ra hàm nhân đôi:

function mul(a, b) {
  return a * b;
}

let double = mul.bind(null, 2);
console.log(double(3)); // = mul(2, 3) = 6
console.log(double(4)); // = mul(2, 4) = 8
console.log(double(5)); // = mul(2, 5) = 10

Trong ví dụ trên mul.bind(null, 2) tạo ra hàm mới double với giá trị this=nulla=2. Khi gọi hàm với double, bạn chỉ cần truyền vào giá trị của tham số b.

Cách này gọi partial function.

Chú ý: hàm double không sử dụng this. Tuy nhiên, hàm bind lại yêu cầu this. Vì vậy, tham số đầu tiên mình để null.

Tương tự, mình có thể tạo ra hàm nhân ba triple như sau:

function mul(a, b) {
  return a * b;
}

let triple = mul.bind(null, 3);
console.log(triple(3)); // = mul(3, 3) = 9
console.log(triple(4)); // = mul(3, 4) = 12
console.log(triple(5)); // = mul(3, 5) = 15

Tại sao lại sử dụng partial function?

Lợi ích của việc sử dụng partial function là mình có các hàm với tên dễ hiểu double, triple.

Đồng thời, mình có thể gọi các hàm này một cách độc lập và không cần phải gọi hàm mul với giá trị tham số đầu tiên luôn luôn cố định bằng 2 với double hoặc bằng 3 với triple.

Partial function không cố định context

Giả sử mình cần cố định một vài tham số của hàm, ngoại trừ context, ví dụ khi hàm là phương thức của object.

Phương thức func.bind không cho phép bạn làm điều đó. Vì bạn không thể bỏ qua context rồi nhảy ngay đến các tham số. Bởi lẽ, func.bind luôn hiểu tham số đầu tiên là context cho this.

Để giải quyết vấn đề này bạn có thể triển khai hàm partial như sau:

function partial(func, ...argsBound) {  return function (...args) {    return func.call(this, ...argsBound, ...args);  };}
// Sử dụng:
let user = {
  firstName: "Alex",
  say(time, message) {
    console.log(`[${time}] ${this.firstName}: ${message}!`);
  },
};

// tạo phương thức partial với giá trị của `time` cố định
user.sayNow = partial(
  user.say,
  new Date().getHours() + ":" + new Date().getMinutes()
);

user.sayNow("Hello");
// Kết quả có dạng như sau: [10:00] Alex: Hello!

Trong ví dụ trên, kết quả của việc gọi hàm partial là một hàm wrapper với:

  • Giá trị của this được lấy lúc gọi hàm. Ví dụ khi gọi user.sayNow thì this=user.
  • ...argsBound là tham số từ hàm partial, giả sử là "10:00".
  • ...args là tham số của hàm wrapper, ví dụ là "Hello".

Tổng kết

Phương thức func.bind(context, ...args) trả về một hàm đặc biệt với việc cố định giá trị this=context và các tham số đầu tiên (nếu có).

Khi một vài tham số đầu tiên được cố định, thì kết quả trả về được gọi là partial function.

Partial functions hữu ích khi bạn không muốn lặp lại việc truyền các tham số giống nhau ở các lần gọi hàm.

Thực hành

Bài 1

Cho đoạn code sau, hỏi kết quả trả về là gì?

"use strict";

function f() {
  console.log(this); // ?
}

let user = {
  g: f.bind(null),
};

user.g();

Kết quả là: null.

Phương thức user.g được gán bằng f.bind(null). Nghĩa là giá trị của this trong hàm f luôn là null.

Bài 2

Khi gọi bind nhiều lần, giá trị của this có thay đổi không?

function f() {
  console.log(this.name);
}

f = f.bind({ name: "Alex" }).bind({ name: "Anna" });

f(); // (*)

Kết quả (*) là gì?

Kết quả (*)Alex.

Khi gọi f.bind(context), kết quả trả về là một đối tượng đặc biệt. Đối tượng này ghi nhớ giá trị của this=context tại thời điểm bind và không thể bị thay đổi.

Do đó, lần gọi bind thứ hai là không có tác dụng.

Bài 3

Giả sử hàm có chứa một thuộc tính. Hỏi sau khi bind, giá trị của thuộc tính như thế nào?

function sayHi() {
  console.log(this.name);
}

// thuộc tính của hàm
sayHi.test = 5;

// gọi bind
let bound = sayHi.bind({
  name: "Alex",
});

console.log(bound.test); // (*)

Kết quả (*) là gì?

Kết quả (*)undefined.

Bởi vì kết quả của bind là một đối tượng khác. Và đối tượng này không chứa thuộc tính test.

Tham khảo:

★ Nếu bạn thấy bài viết này hay thì hãy theo dõi mình trên Facebook để nhận được thông báo khi có bài viết mới nhất nhé:

Kỹ thuật decorator, forwarding của hàm trong JavaScript
Thuộc tính writable, enumerable và configurable của object trong Javascript
Chia sẻ:

Bình luận