Cách hoạt động phạm vi biến (Scope) trong JavaScript

Javascript căn bản | by Học Javascript

Trong JavaScript, phạm vi biến (Scope) đóng vai trò quan trọng trong việc xác định nơi một biến có thể được truy cập và khi nào nó sẽ bị hủy. Việc hiểu rõ phạm vi biến giúp lập trình viên quản lý bộ nhớ hiệu quả, tránh lỗi xung đột biến, và viết code dễ bảo trì hơn.

JavaScript có nhiều loại phạm vi, bao gồm phạm vi toàn cục (Global Scope), phạm vi hàm (Function Scope), phạm vi khối (Block Scope), và còn có các khái niệm nâng cao như Lexical Scope và Closure. Ngoài ra, cơ chế hoisting cũng ảnh hưởng đến cách biến được sử dụng trong từng phạm vi.

Trong bài viết này, mình sẽ cùng tìm hiểu về các loại phạm vi biến trong JavaScript, cách chúng hoạt động, và cách áp dụng phạm vi biến một cách hiệu quả trong lập trình thực tế.

Phạm vi biến (Scope) trong JavaScript

Scope (phạm vi biến) trong JavaScript là vùng mà một biến có thể được truy cập và sử dụng. Nó xác định biến đó có thể được sử dụng ở đâu trong mã nguồn và khi nào nó bị hủy.

JavaScript có nhiều loại phạm vi khác nhau, bao gồm:

  • Phạm vi toàn cục (Global Scope) – Biến có thể được truy cập ở mọi nơi trong chương trình.
  • Phạm vi hàm (Function Scope) – Biến chỉ có thể được truy cập bên trong hàm nơi nó được khai báo.
  • Phạm vi khối (Block Scope) – Biến được khai báo trong một khối {} chỉ có thể được truy cập trong khối đó (áp dụng với letconst).
  • Lexical Scope (Phạm vi từ vựng) – Các hàm con có thể truy cập biến của hàm cha nơi chúng được khai báo.

Ảnh hưởng của phạm vi biến đến việc truy cập và quản lý biến

Phạm vi biến ảnh hưởng đến cách chúng ta có thể truy xuất và thay đổi giá trị của biến:

  • Biến toàn cục có thể truy cập từ bất kỳ đâu nhưng dễ bị thay đổi ngoài ý muốn.
  • Biến cục bộ trong hàm chỉ tồn tại trong hàm và giúp tránh xung đột với biến toàn cục.
  • Biến trong khối (block scope) với letconst giúp kiểm soát biến tốt hơn.
  • Phạm vi từ vựng (Lexical Scope) giúp hàm con truy cập được biến của hàm cha.

Ví dụ minh họa về phạm vi biến

Phạm vi toàn cục (Global Scope)

let globalVar = "Tôi là biến toàn cục";  

function showGlobalVar() {  
    console.log(globalVar); // Có thể truy cập biến toàn cục
}  

showGlobalVar(); // Output: Tôi là biến toàn cục  

console.log(globalVar); // Output: Tôi là biến toàn cục  

Phạm vi hàm (Function Scope)

function myFunction() {  
    let localVar = "Tôi là biến cục bộ";  
    console.log(localVar); // Truy cập được bên trong hàm
}  

myFunction();  

console.log(localVar); // Lỗi: localVar không được định nghĩa ngoài hàm  

Phạm vi khối (Block Scope) với let và const

{
    let blockVar = "Tôi là biến trong khối";  
    console.log(blockVar); // Truy cập được trong khối  
}  

console.log(blockVar); // Lỗi: blockVar không được định nghĩa ngoài khối  

Lexical Scope (Phạm vi từ vựng)

function outerFunction() {  
    let outerVar = "Tôi là biến của hàm cha";  

    function innerFunction() {  
        console.log(outerVar); // Truy cập được biến của hàm cha  
    }  

    innerFunction();  
}  

outerFunction(); // Output: Tôi là biến của hàm cha  

Các loại phạm vi biến trong JavaScript

JavaScript có nhiều loại phạm vi biến khác nhau, ảnh hưởng đến cách biến được truy cập và sử dụng. Dưới đây là chi tiết về từng loại phạm vi biến.

Phạm vi toàn cục (Global Scope)

Phạm vi toàn cục là phạm vi rộng nhất trong JavaScript. Một biến được khai báo bên ngoài tất cả các hàm hoặc khối {} sẽ thuộc phạm vi toàn cục. Biến này có thể được truy cập và thay đổi từ bất kỳ đâu trong chương trình.

