Scope (phạm vi) và Scope Chain (chuỗi phạm vi) là những khái niệm quan trọng trong JavaScript để quản lý biến và cách chúng có thể được truy cập tại các vị trí khác nhau trong chương trình. Hiểu rõ hai khái niệm này giúp bạn tránh các lỗi liên quan đến biến và có thể tối ưu hóa mã của mình một cách hiệu quả.

Scope trong JavaScript là gì?

Scope là ngữ cảnh hoặc phạm vi mà các biến, hàm, và đối tượng có thể được truy cập. Trong JavaScript, có ba loại phạm vi chính:

  1. Global Scope (Phạm vi toàn cục)
  2. Function Scope (Phạm vi hàm)
  3. Block Scope (Phạm vi khối)

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

Các biến được khai báo ở ngoài tất cả các hàm hoặc khối mã thuộc về phạm vi toàn cục (global scope). Các biến toàn cục có thể được truy cập ở bất kỳ đâu trong mã.

Ví dụ:

var globalVar = "I'm global";

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

showGlobal(); // "I'm global"
console.log(globalVar); // "I'm global"

Trong ví dụ này, globalVar là biến toàn cục và có thể được truy cập từ bất kỳ đâu.

2. Function Scope (Phạm vi hàm)

Các biến được khai báo bên trong một hàm chỉ có thể được truy cập từ bên trong hàm đó. Đây là phạm vi hàm, và các biến này được gọi là biến cục bộ (local variables).

Ví dụ:

function myFunction() {
    var localVar = "I'm local";
    console.log(localVar); // Có thể truy cập biến cục bộ
}

myFunction(); // "I'm local"
console.log(localVar); // Lỗi: localVar không được định nghĩa

Ở đây, localVar là biến cục bộ và chỉ có thể được truy cập bên trong hàm myFunction. Bên ngoài hàm, nó không tồn tại.

3. Block Scope (Phạm vi khối)

Với từ khóa letconst (giới thiệu trong ES6), các biến khai báo bên trong một cặp dấu {} (block) có phạm vi chỉ tồn tại trong khối đó. Điều này khác với var, vì var không có phạm vi khối, mà chỉ có phạm vi hàm.

Ví dụ:

if (true) {
    let blockScopedVar = "I'm block-scoped";
    console.log(blockScopedVar); // "I'm block-scoped"
}

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

Trong ví dụ này, blockScopedVar chỉ tồn tại trong khối if và không thể được truy cập từ bên ngoài.

Sự khác biệt giữa var, letconst:

  • var: Có phạm vi hàm hoặc phạm vi toàn cục. Không có phạm vi khối.
  • letconst: Có phạm vi khối và không bị nâng (hoisting) giống như var.

Scope Chain (Chuỗi phạm vi)

Scope Chain là cơ chế cho phép JavaScript tra cứu các biến trong các phạm vi khác nhau. Khi một biến được truy cập, JavaScript sẽ kiểm tra phạm vi hiện tại để xem biến đó có tồn tại hay không. Nếu không tìm thấy, nó sẽ tiếp tục kiểm tra các phạm vi bao quanh (gọi là chuỗi phạm vi) cho đến khi tìm thấy biến hoặc gặp phải phạm vi toàn cục.

Nói cách khác, Scope Chain là chuỗi các đối tượng phạm vi mà JavaScript sử dụng để tìm kiếm các biến. Nó bắt đầu từ phạm vi hiện tại và tiếp tục đến phạm vi toàn cục.

Ví dụ về Scope Chain:

var globalVar = "I'm global";

function outerFunction() {
    var outerVar = "I'm outer";

    function innerFunction() {
        var innerVar = "I'm inner";
        console.log(innerVar);   // Có thể truy cập innerVar
        console.log(outerVar);   // Có thể truy cập outerVar qua scope chain
        console.log(globalVar);  // Có thể truy cập globalVar qua scope chain
    }

    innerFunction();
}

outerFunction();

Giải thích:

  1. innerFunction có thể truy cập được biến innerVar từ phạm vi cục bộ (phạm vi hàm).
  2. Nếu không tìm thấy biến, nó kiểm tra phạm vi bên ngoài, trong trường hợp này là phạm vi của outerFunction, để truy cập biến outerVar.
  3. Nếu vẫn không tìm thấy biến, nó tiếp tục tìm trong phạm vi toàn cục và truy cập biến globalVar.

Hoạt động của Scope Chain:

  • Khi một biến được yêu cầu, JavaScript kiểm tra phạm vi hiện tại.
  • Nếu không tìm thấy, nó đi ngược lên chuỗi phạm vi (scope chain) cho đến khi gặp phạm vi toàn cục.
  • Nếu không có biến nào phù hợp được tìm thấy trong cả chuỗi, JavaScript sẽ ném lỗi.

Ví dụ minh họa chuỗi Scope Chain:

let a = 'global';

function outer() {
    let b = 'outer';
    
    function inner() {
        let c = 'inner';
        console.log(a); // Tìm trong phạm vi hiện tại (inner), không có, tìm tiếp ở outer, vẫn không có, tìm ở global -> 'global'
        console.log(b); // Tìm trong phạm vi hiện tại (inner), không có, tìm tiếp ở outer -> 'outer'
        console.log(c); // Tìm thấy ở phạm vi hiện tại (inner) -> 'inner'
    }

    inner();
}

outer();

Lexical Scoping

JavaScript sử dụng Lexical Scoping, nghĩa là phạm vi của các biến được xác định dựa trên vị trí của chúng trong mã nguồn. Khi JavaScript được biên dịch, các phạm vi được xác định theo thứ tự của mã. Điều này có nghĩa là phạm vi không thay đổi khi chương trình chạy, nó đã được xác định từ trước dựa trên cấu trúc của mã.

Hoisting và Scope

Hoisting là một khái niệm trong JavaScript mà các khai báo biến (var, let, const) và hàm được “đưa lên đầu” (hoisted) phạm vi hiện tại của chúng trước khi mã được thực thi. Tuy nhiên, chỉ có phần khai báo được hoisting, không phải phần gán giá trị.

Ví dụ với var:

console.log(x); // undefined
var x = 5;

Ở đây, biến x được hoisting, nhưng giá trị của nó chưa được gán, vì vậy kết quả là undefined.

Với letconst, hoisting vẫn xảy ra, nhưng chúng không được khởi tạo cho đến khi lệnh gán giá trị được thực hiện:

console.log(y); // Lỗi: y không được định nghĩa
let y = 10;

Tổng kết

  • Scope trong JavaScript là không gian nơi các biến và hàm có thể được truy cập. Các loại scope chính gồm: Global Scope, Function Scope, và Block Scope.
  • Scope Chain là chuỗi phạm vi mà JavaScript duyệt qua để tìm kiếm biến khi chúng được sử dụng.
  • Các biến có thể được hoisting, nhưng hành vi này thay đổi giữa var, let, và const.