Function in Javascript Part 2: Scope and Nested Scope

Trong Part 1, tôi đã giới thiệu về cách khai báo hàm. Bài này tôi sẽ giới thiệu về phạm vi của parameter, biến local (biến cục bộ) và biến global (biến toàn cục).
1. Scope of Variable – Phạm vi của biến
Một tính chất quan trọng của hàm là biến được tạo ra bên trong hàm, kể cả các parameters, là biến cục bộ của hàm. Điều này có nghĩa là, uhm, như trong ví dụ sau:

var square = function(x){
    var result = x * x;
    return result;
};

Trong ví dụ trên thì biến result sẽ được tạo ra mỗi khi hàm được gọi, và mấy cái biến mới tạo này không liên quan gì đến nhau.

Tính cục bộ của biến chỉ được áp dụng với parameter và các biến được tạo ra trong hàm bằng keyword var (kể cả kiểu khai báo function square(x){…} do nó chỉ là cách viết ngắn hơn).

Đoạn code sau sẽ mô tả tính cục bộ của biến:

var x = "outside";

var f1 = function() {
    var x = "inside f1";
};
 
f1();
console.log(x);
 
//outside
var f2 = function() {
    x = "inside f2";
};
f2();
console.log(x);
// . inside f2

Cả 2 hàm f1 và f2 đều gán giá trị cho một biến x. Tuy nhiên hàm f1 tạo ra biến x mới và biến x này chỉ có giá trị bên trong thân hàm và do đó chỉ thay đổi biến x được tạo ra trong thân hàm. Hàm f2 không khai báo biến x do đó biến x trong hàm f2 được hiểu là biến x được khai báo ở đầu đoạn code.

Tính chất này của hàm giúp chúng ta hạn chế được lỗi khi code. Nếu 1 biến được dùng trong cả chương trình, chúng ta sẽ rất mất công để chắc chắn rằng không có hàm nào được đặt trùng tên nhau. Bằng cách giới hạn phạm vi cho biến local trong hàm, js giúp chúng ta dễ dàng đọc và hiểu một hàm mà không phải lo lắng về phần code còn lại.

2. Nested Scope
Javascript không chỉ có sự khác biệt giữa biến local và biến global. Một hàm có thể được tạo ra bên trong một hàm, tạo ra nhiều cấp phạm vi khác nhau.
Ví dụ sau đây in ra một bức tranh phong cảnh :v

var landscape = function() {
    var result = "";
    var flat = function(size) {
        for (var count = 0; count < size; count++){
            result += "_";
        }
    };
    var mountain = function(size) {
        result += "/";
        for (var count = 0; count < size; count++){
            result += "' ";
            result += "\\";
        }
     
    };
    flat(3);
    mountain(4);
    flat(6);
    mountain(1);
    flat(1);
    return result;
};
console.log(landscape());

Kết quả:

 ___/””\______/’\_

Chú ý: Ai vẽ đẹp vui lòng cung cấp ví dụ khác.
Tôi có một hàm lớn lanscape, cùng 2 hàm được khai báo bên trong nó: flatmountain. Hai hàm này có thể “nhìn thấy” biến result, nhưng không thể nhìn thấy biến count của nhau vì chúng không ở trong phạm vi của nhau. (Chú ý trong js các biến trong vòng for vẫn có thể được dùng ngoài vòng for). Vậy là những gì ở ngoài hàm f1 không thể “nhìn trộm” những gì bên trong hàm f1 và ngược lại. Cách tiếp cận với phạm vi của biến như thế này được gọi là lexical scoping

Những người đã từng tiếp xúc với những ngôn ngữ khác có thể sẽ nghĩ rằng một block ({…}) có thể tạo ra một phạm vi mới. Nhưng trong js, function là thứ duy nhất có thể tạo ra một phạm vi mới. Bạn có thể tạo ra một block tự do như thế này:

var something = 1;
{
    var something = 2;
    // Do stuff with variable something...
}
// Outside of the block again...

Nhưng hãy lưu ý là biến something bên trong block trỏ đến cùng một chỗ với biến something bên ngoài block. Trên thực tế, mặc dù các block kiểu này được cho phép, chúng chỉ được dùng để nhóm các câu lệnh trong thân lệnh if và thân vòng for

3. But Wait…The let keyword
À vâng tí quên, ECMAScript 6 giới thiệu keyword let. let làm việc như var nhưng nếu bạn khai báo biến bằng keyword này, nó sẽ có phạm vi trong block chứ không phải hàm.

Ví dụ khi tôi khai báo vòng for thế này:

//count có thể được gọi ở đây
for (var count = 0; count < size; count++){
    //count ở đây
}
//hay là chỗ này

nhưng khi tôi khai báo thế này:

//đéo gọi được count
for (let count = 0; count < size; count++){
    //ở đây vẫn gọi được, may quá
}
//ơ chỗ này cũng đéo gọi được

Vâng như vậy là let nó sẽ tuân theo phạm vi của cái block {…} chứ không theo cái function. Rất đơn giản phải không ạ?
Bạn nào muốn tìm hiểu thêm thì có thể vào link này.

4. Declaration Notation
Chú ý là khi tôi viết code như thế này:

console.log(square(1));
function square(x){
    //...
};

Thì chương trình vẫn chạy bình thường mặc dù hàm square được khai báo ở bên dưới dòng code sử dụng nó. Điều này xảy ra bởi vì việc khai báo hàm không phải một phần của flow “từ trên xuống dưới”. Về mặt khái niệm, nó sẽ được chuyển lên trên cùng trong phạm vi hoạt động của nó và có thể được sử dụng trong toàn bộ phạm vi đó. Việc khai báo hàm không phải một phần của flow “từ trên xuống dưới” đôi khi hữu dụng vì nó cho phép chúng ta sắp xếp code theo một cách “dễ hiểu” mà không phải lo lắng về việc phải “định nghĩa trước khi sử dụng”.
Phạm vi trên cùng là sao? Là nếu tôi viết nó trong hàm khác thế này:

function test(){
    console.log(square(1));
    function square(x){
        //...
    };
}

Thì phạm vi cao nhất của hàm square, dĩ nhiên là bên trong hàm test. Rất dễ hiểu đúng không ạ? Nếu không hiểu vui lòng để lại thắc mắc ở phần comment ạ.

Chú ý
Với việc khai báo hàm một cách “tự do” như thế này, chắc hẳn bạn sẽ nghĩ đến việc khai báo nó trong một lệnh if để “thỏa mãn” nhu cầu cá nhân như thế này chẳng hạn:

function example() {
    function a() {} // Okay
    if (something) {
        function b() {} // Danger!
    }
}

Đừng làm vậy! Các nền tảng Javascript khác nhau trong các trình duyệt khác nhau thường sẽ xử lý khác nhau trong tình huống này, và nền tảng mới nhất cấm hoàn toàn điều này. Nếu bạn muốn một chương trình ổn định, hãy khai báo hàm ở bên ngoài cùng trong một function khác.

2 Likes
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?