Scope, closure, this và tổ chức bộ nhớ trong javascript

Học hỏi chính là kinh nghiệm. Những thứ khác chỉ là thông tin.
-- Albert Einstein

Xin chào mọi người, hôm nay mình xin chia sẻ một số kiến thức về scope, closure và tổ chức bộ nhớ trong js mà mình tìm hiểu, tổng kết được.
Mọi người có thể đọc trên blog của mình tại đây.

Đố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õ. Với mình thì js là một ngôn ngữ lập trình khó, nếu không nắm rõ cách tổ chức bộ nhớ thì sẽ gặp phải những bug nhiều khi rất 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. Về bản chất, các bạn chỉ cần nắm rõ scope và một số chú ý là sẽ tránh được một số lỗi mà ko biết vì sao lỗi :)))

Sau đây mình xin đi vào cụ thể từng mục:

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 từ "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?

Hai đ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, kí hiệu $ 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 :)))

Function Invocation (Gọi thực thi hàm)

Function Invocation xảy ra khi gọi một hàm nào đó bằng cách sử dụng dấu (). Cách gọi thực thi hàm như thế được gọi là Function Invocation Pattern.

Ví dụ:

var add = function(num1, num2) {
    console.log(num1 + num2);
}

add(2, 3); // 5

Lại xét ví dụ sau:

var createCallBack = function() { //First function
        console.log("Here is in first function");

        return function() { //Second function
            console.log("Here is in second function");

            return function() { //Third function
                console.log("Here is in third function");
            }
        }
    }

createCallBack;// caller 1
createCallBack();// caller 2
window.onload = createCallBack; // caller 3
window.onload = createCallBack(); // caller 4
window.onload = createCallBack()();// caller 5

Nhớ là gọi caller 1 tới caller 5 từng lượt một chứ không phải gọi cùng lúc như trên nhé
Theo bạn tương ứng với mỗi câu lệnh caller 1 -> caller 5 thì kết quả in ra console là gì?

Nếu bạn thử kiểm tra từng câu lệnh trên bằng trình duyệt thì hãy gọi từng câu lệnh một chứ đừng gọi cả 5 câu lệnh cùng lúc như trên vì js là xử lý bất đồng bộ (assynchronus) nên sẽ không in ra đúng thứ tự cho bạn đâu.

  • caller 1 sẽ chẳng in ra cho bạn cái gì cả, vì hàm createCallBack đã được gọi thực thi đâu.

  • caller 2 sẽ in ra cho bạn "Here is in first function"

  • caller 3 sẽ in ra cho bạn "Here is in first function". Nhiều người sẽ thắc mắc vì sao caller 1caller 3 trông giống nhau mà kết qủa lại khác nhau?
    caller 3 là hàm createCallBack được truyền vào dưới dạng callback của event (sự kiện trên trình duyệt) nên khi event đó (ở vd trên là window.onload) được trigger thì hàm callback truyền vào sẽ tự động được thực thi.

Mọi hàm callback được truyền vào event thì sẽ được tự động thực thi ngay khi event đó trigger.

  • caller 4 sẽ in ra 2 dòng "Here is in first function" và "Here is in second function". Như bạn thấy caller 4 chỉ là thêm dấu () vào sau caller 3, nghĩa là nó thực thi caller 3 xong rồi thực thi tiếp hàm mà caller 3 trả về (vd trên là second function)

  • caller 5 giải thích tương tự caller 4, kết qủa in ra là "Here is in first function", "Here is in second function" và "Here is in third function".

Thế còn thế này thì sao?:

var createCallBack = function() { //First function
        console.log("first function");

        return function() { //Second function
            console.log("second function");

            return function() { //Third function
                console.log("third function");
            }
        }
    }

window.onload = function() { createCallBack; }; // caller 6
window.onload = function() { createCallBack(); }; // caller 7

Nhắc lại là mọi hàm callback được truyền vào event thì sẽ được tự động thực thi ngay khi event đó trigger.

Nghĩa là 2 function caller 6caller 7 trên đều được thực thi ngay khi window load, thực thi ngay ở đây nghĩa là thực thi nội dung trong dấu {} của 2 function truyền vào cho event window.onload.

  • caller 6 sẽ không in ra cái gì cả vì hàm createCallBack không được gọi thực thi.

  • caller 7 sẽ in ra "first function" vì hàm createCallBack được gọi thực thi bằng cách thêm dấu ()

Closures

Closure là hàm có tham chiếu tới biến nằm ở scope ngoài hàm đó.

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 second function là closure. Tương tự third function cũng là một closure.

