JavaScript 变量声明
在 JavaScript 中,我们通过使用特定的关键字来声明变量:var、let 和 const。
这三个关键字中的每一个都有独特的特性,影响着变量的行为,包括它的作用域(在哪里可以被访问)以及它的值是否可以被改变。
1. 理解 JavaScript 中的变量
1.1 什么是变量?
从本质上讲,JavaScript 中的变量是赋予内存位置的符号名称,该位置保存着一个值。
把它想象成一个贴有标签的盒子。你可以把不同的物品(值)放进盒子里,并且随时可以更改盒子里的物品。盒子上的标签(变量名)允许你在整个程序中引用那个特定的物品。
例如,当你问候某人时,你可能会说 "你好,Alex!" 或 "你好,Sarah!"。在这里,"Alex" 和 "Sarah" 是变化的信息片段,而 "你好,!" 是静态部分。变量允许你的程序存储 "Alex" 或 "Sarah",然后将其插入到问候语中。
// 一个名为 'userName' 的变量,存储字符串值 "Alex"
let userName = "Alex";
console.log("你好, " + userName + "!"); // 输出: 你好, Alex!
// 现在 'userName' 存储一个不同的值
userName = "Sarah";
console.log("你好, " + userName + "!"); // 输出: 你好, Sarah!1.2 为什么我们需要变量?
变量之所以必不可少,有几个原因:
- 存储数据: 它们允许你的程序记住信息片段,无论是用户输入、计算结果还是配置设置。
- 让代码动态化: 你可以使用变量,而不是重复写入相同的值。如果值需要改变(比如用户的得分),你只需要更新变量的值一次,代码中引用该变量的所有部分都会使用新值。
- 可读性: 给变量起有意义的名字(例如 userName 而不是 x)会让你的代码更容易理解,无论是对你自己还是对其他开发者。
- 可复用性: 一旦数据存储在变量中,它就可以在脚本中多次复用,而无需重新输入该值。
2. 使用 var, let, 和 const 声明变量
JavaScript 提供了三个关键字来声明变量:var、let 和 const。虽然它们都用于创建变量,但在作用域、提升(Hoisting)行为以及变量是否可以被重新赋值或重新声明方面存在显著差异。理解这些差异对于现代 JavaScript 开发至关重要。
2.2 var 关键字(旧式/传统)
在 ES6(ECMAScript 2015)引入之前,var 关键字是 JavaScript 中声明变量的唯一方式。虽然它仍然可以使用,但 var 的一些行为可能会导致意想不到的问题,这就是为什么引入 let 和 const 作为更安全、更可预测的替代方案。在现代 JavaScript 中,通常不鼓励使用 var。
2.2.1 声明与赋值
你可以声明一个 var 变量并可选地给它赋值:
var oldSchoolName; // 声明:变量 'oldSchoolName' 被创建,值为 undefined(未定义)
oldSchoolName = "Gandalf"; // 赋值:'Gandalf' 被存储在 'oldSchoolName' 中
console.log(oldSchoolName); // 输出: Gandalf
var favoriteColor = "blue"; // 在一行中进行声明和赋值
console.log(favoriteColor); // 输出: blue2.2.2 函数作用域 (Function Scope)
用 var 声明的变量是函数作用域的。这意味着它们在声明它们的函数内的任何地方都可以访问,不管是在什么块语句(如 if 语句或 for 循环)中。如果 var 在任何函数之外声明,它就变成全局变量,在你的脚本中到处都可以访问。
var globalMessage = "来自全局作用域的问候!";
function greetingFunction() {
var functionMessage = "来自函数作用域的问候!";
if (true) {
var blockMessage = "来自函数内代码块的问候!";
console.log(functionMessage); // 可访问
console.log(blockMessage); // 可访问
}
console.log(functionMessage); // 可访问
console.log(blockMessage); // 这里也可访问!(这可能会让人惊讶)
}
greetingFunction();
console.log(globalMessage); // 可访问
// console.log(functionMessage); // 错误: functionMessage 未定义 (在它的函数作用域之外)
// console.log(blockMessage); // 错误: blockMessage 未定义 (在它的函数作用域之外)blockMessage 在 if 块之外、但在 greetingFunction 内部仍然可以访问,这是 var 函数作用域的一个关键特征。
2.2.3 var 的变量提升 (Hoisting)
用 var 声明的变量会被“提升”到其作用域的顶部。这意味着 JavaScript 会在执行任何代码之前处理 var 变量的声明。然而,只有声明被提升,赋值不会被提升。如果在赋值之前访问变量,会导致变量的值为 undefined。
console.log(myVar); // 输出: undefined
var myVar = 10;
console.log(myVar); // 输出: 10
// JavaScript 内部是这样处理上面的代码的:
// var myVar; // 声明被提升到顶部
// console.log(myVar); // 此时 myVar 存在但没有赋值,所以是 'undefined'
// myVar = 10; // 赋值发生在这里
// console.log(myVar);2.2.4 使用 var 重新声明和重新赋值
var 变量可以在同一作用域内轻松地重新声明和重新赋值,而不会报错。这种灵活性虽然有时很方便,但经常导致变量被意外覆盖,使调试变得困难。
var studentName = "Alice";
console.log(studentName); // 输出: Alice
var studentName = "Bob"; // 重新声明 (对 var 来说完全合法)
console.log(studentName); // 输出: Bob
studentName = "Charlie"; // 重新赋值 (也合法)
console.log(studentName); // 输出: Charlie在现代 JavaScript 中,最佳实践是避免使用 var,因为它有这些潜在的陷阱,特别是它的函数作用域和允许重新声明变量的特性。
2.3 let 关键字(现代实践)
ES6 (ECMAScript 2015) 引入了 let,它是声明值可能会改变的变量的首选方式。它解决了 var 的许多缺点。
2.3.1 声明与赋值
与 var 类似,你可以声明 let 变量并赋值:
let gameScore; // 声明
gameScore = 0; // 赋值
console.log(gameScore); // 输出: 0
gameScore = 100; // 重新赋值 (对 let 来说合法)
console.log(gameScore); // 输出: 100
let playerName = "PlayerOne"; // 声明并赋值
console.log(playerName); // 输出: PlayerOne2.3.2 块级作用域 (Block Scope)
用 let 声明的变量是块级作用域的。这意味着它们只能在定义它们的代码块({})内访问,或者在任何嵌套的块内访问。这是相对于 var 函数作用域的重大改进,因为它有助于防止意外的变量泄漏,并使代码更可预测。
let globalValue = 50;
if (true) {
let blockScopedValue = 20;
console.log(globalValue); // 可访问
console.log(blockScopedValue); // 在此块内可访问
}
console.log(globalValue); // 可访问
// console.log(blockScopedValue); // 错误: blockScopedValue 未定义 (在它的块级作用域之外)这种块级作用域行为对于避免在代码的不同部分使用相同变量名时发生冲突至关重要。
2.3.3 let 的提升(和暂时性死区)
let 变量也会被提升,但它们的行为与 var 不同。虽然它们的声明被提升了,但它们没有被初始化。这意味着如果你在声明之前尝试访问 let 变量,JavaScript 会抛出一个 ReferenceError(引用错误)。从作用域开始到变量声明之间的这段时期称为“暂时性死区” (Temporal Dead Zone, TDZ)。
// console.log(itemCount); // ReferenceError: 无法在初始化之前访问 'itemCount'
let itemCount = 5;
console.log(itemCount); // 输出: 5TDZ 是一种安全机制,强制你在使用变量之前声明它们,防止了 var 中常见的 undefined 意外。
2.3.4 禁止重新声明,但允许重新赋值
在同一作用域内,let 变量不能被重新声明。但是,它可以被重新赋值为一个新值。这在防止意外覆盖和允许动态数据之间取得了很好的平衡。
let productPrice = 9.99;
console.log(productPrice); // 输出: 9.99
// let productPrice = 12.50; // 错误: 标识符 'productPrice' 已经被声明了
// 这防止了意外的重新声明。
productPrice = 12.50; // 重新赋值 (合法)
console.log(productPrice); // 输出: 12.502.4 const 关键字(现代实践中的常量)
同样在 ES6 中引入,const 用于声明常量。const 变量必须在声明时初始化一个值,并且其值在之后不能被重新赋值。
2.4.1 声明和强制赋值
const 变量必须在声明时立即赋值。如果不这样做会导致错误。
// const PI; // 错误: const 声明中缺少初始化器
const PI = 3.14159; // 声明并强制赋值
console.log(PI); // 输出: 3.14159
const WEBSITE_URL = "https://www.example.com";
console.log(WEBSITE_URL); // 输出: https://www.example.com2.4.2 块级作用域 (Block Scope)
像 let 一样,const 变量也是块级作用域的。它们只能在定义它们的块({})内访问。
const appName = "MyCoolApp";
if (true) {
const adminUser = "Admin001";
console.log(appName); // 可访问
console.log(adminUser); // 在此块内可访问
}
console.log(appName); // 可访问
// console.log(adminUser); // 错误: adminUser 未定义 (在它的块级作用域之外)2.4.3 const 的提升(和暂时性死区)
const 变量也参与提升,但像 let 一样,受暂时性死区的影响。你不能在声明之前访问 const 变量。
// console.log(MAX_ATTEMPTS); // ReferenceError: 无法在初始化之前访问 'MAX_ATTEMPTS'
const MAX_ATTEMPTS = 3;
console.log(MAX_ATTEMPTS); // 输出: 32.4.4 禁止重新声明和禁止重新赋值
const 的定义特征是,一旦赋值,它就不能被重新赋值(变量也不能在同一作用域内被重新声明)。这使得 const 非常适合那些在程序执行期间应保持不变的值。
const TAX_RATE = 0.07;
console.log(TAX_RATE); // 输出: 0.07
// TAX_RATE = 0.08; // 错误: 赋值给常数变量。
// 这防止了意外更改固定值。
// const TAX_RATE = 0.06; // 错误: 标识符 'TAX_RATE' 已经被声明了
// 这防止了意外重新声明。2.4.5 重要的细微差别:const 与对象和数组
虽然 const 防止了变量本身的重新赋值,但理解它并不使对象或数组的内容变得不可变是至关重要的。
如果一个 const 变量持有一个对象或数组(你将在后面的模块中学习),你仍然可以修改对象的属性或数组的元素。变量本身继续指向同一个内存位置,但该位置的数据可以改变。
const userProfile = {
name: "Jane Doe",
age: 30
};
console.log(userProfile); // 输出: { name: 'Jane Doe', age: 30 }
userProfile.age = 31; // 合法: 你可以更改对象的属性
console.log(userProfile); // 输出: { name: 'Jane Doe', age: 31 }
// userProfile = { name: "John Doe", age: 25 }; // 错误: 赋值给常数变量。
// 这是非法的,因为你试图重新赋值 'userProfile' 变量本身。当你学习对象和数组时,这个细微差别会变得更清晰,但重要的是要掌握:const 确保的是变量名与其值(或对象/数组的内存地址)之间的绑定保持不变,而不一定是该值的内容(如果它是复杂数据类型)。
3. 选择 var, let, 还是 const?
对于现代 JavaScript 开发,let 和 const 之间的选择非常直接:
- 总是首选
const: 默认使用const。如果一个变量的值在初始化后不需要改变,const是最安全、最清晰的选择。它向其他开发者(以及未来的你)传达了意图:这个值是常量。 - 当需要重新赋值时使用
let: 如果你知道变量的值稍后在代码中需要更新(例如,循环中的计数器、用户的得分、临时值),那么let是合适的关键字。 - 避免使用
var: 由于其令人困惑的作用域和提升行为,var通常被认为是遗留关键字。你可能会在旧代码库中遇到它,但对于新开发,请坚持使用let和const。
这里有一个总结表:
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 (Function-scoped) | 块级作用域 (Block-scoped) | 块级作用域 (Block-scoped) |
| 重新声明 | 是 | 否 | 否 |
| 重新赋值 | 是 | 是 | 否 |
| 变量提升 | 是 (初始化为 undefined) | 是 (暂时性死区 TDZ) | 是 (暂时性死区 TDZ) |
| 初始化 | 可选 | 可选 | 强制 |
4. 实践示例与演示
让我们看几个场景,以巩固你在实践中对 var、let 和 const 的理解。
4.1 示例 1:循环计数器使用 let(演示块级作用域)
虽然我们还没有讲到循环(那是模块 4 的内容!),但这个例子清楚地展示了 let 的块级作用域。用 let 声明的变量 i 只存在于 for 循环的花括号内。
// 使用 let 作为循环计数器
for (let i = 0; i < 3; i++) {
console.log("当前迭代 (循环内部): " + i);
}
// console.log("当前迭代 (循环外部): " + i); // ReferenceError: i 未定义
// 变量 'i' 被限制在循环的代码块中。如果我们使用 var:
// 使用 var 作为循环计数器 (旧方法)
for (var j = 0; j < 3; j++) {
console.log("当前迭代 (使用 var 循环内部): " + j);
}
console.log("当前迭代 (使用 var 循环外部): " + j); // 输出: 3
// 使用 var,'j' 泄漏到了循环块之外并且仍然可以访问。
// 如果 'j' 已经在其他地方定义过,这会导致意外行为。4.2 示例 2:配置值使用 const
想象你正在构建一个简单的应用程序。你可能有一些设置或配置值,在应用程序运行时不应更改。
const APP_TITLE = "我的超棒 App";
const API_KEY = "sk-xxxxxxxxxxxxxxxxxxxx"; // 假设的 API 密钥
const DEFAULT_LANGUAGE = "zh";
console.log(`欢迎来到 ${APP_TITLE}!`);
console.log(`使用 API 密钥: ${API_KEY}`);
console.log(`默认语言设置为: ${DEFAULT_LANGUAGE}`);
// APP_TITLE = "新标题"; // 这会导致错误: 赋值给常数变量。
// API_KEY = "new-key"; // 这也会导致错误。在这里使用 const 清楚地表明这些值是固定的,有助于防止意外更改。
4.3 示例 3:用户输入或可变状态使用 let
考虑用户的当前状态或临时计算。这些值预计会改变。
let userStatus = "活跃";
console.log("用户初始状态: " + userStatus);
// 模拟用户登出
userStatus = "不活跃";
console.log("用户登出后状态: " + userStatus);
let currentTemperature = 25; // 摄氏度
console.log("当前温度: " + currentTemperature + "°C");
// 温度变化
currentTemperature = 28;
console.log("更新后的温度: " + currentTemperature + "°C");4.4 示例 4:演示 var 提升(以及为什么它有问题)
这个例子展示了 var 的提升如何导致 undefined 值并可能掩盖 Bug。
function processData() {
console.log("声明前的数据值:", data); // 输出: undefined
// 在这里,'data' 由于提升是存在的,但它的值还没有被赋值。
var data = "重要信息";
console.log("声明和赋值后的数据值:", data); // 输出: 重要信息
var result = 10;
if (true) {
var result = 20; // 由于 var 的函数作用域,这重新声明并重新赋值了 *同一个* 'result' 变量
console.log("块内的结果:", result); // 输出: 20
}
console.log("块外的结果:", result); // 输出: 20 (内部的 'var result' 覆盖了外部的那个)
}
processData();将其与 let 进行比较:
function processDataModern() {
// console.log("声明前的数据值:", modernData); // ReferenceError (引用错误)
let modernData = "重要信息";
console.log("声明和赋值后的数据值:", modernData);
let resultModern = 10;
if (true) {
let resultModern = 20; // 这创建了一个 *新的*、块级作用域的 'resultModern' 变量。
console.log("块内的结果 (现代):", resultModern); // 输出: 20
}
console.log("块外的结果 (现代):", resultModern); // 输出: 10 (外部的那个没有被覆盖)
}
processDataModern();let 的例子清楚地表明,它的块级作用域如何防止意外覆盖,并使代码的行为更加直观和安全。
5. 总结
我们探讨了用于变量声明的三个关键字:var、let 和 const。
var是遗留关键字,其特点是函数作用域和允许宽松的重新声明/重新赋值,这可能导致意外行为。在现代 JavaScript 中,通常建议避免使用 var。let是会被重新赋值的变量的现代替代方案。它提供块级作用域,防止变量泄漏,并禁止在同一作用域内重新声明。const是值在初始赋值后不应更改的变量的首选关键字。它也具有块级作用域,禁止重新声明,并且至关重要的是,防止重新赋值。虽然const确保变量绑定是常量的,但请记住,用const声明的对象或数组的内容仍然可以被修改。
通过优先使用 const 并在仅必要时使用 let,你将编写出更整洁、更安全且更易于维护的代码。