1. Cloures trong JavaScript là gì?

Closure là một hàm được tạo ra từ bên trong một hàm khác, nó có thể sử dụng các biến toàn cục, biến cục bộ của hàm cha và biến cục bộ của chính nó. Việc viết hàm theo kiểu closure trong một số trường hợp sẽ giúp code nhìn sáng và dễ quản lý hơn, linh hoạt hơn trong một số trường hợp.

Closure trong Js có thể truy cập biến ở 3 phạm vi khác nhau là:

  • Biến toàn cục (global)
  • Biến được khai báo ở ngoài hàm (outer function)
  • Biến ở trên trong hàm closure (local)

Ví dụ:

function sayHi(name){
    let say = function(){
        alert("Xin chào, tôi là " + name);
    };
    return say;
}

Trong ví dụ này thì hàm say() chính là một closure function . Bên trong nó có thể sử dụng được biến của hàm cha name . Bạn cũng có thể return về luôn thay vì đặt tên cho hàm closure đó.

function sayHi(name){
    return function(){
        alert("Xin chào, tôi là " + name);
    };
}

Bây giờ mình sẽ gọi đến hàm sayHi , và gán nó vào biến tên là nam . Sau đó mình log biến nam này xem nó là gì nhé.

var nam = sayHi("Nam");
console.log(nam);

Kết quả trả vè nó là một function . Lý do khá đơn giản, bởi vì trong hàm sayHi mình return về một function . Vì vậy biến nam chính là function mà mình đã return đó. Để in ra thông báo thì chúng ta phải gọi kích hoạt function này.

2. Truy cập biến bên ngoài hàm với Closure

Một trong những “tính năng” hay ho quan trọng của closure đó là hàm bên trong vẫn có thể truy cập đến các biến số của hàm bên ngoài ngay cả khi hàm bên ngoài đã trả về. Khi các hàm trong Js thực thi, chúng sử dụng cùng chuỗi phạm vi. Điều này có nghĩa là sau khi hàm bên ngoài trả về, hàm bên trong vẫn có thể truy cập đến các biến của hàm bên ngoài. Do đó, ta có thể gọi hàm bên trong này trong chương trình sau đó.

Ví dụ:

function celebrityName (firstName) {
    var nameIntro = "This celebrity is ";
    // Đây là hàm bên trong mà có thể truy cập đến biến của hàm bên ngoài, truy cập được tham số của hàm ngoài.
   function lastName (theLastName) {
        return nameIntro + firstName + " " + theLastName;
    }
    return lastName;
}

var mjName = celebrityName ("Michael"); celebrityName (bên ngoài) đã trả về.

// Closure (lastName) được goi ở đây sau khi hàm ngoài đã trả về.
// Closure vẫn có thể truy cập được biến và tham số của hàm bên ngoài.
mjName ("Jackson");

3. Closure với this trong JavaScript

Cũng như những function khác, nếu bạn đang chạy chế độ strict mode thì con trỏ this sẽ là undefined , còn không thì nó là đối tượng window .

Ta có thể tạo một closure trong các phương thức của class . Tuy nhiên, vì this trong closureundefined nên bạn không thể truy cập đến các thuộc tính của class . Có một mẹo khá đơn giản, đó là tạo một biến và gán nó chính bằng con trỏ this ở trong các phương thức của class .

class Student{
    constructor(name){
        this.name = name;
    }
     
    showName(){
        // Đặt một cái tên khác cho this
        let obj = this;
        return function(){
            console.log("Xin chào, tôi là " + obj.name);
        };
    }
}
 
var student1 = new Student("Thành");
var thanh = student1.showName();
thanh();

4. Sử dụng closure để xác định các biến private

Nói chung, các nhà phát triển Js sử dụng dấu gạch dưới (_) làm tiền tố cho các biến private . Nhưng trên thực tế, các biến này vẫn có thể được truy cập và sửa đổi, không phải là biến private . Tại thời điểm này, việc sử dụng closure có thể xác định các biến thực sự private 

Ví dụ:

function Product() {

    var name;

    this.setName = function(value) {
        name = value;
    };

    this.getName = function() {
        return name;
    };
}

var p = new Product();
p.setName("laptrinhtudau.com");

console.log(p.name); // undefined
console.log(p.getName());

Thuộc tính name của đối tượng p là thuộc tính private và không thể truy cập trực tiếp bằng p.name . Mà có thể lấy giá trị chỉ thông qua getName()

5. Closures trong vòng lặp

Một ví dụ:

for (var i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log('Sau ' + i + ' giây: ' + i);
    }, i * 1000);
}

Đoạn code sẽ cho ra kết quả tương tự nhau. Nhưng điều mà mình muốn làm trong vòng lặp là sao chép giá trị của i trong mỗi lần lặp tại thời điểm lặp để hiển thị thông báo sau 1, 2, 3 giây. Bạn thấy cùng một thông báo là do lệnh gọi là được chuyển đến setTimeout() là một hàm closure .

Nó sẽ nhớ giá trị của i từ lần lặp cuối cùng của vòng lặp, là 4. Ngoài ra, cả ba closures được tạo bởi vòng lặp for đều chia sẻ cùng một phạm vi toàn cục truy cập cùng một giá trị của i.

Để giải quyết vấn đề này, bạn cần tạo một closure mới trong mỗi lần lặp của vòng lặp. Có hai giải pháp là: IIFE và từ khóa let .

5.1. Sử dụng IIFE

Trong giải pháp này, bạn sử dụng một biểu thức hàm được gọi ngay lập tức (còn gọi là IIFE) vì IIFE tạo một scope mới bằng cách định nghĩa một hàm và thực thi nó ngay lập tức.

for (var i = 1; i <= 3; i++) {
    (function(i) {
        setTimeout(function() {
            console.log('Sau ' + i + ' giây(s):' + i);
        }, i * 1000);
    })(i);
}

5.2. Sử dụng let

Nếu sử dụng từ khóa let trong vòng lặp for , nó sẽ tạo ra một phạm vi sử dụng mới trong mỗi lần lặp. Nói cách khác là bạn sẽ có một biến i mới trong mỗi lần lặp. Ngoài ra, phạm vi sử dụng mới được liên kết với phạm vi sử dụng trước đó để giá trị trước đó của i được sao chép lại.

for (let i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log('Sau ' + i + ' giây: ' + i);
    }, i * 1000);
}

6. Độ ưu tiên các biến trong closure function

Như ta biết thì closure có thể sử dụng biến tại ba vị trí, đó là biến toàn cục, biến hàm cha và biến của chính nó.Giả sử tên các biến ở ba vị trí đó bị trùng nhau độ ưu tiên sẽ được sắp xếp như thế nào? Trường hợp này nó sẽ ưu tiên từ trong ra ngoài như sau:

  • Xem biến có nằm trong closure function không? Nếu không thì nhảy qua bước sau, nếu có thì sử dụng.
  • Xem biến có nằm trong hàm cha không? Nếu không thì qua bước sau, nếu có thì sử dụng.
  • Xem có phải là biến cục bộ không? Nếu có thì sử dụng, nếu không thì nó sẽ khởi tạo biến mới mới.