Javascript Closure

1. JavaScript Scope

Để hiểu về Closure trước tiên chúng ta bắt đầu với scope (phạm vi) trong JavaScript. Ta có thể hiểu đơn giản scope là khái niệm qui định sự truy xuất và tồn tại của các biến. Trong JavaScript (trước ES6) thì mỗi một hàm (function) là một scope riêng. Từ trong scope có thể truy xuất ra bên ngoài scope, ngược lại bên ngoài thì lại không thể truy cập được vào bên trong scope.

  var hello = 'Hello';
  function helloWorld(){
    var world = 'World!';
    console.log(hello + world);
  }
  helloWorld();
  console.log(world)

Như bạn thấy thì hàm helloWorld() có thể gọi đến biến hello. Biến hello ở ví dụ trên trong JavaScript được gọi là biến tự do (free variable) đối với function helloWorld(). Vậy biến tự do là gì, biến tự do được hiểu là các biến được khai bảo trong trong hàm và phần thân của một hàm (method) đó có thể gọi đến được. Hay nói cách khác biến tự do phải được khai báo ở bên ngoài hàm và ko truyền vào thông qua argument.

Còn với biến world thì chỉ đơn thuần là biến local, không thể gọi ở ngoài helloWorld(). Giống như tôi đã nói ở trên trong JavaScript khi khai báo một function thì nó sẽ tạo ra một scope riêng cho function này. Các biến được định nghĩa trong function sẽ không thể truy cập được từ bên ngoài.

2. Closure

Closure là một khái niệm trong JavaScript, ta có thể hiểu đơn giản là một bao đóng và quy tắc hoạt động của các thành phần trong bao đóng này.

Ví dụ đơn giản thôi, nếu bạn có một cái thùng, đem con mèo của bạn cùng với mớ đồ chơi của nó quẳng hết vào trong thùng sau đó đóng thùng lại, ta có thể thấy cái thùng chính là Closure bao đóng con mèo và đồ chơi của nó. Con mèo vẫn có thể sử dụng đồ chơi của nó bất kể khi nào nó muốn vì tất cả đều ở trong thùng cùng nó.

Từ ngữ cảnh con mèo, đồ chơi và cái thùng, đem hết tất cả vào trong Javascript ta sẽ có cái thùng là Closure. Thay vì quăng con mèo vào thùng thì ta quăng function vào đó, và thay vì đồ chơi ta đặt một số biến vào đó.

Điều quan trọng là function nằm bên trong Closure vẫn có thể truy xuất được tất cả các biến nằm bên trong Closure. Miễn là function còn tồn tại thì các biến bên trong Closure sẽ không bị thu dọn, để cho function có thể truy xuất chúng bất cứ khi nào nó muốn.

Giờ ta bắt đầu thử với Closure trong JavaScript xem sao

function sayHello(){
  var message = 'Hello'; // define a local variable.
  return function(){ // return a function from sayHello.
    console.log(message); // wanna know if 'message' still in scope?
  }
}

var helloSayer = sayHello();
helloSayer(); // call the inner function returned from sayHello.

Ta tạo một hàm sayHello, ta có thể hiểu sayHello là một Closure trong đó gôm có một biến là message và và một hàm sử dụng đến biến message. Theo suy nghĩ thông thường thì sau khi hàm sayHello chạy xong, tất cả các biến local bên trong hàm này sẽ bị out of scope và được thu dọn. Vậy nó có xảy ra với biến ‘message’ trong ví dụ trên, để xem nào.

Khi ta gọi hàm được trả về từ hàm ‘sayHello’, thật ngạc nhiên, console in ra ‘Hello’, vậy điều kì diệu gì đã làm cho hàm nằm trong kết quả trả về của ‘sayHello’ có thể truy xuất được biến ‘message’.

Vâng điều kì diệu tôi đang muốn nói tới ở đây chính là Closure.

Thay đổi một chút như sau, hàm sayHello() sẽ trả về một hàm được định nghĩa từ bên ngoài.

function sayHello2() {
  console.log(message);
}

function sayHello(){
  var message = 'Hello'; // define a local variable.
  return sayHello2;
}

var helloSayer = sayHello();
helloSayer(); // call the inner function returned from sayHello.

Khi chạy thử ta sẽ thấy lỗi là không thể truy xuất được biến message. Như vậy ta có thể nhận thấy một điểm chú ý của Closure là hàm phải được khai báo ở trong chính Closure này.

3. Closure với this key word trong JavaScript

Tiếp theo chúng ta sẽ tìm hiểu từ khóa this, this là một từ khóa khá đặc biệt, nó luôn tham chiếu đến chủ thể của phạm vi nơi mà nó được chạy. Một số hiểu nhầm về cách hoạt động của this có thể gây ra những lỗi nghiêm trọng. Dưới đây là một ví dụ nhỏ trong việc sử dụng this

var Car, tesla;
Car = function() {
  this.start = function() {
    console.log("car started");
  };
  this.turnKey = function() {
    var carKey = document.getElementById('car_key');
    carKey.onclick = function(event) {
      this.start();
    };
  };
  return this;
};
tesla = new Car();
// Once a user clicks the #carKey element they will see "Uncaught TypeError: Object has no method 'start'"
tesla.turnKey();

Trong ví dụ trên ta thấy sự kiên onclick được thực hiện bên trong phần tử DOM carKey. Như vậy khi ta gọi this.start() thì this ở đây được hiểu là DOM object (carKey) chứ không phải là object của class Car. Để giải quyết vấn đề này ta thường đặt một biến tự do và gán giá trị this cho biến này (ví dụ that, _this, self, me).

  ...
  this.turnKey = function() {
    var self = this;
    var carKey = document.getElementById('car_key');
    carKey.onclick = function(event) {
      self.start();
    };
  };
  ...

Ta thấy self là một biến tự do và nó không định nghĩa lại khi sự kiện onclick được gọi. Như vậy self có thể gọi từ phần thân của hàm onclick một cách bình thường. Về mặt kĩ thuật thì việc sử dụng biến tự do như thế này hoàn toàn có thể giải quyết được vấn đề trên. Thực tế tôi cũng thường sử dụng cách này trong những trường hợp tương tự