Hướng dẫn bind trong javascript

Để hiểu rõ về các hàm được đề cập trong bài viết này, các bạn nên ôn lại chút kiến thức về object trong javascript và this trong javascript nhé.

Khái niệm và cách sử dụng các hàm Bind, Call và Apply

Ba hàm bind, callapply là các prototype của Function nên chỉ có Function mới có thể gọi được 3 hàm này. Sở dĩ, một Function có thể gọi Function khác vì trong JavaScript, Function cũng là một loại Object, mà đã là Object thì sẽ có prototype, hay nói cách khác là gọi được phương thức của nó.

1. Bind()

Bind được dùng để xác định tham số this cho một function. Cú pháp:

let boundFunc = func.bind(thisArg[, arg1[, arg2[, ...argN]]])

Trong đó:

  • thisArgGiá trị của this được đưa ra để gọi hàm. Giá trị bị bỏ qua nếu hàm liên kết được xây dựng bằng toán tử new. Khi sử dụng bind để tạo ra một hàm dưới dạng callback trong hàm setTimeout, bất kỳ giá trị nào sơ khai được truyền dưới dạng thisArg sẽ được chuyển đổi thành đối tượng. 
  • arg1, arg2, ...argN: Các đối số cho hàm.

Như trong trường hợp dưới đây, khi ta truyền hàm showName vào như một callback cho hàm button.click, giá trị this ở đây chính là button đó. Để hàm chạy đúng, ta dùng bind để bind giá trị person và this.

var person = {
  firstName: 'Khoa',
  lastName: 'Nguyễn',
  showName: function() {
    console.log(this.firstName + ' ' + this.lastName);
  }
};

//showName truyền vào như callback, ở đây this chính là button
$('button').click(person.showName); 

// Dùng bind để xác định giá trị this
$('button').click(person.showName.bind(person)); //this ở đây vẫn là object person

Không chỉ bind được giá trị this, bind còn bind được các tham số truyền vào cho hàm nữa. Do đó, Bind còn được dùng để viết partial function.

Nói một cách đơn giản, partial function tức là tạo ra 1 function mới từ 1 function cũ bằng cách gán mặc định một số tham số cho function cũ đó.

Ví dụ: Mình có một hàm log đơn giản có 3 tham số

function log(level, time, message) {
  console.log(level + ' - ' + time + ': ' + message);
}

Giả sử mình muốn tạo một hàm log khác, ghi lại các log error của hôm nay, mình có thể viết một hàm mới dựa theo hàm log cũ:

function log(level, time, message) {
  console.log(level + ' - ' + time + ': ' + message);
}

function logErrToday(message) {
  log("Error", "Today", message);
}

logErrToday("Server die."); // Error - Today: Server die.

Thay vì viết như thế, mình có thể viết đơn giản hơn bằng các dùng bind. Ở đây log là function cũ, logErrToday là function mới, được tạo ra bằng cách gán mặc định 2 tham số level và time.

function log(level, time, message) {
  console.log(level + ' - ' + time + ': ' + message);
}

// Không có this nên set this là null
// Set mặc định 2 tham số level và time
var logErrToday = log.bind(null, 'Error', 'Today');

// Hàm này tương ứng với log('Error', 'Today', 'Server die.')
logErrToday("Server die."); 
// Error - Today: Server die.

2. Call()

Call gọi một hàm với giá trị của this và các đối số riêng lẻ. Cú pháp:

function.call(thisArg, arg1, arg2, ...)

Trong đó:

  • thisArgGiá trị của this được đưa ra để gọi hàm. Lưu ý rằng this có thể không phải là giá trị thực tế được thấy bởi phương thức: Nếu phương thức là một hàm trong non-strict mode, giá trị null và undefined sẽ được thay thế với global object và các giá trị sơ khai (primitive) sẽ được chuyển thành các đối tượng (objects).
  • arg1, arg2, ...argN: Các đối số cho hàm.

Các bạn hãy theo dõi các ví dụ dưới để hiểu rõ về cách sử dụng hàm call trong javascript nhé.

Ví dụ 1:  Sử dung hàm call để gọi một hàm ẩn danh (anonymous function)

