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