我被 var 坑惨后,终于搞懂了 let/const 和作用域那点事

55 阅读5分钟

前几天写个循环打印的小功能,for 循环配了个 setTimeout 定时器,本想依次输出 0-9,结果等了一秒,控制台齐刷刷打印了 10 个 10。

当时我盯着屏幕直接懵了,代码逻辑看着没毛病啊?翻来覆去改了半小时,才发现是var这个老伙计的锅。也正是因为这个 bug,我沉下心把 ES6 的三大变量、作用域彻底啃了一遍,今天趁热乎把我踩坑、顿悟的全过程说给你听。

说实话,在这之前我一直觉得 var、let、const 不就是声明变量的吗?能用就行,直到这次栽跟头,才知道 JS 早期的设计有多 “赶工”,ES6 补的坑有多关键。


先搞懂核心:作用域就是变量的「地盘」

排查问题的时候我才反应过来,所有变量的坑,根源都在作用域

不用记教科书定义,我理解的作用域就像套娃

  • 最外层的大套娃 = 全局作用域(整个 JS 文件里都能访问)
  • 函数里的小套娃 = 函数局部作用域(只能在函数里用)
  • 一对大括号{}(if/for/ 单独的代码块)= 块级作用域(ES6 新增的迷你套娃)

规则也特别简单:

  1. 变量在哪声明,就属于哪个套娃,外面的套娃绝对访问不到里面的
  2. 找变量的时候,从当前套娃往里翻,找不到就往外层套娃找,全局都找不到直接报错,像“冒泡”一样
  3. 套娃用完就销毁,里面的变量也会被垃圾回收,省内存

这就是变量的一生:声明→使用→销毁,一步都错不得。

image.png


让我崩溃的经典 bug:for + setTimeout

直接上我当时写错的代码,就是这个玩意儿坑了我:

// 错误示范:用var声明循环变量
for(var i = 0; i < 10; i++){
    // 同步代码,瞬间执行完,i直接变成10
    console.log(i); 
    // 异步定时器,1秒后执行
    setTimeout(function(){
        console.log(`The number is ${i}`);
    },1000)
}

输出结果:瞬间打印 0-9,1 秒后打印 10 个 10。

image.png

我第一反应是定时器出问题了,后来查了才知道:var 没有块级作用域!整个循环里只有一个 i 变量,同步代码瞬间把 i 跑到了 10,定时器再执行的时候,只能拿到最终的 10。

换成 let 直接解决:

// 正确示范:let支持块级作用域
for(let i = 0; i < 10; i++){
    console.log(i);
    setTimeout(function(){
        console.log(`The number is ${i}`); 
    },1000)
}

输出结果:1 秒后依次打印 0-9,完美!

image.png

let 会给循环的每一次都创建一个独立的块级作用域,相当于 10 个小套娃,每个套娃里都有自己的 i,互不干扰。这就是 ES6 最香的地方之一。


三大变量到底差在哪?我总结的大白话版

结合我踩的坑,直接把 var/let/const 的区别说透,全是实战干货:

1. var(ES5 老古董,别用了)

JS 早期设计只用了一周,var 就是赶工的产物:

  • 没有块级作用域,在 if/for 的 {} 里声明,全局都能访问
  • 支持变量提升,会偷偷把变量提到顶部,赋值undefined
  • 没有常量,只能靠程序员自己约束不改值

看个离谱的例子:

var age = 100;
if(age > 12){
    // 我以为这是块级里的变量,结果var不认{}
    var dog = age * 7; 
}
// 外面居然能访问到!这就是bug源头
console.log(dog); // 700

2. let(ES6 变量,var 的完美替代品)

  • 支持块级作用域{}就是它的地盘
  • 有变量提升,但存在暂时性死区,不能先使用后声明
  • 变量值可以随便改,类型也能改(不推荐改类型)
{
    // 块级作用域
    const name = '张三';
    console.log(name); // 张三
}
// 外面访问不到,直接报错
console.log(name); 

当时控制台输出的错误就长这样 → ReferenceError: name is not defined

image.png

3. const(ES6 常量,用它准没错)

全称constant,就是常量,我最开始踩了它的坑:

  • 必须声明时就赋值,不能留空
  • 简单数据类型(数字 / 字符串):值绝对不能改
  • 复杂数据类型(对象 / 数组):能改属性,不能重赋值
// 正确:对象属性可以改
const person = { name: 'xjl', age: 18 }
person.age = 19; 
console.log(person.age); // 19

// 错误:直接重赋值,报错!
person = 'xjl'; 

这个错误我记一辈子 → Assignment to constant variable.

image.png

别为了用 const 而用 const!变量需要修改的时候,果断用 let,强行用 const 只会给自己挖坑。


变量提升:JS 的编译小秘密

我之前还遇到过一个奇葩问题:先使用变量,居然不报错?

// var 变量提升:编译阶段把pizza提到顶部,赋值undefined
console.log(pizza); // undefined
var pizza = 'Deep Dish';

换成 let 直接报错,这就是暂时性死区

console.log(pizza); 
let pizza = 'Deep Dish';

控制台报错 → ReferenceError: Cannot access 'pizza' before initialization

image.png

后来我才知道,JS 执行代码分两步:

  1. 编译阶段:扫描所有变量,准备内存空间
  2. 执行阶段:逐行运行代码

var 会在编译阶段赋值undefined,let/const 只提升声明,不赋值,所以提前访问就报错。这也是别用 var 的原因:完全违背我们写代码的直觉。


我踩过的三个致命坑,给你们避避雷

  1. 误以为 var 在 {} 里是局部变量,结果全局污染,导致变量冲突
  2. 用 const 改数字 / 字符串,盯着Assignment to constant variable报错半天没反应过来
  3. 先使用 let 变量,再声明,被暂时性死区坑到怀疑人生

聊到这其实差不多了,这次啃完 ES6 变量,我脑子里就记住了三句话:

  1. 开发直接抛弃 var,let 声明变量,const 声明常量,是最佳实践
  2. {}就是块级作用域,只有 let/const 认,var 不认
  3. const 简单类型定值不改,复杂类型能改属性,别混用

JS 这门语言确实早期有很多瑕疵,毕竟是一周赶出来的浏览器副产品,但 ES6 之后真的在一点点变好用。

对了,你们有没有踩过 var 或者作用域的坑?评论区聊聊,让我也看看有没有比我更离谱的经历~