🔥 前端小白必懂:变量、作用域和变量提升全解析
从"变量去哪了"到"为什么会提升",用大白话讲清楚 JS 的核心概念
📋 目录
🤔 JavaScript 的黑历史
先给大家讲个冷知识:JavaScript 是一周内开发出来的!
没错,JS 的爸爸 Brendan Eich 在 1995 年只用了 10 天就写出了这门语言。当时是为了蹭 Java 的热度,名字都叫 JavaScript(其实和 Java 没啥关系)。
这种"赶工"导致 JS 有一些"设计瑕疵",比如:
var的作用域问题- 变量提升的反直觉行为
不过别担心,ES6(2015年)已经修复了这些问题,我们有了 let 和 const!
👬 变量声明三兄弟
2.1 var:老大哥(已退休)
var name = "张三";
var age = 18;
// 特点:
// 1. 函数作用域(不是块级作用域)
// 2. 可以重复声明
// 3. 存在变量提升
2.2 let:新大哥(推荐使用)
let name = "张三";
let age = 18;
age = 19; // ✅ 可以重新赋值
// 特点:
// 1. 块级作用域(在 {} 内有效)
// 2. 不能重复声明
// 3. 不存在变量提升(有暂时性死区)
2.3 const:老幺(常量)
const PI = 3.14159;
const name = "李四";
// 特点:
// 1. 块级作用域
// 2. 声明时必须赋值
// 3. 简单类型不能改值,复杂类型可以改属性
const person = {
name: "王五",
age: 20
};
person.age = 21; // ✅ 可以改属性
// person = {}; // ❌ 不能重新赋值
2.4 三兄弟对比表
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 重复声明 | ✅ 允许 | ❌ 不允许 | ❌ 不允许 |
| 重新赋值 | ✅ 允许 | ✅ 允许 | ❌ 不允许(简单类型) |
| 变量提升 | ✅ 存在 | ❌ 不存在 | ❌ 不存在 |
| 声明时赋值 | 可选 | 可选 | 必须 |
🏠 作用域:变量的"地盘"
3.1 作用域类型
// 🏠 全局作用域:整个文件都能访问
var globalVar = "我是全局变量";
function myFunction() {
// 🏠 函数作用域:只有函数内可以访问
var functionVar = "我是函数变量";
if (true) {
// 🏠 块级作用域:只有大括号内可以访问
let blockVar = "我是块级变量";
console.log(blockVar); // ✅ 可以访问
}
console.log(blockVar); // ❌ 访问不到
}
console.log(functionVar); // ❌ 访问不到
3.2 变量查找规则(冒泡查找)
// 查找顺序:当前作用域 → 外层作用域 → 全局作用域
var name = "全局张三";
function outer() {
var name = "外层张三";
function inner() {
var name = "内层张三";
console.log(name); // 输出 "内层张三"
}
inner();
}
outer();
规则:先在自己的"地盘"找,找不到就去"邻居家"找,一直找到全局作用域。
3.3 内存角度看作用域
function sayHello() {
let message = "Hello"; // 在内存中申请一块区域
console.log(message);
} // 函数执行完,内存被回收,message 消失
sayHello();
console.log(message); // ❌ 报错,变量已经被回收了
🎯 经典面试题:for + setTimeout
4.1 问题代码
// 使用 var 的情况
for (var i = 0; i < 3; i++) {
console.log(i); // 同步:0, 1, 2
setTimeout(function() {
console.log(i); // 异步:3, 3, 3 ❌
}, 10);
}
4.2 为什么异步输出全是 3?
时间线:
──────────────────────────────────────────►
循环执行(同步):
i=0 → console.log(0) → 注册 setTimeout
i=1 → console.log(1) → 注册 setTimeout
i=2 → console.log(2) → 注册 setTimeout
i=3 → 循环结束
10ms 后(异步):
所有 setTimeout 执行,此时 i=3,所以输出 3, 3, 3
4.3 解决方案:使用 let
// 使用 let 的情况
for (let i = 0; i < 3; i++) {
console.log(i); // 同步:0, 1, 2
setTimeout(function() {
console.log(i); // 异步:0, 1, 2 ✅
}, 10);
}
为什么能正常工作?
let 每次循环都会创建新的变量副本,每个 setTimeout 捕获的是自己那次循环的 i 值。
⚡ 变量提升:代码的"时间魔法"
5.1 什么是变量提升?
console.log(pizza); // undefined(不会报错!)
var pizza = 'Deep Dish';
背后发生了什么?
JavaScript 代码执行分两步:
- 编译阶段:扫描所有
var声明,把它们"提升"到作用域顶部,值为undefined - 执行阶段:按顺序执行代码
引擎看到的代码其实是这样的:
var pizza; // 编译阶段:声明被提升
console.log(pizza); // 执行阶段:输出 undefined
pizza = 'Deep Dish'; // 执行阶段:赋值
5.2 let/const 没有变量提升
console.log(apple); // ❌ ReferenceError!
let apple = 'red';
console.log(orange); // ❌ ReferenceError!
const orange = 'orange';
暂时性死区(TDZ):从作用域开始到变量声明的位置,变量不可访问。
5.3 函数声明也会提升
hello(); // ✅ 可以调用
function hello() {
console.log("Hello!");
}
💡 实战建议
6.1 最佳实践
// 1. 优先使用 const(不需要重新赋值的变量)
const PI = 3.14;
const config = {
apiUrl: "https://api.example.com"
};
// 2. 其次使用 let(需要重新赋值的变量)
let count = 0;
count++;
// 3. 避免使用 var(除非有特殊需求)
// var name = "张三"; // ❌ 不推荐
6.2 常见错误
// ❌ 错误1:重复声明
let name = "张三";
let name = "李四"; // 报错!
// ❌ 错误2:const 不赋值
const age; // 报错!必须赋值
// ❌ 错误3:const 简单类型重新赋值
const score = 100;
score = 90; // 报错!
// ✅ 正确:const 对象可以改属性
const user = { name: "张三" };
user.name = "李四"; // 可以
✨ 总结
核心要点
- var:老版本,函数作用域,有变量提升
- let:新版本,块级作用域,无变量提升
- const:常量,块级作用域,简单类型不能改值
- 作用域:变量的"地盘",查找规则是从内向外
- 变量提升:
var声明会被提升到作用域顶部
一句话记住
能用 const 就用 const,需要改值用 let,var 赶紧淘汰!