Khi một closure được tạo nó sẽ có 2 thành phần là nội dung hàm (function body)bối cảnh (context), context chính là nơi mà closure được tạo ra.
Nếu closure tham chiếu tới biến ở hàm cha thì context chính là scope của hàm cha, nếu closure tham chiếu tới hàm ông thì context chính là scope của hàm ông.

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 quả in ra màn hình cũng thay đổi theo.

Từ khóa "this" với Function Invocation.

Xét ví dụ:

var value = 500; //Global variable
var obj = {
    value: 0,
    increment: function() {
        this.value++;

        var innerFunction = function() {
            alert(this.value);
        }

        innerFunction(); //Function invocation pattern
    }
}
obj.increment(); //Method invocation pattern

Bạn nghĩ đoạn code trên sẽ alert ra kết qủa nào?

Gọi hàm kiểu Function invocation pattern (gọi trực tiếp bằng cách thêm dấu () ) thì từ khóa this trong hàm đó luôn là global object (window).

Vì vậy đoạn code trên sẽ alert ra 500 chứ không phải 1 như nhiều người nhầm lẫn.

Để khắc phục lỗi trên thì có một cách đơn giản là copy biến this vào that khi khai báo hàm và sử dụng biến that này.

var value = 500; //Global variable
    var obj = {
        value: 0,
        increment: function() {
            var that = this;
            this.value++;// or that.value++

            var innerFunction = function() {
                alert(that.value);
            }

            innerFunction(); //Function invocation pattern
        }
    }

obj.increment();// 1

Constructor Invocation Pattern.

Constructor Invocation Pattern là cách gọi hàm bằng cách thêm từ khóa new phía trước.

Ví dụ:

var createCallBack = function() { //First function
        console.log("first function");

        return new function() { //Second function
            console.log("second function");

            return function() { //Third function
                console.log("third function");
            }
        }
    }

window.onload = createCallBack;

Ví dụ trên giống với ví dụ ở phía trên với lời gọi caller 3. Nhưng lời gọi này ở phía trên chỉ in ra "first function" còn ở ví dụ này là "first function" và "second function". Vì sao?

Câu trả lời nằm ở từ khóa new đặt trước second function.
Từ khóa new chứng tỏ second function đã được gọi thực thi ngay (Constructor Invocation Pattern).

Ví dụ trên có thể được viết lại như sau:

var createCallBack = function() { //First function
        console.log("first function");

        var secondFunction = function() { //Second function
            console.log("second function");

            return function() { //Third function
                console.log("third function");
            }
        }

        return secondFunction();
    }

window.onload = createCallBack;

Hàm được gọi bằng kỹ thuật Constructor Invocation Pattern (dùng từ khóa new) sẽ trả về:

  • Nếu hàm có return các kiểu đơn như number, string, boolean, null hoặc undefined thì giá trị trả về sẽ bị bỏ đi và trả về this (là object được tạo ra từ từ khóa new).

  • Nếu hàm có return là một object (là mọi thứ trừ các kiểu đơn), thì object này sẽ được return thay vì this.

Với ví dụ trên thì second function sẽ return third function.

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 hoặc context 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 (giá trị ban đầu là undefined ) và được gán giá 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:
http://doctrina.org/JavaScript:Why-Understanding-Scope-And-Closures-Matter.html
http://doctrina.org/Javascript-Function-Invocation-Patterns.html#fi
http://stackoverflow.com/questions/111102/how-do-javascript-closures-work

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Đào Văn Hùng

4 bài viết.
13 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
14 0
Cuộc đời là đóa hoa mà tình yêu là mật ngọt. Victor Hugo Các bạn có thể đọc bài viết gốc tại (Link) 1. Nonblocking I/O Trong javascript, hầu ...
Đào Văn Hùng viết 3 tháng trước
14 0
White
12 5
Nếu cho tôi 6 tiếng để đốn hạ một cái cây, tôi sẽ dành 4 tiếng đầu tiên để mài rìu. Abraham Lincoln Bạn có thể đọc bài gốc tại (Link) Khi bắt đ...
Đào Văn Hùng viết 4 tháng trước
12 5
White
11 0
Không gì đẹp bằng nụ cười khi đôi mắt e lệ nhìn xuống. Victor Hugo Các bạn có thể đọc bài viết gốc tại (Link) Tò mò khi tìm hiểu hoạt động của ...
Đào Văn Hùng viết 3 tháng trước
11 0
Bài viết liên quan
White
39 7
Tăng sức mạnh cho javascript với lodash Lần này mình sẽ giới thiệu 1 thư viện javascript vô cùng bá đạo có tên là "lodash]1]", có thể nói nó là LI...
Huy Hoàng Phạm viết hơn 2 năm trước
39 7
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
4 bài viết.
13 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!