author: 专注前端开发,分享JavaScript干货
title: JavaScript高级①|作用域与闭包,从原理搞懂代码的可见范围
update: 2026-04-28
tags: JavaScript,作用域,闭包,词法作用域,执行上下文,作用域链,前端进阶
作者:专注前端开发,分享JavaScript干货
更新时间:2026年4月
适合人群:有JS基础,想深入理解执行机制的开发者
前言:作用域与闭包为什么重要?
作用域决定了一个变量在哪里"看得见"。闭包是JS最强大的特性之一,也是面试高频考点。
很多"疑难杂症":循环里的异步、变量泄漏、内存问题——本质上都是作用域和闭包的问题。
一、作用域的类型
1.1 全局作用域
// 顶层声明的变量属于全局作用域varglobalVar="全局变量";functiontest(){// 函数内部可以访问全局变量console.log(globalVar);// ✅}test();// 但在函数内部声明的变量,外面访问不到functiontest2(){varinsideVar="函数内部变量";}// console.log(insideVar); // ❌ ReferenceError1.2 函数作用域(var)
// var 在函数内声明,只在函数内有效functionouter(){varx=10;if(true){vary=20;// var 不受块级作用域影响console.log(x);// ✅ x在这里可见}console.log(y);// ✅ y在这里也可见(var的特性)}console.log(x);// ❌ x在外面访问不到1.3 块级作用域(let / const)
// let 和 const 有块级作用域functionouter2(){letx=10;if(true){lety=20;constz=30;console.log(x,y,z);// ✅ 全部可见}console.log(x);// ✅// console.log(y); // ❌ y只在这个块里有效// console.log(z); // ❌ z只在这个块里有效}二、词法作用域(静态作用域)
JavaScript采用的是词法作用域(静态作用域),即函数的作用域在定义时就决定了,而不是运行时。
varvalue=1;functionfoo(){console.log(value);// 引用外层的value}functionbar(){varvalue=2;foo();// 打印的是1,不是2!}bar();// 输出:1分析:foo定义时外层能看到的value是全局的1,bar调用时把局部value改成了2,但foo的词法环境不变。
三、作用域链
当访问一个变量时,JavaScript会从内到外逐层查找,直到找到为止,找不到就报错。
vara=1;functionlevel1(){varb=2;functionlevel2(){varc=3;functionlevel3(){vard=4;console.log(a,b,c,d);// a → 全局找到// b → level1找到// c → level2找到// d → level3找到}level3();}level2();}level1();// 输出:1 2 3 4四、闭包(Closure)
4.1 闭包的本质
闭包 = 函数 + 该函数能访问的外部变量。
functionouter(){varcount=0;// 外部变量functioninner(){count++;// 内部函数引用了外部变量returncount;}returninner;// 把inner函数返回}constcounter=outer();// outer已经执行完毕console.log(counter());// 1(count被inner记住了)console.log(counter());// 2console.log(counter());// 3// outer虽然执行完了,但它的count变量依然被inner引用着,没有被回收4.2 闭包的经典场景
计数器:
functioncreateCounter(){letcount=0;return{increment(){count++;},decrement(){count--;},getCount(){returncount;}};}constcounter=createCounter();counter.increment();counter.increment();counter.decrement();console.log(counter.getCount());// 1防抖函数(闭包+定时器):
functiondebounce(fn,delay){lettimer=null;returnfunction(...args){clearTimeout(timer);timer=setTimeout(()=>{fn.apply(this,args);},delay);};}consthandleInput=debounce(function(value){console.log("发送搜索请求:",value);},500);// 每次输入都清除上一个定时器,设置新的500ms定时器input.addEventListener("input",(e)=>handleInput(e.target.value));4.3 循环中的闭包问题(高频面试题)
// ❌ 错误写法:所有点击都输出6for(vari=0;i<6;i++){setTimeout(()=>console.log(i),100);}// 输出:6 6 6 6 6 6(循环结束后i变成6)// ✅ 解决方案1:用let(let有块级作用域,每次循环i都是新变量)for(leti=0;i<6;i++){setTimeout(()=>console.log(i),100);}// 输出:0 1 2 3 4 5// ✅ 解决方案2:立即执行函数(IIFE)捕获ifor(vari=0;i<6;i++){(function(j){setTimeout(()=>console.log(j),100);})(i);}// 输出:0 1 2 3 4 5五、内存泄漏与闭包
闭包会阻止外部变量被垃圾回收,要注意:
// ❌ 可能造成内存泄漏:大型数据被闭包持有functionheavyProcess(){constbigData=newArray(1000000).fill("x");// 占用大量内存returnfunction(){returnbigData[0];// bigData永远不会被释放};}// ✅ 正确做法:用完主动释放functionheavyProcessFixed(){constbigData=newArray(1000000).fill("x");constgetFirst=function(){returnbigData[0];};// 用完后清空引用bigData=null;returngetFirst;}六、知识卡
| 概念 | 说明 |
|---|---|
| 全局作用域 | 整个程序任何地方都可见 |
| 函数作用域(var) | 仅在函数内部有效 |
| 块级作用域(let/const) | 仅在{}内部有效 |
| 词法作用域 | 函数定义位置决定作用域 |
| 作用域链 | 从内到外逐层查找变量 |
| 闭包 | 函数 + 可访问的外部变量 |
| 循环闭包问题 | 用let或IIFE解决 |
七、课后作业
- 分析下面代码的输出结果并解释原因:
for(vari=0;i<3;i++){setTimeout(function(){console.log(i);},100);} - 用闭包实现一个"记忆函数":相同参数直接返回缓存结果
- 写一个函数,使其返回一个函数数组,每个函数返回自己的索引(0, 1, 2…)
有问题欢迎评论区留言,大家一起讨论!
标签:JavaScript | 作用域 | 闭包 | 词法作用域 | 执行上下文 | 作用域链 | 前端进阶