Site logo
Authors
  • avatar Nguyễn Đức Xinh
    Name
    Nguyễn Đức Xinh
    Twitter
Published on
Published on

Tìm hiểu chi tiết về var, const, let trong JavaScript: Sự khác biệt và cách sử dụng

Tổng quan về cách khai báo biến trong JavaScript

Trong JavaScript, việc khai báo biến là một trong những hoạt động cơ bản nhất mà mọi lập trình viên cần thực hiện. Tuy nhiên, JavaScript cung cấp ba cách khác nhau để khai báo biến: var, let, và const. Mỗi cách khai báo có những đặc điểm, phạm vi, và hành vi khác nhau. Hiểu rõ về sự khác biệt giữa chúng không chỉ giúp bạn viết code sạch hơn mà còn tránh được nhiều lỗi tiềm ẩn.

Trước khi ECMAScript 2015 (ES6) ra đời, var là cách duy nhất để khai báo biến trong JavaScript. Với ES6, hai từ khóa mới là letconst được giới thiệu, mang đến những cách mới để xử lý biến, giúp code trở nên chặt chẽ và dễ đoán hơn. Bài viết này sẽ đi sâu vào phân tích cả ba cách khai báo, giúp bạn hiểu rõ khi nào nên sử dụng mỗi loại.

var - Cách khai báo biến truyền thống

var là cách khai báo biến đầu tiên và đã tồn tại từ khi JavaScript ra đời. Tuy nhiên, nó có một số đặc điểm mà đôi khi có thể gây ra các lỗi khó lường trong ứng dụng lớn.

Cú pháp và cách sử dụng var

var message = "Xin chào";
var count = a5;
var isActive = true;

// Khai báo nhiều biến
var x = 10, y = 20, z = 30;

// Khai báo mà không gán giá trị
var result; // giá trị là undefined

Đặc điểm của var

1. Function scope

Biến được khai báo bằng var có phạm vi (scope) là function chứa nó, hoặc global scope nếu được khai báo ngoài function.

function demoVarScope() {
    var message = "Bên trong function";
    console.log(message); // "Bên trong function"
}

demoVarScope();
// console.log(message); // Lỗi: message is not defined

// Ví dụ về global scope
var globalVar = "Tôi là biến toàn cục";
function accessGlobal() {
    console.log(globalVar); // "Tôi là biến toàn cục"
}

2. Hoisting

var có đặc tính hoisting - nghĩa là khai báo biến được "đưa lên" đầu phạm vi của nó, nhưng không phải giá trị.

console.log(hoistedVar); // undefined (không lỗi)
var hoistedVar = "Tôi đã được hoisted";
console.log(hoistedVar); // "Tôi đã được hoisted"

// Tương đương với:
var hoistedVar;          // Khai báo được hoisted
console.log(hoistedVar); // undefined
hoistedVar = "Tôi đã được hoisted"; // Gán giá trị
console.log(hoistedVar); // "Tôi đã được hoisted"

3. Có thể khai báo lại

Biến var có thể được khai báo lại mà không gây lỗi:

var user = "Nam";
console.log(user); // "Nam"

var user = "Hoa"; // Không có lỗi khi khai báo lại
console.log(user); // "Hoa"

4. Không có block scope

var không có phạm vi khối (block scope), nghĩa là nó có thể được truy cập từ bên ngoài các khối lệnh như if, for, while:

if (true) {
    var blockVar = "Tôi nằm trong block";
}
console.log(blockVar); // "Tôi nằm trong block" - truy cập được từ bên ngoài block

for (var i = 0; i < 3; i++) {
    // Xử lý gì đó
}
console.log(i); // 3 - biến i vẫn tồn tại và có thể truy cập sau vòng lặp

Những vấn đề với var

Đặc tính không có block scope và có thể khai báo lại của var có thể dẫn đến những lỗi khó phát hiện:

var counter = 10;

// Sau một đoạn code dài...

// Vô tình khai báo lại biến counter
var counter = 0; // Không gây lỗi, nhưng ghi đè giá trị trước đó

// Nếu bạn không biết về khai báo trước đó, điều này có thể gây bug

