Nếu đã từng làm việc với JavaScript, chắc hẳn bạn từng nghe qua ba phương thức bind, apply, call. Chúng đều liên quan đến việc kiểm soát giá trị của this bên trong hàm - một trong những khái niệm gây bối rối nhất với người mới học JS. Bài viết này sẽ giải thích bản chất của this, sau đó đi sâu vào cách dùng của từng phương thức kèm ví dụ cụ thể.

1. Vì sao cần bind, apply, call?

Trong JavaScript, giá trị của this bên trong một hàm không cố định - nó phụ thuộc vào cách hàm đó được gọi, chứ không phải nơi nó được định nghĩa. Đây là nguồn gốc của rất nhiều lỗi khó hiểu.

const person = {
  name: "Nguyen",
  greet() {
    console.log(`Xin chào, tôi là ${this.name}`);
  }
};

person.greet(); // "Xin chào, tôi là Nguyen" - this = person

const greetFn = person.greet;
greetFn(); // "Xin chào, tôi là undefined" - this không còn trỏ tới person nữa!

Khi hàm greet được gán ra một biến rồi gọi độc lập, this bị mất kết nối với person. Đây chính là lúc bind, apply, call phát huy tác dụng - chúng cho phép ta chỉ định rõ ràng this sẽ là gì khi hàm chạy.

2. call() - Gọi hàm ngay, truyền tham số rời rạc

call() gọi hàm ngay lập tức, với this được chỉ định là tham số đầu tiên, và các tham số còn lại được truyền lần lượt.

function greet(greeting, punctuation) {
  console.log(`${greeting}, tôi là ${this.name}${punctuation}`);
}

const user = { name: "Linh" };

greet.call(user, "Xin chào", "!"); // "Xin chào, tôi là Linh!"

Ví dụ thực tế: mượn hàm giữa các object

const car = {
  drive() {
    console.log(`${this.brand} đang chạy`);
  }
};

const motorbike = { brand: "Honda" };

car.drive.call(motorbike); // "Honda đang chạy"

Ở đây, motorbike không có hàm drive riêng, nhưng ta có thể "mượn" hàm drive từ car và áp dụng cho motorbike bằng call.

3. apply() - Giống call, nhưng tham số truyền dưới dạng mảng

apply() hoạt động giống hệt call() về mục đích, chỉ khác ở cách truyền tham số: thay vì liệt kê rời rạc, ta truyền chúng dưới dạng một mảng.

function greet(greeting, punctuation) {
  console.log(`${greeting}, tôi là ${this.name}${punctuation}`);
}

const user = { name: "Linh" };

greet.apply(user, ["Xin chào", "!"]); // "Xin chào, tôi là Linh!"

Ví dụ thực tế: tìm giá trị lớn nhất trong mảng

Trước khi có toán tử spread (...), apply thường được dùng để truyền một mảng làm danh sách tham số cho các hàm như Math.max:

const numbers = [5, 12, 8, 130, 44];

const max = Math.max.apply(null, numbers); // 130
// Tương đương với cách viết hiện đại:
const maxModern = Math.max(...numbers); // 130

Ngày nay, spread operator đã thay thế phần lớn use-case này của apply, nhưng hiểu về nó vẫn giúp bạn đọc hiểu code cũ dễ dàng hơn.

4. bind() - Không gọi ngay, mà trả về một hàm mới

Khác với callapply, bind() không thực thi hàm ngay lập tức. Thay vào đó, nó trả về một hàm mới đã được "gắn cứng" giá trị this, sẵn sàng để gọi sau này.

function greet() {
  console.log(`Xin chào, tôi là ${this.name}`);
}

const user = { name: "Minh" };

const boundGreet = greet.bind(user);
boundGreet(); // "Xin chào, tôi là Minh"

Ví dụ thực tế: giữ đúng this trong event handler / callback

Đây là use-case phổ biến nhất của bind trong thực tế, đặc biệt với các class trong JavaScript (React class component chẳng hạn):

class Button {
  constructor(label) {
    this.label = label;
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log(`Đã click nút: ${this.label}`);
  }
}

const btn = new Button("Đăng ký");
document.querySelector("#btn").addEventListener("click", btn.handleClick);
// Nhờ bind, "this" bên trong handleClick vẫn trỏ đúng về instance btn

Nếu không dùng bind, khi handleClick được truyền làm callback cho addEventListener, this bên trong nó sẽ trỏ tới phần tử DOM thay vì instance btn, dẫn đến lỗi this.label is undefined.

bind cũng hỗ trợ truyền trước tham số (partial application)

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

const double = multiply.bind(null, 2);
console.log(double(5)); // 10
console.log(double(10)); // 20

bind cho phép "khóa cứng" một số tham số đầu, tạo ra hàm mới chuyên biệt hơn - kỹ thuật này gọi là partial application.

5. Bảng so sánh nhanh

Phương thức Gọi hàm ngay? Cách truyền tham số Trả về gì?
call() Liệt kê rời rạc: fn.call(obj, a, b) Kết quả của hàm
apply() Dạng mảng: fn.apply(obj, [a, b]) Kết quả của hàm
bind() Không Liệt kê rời rạc (có thể truyền trước một phần) Một hàm mới

6. Mẹo ghi nhớ

Một cách dễ nhớ được nhiều lập trình viên chia sẻ:

  • Call - Comma (tham số cách nhau bằng dấu phẩy).
  • Apply - Array (tham số nằm trong mảng).
  • Bind - không gọi ngay, chỉ "trói" this lại để dùng sau.

Kết luận

call, apply, bind đều xoay quanh việc kiểm soát this, nhưng phục vụ các mục đích hơi khác nhau: callapply phù hợp khi bạn muốn gọi hàm ngay với một this cụ thể, còn bind phù hợp khi bạn cần tạo ra một hàm mới để dùng sau (ví dụ như event handler, callback bất đồng bộ). Trong code hiện đại, apply ngày càng ít được dùng nhờ có spread operator, nhưng call và đặc biệt là bind vẫn rất hữu ích, nhất là khi làm việc với class và các API dựa trên callback.