JS变量三兄弟:var退休、let上位、const稳如老狗

0 阅读5分钟

🔥 前端小白必懂:变量、作用域和变量提升全解析

从"变量去哪了"到"为什么会提升",用大白话讲清楚 JS 的核心概念


📋 目录

  1. JavaScript 的黑历史
  2. 变量声明三兄弟:var、let、const
  3. 作用域:变量的"地盘"
  4. 经典面试题:for + setTimeout
  5. 变量提升:代码的"时间魔法"
  6. 实战建议

🤔 JavaScript 的黑历史

先给大家讲个冷知识:JavaScript 是一周内开发出来的!

没错,JS 的爸爸 Brendan Eich 在 1995 年只用了 10 天就写出了这门语言。当时是为了蹭 Java 的热度,名字都叫 JavaScript(其实和 Java 没啥关系)。

这种"赶工"导致 JS 有一些"设计瑕疵",比如:

  • var 的作用域问题
  • 变量提升的反直觉行为

不过别担心,ES6(2015年)已经修复了这些问题,我们有了 letconst


👬 变量声明三兄弟

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 三兄弟对比表

特性varletconst
作用域函数作用域块级作用域块级作用域
重复声明✅ 允许❌ 不允许❌ 不允许
重新赋值✅ 允许✅ 允许❌ 不允许(简单类型)
变量提升✅ 存在❌ 不存在❌ 不存在
声明时赋值可选可选必须

🏠 作用域:变量的"地盘"

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 代码执行分两步:

  1. 编译阶段:扫描所有 var 声明,把它们"提升"到作用域顶部,值为 undefined
  2. 执行阶段:按顺序执行代码

引擎看到的代码其实是这样的:

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 = "李四";  // 可以

✨ 总结

核心要点

  1. var:老版本,函数作用域,有变量提升
  2. let:新版本,块级作用域,无变量提升
  3. const:常量,块级作用域,简单类型不能改值
  4. 作用域:变量的"地盘",查找规则是从内向外
  5. 变量提升var 声明会被提升到作用域顶部

一句话记住

能用 const 就用 const,需要改值用 let,var 赶紧淘汰!