Vấn đề này đặc biệt nghiêm trọng trong các ứng dụng lớn hoặc khi làm việc với nhiều thư viện.

let - Giải pháp thay thế hiện đại cho var

Từ khóa let được giới thiệu trong ES6 để khắc phục một số vấn đề của var, đặc biệt là vấn đề về phạm vi.

Cú pháp và cách sử dụng let

let message = "Xin chào";
let count = 5;
let isActive = true;

// Khai báo nhiều biến
let x = 10, y = 20, z = 30;

// Khai báo mà không gán giá trị
let result; // giá trị là undefined

Đặc điểm của let

1. Block scope

Khác với var, biến được khai báo bằng let có phạm vi là khối lệnh (block) gần nhất chứa nó, bao gồm các khối như if, for, while, và các cặp ngoặc nhọn đơn giản.

if (true) {
    let blockScoped = "Tôi chỉ tồn tại trong block này";
    console.log(blockScoped); // "Tôi chỉ tồn tại trong block này"
}
// console.log(blockScoped); // Lỗi: blockScoped is not defined

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

2. Hoisting có giới hạn

let cũng được hoisted, nhưng khác với var, biến let không được khởi tạo với giá trị undefined. Nếu bạn cố gắng truy cập biến trước khi nó được khai báo, bạn sẽ gặp lỗi "Temporal Dead Zone" (TDZ).

// console.log(tdz); // Lỗi: Cannot access 'tdz' before initialization
let tdz = "Temporal Dead Zone demo";

3. Không thể khai báo lại

Không thể khai báo lại biến let trong cùng một phạm vi:

let user = "Nam";
// let user = "Hoa"; // Lỗi: Identifier 'user' has already been declared

// Tuy nhiên, có thể khai báo lại trong phạm vi khác
if (true) {
    let user = "Hoa"; // Hợp lệ, đây là biến khác trong phạm vi khác
    console.log(user); // "Hoa"
}
console.log(user); // "Nam"

4. Có thể cập nhật giá trị

Biến được khai báo bằng let có thể được gán lại giá trị:

let counter = 1;
counter = 2; // Hợp lệ
console.log(counter); // 2

const - Khai báo hằng số không thay đổi

const cũng được giới thiệu trong ES6 và được sử dụng để khai báo các biến mà giá trị không thay đổi sau khi được gán.

Cú pháp và cách sử dụng const

const PI = 3.14159;
const APP_NAME = "My JavaScript App";
const IS_DEVELOPMENT = true;

// Khai báo object hoặc array với const
const user = { name: "Nam", age: 30 };
const colors = ["red", "green", "blue"];

Đặc điểm của const

1. Block scope

Giống như let, biến const có phạm vi là khối lệnh (block).

if (true) {
    const MAX_SIZE = 100;
    console.log(MAX_SIZE); // 100
}
// console.log(MAX_SIZE); // Lỗi: MAX_SIZE is not defined

2. Hoisting giống let

const cũng có hoisting giống như let, với Temporal Dead Zone.

// console.log(API_KEY); // Lỗi: Cannot access 'API_KEY' before initialization
const API_KEY = "abc123xyz";

3. Không thể khai báo lại

Giống let, không thể khai báo lại biến const trong cùng một phạm vi.

const ENV = "production";
// const ENV = "development"; // Lỗi: Identifier 'ENV' has already been declared

4. Không thể gán lại giá trị

Điểm khác biệt chính giữa constlet là biến const không thể được gán lại giá trị sau khi khai báo:

const API_VERSION = "v1";
// API_VERSION = "v2"; // Lỗi: Assignment to constant variable

Tuy nhiên, cần lưu ý rằng const chỉ ngăn chặn việc gán lại biến, chứ không phải làm cho giá trị của nó bất biến.

5. Không bất biến với objects và arrays

Với các kiểu dữ liệu tham chiếu như objects và arrays, const chỉ ngăn chặn việc gán lại biến đó cho một object/array khác, nhưng không ngăn chặn việc thay đổi thuộc tính hoặc phần tử bên trong:

