Skip to content

JavaScript 变量、常量与声明:存储数据的基石

在程序的世界里,我们需要一个地方来存放各种数据——就像在现实生活中,我们用便签纸记录待办事项,用笔记本记录想法,用标签标记物品。在 JavaScript 中,变量和常量就扮演着这样的角色,它们是存储数据的容器。

什么是变量?

变量(Variable)就像是一个贴了标签的盒子,你可以在盒子里放东西,也可以随时更换盒子里的内容。标签是变量名,盒子里的内容是变量值。

javascript
// 创建一个变量就像准备一个盒子
let userName = "Emily";

// 可以查看盒子里的内容
console.log(userName); // "Emily"

// 可以更换盒子里的内容
userName = "David";
console.log(userName); // "David"

三种声明方式:var、let、const

JavaScript 提供了三种声明变量的关键字,它们就像三种不同类型的容器,各有特点。

var——旧时代的遗留物

var 是 JavaScript 最早的变量声明方式,在 ES6(2015 年)之前,它是唯一的选择。但由于存在一些问题,现在已经不推荐使用了。

javascript
// 使用 var 声明变量
var age = 25;
var city = "New York";
var isStudent = true;

console.log(age, city, isStudent); // 25 "New York" true

// var 可以重复声明同名变量(这是个问题!)
var age = 30; // 不会报错,但容易引起混乱
console.log(age); // 30

var 的主要问题:

  1. 函数作用域:声明的变量只在函数内有效,没有块级作用域
  2. 变量提升:声明会被提升到作用域顶部,可能导致混乱
  3. 可重复声明:同名变量可以被重复声明,容易出错
javascript
// 问题示例 1:没有块级作用域
if (true) {
  var message = "Hello";
}
console.log(message); // "Hello" - 变量泄露到外部!

for (var i = 0; i < 3; i++) {
  // 循环体
}
console.log(i); // 3 - 循环变量泄露!

// 问题示例 2:变量提升
console.log(fruit); // undefined - 不会报错,但结果令人困惑
var fruit = "apple";

// 上面的代码实际上被解释为:
var fruit;
console.log(fruit);
fruit = "apple";

let——现代的变量声明

let 是 ES6 引入的新关键字,它解决了 var 的大部分问题。就像给盒子加了明确的使用范围,让管理更加规范。

javascript
// 使用 let 声明变量
let userName = "Alice";
let userAge = 28;
let isActive = true;

// 可以修改 let 声明的变量
userName = "Bob";
userAge = 29;
isActive = false;

console.log(userName, userAge, isActive); // "Bob" 29 false

let 的优势:

  1. 块级作用域:变量只在声明的代码块内有效
  2. 不能重复声明:同一作用域内不能声明同名变量
  3. 暂时性死区:在声明前不能使用变量
javascript
// 优势 1:块级作用域
if (true) {
  let message = "Hello";
  console.log(message); // "Hello"
}
console.log(message); // ReferenceError: message is not defined

for (let i = 0; i < 3; i++) {
  console.log(i); // 0, 1, 2
}
console.log(i); // ReferenceError: i is not defined

// 优势 2:不能重复声明
let score = 100;
let score = 200; // SyntaxError: Identifier 'score' has already been declared

// 优势 3:暂时性死区(Temporal Dead Zone,TDZ)
console.log(color); // ReferenceError: Cannot access 'color' before initialization
let color = "blue";

const——常量的守护者

const 也是 ES6 引入的,用于声明常量。就像用强力胶封死的盒子,一旦放进东西,就不能再更换了。

javascript
// 使用 const 声明常量
const PI = 3.14159;
const API_URL = "https://api.example.com";
const MAX_USERS = 100;

// 尝试修改 const 变量会报错
PI = 3.14; // TypeError: Assignment to constant variable

console.log(PI); // 3.14159

const 的特点:

  1. 必须初始化:声明时必须赋值
  2. 不能重新赋值:值不能被改变
  3. 块级作用域:与 let 相同
  4. 对象属性可变:如果是对象,对象的属性仍可修改
javascript
// 特点 1:必须初始化
const name;  // SyntaxError: Missing initializer in const declaration
const name = "John";  // 正确

// 特点 2:不能重新赋值
const age = 25;
age = 26;  // TypeError: Assignment to constant variable

// 特点 4:对象属性可变(重要!)
const user = {
  name: "Sarah"
  age: 25
};

// 不能重新赋值整个对象
user = { name: "Tom", age: 30 };  // TypeError

