1. JavaScript 变量与常量基础解析
在编程世界中,数据存储和访问是最基础也最重要的概念之一。JavaScript 作为一门动态类型的脚本语言,其变量和常量的使用方式与其他静态类型语言有着显著区别。理解这些基础概念对于后续学习函数、对象等高级主题至关重要。
变量本质上是一个命名的存储位置,其值可以在程序执行过程中改变。而常量则是初始化后不能再被重新赋值的命名存储单元。在 ES6 之前,JavaScript 实际上只有变量的概念,使用 var 关键字声明。ES6 引入了 let 和 const,分别用于声明块级作用域的变量和常量。
重要提示:现代 JavaScript 开发中应该避免使用 var,优先使用 let 和 const。var 存在变量提升和函数作用域等特性,容易导致意料之外的行为。
1.1 变量声明方式演变
JavaScript 的变量声明经历了三个阶段的发展:
- var 时代(ES5 及之前):
- 函数作用域而非块级作用域
- 存在变量提升现象
- 可以重复声明
- 没有暂时性死区
javascript复制// var 示例
function varExample() {
console.log(x); // 输出 undefined 而非报错,因为变量提升
var x = 10;
if (true) {
var x = 20; // 同一个变量
console.log(x); // 20
}
console.log(x); // 20
}
- let 引入(ES6):
- 块级作用域
- 不存在变量提升
- 不可重复声明
- 存在暂时性死区
javascript复制// let 示例
function letExample() {
// console.log(y); // 报错,暂时性死区
let y = 10;
if (true) {
let y = 20; // 不同的变量
console.log(y); // 20
}
console.log(y); // 10
}
- const 引入(ES6):
- 必须初始化
- 不能重新赋值
- 其他特性与 let 相同
- 对于对象和数组,内容可以修改(只是绑定不可变)
javascript复制// const 示例
const PI = 3.14159;
// PI = 3; // 报错,不能重新赋值
const obj = { name: 'John' };
obj.name = 'Mike'; // 允许,因为对象内容可变
// obj = {}; // 报错,不能重新赋值
1.2 命名规则与最佳实践
JavaScript 的标识符命名遵循以下规则:
- 可以包含字母、数字、下划线和美元符号
- 不能以数字开头
- 区分大小写
- 不能使用保留关键字
命名最佳实践:
- 使用驼峰命名法(camelCase)
- 常量通常全大写(UPPER_CASE)
- 使用有意义的名称
- 避免单个字母命名(除了简单的循环计数器)
- 布尔变量通常以 is、has、can 等开头
javascript复制// 好的命名示例
let userAge = 25;
const MAX_RETRY_COUNT = 3;
let isActive = true;
let hasPermission = false;
// 不好的命名示例
let a = 10; // 无意义
let flag = true; // 不明确
const x = 100; // 不明确
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 变量作用域深入理解
作用域决定了变量的可见性和生命周期。JavaScript 有三种作用域:
- 全局作用域
- 函数作用域
- 块级作用域(ES6 引入)
2.1 作用域链与变量查找
当访问一个变量时,JavaScript 引擎会按照以下顺序查找:
- 当前作用域
- 外层作用域
- 直到全局作用域
- 如果找不到,报 ReferenceError
javascript复制let globalVar = 'global';
function outer() {
let outerVar = 'outer';
function inner() {
let innerVar = 'inner';
console.log(innerVar); // 'inner'
console.log(outerVar); // 'outer'
console.log(globalVar); // 'global'
// console.log(notExist); // ReferenceError
}
inner();
}
outer();
2.2 闭包与变量捕获
闭包是指函数能够记住并访问其词法作用域,即使函数在其词法作用域之外执行。这是 JavaScript 中一个强大但容易误用的特性。
javascript复制function createCounter() {
let count = 0; // 这个变量被内部函数捕获
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
注意:不当使用闭包可能导致内存泄漏,因为被捕获的变量不会被垃圾回收,直到闭包本身不再被引用。
3. 变量提升与暂时性死区
3.1 var 的变量提升
var 声明的变量会被提升到函数或全局作用域的顶部,但只有声明被提升,赋值不会。
javascript复制console.log(hoistedVar); // undefined,不是 ReferenceError
var hoistedVar = 'value';
实际上代码被解释为:
javascript复制var hoistedVar;
console.log(hoistedVar);
hoistedVar = 'value';
3.2 let/const 的暂时性死区
let 和 const 也存在提升,但与 var 不同,它们在声明前不能被访问,这个区域称为暂时性死区(Temporal Dead Zone, TDZ)。
javascript复制// console.log(tdzVar); // ReferenceError
let tdzVar = 'value';
4. 常量与不可变性
const 声明的常量具有以下特点:
- 必须初始化
- 不能重新赋值
- 块级作用域
- 不存在变量提升,有暂时性死区
4.1 常量与对象
虽然 const 声明的对象不能重新赋值,但其属性可以修改。要创建真正不可变的对象,可以使用 Object.freeze()。
javascript复制const person = {
name: 'Alice'
};
person.name = 'Bob'; // 允许
// person = {}; // 报错
// 使用 Object.freeze 创建不可变对象
const frozenPerson = Object.freeze({
name: 'Charlie'
});
frozenPerson.name = 'David'; // 静默失败(严格模式下报错)
console.log(frozenPerson.name); // 'Charlie'
4.2 何时使用 const
建议默认使用 const,只有在确实需要重新赋值时才使用 let。这种做法可以:
- 避免意外修改
- 使代码意图更明确
- 帮助编译器优化
- 符合函数式编程原则
javascript复制// 好的实践
const PI = 3.14159;
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
let counter = 0; // 确实需要改变的值
5. 常见问题与陷阱
5.1 全局变量污染
意外创建全局变量是常见问题:
javascript复制function createGlobal() {
accidentalGlobal = 'oops'; // 忘记 var/let/const,成为全局变量
}
解决方法:使用严格模式('use strict'),这会禁止意外创建全局变量。
5.2 循环中的变量捕获
for 循环中使用 var 会导致问题:
javascript复制for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出 3 三次
}, 100);
}
// 解决方案1:使用 let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出 0, 1, 2
}, 100);
}
// 解决方案2:使用闭包
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 输出 0, 1, 2
}, 100);
})(i);
}
5.3 类型转换问题
JavaScript 是弱类型语言,变量类型可以自动转换:
javascript复制let a = '10';
let b = 5;
console.log(a + b); // '105' (字符串拼接)
console.log(a - b); // 5 (数字运算)
建议:使用严格相等(===)避免隐式类型转换带来的问题。
6. 高级主题与性能考量
6.1 内存管理
JavaScript 使用自动垃圾回收,但不当的变量使用仍可能导致内存泄漏:
- 意外的全局变量
- 遗忘的定时器或回调
- DOM 引用
- 闭包
javascript复制// 内存泄漏示例
function leakMemory() {
const bigArray = new Array(1000000).fill('*');
setInterval(function() {
console.log(bigArray.length); // bigArray 永远不会被回收
}, 1000);
}
6.2 变量与执行上下文
每个函数调用都会创建一个新的执行上下文,包含:
- 变量环境(var 声明)
- 词法环境(let/const 声明)
- this 绑定
- 外部环境引用
javascript复制let globalLet = 'global';
var globalVar = 'global';
function contextTest() {
console.log(globalLet); // 'global'
console.log(globalVar); // 'global'
console.log(this); // 取决于调用方式
// 新的变量环境
var innerVar = 'inner';
// 新的词法环境
let innerLet = 'inner';
}
6.3 严格模式的影响
'use strict' 改变了 JavaScript 的某些行为:
- 禁止意外创建全局变量
- 使静默失败抛出错误
- 禁止重复参数名
- 禁用 with 语句
- 改变 this 在全局函数中的值(undefined 而非全局对象)
javascript复制'use strict';
// 严格模式下的变化
function strictChanges() {
// undeclaredVar = 'error'; // ReferenceError
// let duplicateParam = function(a, a) {}; // SyntaxError
console.log(this); // undefined
}
7. 现代 JavaScript 模块中的变量
ES6 模块有自己的作用域规则:
- 每个模块有自己的顶级作用域
- 变量默认不共享
- 使用 export/import 显式共享
javascript复制// module1.js
let privateVar = 'private';
export let publicVar = 'public';
// module2.js
import { publicVar } from './module1.js';
console.log(publicVar); // 'public'
// console.log(privateVar); // ReferenceError
8. 类型系统与变量
JavaScript 是动态类型语言,但 TypeScript 和 Flow 等工具可以添加静态类型检查:
typescript复制// TypeScript 示例
let typedVar: string = 'hello';
typedVar = 123; // 编译错误
interface User {
name: string;
age: number;
}
const user: User = {
name: 'Alice',
age: 30
};
9. 实用技巧与最佳实践
- 变量声明风格:
- 相关变量一起声明
- 复杂变量先声明后初始化
- 使用解构赋值
javascript复制// 好的声明风格
const MAX = 100,
MIN = 0;
let user = {},
profile = {};
// 解构赋值
const { name, age } = getUser();
-
减少可变性:
- 优先使用 const
- 使用 Object.freeze() 冻结对象
- 考虑不可变数据结构库(如 Immutable.js)
-
命名约定:
- 私有变量以下划线开头(约定,非强制)
- 构造函数首字母大写
- 常量全大写
javascript复制class User {
constructor(name) {
this._name = name; // 约定为私有
}
getName() {
return this._name;
}
}
const DEFAULT_CONFIG = {};
- 性能优化:
- 避免在循环中频繁创建对象
- 重用变量而非重复创建
- 注意闭包的内存占用
javascript复制// 不好的做法
function processItems(items) {
items.forEach(item => {
const processed = transform(item); // 每次迭代创建新变量
console.log(processed);
});
}
// 更好的做法
function processItemsBetter(items) {
let processed; // 在外部声明
items.forEach(item => {
processed = transform(item);
console.log(processed);
});
}