Rủi ro khi sử dụng biến toàn cục

  • Gây xung đột biến: Nếu nhiều phần của chương trình sử dụng cùng một biến toàn cục, có thể xảy ra ghi đè giá trị không mong muốn.
  • Gây khó khăn khi debug: Khi nhiều hàm có thể thay đổi giá trị của biến toàn cục, việc tìm nguyên nhân lỗi trở nên phức tạp.
  • Chiếm tài nguyên không cần thiết: Biến toàn cục tồn tại trong toàn bộ vòng đời của chương trình, có thể gây lãng phí bộ nhớ.
let globalVar = "Tôi là biến toàn cục";  

function showGlobalVar() {  
    console.log(globalVar); // Truy cập được biến toàn cục
}  

showGlobalVar(); // Output: Tôi là biến toàn cục  

console.log(globalVar); // Output: Tôi là biến toàn cục  

Giải thích: Biến globalVar được khai báo ngoài hàm, nên có thể truy cập từ bất cứ đâu.

Phạm vi hàm (Function Scope)

Phạm vi hàm là phạm vi của một biến khi nó được khai báo bên trong một hàm bằng var, let hoặc const. Biến có phạm vi hàm chỉ có thể được truy cập bên trong hàm đó.

Đặc điểm quan trọng

  • Biến trong phạm vi hàm không thể truy cập từ bên ngoài hàm.
  • Khi hàm kết thúc, biến trong hàm sẽ bị xóa khỏi bộ nhớ (trừ khi được lưu trữ trong closure).
function myFunction() {  
    let localVar = "Tôi là biến cục bộ";  
    console.log(localVar); // Truy cập được bên trong hàm
}  

myFunction();  

console.log(localVar); //  Lỗi: localVar không được định nghĩa ngoài hàm  

Giải thích: localVar chỉ tồn tại trong myFunction(), nên khi gọi console.log(localVar) bên ngoài, JavaScript báo lỗi.

Phạm vi khối (Block Scope – let, const)

Phạm vi khối (Block Scope) chỉ áp dụng với biến khai báo bằng letconst. Một biến có phạm vi khối chỉ có thể được truy cập trong cặp {} nơi nó được khai báo.

So sánh với var

  • var không có block scope – Nếu khai báo trong {}, biến vẫn có thể truy cập bên ngoài.
  • letconst có block scope – Biến không thể truy cập ngoài {}.
{
    let blockVar = "Tôi là biến trong khối";  
    console.log(blockVar); //  Truy cập được bên trong khối  
}  

console.log(blockVar); //  Lỗi: blockVar không được định nghĩa ngoài khối  

Giải thích: blockVar chỉ tồn tại bên trong {}, nên khi gọi bên ngoài sẽ báo lỗi.

Khi dùng var:

{
    var testVar = "Biến với var";
}
console.log(testVar); // "Biến với var" (Không bị giới hạn phạm vi khối)

Cẩn thận: var có thể gây lỗi do không bị giới hạn trong {}!

Phạm vi của từ khóa var (Function Scope vs Block Scope)

var chỉ có phạm vi hàm, không có phạm vi khối

  • Nếu khai báo var trong hàm, nó chỉ có thể truy cập trong hàm đó.
  • Nếu khai báo var trong khối {}, nó vẫn có thể truy cập bên ngoài khối.
function varScopeExample() {
    var functionVar = "Tôi là biến phạm vi hàm";
    console.log(functionVar); // Truy cập được trong hàm
}

varScopeExample();
console.log(functionVar); //  Lỗi: functionVar không được định nghĩa ngoài hàm

Giải thích: var khi khai báo trong hàm có phạm vi hàm, giống như letconst.

Nhưng nếu khai báo var trong khối {}

{
    var blockTest = "Biến với var";
}
console.log(blockTest); //  "Biến với var" (Không bị giới hạn phạm vi khối)

Cẩn thận: var bị "rò rỉ" ra khỏi block scope. Điều này có thể gây lỗi khi viết mã phức tạp.

Sự khác biệt giữa var, let, const

Đặc điểm var let const
Hoisting Có (gán undefined) Có (không gán giá trị) Có (không gán giá trị)
Phạm vi Hàm Khối Khối
Có thể gán lại không? Không
Có thể khai báo lại không? Không Không

Hoisting và tác động đến phạm vi biến trong JavaScript

Hoisting là một cơ chế quan trọng trong JavaScript ảnh hưởng đến cách biến và hàm được sử dụng trước khi khai báo. Tuy nhiên, cách hoisting hoạt động với var, let, và const là khác nhau. Hãy cùng tìm hiểu chi tiết!

Định nghĩa Hoisting