const user = { name: "Nam", age: 30 };
user.age = 31; // Hợp lệ
user.role = "Developer"; // Hợp lệ, thêm thuộc tính mới
console.log(user); // { name: "Nam", age: 31, role: "Developer" }

// Nhưng không thể gán lại biến user
// user = { name: "Hoa", age: 25 }; // Lỗi: Assignment to constant variable

const numbers = [1, 2, 3];
numbers.push(4); // Hợp lệ
numbers[0] = 0; // Hợp lệ
console.log(numbers); // [0, 2, 3, 4]

// Nhưng không thể gán lại biến numbers
// numbers = [5, 6, 7]; // Lỗi: Assignment to constant variable

Để tạo object hoặc array bất biến thực sự, bạn cần sử dụng các phương thức như Object.freeze():

const immutableUser = Object.freeze({ name: "Nam", age: 30 });
// immutableUser.age = 31; // Không gây lỗi nhưng cũng không thay đổi giá trị trong strict mode
console.log(immutableUser); // { name: "Nam", age: 30 }

So sánh var, let, và const

Để dễ so sánh, hãy xem bảng tổng hợp các đặc điểm chính của ba cách khai báo:

Đặc điểm var let const
Phạm vi (Scope) Function scope Block scope Block scope
Hoisting Có, được khởi tạo là undefined Có, nhưng có Temporal Dead Zone Có, nhưng có Temporal Dead Zone
Có thể khai báo lại Không (trong cùng block) Không (trong cùng block)
Có thể gán lại giá trị Không
Khởi tạo khi khai báo Không bắt buộc Không bắt buộc Bắt buộc

Ví dụ minh họa sự khác biệt

// Phạm vi
function scopeExample() {
    var varScoped = "function scoped";
    let letScoped = "block scoped";
    const constScoped = "also block scoped";
    
    if (true) {
        var varInBlock = "still function scoped";
        let letInBlock = "block scoped";
        const constInBlock = "also block scoped";
        
        console.log(varScoped);   // Truy cập được
        console.log(letScoped);   // Truy cập được
        console.log(constScoped); // Truy cập được
    }
    
    console.log(varInBlock);   // Truy cập được
    // console.log(letInBlock);   // Lỗi
    // console.log(constInBlock); // Lỗi
}

// Hoisting
function hoistingExample() {
    console.log(hoistedVar);  // undefined, không lỗi
    // console.log(hoistedLet);  // Lỗi: Cannot access before initialization
    // console.log(hoistedConst); // Lỗi: Cannot access before initialization
    
    var hoistedVar = "var is hoisted";
    let hoistedLet = "let has TDZ";
    const hoistedConst = "const has TDZ too";
}

// Khai báo lại
function redeclarationExample() {
    var user = "Nam";
    var user = "Hoa"; // Hợp lệ
    
    let age = 25;
    // let age = 26; // Lỗi: already declared
    
    const PI = 3.14;
    // const PI = 3.14159; // Lỗi: already declared
}

// Gán lại giá trị
function reassignmentExample() {
    var count = 1;
    count = 2; // Hợp lệ
    
    let score = 10;
    score = 20; // Hợp lệ
    
    const MAX = 100;
    // MAX = 200; // Lỗi: Assignment to constant variable
}

Loop và Closure: Sự khác biệt quan trọng

Một trong những trường hợp phổ biến thể hiện sự khác biệt giữa varlet là khi làm việc với vòng lặp và closures:

// Sử dụng var trong vòng lặp với setTimeout
function demoVarLoop() {
    for (var i = 0; i < 3; i++) {
        setTimeout(function() {
            console.log("var i =", i);
        }, 100);
    }
}
demoVarLoop();
// Output:
// var i = 3
// var i = 3
// var i = 3

// Sử dụng let trong vòng lặp với setTimeout
function demoLetLoop() {
    for (let i = 0; i < 3; i++) {
        setTimeout(function() {
            console.log("let i =", i);
        }, 100);
    }
}
demoLetLoop();
// Output:
// let i = 0
// let i = 1
// let i = 2

