1. 作用域到底是什么:先把“变量去哪儿了”这件事讲透
我在面试别人的时候,几乎每次都会问一个摸底问题:“详细说说你对作用域和作用域链的理解”。问十个人里,大概有七八个能背出“作用域是变量的有效访问范围,作用域链是变量查找的链路”这种标准答案,但再追问一句“那作用域链到底是什么时候形成的”,一半人就卡住了。这个现象特别有意思——大家都觉得自己知道,实际上只是记住了概念的壳,没拿到内核。
作用域(Scope)本质上是一套规则,用来决定“当前代码能访问哪些变量”。你可以把它想象成一张通行证:你在哪个区域里,决定了你能进哪些门。JavaScript引擎在执行代码之前,会先做一遍完整的词法解析,把每一段代码里声明了哪些变量、这些变量归属于哪个区域,全部登记在案。这个阶段不执行任何逻辑,纯粹是“查户口”,登记完毕之后才开始真正跑代码。
很多人把作用域理解成一个“容器”,习惯说“变量在某个作用域里”。这个说法不算错,但容易误导。作用域其实是一种约束规则,它规定了:当你在某个位置写下一个变量名时,引擎应该去哪里找这个变量对应的值。至于“作用域链”,就是这套查找规则在嵌套结构下的具体路径。
这里有个关键点必须掰扯清楚:JavaScript采用的是静态作用域,也叫词法作用域。意思是,一个函数能访问哪些变量,在它被定义的那一刻就已经决定了,跟你这个函数在哪被调用没有任何关系。这是作用域链和 this 指向最大的区别——this 是动态绑定的,谁调用指向谁;作用域链是静态绑定的,定义在哪就指向哪。
我举个例子你就明白了:
javascript复制const a = 1;
function outer() {
const b = 2;
function inner() {
console.log(a + b); // 3
}
return inner;
}
const fn = outer();
fn(); // 3
inner 定义在 outer 内部,所以它能同时访问到 outer 里的 b 和全局的 a。关键在于,哪怕我们把 inner 通过返回值带到了全局环境里执行,它依然认识自己的“出生地”,依然能找到 b。这就是词法作用域的含义——看代码结构就能推断出结果,不需要关心调用位置。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 全局、函数、块级:JavaScript的三种作用域类型
JavaScript的作用域类型经历过一个演变过程,从最早只有全局和函数两种,到后来补上了块级,这段历史直接影响了很多老代码的“怪异行为”,也解释了为什么现在面试特别喜欢考 var 和 let 的区别。
2.1 全局作用域:所有变量的默认归宿
在浏览器环境里,全局作用域的顶层对象是 window,在 Node.js 里是 global。在任何位置都能访问到的变量,就是全局变量。全局变量有两种常见来源:一种是在顶层用 var 或 function 声明的,另一种是根本没写声明关键字、直接赋值的。
后者是个大坑。在非严格模式下,给一个未声明的变量赋值,JavaScript会自动帮你把它挂到全局对象上。这意味着,你在某个函数内部随手写了个 count = 1,它就成了全局变量,全项目的任何地方都能改它。多人协作时这种代码简直就是定时炸弹。所以现在写代码,严格模式几乎是标配,至少能在第一步拦住这种隐式全局。
2.2 函数作用域:JavaScript最初的“局部”概念
ES6 之前,函数是JavaScript里唯一能创造局部作用域的结构。var 声明的变量,在函数内部就是局部的,出了函数谁也访问不到。但 var 有个著名的毛病——不存在块级作用域:
javascript复制if (true) {
var leak = '我在外面也能访问到';
}
console.log(leak); // '我在外面也能访问到'
var 只看函数边界,花括号圈不住它。所以在早期代码里,for 循环里定义的循环变量,循环结束后依然活着,这不是bug,是设计如此。很多老前端写代码时必须刻意用函数去“包一层”,就是为了制造隔离。IIFE(立即执行函数表达式)在当年那么流行,根本原因就是当时只有函数能隔离作用域。
2.3 块级作用域:let/const带来的真正改变
ES6 引入了 let 和 const,块级作用域才正式登场。所谓块,就是一对花括号包起来的区域——if 分支、for 循环、while 循环,甚至单独的 {},都能形成独立的变量空
