Những trò vui vẻ với Apply(), Call() và Bind() trong Javascript.

            Chắc có thể mọi người đã biết rồi, hàm (function) trong Javascript cũng được coi là những đối tượng (object). Vậy thì sao? Trả lời: Bản thân hàm cũng sẽ có những phương thức được gắn với nó, kiểu như phương thức gắn với đối tượng vậy đó. Điều này là một trong điều khiến Javascript trở thành ngôn ngữ kì dị, kì lạ kì cục nhất trên cõi đời này. Có thể kể đến những phương thức gắn liền với hàm như: Apply(), Call() và Bind().

            Vậy ta có thể làm trò gì vui vẻ với những hàm này? Ở trong Javascript, những hàm này được sử dụng để thiết đặt hoặc thay đổi giá trị của con trỏ “this” một cách tường minh, cả ba hàm này đều có ý nghĩa tương tự nhau, ta sẽ bàn tới các khác biệt chi tiết ở phần tiếp theo đây. Nếu ai đã làm việc với Javascript thì chắc cũng nhận ra được sự nhập nhằng của giá trị con trỏ “this” trong ngôn ngữ này, mình cũng đã viết một bài chi tiết về nó (ở đây), nếu ai chưa nắm vững con trỏ “this” thì nên đọc bài đó trước khi xem tiếp bài này nhé. Nội dung bài này mình sẽ nói về vài cách thức để thay đổi giá trị con trỏ “this” cách tường minh và rõ ràng hơn. Bắt đầu thôi nào!

1. Có thể làm trò gì với hàm Bind()?

         Hàm Bind() này được sử dụng với mục đích trả về cho ta một hàm khác với ngữ cảnh con trỏ “this” đã được thiết đặt. Nói cách khác, hàm bind() cho phép chúng ta gán giá trị của một đối tượng cụ thể nào đó vào con trỏ “this” của hàm được kích hoạt.

          Thông thường trong hầu hết các trường hợp thì con trỏ “this” sẽ mang đúng giá trị của đối tượng mà chúng ta mong muốn (là đối tượng gọi hàm), tuy nhiên vì lí do nào đó mà ta muốn thay đổi giá trị của con trỏ này sang một đối tượng khác, khi đó hàm bind() sẽ là thứ chúng ta nghĩ đến.

         Cần phải phân biệt ở đây rằng, giá trị trả về của hàm bind() cũng là 1 hàm, nhưng hàm này đã được thay đổi ngữ cảnh (context mà con trỏ “this” gắn tới), hàm bind() không trực tiếp kích hoạt hàm để thực thi, và đây chính là điểm khác biệt chính giữa bind() so với apply() và call().

          Hãy xét ví dụ sau đây, trong ví dụ này ta sẽ dùng jQuery để lắng nghe sự kiện click chuột vào button để thực thi lệnh hiển thị :

var user = {
  data: [
     {name:"T. Woods", age:37},
     {name:"P. Mickelson", age:43}
  ],
  showInfo: function (event) {
     var randomNum = ((Math.random () * 2 | 0) + 1) - 1; // số ngẫu nhiên 0 hoặc 1

     // Hiển thị thông tin một người ngẫu nhiên lên màn hình
     Console.log (this.data[randomNum].name + " " + this.data[randomNum].age);
   }
}

// Gán hàm thực thi lên sự kiện click chuột vào button
$ ("button").click (user.showInfo);

         Thử đoán xem kết quả có đúng như ta mong muốn không? Rất tiếc câu trả lời là không. Tại sao ư? Chúng ta chú ý tới hàm user.showInfo được truyền vào, hàm này được truyền vào hàm click() của jQuery như một tham số callback, tức là sau khi hàm click được kích hoạt bởi đối tượng “button”, thì hàm callback này sẽ được kích hoạt, vấn đề là ở chỗ này đây.

              Đối tượng kích hoạt hàm callback này chính là đối tượng “button”, do vậy mà giá trị con trỏ this bên trong hàm user.showInfo này khi chạy sẽ mang giá trị của đối tượng “button” (chứ không phải là đối tượng “user” như mong muốn). Chúng ta muốn giá trị của con trỏ “this” sẽ gắn với đối tượng “user”, vậy hãy dùng bind() để làm điều này:

