PHẠM VI BIẾN (SCOPE) & CƠ CHẾ KHAI BÁO BIẾN TRONG JAVASCRIPT FOR PYTHON DEVELOPERS

1. Mục tiêu bài học

Sau khi hoàn thành bài học này, bạn sẽ:

  • Hiểu sự khác biệt về Scope: Nắm rõ sự khác biệt giữa Function Scope (Python/JS var) và Block Scope (JS let/const).

  • Làm chủ 3 từ khóa khai báo: Phân biệt chính xác khi nào dùng const, let, và lý do tại sao nên loại bỏ var trong JS hiện đại.

  • Giải mã hiện tượng Hoisting: Hiểu cách JavaScript Engine biên dịch và khởi tạo biến khác với cờ thông dịch từng dòng của Python.

  • Tránh bẫy Scope trong Vòng lặp: Khắc phục lỗi biến toàn cục/phạm vi hàm phổ biến khi viết hàm bất đồng bộ hoặc callback.

2. So sánh Tư duy Scope: Python vs. JavaScript

Trong Python, phạm vi của biến được quyết định bởi Hàm (Function), Lớp (Class), hoặc Module (Quy tắc LEGB: Local, Enclosing, Global, Built-in). Cấu trúc khối như if, for, while trong Python không tạo ra scope mới.

Trong JavaScript hiện đại (ES6+), ngoài Function Scope, chúng ta có Block Scope (Phạm vi khối {}): bất kỳ cặp ngoặc nhọn nào (if, for, while, hoặc thậm chí {} đứng độc lập) đều tạo ra một vùng phạm vi riêng biệt cho let và const.

Bảng So sánh Cơ chế Khai báo

Tiêu chí Python (Default) JS var (ES5) JS let (ES6+) JS const (ES6+)
Phạm vi (Scope) Function / Module Function Scope Block Scope {} Block Scope {}
Gán lại giá trị (Re-assignment) Có Có Có Không (Hằng số)
Khai báo lại (Re-declaration) Có (Ghi đè) Có (Dễ gây bug) Lỗi SyntaxError Lỗi SyntaxError
Hoisting Behavior Không (NameError) Undefined Temporal Dead Zone (TDZ) Temporal Dead Zone (TDZ)

3. Phân tích Chi tiết & Mã nguồn Minh họa

3.1. Block Scope vs Function Scope

Hãy xem cách vòng lặp for và câu lệnh if xử lý biến trong hai ngôn ngữ.

  • Trường hợp Python: Biến i và x "lọt" ra ngoài khối for/if.

# Python
if True:
    x = "Hello Python"

for i in range(3):
    pass

print(x)  # In ra: Hello Python (x vẫn tồn tại ngoài khối if)
print(i)  # In ra: 2 (i vẫn tồn tại ngoài vòng lặp)

Trường hợp JavaScript với let/const (Chuẩn ES6+): Biến bị cô lập bên trong {}.

// JavaScript (ES6+)
if (true) {
    let x = "Hello JS";
    const y = "Permanent Value";
}
// console.log(x); // Uncaught ReferenceError: x is not defined
// console.log(y); // Uncaught ReferenceError: y is not defined

for (let i = 0; i < 3; i++) {
    // i chỉ tồn tại trong vòng lặp này
}
// console.log(i); // Uncaught ReferenceError: i is not defined

Trường hợp JS Cổ điển (var - Tránh dùng): var bỏ qua Block Scope {} và hoạt động giống Python.

if (true) {
    var z = "I am global or function scoped";
}
console.log(z); // In ra: "I am global or function scoped" (var bị "lọt" ra ngoài block)

3.2. Đột biến Giá trị (Mutability) với const

Một hiểu lầm phổ biến của lập trình viên Python: const trong JS không hoàn toàn giống việc tạo một giá trị không thể thay đổi (Immutable).

Quy tắc: const ngăn cản việc gán lại tái chỉ định vùng nhớ (Re-binding), chứ không ngăn cản việc thay đổi nội dung bên trong (Mutation) của Object hoặc Array.

// 1. Với kiểu dữ liệu Nguyên thủy (Primitive Data Types: number, string, boolean)
const pi = 3.14159;
// pi = 3.14; // TypeError: Assignment to constant variable.

// 2. Với Kiểu Dữ liệu Tham chiếu (Reference Types: Object, Array) - Tương đương dict, list trong Python
const user = {
    name: "Tuan",
    role: "Admin"
};

// CÓ THỂ thay đổi thuộc tính bên trong:
user.role = "Superadmin"; // Hợp lệ!
user.age = 30;            // Hợp lệ!

// KHÔNG THỂ gán lại bằng một Object hoàn toàn mới:
// user = { name: "Nam" }; // TypeError: Assignment to constant variable.