// 但可以修改对象的属性
user.age = 26;
user.city = "London";
console.log(user);  // { name: "Sarah", age: 26, city: "London" }

// 同样适用于数组
const numbers = [123];
numbers.push(4);  // 可以修改数组内容
numbers[0] = 100;  // 可以修改元素
console.log(numbers);  // [100, 2, 3, 4]

numbers = [567];  // TypeError: Assignment to constant variable

如何使用 Object.freeze 创建真正的常量

如果你想让对象完全不可变,可以使用 Object.freeze():

javascript
const config = Object.freeze({
  apiUrl: "https://api.example.com"
  timeout: 5000
});

// 尝试修改会静默失败(非严格模式)或报错(严格模式)
config.apiUrl = "https://new-api.example.com";
config.newProp = "value";

console.log(config);
// { apiUrl: "https://api.example.com", timeout: 5000 }
// 没有任何改变

// 严格模式下会报错
("use strict");
const settings = Object.freeze({ theme: "dark" });
settings.theme = "light"; // TypeError: Cannot assign to read only property

变量命名规则

给变量起名字就像给宠物起名字,需要遵循一些规则,同时也有一些约定俗成的最佳实践。

必须遵守的规则

javascript
// 1. 只能包含字母、数字、下划线($)和美元符号($)
let userName = "valid";
let user_name = "valid";
let $user = "valid";
let _user = "valid";

// 2. 不能以数字开头
let 1user = "invalid";  // SyntaxError
let user1 = "valid";

// 3. 不能使用 JavaScript 保留字
let let = "invalid";      // SyntaxError
let const = "invalid";    // SyntaxError
let function = "invalid"; // SyntaxError
let if = "invalid";       // SyntaxError

// 4. 区分大小写
let name = "John";
let Name = "Sarah";
let NAME = "Michael";
// 这是三个不同的变量!

最佳实践

javascript
// 1. 使用驼峰命名法(camelCase)
let userName = "John";
let userAge = 25;
let isActive = true;
let getUserInfo = function () {};

// 2. 常量使用大写+下划线
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = "https://api.example.com";
const DEFAULT_TIMEOUT = 5000;

// 3. 使用有意义的名称
// ❌ 不好的命名
let x = "John";
let d = new Date();
let arr = [123];

// ✅ 好的命名
let userName = "John";
let currentDate = new Date();
let userScores = [123];

// 4. 布尔值用 is/has/can 等前缀
let isLoggedIn = true;
let hasPermission = false;
let canEdit = true;

// 5. 对于私有变量,可以用下划线前缀(约定)
let _privateVar = "private";
let _internalCounter = 0;

作用域理解

作用域(Scope)决定了变量的可访问范围,就像房子里的不同房间有不同的权限范围。

全局作用域

在任何函数或代码块之外声明的变量具有全局作用域,可以在代码的任何地方访问。

javascript
// 全局作用域
let globalVar = "I am global";
const GLOBAL_CONST = 100;

function test() {
  console.log(globalVar); // 可以访问
  console.log(GLOBAL_CONST); // 可以访问
}

if (true) {
  console.log(globalVar); // 可以访问
}

test();

函数作用域

在函数内部声明的变量只能在函数内部访问。

javascript
function myFunction() {
  let localVar = "I am local";

  console.log(localVar); // 可以访问
}

myFunction();
console.log(localVar); // ReferenceError: localVar is not defined

块级作用域

letconst 声明的变量具有块级作用域,只在声明它们的代码块(用 {} 包围的区域)内有效。

javascript
if (true) {
  let blockVar = "I am in a block";
  const BLOCK_CONST = 200;

  console.log(blockVar); // 可以访问
  console.log(BLOCK_CONST); // 可以访问
}

console.log(blockVar); // ReferenceError
console.log(BLOCK_CONST); // ReferenceError

// 循环中的块级作用域
for (let i = 0; i < 3; i++) {
  // 每次循环 i 都是新的变量
  setTimeout(() => console.log(i), 100);
}
// 输出: 0, 1, 2

// 对比 var 的行为
for (var j = 0; j < 3; j++) {
  // 所有回调共享同一个 j
  setTimeout(() => console.log(j), 100);
}
// 输出: 3, 3, 3

变量提升(Hoisting)

变量提升是 JavaScript 的一个特性,变量和函数声明会被移动到作用域的顶部。理解这个概念很重要,虽然现代代码很少依赖它。

var 的提升

javascript
console.log(myVar); // undefined
var myVar = "Hello";

// 上面的代码实际执行顺序:
var myVar; // 声明被提升
console.log(myVar); // undefined
myVar = "Hello"; // 赋值留在原处

