Đối với những bạn lập trình web nói chung và lập trình js nói riêng thì những kiến thức về scope, closure là cần phải nắm rõ. Js là một ngôn ngữ lập trình khá khó, nếu không nắm rõ cách hoạt động thì sẽ gặp phải những vấn đề khá nan giải.

Nhiều người ban đầu dùng js thì sẽ thấy rất dễ, càng làm vào sâu hơn sẽ thấy phức tạp dần với các vấn đề khó trong js là scope, closure, hay từ khóa this.

1. Scope.

Giới thiệu về scope.

Scope là một block memory để lưu trữ các biến cụ thể nào đó.

Nếu ai đó đã từng lập trình C sẽ biết rằng trong C, scope được tạo khi sử dụng toán tử {}, gọi là block scope. Mỗi khi dấu ngoặc nhọn được khai báo thì trình biên dịch (compiler) sẽ tạo ra một scope.

Javascript cũng sử dụng toán tử {} nhưng lại không sử dụng scope mỗi khi có toán tử đó khai báo giống trong C.

Ví dụ một đoạn code cơ bản trong C như sau:

for (i=0; i < 4; i++) { //Outer loop
    for (i=0; i < 2; i++) { //Inner loop
        document.write("Hello World"); 
    }
}

Đoạn code trên thông thường sẽ in ra 8 lần câu "Hello World" trong C hoặc 1 số ngôn ngữ lập trình khác, nhưng trong JS nếu bạn viết đoạn code trên thì vòng lặp sẽ chạy mãi không dừng.

Bạn có biết vì sao không?

Vì đơn giản, JS không tạo scope lưu biến i khi có toán tử {} như trong C nên biến i trong Outer loop và trong Inner loop là một. Do đó vòng lặp Inner loop luôn luôn reset lại biến i dẫn tới vòng lặp Outer loop có biến i không thể đạt tới gía trị  = 4 để dừng.

Những người phát triển JS đã nhận ra sự thiếu sót đó dẫn tới nhiều lập trình khá lúng túng khi tiếp cận nên trong những phiên bản JS sau này, họ đã cung cấp thêm từ khóa let để tạo block scope:

for (let i=0; i < 4; i++) { //Outer loop
    for (let i=0; i < 2; i++) { //Inner loop
        document.write("Hello World"); 
    }
}

JS không phải không sử dụng scope mà chúng chỉ tạo scope khi nó là một hàm, hay còn gọi là function scope.

Function Scope

Function scope là scope được tạo ra chỉ cho function đó sử dụng. Nó chính là mọi thứ nằm trong dấu {} của hàm.

var foo = "Goodbye";
var message = function() {
    var foo = "Hello";
    document.write(foo);
}

message(); //Hello
document.write(foo); //Goodbye

Đoạn code trên có 1 function scope được sử dụng cho hàm message. Biến foo trong hàm message và biến foo ngoài hàm đó là hoàn toàn khác nhau.

Nếu bạn sử dụng một biến trong function scope ở ngoài function đó thì sẽ báo lỗi ngay:

var message = function() {
    var foo = "Hello";
    document.write(foo);
}

message(); //Hello
console.log(foo); //error here 

Nhưng nếu bạn khai báo biến trong scope mà không có từ var thì js sẽ hiểu biến đó chính là global nên bạn vẫn có thể truy cập được ngoài scope.

var message = function() {
    foo = "Hello";
    document.write(foo);
}

message(); //Hello
document.write(foo); //Hello

 

Nếu bạn đã từng đọc best practice trong jQuery thì các chuyên gia khuyên bạn nên khai báo jQuery cách như sau:

(function($) {
    //Do things here - they are scoped
}(JQuery))

hoặc:

(function($) {
    //Do things here - they are scoped
})(JQuery)

 

Bạn có từng thắc mắc vì sao họ lại khuyên nên code như thế bao giờ chưa?

2 đoạn code trên là như nhau và nó được gọi là Immediately-Invoked Function Expression (IIFE) tức là nó được gọi thực thi ngay sau khi hàm được khai báo. Bản chất của 2 đoạn code trên là:

var rootFunction = function($) {// $ là tham số truyền vào function
    //Do things here - they are scoped
}

rootFunction(JQuery) // lời gọi function ở đây, JQuery là đối số tryền vào

 

Quay lại 2 đoạn code IIFE phía trên, phía cuối cùng họ sử dụng dấu () để gọi thực thi hàm, với đối số truyền vào là JQuery.

Trong JS, dấu () để gọi thực thi hàm.

Có dấu () khiến cho hàm khai báo ngay trước sẽ được thực thi ngay. Trong JQuery, $ là kiểu viết rút gọn của hàm JQuery. Nhưng $ cũng là cách viết rút gọn của nhiều thư viện JS khác (vd ProtoTypeJS). Để tránh nhầm lẫn giữa các biến $ của Jquery khai báo global và tránh xung đột giữa các biến $ của thư viện JS khác nếu bạn dùng nhiều thư viện JS cùng lúc thì bạn nên đặt mọi thứ vào scope.

Và đoạn code trên đã làm thế cho chúng ta :)))

Lexical Scope

