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解决

七、课后作业

  1. 分析下面代码的输出结果并解释原因:
    for (var i = 0; i < 3; i++) {
        setTimeout(function() { console.log(i); }, 100);
    }
    
  2. 用闭包实现一个"记忆函数":相同参数直接返回缓存结果
  3. 写一个函数,使其返回一个函数数组,每个函数返回自己的索引(0, 1, 2…)

有问题欢迎评论区留言,大家一起讨论!


标签:JavaScript | 作用域 | 闭包 | 词法作用域 | 执行上下文 | 作用域链 | 前端进阶

更多推荐