- Authors
- Name
- Nguyễn Đức Xinh
- 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à let
và const
đượ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 const
và let
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 | Có | Không (trong cùng block) | Không (trong cùng block) |
Có thể gán lại giá trị | Có | Có | 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 var
và let
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
- Luôn khởi tạo biến khi khai báo
- Sử dụng
const
trừ khi bạn cần gán lại biến - Nếu cần gán lại, sử dụng
let
- Hạn chế phạm vi của biến càng nhỏ càng tốt
- Đặ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 let
và const
. Để 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 &&