$ ("button").click( user.showInfo.bind(user) );

              Ở đây, khi sử dụng hàm bind() có truyền vào tham số user, ta nhận được hàm trả về là “user.showInfo có giá trị this là user”, và giá trị trả về này được truyền vào hàm click() với vai trò là hàm callback như trước. Do ta đã gán giá trị “this” tường minh như vậy rồi, nên giá trị này sẽ không bị hiểu nhầm là đối tượng “button” như trước nữa.

Hàm bind() còn làm được trò vui vẻ gì nữa không?

           Hàm bind() còn làm được nhiều trò vi diệu hơn nữa, đó là chúng ta sẽ dùng các hàm mượn (borrowing function) và “hàm đã gán sẵn các tham số” (tiếng anh gọi là currying function, ai biết tiếng việt gọi là gì xin chỉ giáo ^^). Đoạn code dưới đây sẽ cho chúng ta thấy sự vi diệu này, bắt đầu với hàm mượn nhé:

//Hàm mượn
//Khai báo đối tượng cars tương tự như đối tượng users nhưng không có hàm showInfo
var cars = {
 data:[
   {name:"Honda Accord", age:14},
   {name:"Tesla Model S", age:2}
 ]
}

//Thực hiện hiển thị đối tượng car bằng cách mượn hàm showInfo của đối tượng users
cars.showData = user.showData.bind (cars);
cars.showData (); // Honda Accord 14

           Dùng bind() để gán sẵn giá trị tham số trông còn có vẻ vi diệu hơn nữa cơ, xét hàm cần 3 tham số đầu vào như hàm sau (giới tính, tuổi và tên) để in ra câu chào hỏi kèm theo danh xưng:

function greet (gender, age, name) {
  // if a male, use Mr., else use Ms.
  var salutation = gender === "male" ? "Mr. " : "Ms. ";

  if (age > 25) {
    return "Hello, " + salutation + name + ".";
  }
  else {
    return "Hey, " + name + ".";
  }
}

            Chúng ta sẽ dùng hàm bind() để gán sẵn một số tham số cho hàm chào hỏi trên để tạo ra những hàm mới vui vẻ hơn:

//Dùng hàm bind() để gán sẵn giá trị tham số
var greetAnAdultMale = greet.bind (null, "male", 45);

//Gọi thử hàm này
greetAnAdultMale ("John Hartlove"); // "Hello, Mr. John Hartlove."

              Như vậy từ hàm chào hỏi tổng quát ban đầu, ta đã tạo ra được một hàm chào hỏi mới cụ thể hơn (chào hỏi người lớn và là nam) một cách không thể dễ dàng hơn. Thấy vi diệu chưa nào 🙂

2. Hàm Apply() và Call() thì làm được gì?

            Bên cạnh hàm Bind() thì hàm Apply() và Call() cũng là những hàm được sử dụng khá thường xuyên, chúng ta cũng sẽ có nhiều trò vui vẻ với hai hàm này ^^. Về mặt ý nghĩa, hai hàm này cũng giúp chúng ta gán được tường minh giá trị của con trỏ “this” bên trong hàm được kích hoạt, tuy nhiên điều khác biệt lớn nhất so với hàm bind() chính là việc hàm apply() và call() sẽ kích hoạt ngay hàm được gọi chứ không trả về một hàm khác như bind().

               Ngoài ra thì điểm khác biệt giữa Apply() và Call() là việc hàm apply() sẽ nhận truyền một mảng các tham số đầu vào, còn hàm call() thì các tham số đầu vào sẽ được truyển riêng lẻ. Chúng ta cùng xem kĩ hơn sau đây:

Gán giá trị của this với hàm apply() và call()

            Như đã nói ở trên thì 2 hàm này cũng sẽ dùng để thay thế giá trị con trỏ “this” giống như hàm bind(), điểm khác là nó sẽ kích hoạt hàm được gọi ngay lập tức:

// biến toàn cục
var avgScore = "global avgScore";