Hoisting là một cơ chế trong JavaScript giúp "đưa lên trên" (hoist) khai báo biến và hàm trong phạm vi của chúng trước khi mã được thực thi. Điều này có nghĩa là bạn có thể sử dụng biến hoặc hàm trước khi khai báo chúng trong mã nguồn.

Nhưng hoisting hoạt động khác nhau với var, let, và const!

  • var: Biến được hoisted nhưng giá trị mặc định là undefined.
  • letconst: Cũng bị hoisted nhưng không được khởi tạo, dẫn đến lỗi nếu truy cập trước khi khai báo.
  • Hàm khai báo (function declaration): Được hoisted hoàn toàn, có thể gọi trước khi khai báo.

Biến khai báo bằng var được hoisting nhưng chưa được gán giá trị

Khi khai báo biến bằng var, JavaScript sẽ hoist khai báo lên đầu phạm vi, nhưng giá trị của biến sẽ là undefined cho đến khi nó thực sự được gán giá trị.

Ví dụ minh họa với var

console.log(a); //  Output: undefined  
var a = 10;  
console.log(a); // Output: 10  

Giải thích:

JavaScript sẽ hiểu mã trên như sau:

var a; // Hoisting đưa khai báo lên đầu
console.log(a); // undefined, vì chưa gán giá trị
a = 10; // Gán giá trị
console.log(a); // 10
  • Không báo lỗi, nhưng giá trị ban đầu là undefinedvar chỉ hoisted phần khai báo, không hoisted giá trị gán.

let và const cũng bị hoisting nhưng không thể sử dụng trước khi khai báo

Mặc dù letconst cũng bị hoisting, nhưng chúng không được khởi tạo mặc định như var. Nếu truy cập trước khi khai báo, sẽ nhận lỗi ReferenceError.

Ví dụ minh họa với letconst

console.log(b); //  ReferenceError: Cannot access 'b' before initialization  
let b = 20;  
console.log(b); //  Output: 20  

Giải thích:

  • b bị hoisted nhưng không được gán giá trị ban đầu → Gây lỗi khi truy cập trước khi khai báo.

Tương tự với const:

console.log(c); //  ReferenceError: Cannot access 'c' before initialization  
const c = 30;  
console.log(c); //  Output: 30  

const hoạt động như let, nhưng biến const phải được khởi tạo ngay khi khai báo.

Sự khác biệt giữa hoisting của var, let, const

Từ khóa Hoisting? Có giá trị mặc định? Có thể sử dụng trước khi khai báo?
var undefined Có, nhưng giá trị là undefined
let Không Không, gây ReferenceError
const Không Không, gây ReferenceError

Kết luận: Dùng letconst giúp tránh lỗi logic do hoisting của var.

Hoisting trong hàm

Hàm có hai cách khai báo:

  • Hàm khai báo (Function Declaration)
  • Hàm biểu thức (Function Expression)

Hoisting với Function Declaration

sayHello(); //  Output: "Xin chào!"  

function sayHello() {  
    console.log("Xin chào!");  
}  

Giải thích:

  • Hàm sayHello() được hoisted hoàn toàn → có thể gọi trước khi khai báo.

Hoisting với Function Expression (hàm gán vào biến)

sayHi(); //  TypeError: sayHi is not a function  

var sayHi = function () {  
    console.log("Xin chào!");  
};  

Giải thích:

  • sayHi bị hoisted nhưng giống như var, nó có giá trị undefined ban đầu.
  • Vì vậy, khi gọi sayHi(), JavaScript hiểu là undefined(), gây lỗi.

Lexical Scope (Phạm vi từ vựng) và Closure trong JavaScript

Phạm vi từ vựng (Lexical Scope) và Closure là hai khái niệm quan trọng giúp hiểu cách JavaScript xử lý biến và phạm vi trong các hàm. Chúng giúp tổ chức mã tốt hơn, tránh lỗi truy cập biến và tối ưu hóa bộ nhớ.

Lexical Scope là gì?

Lexical Scope (Phạm vi từ vựng) là phạm vi biến được xác định dựa trên vị trí khai báo của biến trong mã nguồn. Khi một hàm được khai báo bên trong một hàm khác, nó có thể truy cập các biến của hàm cha, nhưng ngược lại, hàm cha không thể truy cập biến của hàm con.

 function outerFunction() {
    let outerVar = "Biến trong outerFunction";

    function innerFunction() {
        console.log(outerVar); // Truy cập được outerVar
    }

    innerFunction();
}

outerFunction();

Giải thích:

  • innerFunction() có thể truy cập outerVar vì nó nằm trong phạm vi của outerFunction().
  • Nhưng outerFunction() không thể truy cập biến trong innerFunction().