const colors = ["red", "green"];
colors.push("blue"); // Hợp lệ!

Để tạo một Object thực sự Immutable trong JS (tương tự tuple hoặc MappingProxyType trong Python), ta dùng Object.freeze():

const config = Object.freeze({
    apiEndpoint: "https://api.example.com",
    timeout: 5000
});
// config.timeout = 10000; // Bị bỏ qua ở Sloppy mode hoặc ném lỗi trong Strict mode

3.3. Hiện tượng Hoisting & Temporal Dead Zone (TDZ)

Trong Python, bạn không thể truy cập biến trước dòng mã nó được định nghĩa:

# Python
print(a)  # NameError: name 'a' is not defined
a = 10

Trong JavaScript, cơ chế Hoisting sẽ nâng phần khai báo biến lên đầu Scope trong đoạn dịch mã (Compile Phase). Tuy nhiên, cách xử lý giữa var và let/const hoàn toàn khác nhau:

// --- BÀI TÁN 1: Hoisting với `var` ---
console.log(varVariable); // In ra: undefined (Không báo lỗi crash chương trình!)
var varVariable = "Python devs hate this";

/* JS Engine thực tế hiểu là:
   var varVariable; // Khai báo được nâng lên đầu, giá trị mặc định là undefined
   console.log(varVariable);
   varVariable = "Python devs hate this";
*/

// --- BÀI TÁN 2: Hoisting với `let` / `const` ---
console.log(letVariable); // Uncaught ReferenceError: Cannot access 'letVariable' before initialization
let letVariable = "Modern JS Safety";

Temporal Dead Zone (TDZ): Với let và const, biến vẫn được hoisting, nhưng nó nằm trong "Vùng chết theo thời gian" từ đầu Block cho đến khi dòng mã gán giá trị được thực thi. Bất kỳ thao tác truy cập nào vào biến trong vùng TDZ đều bắn ra lỗi ReferenceError ngay lập tức. Điều này giúp JS hành động an toàn và dự đoán được giống như Python.

4. Tóm tắt Quy tắc Thực hành Best Practices

  1. Mặc định sử dụng const cho mọi khai báo biến.

  2. Chỉ chuyển sang let khi bạn biết chắc biến đó sẽ cần gán lại giá trị (ví dụ: biến đếm vòng lặp, biến tích lũy).

  3. Tuyệt đối KHÔNG dùng var trong mã JS hiện đại để tránh các lỗi rò rỉ Scope ngoài ý muốn.

5. Câu hỏi Suy ngẫm & Bài tập Thực hành

Bài tập 1: Phân tích Output (Trắc nghiệm tư duy)

Đoạn mã JavaScript sau đây sẽ in ra kết quả gì? Hãy giải thích dựa trên cơ chế Block Scope.

let count = 10;

function compute() {
    if (true) {
        let count = 5;
        count++;
        console.log("In-block count:", count);
    }
    console.log("Function count:", count);
}

compute();
console.log("Global count:", count);

Bài tập 2: Sửa lỗi Scope trong Vòng lặp Bất đồng bộ

Một lập trình viên Python viết đoạn mã JS dưới đây với mục tiêu: Sau mỗi 1 giây, in ra số thứ tự tăng dần từ 0 đến 2. Tuy nhiên, đoạn mã chạy ra kết quả 3, 3, 3.

// Mã lỗi:
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log("Index:", i);
    }, 1000);
}

Yêu cầu:

  1. Giải thích tại sao var lại gây ra lỗi in ra 3, 3, 3.

  2. Hãy sửa lại đoạn mã trên chỉ bằng cách thay đổi đúng 1 từ khóa khai báo biến để chương trình in đúng 0, 1, 2.

Đáp án gợi ý:

  • Bài tập 1:

    • Output:

      Plaintext
      In-block count: 6
      Function count: 10
      Global count: 10
      
    • Giải thích: Biến count = 5 bên trong if là một biến hoàn toàn độc lập nhờ Block Scope (let), nó không làm ảnh hưởng hay ghi đè lên biến count = 10 ở bên ngoài.

  • Bài tập 2:

    1. Nguyên nhân: Do var có Function Scope, chỉ có duy nhất 1 ô nhớ i được tạo ra và dùng chung cho cả 3 lần lặp. Sau 1000ms, khi hàm setTimeout chạy, vòng lặp for đã kết thúc và giá trị cuối cùng của i lúc này là 3.

    2. Cách sửa: Thay var i = 0 bằng let i = 0.

for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log("Index:", i);
    }, 1000);
}

Cơ chế: Với let, mỗi lần lặp (iteration) JS Engine sẽ tạo ra một Block Scope mới và gắn giữ (bind) giá trị i riêng biệt cho lần lặp đó.