Trong ví dụ trên, khi sử dụng var, tất cả các hàm callback đều tham chiếu đến cùng một biến i (có phạm vi function), và khi các callback được thực thi, giá trị của i đã là 3. Ngược lại, khi sử dụng let, mỗi lần lặp tạo ra một biến i mới với phạm vi block, nên mỗi callback tham chiếu đến một biến i khác nhau với giá trị tương ứng từ 0 đến 2.

Khi nào nên sử dụng var, let, và const

Trong JavaScript hiện đại, có một số hướng dẫn tốt nhất về việc sử dụng các từ khóa khai báo biến:

Ưu tiên sử dụng const

  • Sử dụng const làm mặc định cho tất cả các khai báo biến
  • Điều này đảm bảo bạn không thể vô tình gán lại biến
  • Giúp code rõ ràng hơn: khi nhìn thấy const, bạn biết biến đó sẽ không bị gán lại
const API_URL = "https://api.example.com";
const MAX_RETRY = 3;
const user = fetchUser();

Sử dụng let khi cần gán lại

  • Sử dụng let khi bạn biết rằng giá trị của biến sẽ thay đổi
  • Phổ biến trong các vòng lặp, counters, flags
let counter = 0;
let isLoading = true;

// Sau đó
counter++;
isLoading = false;

Hạn chế sử dụng var

  • Trong code hiện đại, hầu như không có lý do gì để sử dụng var
  • Chỉ sử dụng var khi bạn cần hỗ trợ các trình duyệt cũ không hỗ trợ ES6 mà không sử dụng transpilers như Babel

Nguyên tắc chung

  1. Luôn khởi tạo biến khi khai báo
  2. Sử dụng const trừ khi bạn cần gán lại biến
  3. Nếu cần gán lại, sử dụng let
  4. Hạn chế phạm vi của biến càng nhỏ càng tốt
  5. Đặt tất cả khai báo ở đầu phạm vi (block hoặc function)
// Tốt
function processData(data) {
    const userId = data.id;
    const username = data.name;
    let processedCount = 0;
    let hasErrors = false;
    
    // Xử lý data
}

// Không tốt
function processData(data) {
    const userId = data.id;
    // code...
    const username = data.name;
    // code...
    let processedCount = 0;
    // Xử lý
    let hasErrors = false;
}

Các vấn đề phổ biến và cách giải quyết

1. Temporal Dead Zone (TDZ)

TDZ là nguồn gốc của nhiều lỗi khi làm việc với letconst. Để tránh lỗi, hãy luôn khai báo biến trước khi sử dụng:

// Lỗi
function processTDZ() {
    console.log(value); // Error: Cannot access 'value' before initialization
    let value = 42;
}

// Đúng
function processCorrect() {
    let value = 42;
    console.log(value); // 42
}

2. Vấn đề với vòng lặp và closure

Như đã thấy ở ví dụ trước, sử dụng var trong vòng lặp kết hợp với closures có thể gây ra lỗi khó phát hiện. Luôn sử dụng let trong các vòng lặp:

// Vấn đề với var
const actions = [];
for (var i = 0; i < 3; i++) {
    actions.push(function() {
        console.log(i);
    });
}
actions.forEach(function(action) {
    action(); // Logs: 3, 3, 3
});

// Giải pháp với let
const betterActions = [];
for (let i = 0; i < 3; i++) {
    betterActions.push(function() {
        console.log(i);
    });
}
betterActions.forEach(function(action) {
    action(); // Logs: 0, 1, 2
});

3. Hiểu sai về tính bất biến của const

Nhiều developers mới hiểu sai rằng const làm cho giá trị hoàn toàn bất biến:

const user = { name: "Nam", role: "Developer" };

// Điều này vẫn hợp lệ:
user.role = "Team Lead";
user.department = "Engineering";

// Để tạo object thực sự bất biến:
const immutableUser = Object.freeze({ name: "Nam", role: "Developer" });
// immutableUser.role = "Team Lead"; // Không thay đổi trong strict mode

Đối với objects phức tạp hơn, bạn có thể cần deep freeze:

function deepFreeze(obj) {
    Object.freeze(obj);
    for (const prop in obj) {
        if (obj.hasOwnProperty(prop) && 
            typeof obj[prop] === 'object' && 
            obj[prop] !== null &&