let 和 const 的提升

letconst 也会提升,但不会初始化,导致"暂时性死区"。

javascript
console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
let myLet = "Hello";

// 暂时性死区(TDZ)
let x = 1;
{
  // 这个块中,x 在声明前不可用
  console.log(x); // ReferenceError
  let x = 2; // 这里的 x 是新变量,遮蔽了外部的 x
}

选择使用哪种声明方式

这是一个实际开发中的重要决策,以下是推荐的选择策略:

javascript
// 1. 默认使用 const
const userName = "Alex";
const userAge = 30;
const config = { theme: "dark" };

// 2. 只有当变量需要重新赋值时才使用 let
let counter = 0;
counter++;
counter++;

let currentPage = 1;
currentPage = 2;

// 3. 避免使用 var(除非需要兼容老代码)
// 不推荐
var oldStyleVar = "avoid this";

// 实际示例:循环
// ✅ 推荐
for (let i = 0; i < 10; i++) {
  console.log(i);
}

// ❌ 不推荐
for (var j = 0; j < 10; j++) {
  console.log(j);
}

// 实际示例:配置对象
// ✅ 推荐
const appConfig = {
  name: "MyApp"
  version: "1.0.0"
};

// 可以修改属性
appConfig.version = "1.0.1";

// 但不能重新赋值
// appConfig = {};  // TypeError

声明但不赋值

变量可以先声明,后赋值。

javascript
// let 可以声明后再赋值
let userName;
console.log(userName);  // undefined

userName = "Jessica";
console.log(userName);  // "Jessica"

// const 必须在声明时赋值
const userAge;  // SyntaxError: Missing initializer in const declaration

// 正确做法
const userAge = 25;

多变量声明

可以在一条语句中声明多个变量。

javascript
// 分别声明(推荐,更清晰)
let firstName = "Tom";
let lastName = "Smith";
let age = 35;

// 一起声明(紧凑但可读性稍差)
let x = 1
  y = 2
  z = 3;

// 混合声明不同类型
let name = "Laura"
  score = 95
  isPassed = true
  grade; // 可以不初始化

// const 也可以
const PI = 3.14
  E = 2.718
  GOLDEN_RATIO = 1.618;

常见问题与解决方案

问题 1:const 对象属性可以修改吗?

答案:可以修改属性,但不能重新赋值整个对象。

javascript
const person = {
  name: "Anna"
  age: 28
};

// ✅ 可以修改属性
person.age = 29;
person.city = "Paris";

// ❌ 不能重新赋值
person = { name: "Bob" }; // TypeError

// 如果需要完全冻结对象
const frozenPerson = Object.freeze({
  name: "Charlie"
  age: 40
});

frozenPerson.age = 41; // 静默失败(非严格模式)
console.log(frozenPerson.age); // 仍然是 40

问题 2:为什么循环中推荐用 let?

答案:let 在每次循环中创建新的绑定,而 var 共享同一个变量。

javascript
// 使用 let(正确)
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 输出 0, 1, 2
  }, 1000);
}

// 使用 var(问题)
for (var j = 0; j < 3; j++) {
  setTimeout(() => {
    console.log(j); // 输出 3, 3, 3
  }, 1000);
}

// var 的问题解决方案(使用闭包)
for (var k = 0; k < 3; k++) {
  (function (index) {
    setTimeout(() => {
      console.log(index); // 输出 0, 1, 2
    }, 1000);
  })(k);
}

问题 3:未声明的变量会怎样?

答案:直接使用未声明的变量会创建全局变量(非严格模式)或报错(严格模式)。

javascript
// 非严格模式
function test() {
  undeclaredVar = "Oops!"; // 意外创建全局变量
}

test();
console.log(undeclaredVar); // "Oops!" - 全局污染!

// 严格模式(推荐)
("use strict");
function strictTest() {
  undeclaredVar = "Error!"; // ReferenceError: undeclaredVar is not defined
}

总结

变量和常量是 JavaScript 编程的基础,选择正确的声明方式能让代码更安全、更易维护。

本节要点回顾:

  • var:函数作用域,有变量提升,不推荐使用
  • let:块级作用域,可重新赋值,现代开发的首选
  • const:块级作用域,不可重新赋值,用于常量和不变的引用
  • 最佳实践:默认使用 const,需要改变时使用 let,避免 var
  • 命名规则:使用有意义的名称,遵循驼峰命名法
  • 作用域:理解全局、函数和块级作用域的区别
  • const 声明的对象属性可以修改,但对象引用不能改变