Khi function A nằm trong function B mà trong function A có biến tham chiếu tới function B thì ta nói function A có lexical scopelexical scope của function A chính là scope của function A cộng với scope của function B.

VD:

function  init() {
  var name = 'hungdv'; // name is a local variable created by init
  function displayName() { // displayName() is the inner function, a closure
    alert(name); // use variable declared in the parent function    
  }
  displayName();    
}
init();

2. Closures

Closure là tập hợp gồm function và lexical scope của function đó.

Vì lexical scope là khái niệm đi với function nên có thể nói closure là function có lexical scope.

Ví dụ:

var createCallBack = function() { //First function
        var firstVar = 1;

        return function() { //Second function
            console.log("Log firstVar in second function:", firstVar);
            var secondVar = 2;

            return function() { //Third function
                console.log("Log secondVar in third function:", secondVar);
            }
        }
    }

Khi bạn khai báo một hàm trong hàm mà hàm đó có biến tham chiếu tới scope cha, ông thì hàm đó được gọi là closure. Ví dụ trên có second function có biến firstVar là biến nằm trong scope của hàm cha là first function nên seconde function là closure. Tương tự third function cũng là một closure.

Closure sử dụng biến là con trỏ tới biến thuộc scope cha chứ không phải copy biến của scope cha vào scope của mình. Ví dụ:

function say() {
  var num = 42;
  var say = function() { console.log(num); }
  num++;
  return say;
}

var sayNumber = say();
sayNumber(); // logs 43

Đoạn code trên sẽ log 43 chứ không phải 42 chứng tỏ closure sử dụng biến num chính là biến num của scope hàm say thứ nhất. Khi biến num của context này thay đổi thì kết qủa in ra màn hình cũng thay đổi theo.

Một số ví dụ kiểm tra.

VD1:

function sayHello(name) {
  var text = 'Hello ' + name;
  var say = function() { console.log(text); }
  say();
}
sayHello('Joe'); // Hello Joe

 

VD2:

function sayHello2(name) {
  var text = 'Hello ' + name; // Local variable
  var say = function() { console.log(text); }
  return say;
}

var say2 = sayHello2('Bob');
say2(); // logs "Hello Bob"

 

VD3:

function say667() {
  // Local variable that ends up within closure
  var num = 42;
  var say = function() { console.log(num); }
  num++;
  return say;
}

var sayNumber = say667();
sayNumber(); // logs 43

 

VD4:

var gLogNumber, gIncreaseNumber, gSetNumber;

function setupSomeGlobals() {
  var num = 42;
  // Store some references to functions as global variables
  gLogNumber = function() { console.log(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}

setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5

var oldLog = gLogNumber;// here will copy function and context

setupSomeGlobals();
gLogNumber(); // 42

oldLog() // 5

 

VD5:

function buildList(list) {
        var result = [];
        for (var i = 0; i < list.length; i++) {
            var item = 'item' + i;
            result.push( function() {console.log(item + ' ' + list[i])} );
        }
        return result;
    }

var fnlist = buildList([1,2,3]);

//Using j only to help prevent confusion -- could use i.
for (var j = 0; j < fnlist.length; j++) {
   fnlist[j]();
}

 

Đoạn code trên sẽ in ra "item2 undefined" 3 lần vì cả 3 closure đều sử dụng chung một tham chiếu tới item và biến i (Lúc này item đã là item2 và i đã có gía trị là 3).

Để đoạn code trên chạy theo ý muốn của bạn, chỉ cần đơn giản sửa closure sao cho mỗi closure sử dụng một scope riêng. Một trong vài cách đó là sử dụng từ khóa let giúp biến được đóng trong scope của dấu {} mà không cần nằm trong hàm:

function buildList(list) {
        var result = [];
        for (let i = 0; i < list.length; i++) {
            let item = 'item' + i;
            result.push( function() {console.log(item + ' ' + list[i])} );
        }
        return result;
    }

var fnlist = buildList([1,2,3]);

//Using j only to help prevent confusion -- could use i.
for (var j = 0; j < fnlist.length; j++) {
   fnlist[j]();
}

 

VD6:

function sayAlice() {
    var say = function() { console.log(alice); }
    var alice = 'Hello Alice';
    return say;
}

sayAlice()();// logs "Hello Alice"

 

Mọi biến trong js khi khai báo sẽ được đưa lên đầu scope (gía trị ban đầu là undefined ) và được gán gía trị tại câu lệnh gán của biến đó (variable hoisting). ví dụ:

function testHoisting() {
    console.log("a1:", a); // log undefined, not error
    var a = 3;
    console.log("a2:", a); // log 3
}

testHoisting();

 

VD7:

function newClosure(someNum, someRef) {
    // Local variables that end up within closure
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '; anArray: ' + anArray.toString() +
            '; ref.someVar: ' + ref.someVar + ';');
      }
}

obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;

 

Link tham khảo:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
https://medium.com/dailyjs/i-never-understood-javascript-closures-9663703368e8
http://doctrina.org/JavaScript:Why-Understanding-Scope-And-Closures-Matter.html
http://stackoverflow.com/questions/111102/how-do-javascript-closures-work