为什么有的变量在函数里能用,在外面却报错?为什么循环里的i总是最后一个值?今天我们就来聊聊JavaScript的作用域和作用域链,顺便揭开闭包的神秘面纱。保证你看完之后,再也不用背面试题了。
前言
想象一下这样的场景:你在自己房间里找手机,找不到就去客厅找,再找不到就去邻居家借手机打电话。如果所有地方都找不到,那就只能放弃——手机丢了。
JavaScript在查找变量时,也是这么个流程。这个“找东西”的规则,就是作用域链。而变量能在哪些地方被找到,由它的作用域决定。
今天我们就来把这件事彻底捋清楚。
一、作用域:变量的“活动范围”
作用域就是变量能够被访问到的范围。JS中有三种主要作用域:
1. 全局作用域:公共场所
在函数外面定义的变量,或者没加任何关键字直接写的变量(严格模式会报错),都属于全局作用域。
varglobalVar='我是全局的';letalsoGlobal='我也是全局的';functionsayHello(){console.log(globalVar);// 能访问}全局变量就像公共场所的设施,谁都能用,但正因为谁都能改,所以容易出问题。而且全局变量会一直存在,直到页面关闭。
2. 函数作用域:自己家
在函数内部用var声明的变量,只能在这个函数内部访问。外面进不去,里面可以出去(找外面的变量)。
functionmyHouse(){varsecret='我藏起来的零食';console.log(secret);// 能访问}console.log(secret);// 报错:secret is not defined函数作用域像自己家,外人不能随便进,但你可以从家里出去(访问全局)。
3. 块级作用域:卧室里的保险柜
ES6新增的let和const带来了块级作用域。块就是大括号{}包起来的地方,比如if、for、while里面。
if(true){letblockVar='我只能在块里用';varfunctionVar='我可以在整个函数用';// var没有块级作用域}console.log(blockVar);// 报错console.log(functionVar);// 能访问,因为var只有函数作用域块级作用域就像卧室里的保险柜,只有在这个房间里才能打开。var则像家里的公共区域,虽然写在卧室里,但实际还是公共的。
二、作用域链:找变量的路径
当你在一个作用域里使用变量时,JS引擎会按照这个顺序找:
- 当前作用域:先看自己家里有没有。
- 外层作用域:没有就去上一层找。
- 继续往外:一层一层往上,直到全局作用域。
- 全局也没有:那就报错
not defined。
这种嵌套的作用域形成的链条,就是作用域链。
来看个例子:
varglobal='全球通';functionouter(){varouterVar='外层的';functioninner(){varinnerVar='内层的';console.log(innerVar);// 找到自己家的console.log(outerVar);// 自己家没有,去外层找console.log(global);// 自己家没有,外层没有,再去全局}inner();}outer();这个过程就像你在家找东西:先翻自己口袋,没有就去客厅找,还没有就去小区便利店,再没有就只能放弃了。
三、闭包:虽然离开了,但我还记得
闭包是JS里一个常考常新、常学常忘的概念。简单来说:闭包就是函数记住了它定义时的作用域,即使这个函数在其他地方执行,也能访问那个作用域里的变量。
举个例子:
functioncreateCounter(){letcount=0;// count 被闭包记住了returnfunction(){count++;console.log(count);};}constcounter=createCounter();counter();// 1counter();// 2counter();// 3这里createCounter执行后返回了一个函数,按说count应该被销毁了,但返回的函数依然能访问count——这就是闭包的力量。
闭包的生活比喻
想象你从小长大的家,后来搬走了,但你还记得家里的WiFi密码。每次你路过楼下,还能连上那个WiFi。这个“记住密码”的能力,就是闭包。
闭包的用途:
- 数据私有化(比如上面的计数器,外部无法直接修改count)
- 函数工厂(生成特定功能的函数)
- 回调函数中保持状态(比如事件监听)
闭包的坑
闭包虽然好用,但也要注意内存问题。因为被记住的变量不会释放,如果闭包一直存在,这些变量就会一直占用内存。比如上面例子,只要counter这个函数还在,count就不会被垃圾回收。
四、经典面试题:循环中的var
这是JS初学者最容易踩的坑之一:
for(vari=0;i<5;i++){setTimeout(function(){console.log(i);},100);}你期望输出0,1,2,3,4,但实际输出5,5,5,5,5。为什么?
因为var没有块级作用域,循环里的i其实是全局(或函数级)的同一个变量。循环结束后i变成了5,然后setTimeout的回调执行时,访问的都是同一个i,所以全是5。
解决方式:
- 用let:let有块级作用域,每次循环都会创建一个新的变量。
for(leti=0;i<5;i++){setTimeout(function(){console.log(i);// 0,1,2,3,4},100);}- 用闭包(老办法):
for(vari=0;i<5;i++){(function(j){setTimeout(function(){console.log(j);},100);})(i);}用立即执行函数创建新的作用域,把每次的i传进去保存下来。
五、词法作用域:写在哪就在哪找
JS采用的是词法作用域(也叫静态作用域),也就是说变量的查找范围在代码编写时就决定了,而不是在运行时。
varvalue=1;functionfoo(){console.log(value);}functionbar(){varvalue=2;foo();// 输出什么?}bar();// 输出1这里foo定义在全局,所以它访问的value是全局的1,而不是bar里的2。因为作用域由函数定义的位置决定,而不是调用位置。
这个特性是闭包能工作的基础。
六、执行上下文:运行时的小剧场
作用域是静态的规则,而执行上下文是运行时动态的环境。每当函数执行,都会创建自己的执行上下文,里面包含了变量、参数、以及对外部作用域的引用。
执行上下文有点像每次进家门时拿的钥匙串,上面有自己家的钥匙,还有父母家的钥匙(通过作用域链)。
七、总结:今天你学到了什么?
- 作用域就是变量的可见范围:全局(公共场所)、函数(自己家)、块级(卧室保险柜)。
- 作用域链就是找变量的路径:当前 → 外层 → 全局,找不到就报错。
- 闭包是函数记住了它出生时的环境,即使离开了也能访问那些变量。用途广泛,但要注意内存。
- 词法作用域意味着变量的查找在写代码时决定,和运行位置无关。
- 循环中用
var容易踩坑,用let或闭包解决。
现在你再看到作用域相关的问题,应该能像老司机一样游刃有余了。明天我们将继续深入,聊聊JavaScript里最让人迷惑的概念之一:闭包的应用场景和内存管理,看看闭包在实际项目中到底怎么用,怎么避免内存泄漏。
如果你觉得今天的文章对你有帮助,点个赞让更多人看到,也欢迎在评论区聊聊你遇到过的作用域坑。我们明天见!