变量声明
JavaScript中有三种声明变量的方式:
| 关键字 | 作用域 | 可重新赋值 | 可重新声明 | 提升 | 暂时性死区 |
|---|---|---|---|---|---|
var |
函数作用域 | 是 | 是 | 是(初始化为undefined) | 否 |
let |
块作用域 | 是 | 否 | 是(未初始化) | 是 |
const |
块作用域 | 否 | 否 | 是(未初始化) | 是 |
变量声明历史
在ES6之前,JavaScript只有var一种变量声明方式。ES6引入了let和const,解决了var的一些问题,如变量提升、块级作用域等。
变量声明示例
变量声明
// var 声明 (ES5)
var name = "张三";
var age = 25;
// let 声明 (ES6)
let score = 95;
let isActive = true;
// const 声明 (ES6) - 常量
const PI = 3.14159;
const API_URL = "https://api.example.com";
// 重新赋值
name = "李四"; // ✅ 允许
score = 100; // ✅ 允许
// PI = 3.14; // ❌ 错误: 常量不能重新赋值
// 重新声明
// var name = "王五"; // ✅ 允许 (但不推荐)
// let score = 90; // ❌ 错误: 不能重新声明
// const 对象的特殊情况
const person = { name: "John", age: 30 };
person.age = 31; // ✅ 允许 - 修改对象属性
// person = { name: "Jane" }; // ❌ 错误 - 不能重新赋值
注意:
const声明创建一个值的只读引用,但这并不意味着它所持有的值是不可变的,只是变量标识符不能重新赋值。如果const变量引用的是一个对象,那么对象本身的内容是可以修改的。
作用域(Scope)
作用域决定了变量的可访问范围:
1. 全局作用域
全局作用域
// 全局变量 - 在任何地方都可访问
var globalVar = "我是全局变量";
let globalLet = "我也是全局变量";
function testGlobal() {
console.log(globalVar); // 可以访问
console.log(globalLet); // 可以访问
}
testGlobal();
注意:
在全局作用域中使用
var声明的变量会成为全局对象的属性(在浏览器中是window对象),而使用let和const声明的变量不会。
2. 函数作用域 (var)
函数作用域
function functionScope() {
var functionVar = "我在函数内部";
if (true) {
var ifVar = "我在if块内";
}
console.log(functionVar); // ✅ 可以访问
console.log(ifVar); // ✅ 可以访问 (var没有块级作用域)
}
functionScope();
// console.log(functionVar); // ❌ 错误: 在函数外部无法访问
3. 块级作用域 (let, const)
块级作用域
function blockScope() {
if (true) {
let blockLet = "我在块内部";
const blockConst = "我也是";
var blockVar = "我使用var";
}
console.log(blockVar); // ✅ 可以访问 (var)
// console.log(blockLet); // ❌ 错误: let有块级作用域
// console.log(blockConst); // ❌ 错误: const有块级作用域
}
blockScope();
4. 模块作用域 (ES6)
在ES6模块中,每个模块都有自己的作用域,变量默认不是全局的:
模块作用域
// module.js
let privateVar = "我是模块私有的";
export const publicVar = "我可以被其他模块导入";
// main.js
import { publicVar } from './module.js';
console.log(publicVar); // ✅ 可以访问
// console.log(privateVar); // ❌ 错误: 无法访问
变量提升(Hoisting)
JavaScript会将变量和函数声明提升到其作用域的顶部:
变量提升
// 实际执行顺序:
console.log(a); // undefined (不是错误!)
var a = 5;
// 相当于:
var a; // 声明被提升
console.log(a); // undefined
a = 5; // 赋值保持不变
// let 和 const 的暂时性死区
// console.log(b); // ❌ 错误: 不能在初始化前访问
let b = 10;
// 函数提升
sayHello(); // ✅ 可以调用
function sayHello() {
console.log("Hello!");
}
最佳实践: 总是先声明变量再使用,避免依赖变量提升的行为。
函数表达式 vs 函数声明
函数声明会被提升,但函数表达式不会:
函数提升
// 函数声明 - 会被提升
sayHello(); // ✅ 可以调用
function sayHello() {
console.log("Hello!");
}
// 函数表达式 - 不会被提升
// sayGoodbye(); // ❌ 错误: sayGoodbye不是函数
var sayGoodbye = function() {
console.log("Goodbye!");
};
作用域链
JavaScript使用作用域链来解析变量:
作用域链示例
let global = "全局变量";
function outer() {
let outerVar = "外部变量";
function inner() {
let innerVar = "内部变量";
console.log(innerVar); // 内部变量 - 当前作用域
console.log(outerVar); // 外部变量 - 父作用域
console.log(global); // 全局变量 - 全局作用域
}
inner();
}
outer();
词法环境
在JavaScript中,每个执行上下文都有一个关联的词法环境。词法环境包含两个主要部分:
- 环境记录 - 存储变量和函数声明的实际位置
- 对外部词法环境的引用 - 用于解析外部变量
当查找变量时,JavaScript引擎会:
- 在当前词法环境中查找
- 如果找不到,沿着作用域链向上查找
- 直到全局词法环境
- 如果仍然找不到,返回
undefined(非严格模式)或报错(严格模式)
闭包(Closure)
闭包是指函数能够访问并记住其词法作用域,即使函数在其作用域外执行:
闭包示例
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// 每个闭包都有自己独立的作用域
const counter2 = createCounter();
console.log(counter2()); // 1 (独立的计数)
闭包的实际应用
闭包在JavaScript中有许多实际应用:
闭包应用
// 1. 数据私有化
function createPerson(name) {
let _name = name; // 私有变量
return {
getName: function() { return _name; },
setName: function(newName) { _name = newName; }
};
}
const person = createPerson("John");
console.log(person.getName()); // "John"
person.setName("Jane");
console.log(person.getName()); // "Jane"
// console.log(person._name); // undefined - 无法直接访问
// 2. 函数工厂
function createMultiplier(multiplier) {
return function(x) {
return x * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
注意: 闭包会导致外部函数的变量无法被垃圾回收,如果滥用可能会导致内存泄漏。
实践练习
作用域与闭包演示
👆 请点击上方按钮进行演示操作
选择不同的演示按钮来探索JavaScript的各种功能和用法
练习代码
// 练习1: 作用域理解
var x = 1;
let y = 2;
const z = 3;
{
var x = 10; // 重新声明 (不推荐)
let y = 20; // 新的块级变量
// const z = 30; // ❌ 错误: 不能重新声明
}
console.log(x); // 10 (var没有块级作用域)
console.log(y); // 2 (外部的y)
console.log(z); // 3
// 练习2: 闭包应用
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// 练习3: 变量提升
console.log(hoistedVar); // undefined
var hoistedVar = "我被提升了";
// console.log(hoistedLet); // ❌ 错误: 不能在初始化前访问
let hoistedLet = "我也有提升,但有暂时性死区";
常见问题与解答
1. 什么时候使用var、let和const?
现代JavaScript开发中:
- 优先使用
const,用于不会重新赋值的变量 - 需要重新赋值的变量使用
let - 避免使用
var,除非有特殊需求
2. 什么是暂时性死区?
暂时性死区是指从代码块开始到let或const声明语句执行之间的区域。在这段时间内访问变量会抛出引用错误。
3. 闭包会导致内存泄漏吗?
闭包本身不会导致内存泄漏,但如果闭包持有对大对象的引用,并且该闭包的生命周期很长,可能会导致内存无法被回收。在不需要时应及时解除对闭包的引用。
最佳实践
- 优先使用
const,除非需要重新赋值 - 使用
let代替var - 避免使用未声明的变量
- 变量名要有意义,使用camelCase命名法
- 在作用域顶部声明变量
- 使用严格模式避免意外创建全局变量
- 注意闭包的内存管理
- 使用模块化组织代码,避免全局命名空间污染