//hàm toàn cục
function avg (arrayOfScores) {
  // Add all the scores and return the total
  var sumOfScores = arrayOfScores.reduce (function (prev, cur, index, array) {
     return prev + cur;
  });

  // Giá trị "this" sẽ được gắn với biến toàn cục (trừ khi được gán lại với hàm apply hoặc call)
  this.avgScore = sumOfScores / arrayOfScores.length;
}

//Một đối tượng có thuộc tính avgScore và mảng giá trị scores
var gameController = {
  scores :[20, 34, 55, 46, 77],
  avgScore:null
}

             Sau đây chúng ta sẽ xem xét kết quả chạy với 2 cách gọi thực thi khác nhau. Cách đầu tiên là chúng ta sẽ gọi hàm theo cách hay dùng, không sử dụng apply() hoặc call():

//Gọi theo cách thông thường, "this" được gắn với giá trị object gọi nó (ở đây là window):
avg (gameController.scores);
// In ra giá trị nhằm truy vết giá trị con trỏ “this”
console.log (window.avgScore); // 46.4
console.log (gameController.avgScore); // null

             Cách 2 là sử dụng call() để thay đổi giá trị con trỏ “this”, giá trị tham số đầu tiên của hàm call() là giá trị con trỏ “this” ta muốn gán tới, các tham số tiếp sau là tham số tương ứng của hàm gọi nó (hàm avg() ) :

// We use the call() method:
avg.call (gameController, gameController.scores);
//Kiểm tra
console.log (window.avgScore); //global avgScore
console.log (gameController.avgScore); // 46.4

            Các dùng hàm apply() thì tương tự hàm call(), có điều là các tham số được truyền vào theo kiểu mảng (ngoại trừ tham số đầu tiên cho con trỏ “this”):

// We use the apply() method:
avg.apply( gameController, [gameController.scores] );
//Kiểm tra
console.log (window.avgScore); //global avgScore
console.log (gameController.avgScore); // 46.4

Tóm lại là gì

           Với việc coi hàm (function) tương tự như đối tượng (object), Javascript có nhiều trò vui vẻ để làm hơn so với nhiều ngôn ngữ lập trình khác, một trong số đó là việc bản thân hàm cũng có những hàm khác gắn với nó (tương tự như hàm gắn với đối tượng).

         Một trong số các việc vui vẻ mà cũng nhức đầu đó là việc kiểm soát giá trị con trỏ “this” bên trong hàm, và công cụ để ta thực hiện điều này là các hàm như bind(), apply() và call(). Cả 3 hàm này đều có cùng ý nghĩa là gán gián trị con trỏ “this” một cách tường minh.

         Khác biệt của bind() là nó có giá trị trả về là “hàm gán sẵn giá trị cho this”, hàm apply() và call() đều thực hiện truyền giá trị cho “this” và kích hoạt hàm mục tiêu chạy, điểm khác biệt của apply() là nó nhận giá trị truyền vào là 1 mảng thay vì cần truyền rời rạc như hàm call().

               Hi vọng mọi người sẽ làm được nhiều trò vui vẻ với những công cụ này 🙂

Vcttai.

Tham khảo:

1. http://javascriptissexy.com/javascript-apply-call-and-bind-methods-are-essential-for-javascript-professionals/

2. http://stackoverflow.com/questions/15455009/javascript-call-apply-vs-bind

3. https://codeplanet.io/javascript-apply-vs-call-vs-bind/

7 thoughts on “Những trò vui vẻ với Apply(), Call() và Bind() trong Javascript.

  1. Pingback: Vượt qua các bài phòng vấn Javascript. – Webbynat

  2. Pingback: [Javascript] Promise – Lời hứa ngọt ngào (P1) – Webbynat

  3. Pingback: [ Javascript ] Bàn về khái niệm Object trong Javascript. – Những dòng code vui

  4. Pingback: [ Javascript ] Callback function và Higher-order function trong Javascript – Những dòng code vui

  5. Pingback: Làm thế nào để cải thiện khả năng viết code của bạn (P.1) – Những dòng code vui

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s