前言
作为前端入门的“拦路虎”,作用域、作用域链和闭包常常让新手宝子们头疼😣!其实它们一点都不复杂,我们结合真实代码实例,把这三个知识点讲得明明白白,还会给闭包安排超有意思的案例,保证你看完就能懂、懂了就能用~
一、 先搞懂:什么是作用域?
简单来说,作用域就是变量和函数的可访问范围,控制着变量和函数的可见性和生命周期。作用域就规定了“哪些变量能在哪个区域被访问”,还决定了变量的生命周期(什么时候创建、什么时候销毁)。
JS里有3种常见作用域,每一种都有明确的“访问规则”,结合代码实例一看就懂👇
1.1 全局作用域
最外层的作用域,代码在全局范围内都能访问到,就像家里的客厅,所有人都能使用客厅里的物品。
注意哦⚠️:全局作用域的变量,在任何函数、代码块里都能访问到,除非被局部变量“覆盖”~
1.2 函数作用域
定义在函数内部的作用域,就像自己的卧室,里面的东西(变量、函数)只有在卧室里能用到,出了卧室就找不到啦。
var a = 1 // 全局变量
function fn(){
var a = 2 // 函数作用域的局部变量
console.log(a); // 访问局部变量,输出:2
}
fn()
console.log(a); // 访问全局变量,输出:1(局部变量不影响全局)
划重点✅:函数作用域里的变量,只能在函数内部访问,外部无法访问;而且局部变量会“覆盖”同名的全局变量哦~
1.3 块级作用域
ES6之后新增的作用域,用 let、const 声明的变量,在 if、for、{} 等代码块里,就是块级作用域,出了代码块就无法访问(和 var 有很大区别!)。
// 用let声明的块级变量
if(1){
let a = 1 // 块级作用域变量
}
console.log(a); // 报错:a is not defined(出了块级作用域就找不到啦)
// 对比var(没有块级作用域)
if(true){
var b = 2
}
console.log(b); // 输出:2(var声明的变量会“溢出”到全局)
小提醒🥰:let、const 才有块级作用域,var 没有,这也是为什么现在推荐用 let/const 而不是 var
💡 核心规则:作用域只能“由内到外”访问
不管是哪种作用域,都遵循一个铁律:内部能访问外部的变量,外部不能访问内部的变量!
🐒 再进阶:作用域链是什么?
搞懂了作用域,作用域链就很简单啦!我们可以把它理解为「变量的“查找路线”」—— 当JS引擎在当前作用域找不到某个变量时,就会去“上一级作用域”找,找不到再往上找,层层往上,直到找到全局作用域,如果还是找不到,就会报错。
举个代码实例,一看就懂👇
// 全局作用域
var myName = '米妮'
function bar(){
console.log(myName); // 当前作用域没有myName,去上一级找
}
function foo(){
var myName = '米奇' // 函数作用域
bar() // 调用bar,bar的上一级是全局作用域(不是foo!)
}
foo() // 输出:米妮
这里有个关键知识点📝:函数的“上一级作用域”,是它「声明时的作用域」,不是调用时的作用域!上面的bar函数在全局声明,所以它的上一级是全局作用域,哪怕在foo里调用,也不会去foo的作用域找myName~
JS引擎在编译函数时,会自动用一个 outer 指针,记录该函数的外层作用域是谁,这就是作用域链的底层原理哦!
二、闭包:JavaScript的“变量保鲜术”
2.1 闭包的本质:为什么变量能“续命”? 🤔
先明确一个基础前提:通常情况下,一个函数执行完毕后,它的执行上下文会从调用栈中被销毁,函数内部的变量也会随之释放,无法再被访问。但闭包打破了这个规则 。
闭包的官方定义:有权访问另一个函数作用域中变量的函数。更通俗的解释是:当一个内部函数引用了外部函数的变量,即使外部函数执行完毕,内部函数依然能访问这些变量——因为外部函数的作用域不会被完全销毁,而是会保留一个“变量集合”,专门存放内部函数需要引用的变量,这个集合就是闭包 。
2.2 手把手看懂闭包 💻
枯燥的理论不如有趣的代码,下面结合3个实例,从简单到复杂,带你吃透闭包的用法和原理,所有实例均基于你提供的代码优化,更具趣味性和实用性。
实例1:最基础的闭包——延长变量生命周期(变量 “保鲜”)
这个实例能直观看到闭包的“保鲜”效果,我们用“保存人名”来演示,代码简单易懂,一眼就能看出闭包的作用:
function saveName() {
var myname = '米奇';
var age = 18; // 这个变量内部函数没引用,不会被闭包保留
function showName() {
console.log(myname); // 引用外部函数的myname
}
return showName;
}
// 1. 执行外部函数,得到内部函数,此时外部函数已执行完毕
var getSavedName = saveName();
// 2. 调用内部函数,依然能访问到外部函数的myname
getSavedName(); // 输出:米奇
解析:saveName执行完毕后,按常理myname和age都会被销毁,但因为内部函数showName引用了myname,闭包会保留myname这个变量,而age没有被引用,会正常释放。这就是闭包的核心:只保留内部函数需要的变量,不浪费内存 💡。
实例2:闭包解决经典问题——“循环变量污染”
这是面试中高频出现的闭包场景,也是开发中容易踩坑的点。我们用“给数组添加函数,输出正确索引”来演示,对比两种写法,更能体现闭包的价值:
// 需求:创建一个数组,存入5个函数,每个函数执行时输出对应的索引(1-5)
var arr = [];
// 错误写法:没有闭包,所有函数都引用同一个变量i ❌
// for(var i = 1; i <= 5; i++){
// arr.push(function(){
// console.log(i); // 所有函数都指向全局的i,最终i=6
// })
// }
// 正确写法:用闭包“锁住”每一次循环的i值 ✅
for(var i = 1; i <= 5; i++){
// 外部函数foo,接收每次循环的i作为参数
function foo(j) {
// 内部函数,引用外部函数的j(每次循环的i值)
arr.push(function(){
console.log(j);
})
}
// 调用foo,传入当前i值,闭包保留每次的j
foo(i)
}
// 执行数组中的函数,输出正确的索引
for(let n = 0; n < arr.length; n++){
arr[n](); // 输出:1 2 3 4 5(完美符合需求)
}
解析:错误写法中,var声明的i是全局变量,循环结束后i变成6,所有函数都引用同一个i,所以输出都是6。而正确写法中,每次循环调用foo(i),foo作为外部函数,j接收当前i值,内部函数引用j,闭包会为每一次循环保留对应的j值,因此每个函数执行时能输出正确的索引。
这个实例也能帮你理解:闭包可以创建独立的作用域,避免变量污染 。
实例3:闭包的“隐藏技能”——私有变量封装
闭包的一大实用价值的是封装私有变量,让变量只能通过指定方法访问和修改,避免被外部随意篡改。我们用“个人信息保护”来演示,模拟一个简单的用户模块:
// 封装用户信息模块,用闭包创建私有变量
function createUser() {
// 私有变量:只能通过内部方法访问,外部无法直接修改
var username = '米奇';
var password = '123456'; // 实际开发中会加密,这里仅演示
// 内部方法:操作私有变量
return {
// 查看用户名(只读)
getUsername: function() {
return username;
},
// 修改密码(可控修改)
setPassword: function(newPwd) {
if(newPwd.length >= 6) { // 限制密码长度,增加安全性
password = newPwd;
return '密码修改成功';
} else {
return '密码长度不能小于6位';
}
},
// 查看密码(模拟授权访问)
getPassword: function(auth) {
if(auth === 'admin') {
return password;
} else {
return '无访问权限';
}
}
}
}
// 创建用户实例
var user = createUser();
// 测试私有变量访问
console.log(user.username); // 输出:undefined(外部无法直接访问)❌
console.log(user.getUsername()); // 输出:米奇(通过指定方法访问)✅
console.log(user.setPassword('654321')); // 输出:密码修改成功(可控修改)✅
console.log(user.getPassword('test')); // 输出:无访问权限(授权验证)🔒
console.log(user.getPassword('admin')); // 输出:654321(授权后访问)✅
为什么不能直接访问username?
核心原因:username是闭包中的私有变量,被作用域限制在createUser函数内部,外部无法直接访问,具体解析如下:
- 代码中username是在createUser函数内部用var声明的变量,属于「函数作用域」(仅在createUser内部可访问);
- 闭包的封装特性:createUser返回的是内部方法(getUsername、setPassword等),而非username本身,外部只能拿到返回的方法,无法直接触及函数内部的username;
- JavaScript 作用域规则:内层作用域的变量,外层无法直接访问(作用域只能由内到外访问),username在createUser内部(内层),外部代码(如user.username)属于外层,因此无法直接访问,输出undefined。
2.3 闭包的优缺点:合理使用才是关键
闭包不是“万能的”,有优点也有缺点,掌握其特性才能避免踩坑。
优点:
-
✅ 封装私有变量:避免变量污染,提高代码安全性(如实例3);
-
✅ 延长变量生命周期:让需要长期使用的变量(如缓存数据)不被轻易销毁;
-
✅ 实现模块化:将相关逻辑和变量封装在一起,提高代码可维护性。
缺点:
最核心的缺点是可能导致内存泄漏 ⚠️。因为闭包会保留外部函数的变量,若闭包长期被引用(如挂载在window上),这些变量会一直占用内存,无法被垃圾回收机制回收,久而久之会导致内存占用过高,影响页面性能。
解决办法:不需要使用闭包时,及时解除引用(如将闭包赋值为null),让垃圾回收机制回收相关变量 。
2.4 补充:闭包与作用域链的关系
很多人会混淆闭包和作用域链,其实两者是“因果关系”:
作用域链是JavaScript的查找规则:V8引擎在当前作用域查找变量时,若找不到,就会去上一级作用域查找,层层往上,直到找到全局作用域。而闭包之所以能访问外部函数的变量,本质就是依靠作用域链——内部函数的作用域链中,包含了外部函数的作用域,即使外部函数执行完毕,这个作用域链依然存在,从而能找到对应的变量。
简单记:作用域链是闭包的“基础”,闭包是作用域链的“高级应用” 。
三、总结:闭包的核心价值 🌟
闭包的本质,是作用域规则下的“变量保鲜机制”——它让内部函数能“记住”外部函数的变量,打破了“函数执行完毕变量就销毁”的常规。从基础的变量保留,到解决循环污染,再到私有变量封装,闭包在实际开发中应用广泛,也是面试中的高频考点 。
掌握闭包的关键,不是死记硬背定义,而是理解“内部函数引用外部变量”这个核心条件,结合代码实例多练习,就能灵活运用。同时也要注意规避内存泄漏的问题,合理使用闭包,让它成为提升代码质量的工具,而不是踩坑的源头 。