var animals = [
  { species: 'Lion', name: 'King' },
  { species: 'Whale', name: 'Fail' }
];

for (var i = 0; i < animals.length; i++) {
  (function(i) {
    this.print = function() {
      console.log('#' + i + ' ' + this.species
                  + ': ' + this.name);
    }
    this.print();
  }).call(animals[i], i);
}

Trong ví dụ trên, chúng ta tạo một hàm ẩn danh và sử dụng hàm call để gọi hàm đó nhận mọi đối tượng trong một mảng. Mục đích chính của hàm ẩn danh này là thêm tính năng hàm print cho mọi đối tượng, từ đó các đối tượng này có thể in ra vị trí của chúng trong mảng. Việc này có thể không cần thiết nhưng được đưa ra với mục đích ví dụ.

Ví dụ 2: Sử dung hàm call để gọi hàm và đưa ra một giá trị cho đối tượng 'this'

function greet() {
  var reply = [this.person, 'Is An Awesome', this.role].join(' ');
  console.log(reply);
}

var x = {
  person: 'Khoa Nguyen', role: 'Javascript Developer'
};

greet.call(x); // Khoa Nguyen Is An Awesome Javascript Developer

Trong ví dụ dưới trên khi chúng ta gọi hàm greet , giá trị của this trong hàm greet chính là đối tượng x.

Ví dụ 3: Sử dung call để chain constructors cho một đối tượng

Bạn có thể sử dụng hàm call để chain constructors cho một đối tượng giống như trong Java. Trong ví dụ dưới đây, hàm khởi tại của đối tượng Product được định nghĩa với 2 tham số, name và price. Hai hàm Food và Toy gọi Product với tham số this , name và price. Product khởi tạo thuộc tính name và price, cả 2 hàm này định nghĩa category.

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}

var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);

3. Apply()

Apply gọi một hàm với giá trị của this và các đối số được truyền vào dưới dạng mảng. Cú pháp:

func.apply(thisArg, [ argsArray])

Trong đó:

  • thisArgGiá trị của this được đưa ra để gọi hàmLưu ý rằng this có thể không phải là giá trị thực tế được thấy bởi phương thức: Nếu phương thức là một hàm trong non-strict mode, giá trị null và undefined sẽ được thay thế với global object và các giá trị sơ khai (primitive) sẽ được chuyển thành các đối tượng (objects).
  • argsArray: Một mảng chỉ định các đối số cho hàm, hoặc null hoặc undefined nếu không có đối số nào được cung cấp.

Các bạn hãy theo dõi các ví dụ dưới để hiểu rõ về cách sử dụng hàm apply trong javascript nhé.

Ví dụ 1: Sử dụng hàm apply với built-in functions

Một cách sử dụng thông minh hơn với hàm apply cho phép bạn sử dụng các buily-in function cho một số tác vụ có thể đã được viết bằng cách lặp lại các giá trị mảng.

// min/max number in an array
const numbers = [5, 6, 2, 3, 7];

// using Math.min/Math.max apply
let max = Math.max.apply(null, numbers); 
// This about equal to Math.max(numbers[0], ...)
// or Math.max(5, 6, ...)

let min = Math.min.apply(null, numbers);

// vs. simple loop based algorithm
max = -Infinity, min = +Infinity;

for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] > max) {
    max = numbers[i];
  }
  if (numbers[i] < min) {
    min = numbers[i];
  }
}

Ví dụ 2: Sử dụng hàm apply để nối 1 mảng vào 1 mảng khác

Bạn có thể sử dụng push để nối thêm phần tử vào mảng. Và, bởi vì push chấp nhận một số lượng thay đổi đối số, bạn cũng có thể đẩy nhiều phần tử cùng một lúc. Tuy nhiên, nếu bạn chuyển một mảng để đẩy, nó sẽ thực sự thêm mảng đó dưới dạng một phần tử duy nhất, thay vì thêm các phần tử riêng lẻ. Vì vậy, bạn kết thúc với một mảng bên trong một mảng.

