Bài viết được dịch từ: javascriptissexy.com

Chúng ta thường xuyên sử dụng closures trong JavaScript, kinh nghiệm về JavaScript của bạn không quan trọng, chắc chắn bạn sẽ bắt gặp chúng hết lần này đến lần khác. Closures có thể khá phức tạp và nằm ngoài khả năng của bạn, nhưng sau khi đọc bài viết này, closures sẽ trở lên dễ hiểu hơn và bạn có thể sử dụng chúng cho các task JavaScript hàng ngày của mình.

Bạn nên hiểu rõ scope (phạm vi) của biến trong JavaScript trước khi đọc tiếp, bởi vì để hiểu closures bạn phải hiểu scope của biến trong JavaScript.

Closure là gì?

Một closure là một inner function (hàm khai báo bên trong một hàm khác), nó có thể truy cập tới các biến của outer function (hàm chứa inner function) - scope chain. Closure có 3 scope chain, nó có thể: truy cập tới các biến khai báo bên trong nó, truy cập tới các biến của outer function, và truy cập tới các biến global.

Closure có 3 scope chain

Closures không chỉ truy cập được tới các biến, mà còn có thể truy cập các tham số của outer function. Chú ý, closures không thể truy cập đối tượng arguments của outer function.

Bạn có thể tạo một closure bằng cách thêm một function bên trong function khác.

Tham khảo các khóa học lập trình online, onlab, và thực tập lập trình tại TechMaster

Ví dụ cơ bản về Closures trong JavaScript:

function showName (firstName, lastName) {

​  var nameIntro = "Your name is ";
  // this inner function has access to the outer function's variables, including the parameter​
  ​function makeFullName () {
        
    ​return nameIntro + firstName + " " + lastName;
    
  }
​
  ​return makeFullName ();

}

​
showName ("Michael", "Jackson"); // Your name is Michael Jackson


Closures được sử dụng rộng rãi trong Node.js; chúng là những con ngựa kéo (workhorses) trong kiến trúc asynchronous, non-blocking của Node.js. Closures cũng được sử dụng thường xuyên trong jQuery.

Một ví dụ về Closures trong jQuery:

$(function() {
​
  ​var selections = []; 
  $(".niners").click(function() { // this closure has access to the selections variable​
    selections.push (this.prop("name")); // update the selections variable in the outer function's scope​
  });
​
});

Các quy tắc của Closures và Hiệu ứng phụ

1. Closures có thể truy cập tới các biến của outer function ngay cả sau khi outer function return:

Một trong những tính năng quan trọng và nổi bật của closures là inner function vẫn có thể truy cập tới các biến của outer function ngay cả sau khi outer function đã return. Hãy xem ví dụ này:

function celebrityName (firstName) {
    var nameIntro = "This celebrity is ";
    // this inner function has access to the outer function's variables, including the parameter​
   function lastName (theLastName) {
        return nameIntro + firstName + " " + theLastName;
    }
    return lastName;
}
​
​var mjName = celebrityName ("Michael"); // At this juncture, the celebrityName outer function has returned.​
​
​// The closure (lastName) is called here after the outer function has returned above​
​// Yet, the closure still has access to the outer function's variables and parameter​
mjName ("Jackson"); // This celebrity is Michael Jackson


2. Closures lưu trữ các tham chiếu tới các biến của outer function, chúng không lưu trữ các giá trị thực sự.

Sẽ thú vị hơn khi giá trị các biến của outer function thay đổi "trước khi closure được gọi". Và tính năng mạnh mẽ này có thể được sử dụng theo nhiều cách sáng tạo, chẳng hạn ví dụ các biến private (private variable), được demo lần đầu tiên bởi Douglas Crockford:

function celebrityID () {
    var celebrityID = 999;
    // We are returning an object with some inner functions​
    // All the inner functions have access to the outer function's variables​
    return {
        getID: function ()  {
            // This inner function will return the UPDATED celebrityID variable​
            // It will return the current value of celebrityID, even after the changeTheID function changes it​
          return celebrityID;
        },
        setID: function (theNewID)  {
            // This inner function will change the outer function's variable anytime​
            celebrityID = theNewID;
        }
    }
​
}
​
​var mjID = celebrityID (); // At this juncture, the celebrityID outer function has returned.​
mjID.getID(); // 999​
mjID.setID(567); // Changes the outer function's variable​
mjID.getID(); // 567: It returns the updated celebrityId variable


3. Hiệu ứng phụ của Closures

Bởi vì closures lưu trữ các tham chiếu đến các biến của outer function, nên chúng có thể dẫn tới các bug khi các biến của của outer function thay đổi với một vòng lặp for. Ví dụ:

// This example is explained in detail below (just after this code box).​
​function celebrityIDCreator (theCelebrities) {
    var i;
    var uniqueID = 100;
    for (i = 0; i < theCelebrities.length; i++) {
      theCelebrities[i]["id"] = function ()  {
        return uniqueID + i;
      }
    }
    
    return theCelebrities;
}
​
​var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];
​
​var createIdForActionCelebs = celebrityIDCreator (actionCelebs);
​
​var stalloneID = createIdForActionCelebs [0];

console.log(stalloneID.id()); // 103

Trong ví dụ trên, khi kết thúc vòng lặp for giá trị của i là 3. Số 3 sẽ được cộng với uniqueID thành 103 cho tất cả các celebritiesID. Vì thế tất cả các phần tử trong mảng được return đều có id = 103, thay vì 100, 101, 102 như mong đợi.

Lý do điều này xảy ra bởi vì closure (anonymous function trong ví dụ trên) truy cập tới các biến của outer function bởi tham chiếu, không phải giá trị. Bạn đã thấy trong ví dụ trước đó, chúng ta có thể truy cập biến đã được cập nhật giá trị với closure, tương tự trong ví dụ này closure truy cập biến i khi giá trị của nó đã được thay đổi, khi outer function chạy xong toàn bộ vòng lặp giá trị của i lúc này là 3 và tất cả id có giá trị là uniqueID (100) + i (3).

Để fix bug này trong closures, bạn có thể sử dụng IIFE (Immediately Invoked Function Expression), như sau:

function celebrityIDCreator (theCelebrities) {
    var i;
    var uniqueID = 100;
    for (i = 0; i < theCelebrities.length; i++) {
        theCelebrities[i]["id"] = function (j)  { // the j parametric variable is the i passed in on invocation of this IIFE​
            return function () {
                return uniqueID + j; // each iteration of the for loop passes the current value of i into this IIFE and it saves the correct value to the array​
            } () // BY adding () at the end of this function, we are executing it immediately and returning just the value of uniqueID + j, instead of returning a function.​
        } (i); // immediately invoke the function passing the i variable as a parameter​
    }
​
    return theCelebrities;
}
​
​var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];
​
​var createIdForActionCelebs = celebrityIDCreator (actionCelebs);
​
​var stalloneID = createIdForActionCelebs [0];

console.log(stalloneID.id); // 100​
​
​var cruiseID = createIdForActionCelebs [1];
console.log(cruiseID.id); // 101

Đọc thêm

https://www.youtube.com/watch?v=yiEeiMN2Khs

https://www.youtube.com/watch?v=71AtaJpJHw0

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

https://kipalog.com/posts/JavaScript-Closures

https://kipalog.com/posts/Closure-va-scope-trong-javascript