Closure là gì?

Closure là một hàm có thể "ghi nhớ" phạm vi tại thời điểm nó được tạo ra, ngay cả khi nó được gọi bên ngoài phạm vi đó.

Ví dụ Closure lưu trữ trạng thái

function counter() {
    let count = 0; // Biến được "ghi nhớ" trong closure

    return function () {
        count++; // Tăng giá trị count
        console.log(count);
    };
}

const increment = counter(); // Gán closure vào biến
increment(); // Output: 1
increment(); // Output: 2
increment(); // Output: 3

Giải thích:

  • Hàm counter() trả về một hàm con.
  • Biến count không bị mất đi sau mỗi lần gọi increment().
  • Closure giúp lưu trữ trạng thái của biến mà không làm rò rỉ ra phạm vi toàn cục.

Strict Mode và ảnh hưởng đến phạm vi biến trong JavaScript

use strict là gì?

"use strict"; là một chế độ giúp kiểm tra lỗi chặt chẽ hơn trong JavaScript, giúp tránh các lỗi phổ biến liên quan đến phạm vi biến.

Ảnh hưởng của use strict

Không thể sử dụng biến chưa khai báo.

"use strict";
x = 10; //  ReferenceError: x is not defined

Không thể khai báo trùng tên tham số trong hàm.

"use strict";
function test(a, a) { //  SyntaxError
    console.log(a);
}

Không thể gán giá trị cho biến chỉ đọc (read-only).

"use strict";
Object.defineProperty(window, "PI", { value: 3.14, writable: false });

PI = 3.1415; // TypeError: Cannot assign to read-only property
  • Phát hiện lỗi sớm.
  • Ngăn chặn khai báo biến vô tình làm ảnh hưởng đến phạm vi toàn cục.
  • Cải thiện hiệu suất bằng cách giúp trình thông dịch tối ưu mã tốt hơn.

Ứng dụng thực tế của phạm vi biến trong JavaScript

Phạm vi biến không chỉ là một khái niệm lý thuyết, mà nó có ảnh hưởng lớn đến cách tổ chức mã nguồn trong dự án thực tế.

Tránh xung đột biến toàn cục

Vấn đề: Khi nhiều file JavaScript được tải vào một dự án, nếu tất cả sử dụng biến toàn cục, có thể xảy ra xung đột biến.

Giải pháp: Sử dụng phạm vi cục bộ để bọc mã bằng IIFE (Immediately Invoked Function Expression).

(function () {
    let localVar = "Biến cục bộ";
    console.log(localVar); //  Không ảnh hưởng đến phạm vi toàn cục
})();
console.log(localVar); //  ReferenceError: localVar is not defined

Tạo biến private bằng Closure

Closure giúp tạo biến private để bảo vệ dữ liệu khỏi bị thay đổi từ bên ngoài.

function createAccount() {
    let balance = 1000; // Biến private

    return {
        deposit: function (amount) {
            balance += amount;
            console.log(`Số dư: ${balance}`);
        },
        withdraw: function (amount) {
            if (amount > balance) {
                console.log("Không đủ tiền!");
            } else {
                balance -= amount;
                console.log(`Số dư: ${balance}`);
            }
        }
    };
}

const myAccount = createAccount();
myAccount.deposit(500); //  Số dư: 1500
myAccount.withdraw(2000); //  Không đủ tiền!

Lợi ích:

  • Biến balance chỉ có thể thay đổi thông qua deposit()withdraw().
  • Tránh thay đổi biến ngoài ý muốn từ bên ngoài.

Kết bài

Phạm vi biến (Scope) là một trong những khái niệm cốt lõi trong JavaScript, ảnh hưởng trực tiếp đến cách biến được truy cập và quản lý trong chương trình. Hiểu rõ về phạm vi toàn cục, phạm vi hàm, phạm vi khối, hoisting, lexical scope và closure sẽ giúp bạn viết mã hiệu quả, tối ưu hiệu suất và tránh các lỗi không mong muốn.

Bên cạnh đó, việc áp dụng Strict Mode giúp kiểm soát lỗi chặt chẽ hơn, trong khi Closure giúp tạo biến private và bảo vệ dữ liệu. Trong thực tế, việc sử dụng phạm vi biến một cách hợp lý giúp tránh xung đột biến toàn cục, cải thiện bảo mật và tối ưu bộ nhớ.

Việc nắm vững phạm vi biến không chỉ giúp bạn hiểu sâu hơn về JavaScript mà còn giúp bạn trở thành một lập trình viên chuyên nghiệp, có khả năng viết mã chặt chẽ, tối ưu và dễ bảo trì.

Bài viết liên quan