Điều gì xảy ra nếu đó không phải là điều bạn muốn? concat có thể thực hiện điều bạn muốn trong trường hợp này, nhưng nó không nối vào mảng hiện có — thay vào đó nó tạo và trả về một mảng mới.

Nhưng bạn muốn thêm vào mảng hiện có ... Vậy bây giờ thì sao? Viết một vòng lặp? Chắc chắn là không? apply chính là thứ bạn cần!

const array = ['a', 'b'];
const elements = [0, 1, 2];
array.push.apply(array, elements);
console.info(array); // ["a", "b", 0, 1, 2]

Ví dụ 3: Sử dung apply để chain constructors

Bạn có thể sử dụng hàm call để chain constructors cho một đối tượng giống như trong Java.

Trong ví dụ dưới đây chúng ta sẽ tạo ra một global Function method gọi là construct, phương thức này sẽ cho phép bạn sử dụng một đối tượng giống mảng với một hàm tạo thay vì một danh sách đối số.

Function.prototype.construct = function(aArgs) {
  let oNew = Object.create(this.prototype);
  this.apply(oNew, aArgs);
  return oNew;
};

Tiếp theo, chúng ta cùng xét qua 1 ví dụ để nhìn thấy được điểm khác nhau trong việc sử dụng 3 hàm này nhé.

Call

var person1 = {firstName: 'Khoa', lastName: 'Nguyễn'};
var person2 = {firstName: 'Vân', lastName: 'Thanh'};

function say(greeting1, greeting2) {
 console.log(greeting1 + ',' + greeting2 + ' ' + this.firstName + ' ' + this.lastName);
}

say.call(person1, 'Hello', 'Good morning'); // => Hello,Good morning Khoa Nguyen
say.call(person2, 'Hello', 'Good morning'); // => Hello,Good morning Vân Thanh

Apply

var person1 = {firstName: 'Khoa', lastName: 'Nguyễn'};
var person2 = {firstName: 'Vân', lastName: 'Thanh'};

function say(greeting0, greeting1) {
 console.log(greeting0 + ',' + greeting1 + ' ' + this.firstName + ' ' + this.lastName);
}

say.apply(person1, ['Hello', 'Good moring']); // => Hello,Good moring Khoa Nguyễn
say.apply(person2, ['Hello', 'Good moring']); // => Hello,Good moring Vân Thanh

Bind

var person1 = {firstName: 'Khoa', lastName: 'Nguyễn'};
var person2 = {firstName: 'Vân', lastName: 'Thanh'};

function say(greeting0, greeting1) {
 console.log(greeting0 + ',' + greeting1 + ' ' + this.firstName + ' ' + this.lastName);
}

var sayHelloKhoa = say.bind(person1, 'Hello', 'Good morning');
var sayHelloVan = say.bind(person2, 'Hello', 'Good morning');

sayHelloKhoa(); // => Hello,Good morning Khoa Nguyễn
sayHelloVan(); // => Hello,Good morning Vân Thanh

Sau khi xem qua 3 ví dụ sau, chúng ta có thể rút ra 1 vài nhận xét:

  • Nhìn chung, hàm call và apply là gần giống nhau. Chúng đều gọi hàm trực tiếp. Chỉ khác ở cách truyền tham số vào (apply truyền vào một array chứa toàn bộ các tham số còn call truyền lần lượt từng tham số.)
  • Hàm bind thì hơi khác hơn một chút. Hàm này không gọi hàm trực tiếp mà nó sẽ trả về một hàm mới. Và bạn có thể sử dụng hàm số mới này sau. Về cách truyền tham số vào thì nó giống với hàm call.

Tạm kết

Như vậy trong bài viết này chúng ta đã cùng nhau tìm hiểu về 3 hàm bind, call và apply trong javascript cùng một vài điểm khác nhau giữa chúng. Bạn thấy thế nào về JS, hãy đưa ra những ý kiến trong quá trình sử dụng js nhé. Nếu các bạn thấy bài viết hữu ích hãy rate 5* và share cho mọi người tham khảo!

Hãy để lại comment để mình có thể hoàn thiện bản thân hơn trong tương lai. Cám ơn các bạn!