1. JavaScript 变量与常量基础解析
十年前我刚接触JavaScript时,最困惑的就是变量声明那些事儿。let、const、var这三个关键字看似简单,实际开发中却藏着不少门道。今天我们就从最基础的常量和变量说起,聊聊那些新手容易踩的坑。
在ES6之前,JavaScript只有var一种变量声明方式,这导致了很多作用域混乱的问题。现在虽然有了let和const,但老项目中的var依然随处可见。理解它们的区别,是写出健壮代码的第一步。我会通过实际案例,带你掌握变量提升、暂时性死区这些核心概念。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 变量声明方式深度对比
2.1 var的时代困境
javascript复制function varTest() {
console.log(foo); // 输出undefined而不是报错
var foo = "bar";
if (true) {
var foo = "baz";
console.log(foo); // 输出baz
}
console.log(foo); // 输出baz
}
var的变量提升特性会让声明被"提升"到函数顶部,但赋值不会。这容易导致:
- 内层块级作用域变量覆盖外层变量
- 循环计数器泄漏到全局作用域
- 难以预测的变量状态
实际项目中,我建议用ESLint配置禁止使用var,强制使用let/const
2.2 let的块级作用域革命
javascript复制function letTest() {
let foo = "bar";
if (true) {
let foo = "baz"; // 新的块级作用域变量
console.log(foo); // 输出baz
}
console.log(foo); // 输出bar
}
let解决了var的核心问题:
- 真正的块级作用域({}内有效)
- 不存在变量提升
- 禁止重复声明
2.3 const的本质特性
javascript复制const PI = 3.1415;
PI = 3; // TypeError: Assignment to constant variable
const user = { name: "John" };
user.name = "Mike"; // 允许修改属性
const并非完全不可变:
- 基本类型确实不可重新赋值
- 引用类型只是不能改变引用指向
- 适合声明配置常量、模块导出等场景
3. 作用域链与闭包实战
3.1 作用域链的形成
javascript复制let global = "global";
function outer() {
let outerVar = "outer";
function inner() {
let innerVar = "inner";
console.log(global + outerVar + innerVar); // 可以访问所有外层变量
}
return inner;
}
JavaScript采用词法作用域:
- 函数定义时确定作用域链
- 内层函数可访问外层变量
- 外层不能访问内层变量
3.2 闭包的经典应用
javascript复制function createCounter() {
let count = 0;
return {
increment: () => ++count,
get: () => count
};
}
const counter = createCounter();
counter.increment(); // 1
counter.get(); // 1
闭包的实际价值:
- 创建私有变量(如计数器状态)
- 模块化开发的基础
- 回调函数保持上下文
4. 变量相关的最佳实践
4.1 命名规范建议
- 常量:全大写加下划线(API_BASE_URL)
- 类名:PascalCase(UserModel)
- 变量/函数:camelCase(getUserInfo)
- 布尔值:以is/has开头(isLoading)
4.2 声明位置建议
javascript复制// 不好的写法
function processData(data) {
if (!data) return;
let result; // 过早声明
// ...50行代码...
result = transform(data);
return result;
}
// 好的写法
function processData(data) {
if (!data) return;
// ...业务逻辑...
const result = transform(data); // 用时声明
return result;
}
变量声明应该:
- 尽量靠近使用位置
- 优先使用const
- 必要时用let
- 避免var
4.3 类型检测技巧
javascript复制// 安全检测undefined
if (typeof variable === 'undefined') {
// 处理未定义情况
}
// 检测null
if (variable === null) {
// 处理null情况
}
// 现代可选链操作符
const name = user?.profile?.name ?? 'default';
5. 常见问题排查指南
5.1 变量提升陷阱
javascript复制console.log(x); // undefined
var x = 5;
console.log(y); // ReferenceError
let y = 5;
解决方案:
- 使用let/const替代var
- 启用严格模式('use strict')
- 使用ESLint的no-use-before-define规则
5.2 循环闭包问题
javascript复制for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出3个3
}
// 解决方案1:改用let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0,1,2
}
// 解决方案2:IIFE
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
5.3 常量修改检测
javascript复制const arr = [1, 2, 3];
arr.push(4); // 不会报错
Object.freeze(arr);
arr.push(5); // TypeError
深度冻结方案:
javascript复制function deepFreeze(obj) {
Object.freeze(obj);
Object.getOwnPropertyNames(obj).forEach(prop => {
if (obj[prop] !== null
&& typeof obj[prop] === 'object'
&& !Object.isFrozen(obj[prop])) {
deepFreeze(obj[prop]);
}
});
return obj;
}
6. 现代JavaScript新特性
6.1 解构赋值进阶
javascript复制// 对象解构
const { name, age } = getUser();
// 数组解构
const [first, ...rest] = [1, 2, 3];
// 默认值
const { settings = {} } = config;
// 重命名
const { name: userName } = user;
6.2 模板字符串妙用
javascript复制const name = "John";
console.log(`Hello ${name}`);
// 标签模板
function highlight(strings, ...values) {
return strings.reduce((result, str, i) =>
`${result}${str}<mark>${values[i] || ''}</mark>`, '');
}
const message = highlight`Hello ${name}`;
6.3 可选链与空值合并
javascript复制// 旧写法
const street = user && user.address && user.address.street;
// 新写法
const street = user?.address?.street;
// 空值合并
const apiTimeout = config.timeout ?? 3000;
在大型项目中,我通常会配置Prettier和ESLint来强制变量声明规范。比如要求所有导出的常量必须使用const,所有函数参数必须做类型检查等。这些规范虽然初期需要适应,但长期来看能显著降低维护成本。
