#JavaScript 作用域链与闭包:从小白到大神,一篇搞定!

36 阅读8分钟

JavaScript 作用域链与闭包:从小白到大神,一篇搞定!

为什么需要理解作用域链?

大家好,我在学习JavaScript过程中发现,很多初学者(包括我自己)都会被"作用域链"、"闭包"这些概念搞得晕头转向。经过大量的代码实践和调试分析,我终于搞懂了这些概念!今天就把我的学习笔记整理出来,用真实的代码示例和我调试时截取的图片,让大家真正理解这些核心概念。

本文特点:

  • 使用真实的代码示例
  • 配合调试截图理解执行过程
  • 从简单到复杂逐步深入
  • 解决实际开发中的常见问题

一、基础概念:什么是作用域?

1.1 作用域:变量的"生活圈子"

想象一下,每个人都有自己的生活圈子:

  • 家里的事情,只有家人知道 → 局部作用域
  • 小区的事情,所有邻居都知道 → 全局作用域

在JavaScript中,作用域决定了变量在哪里可以被访问。

// 全局作用域 - 整个小区都知道
var globalName = '极客时间';

function myFunction() {
    // 函数作用域 - 只有家里知道
    var secret = '我的秘密';
    console.log(globalName); // ✓ 可以访问全局变量
    console.log(secret);     // ✓ 可以访问自己的变量
}

myFunction();
console.log(globalName); // ✓ 可以访问全局变量
// console.log(secret);  // ✗ 错误!不能访问函数内的变量

二、作用域链:如何查找变量?

2.1 经典示例分析

让我们从最简单的例子开始,理解作用域链的工作原理:

// 文件:1.js
function bar(){
    console.log(myName);  // 问题:这里会输出什么?
}
function foo(){
    var myName = '极客邦';
    bar();  // 在foo内部调用bar
}
var myName = '极客时间';
foo();

运行结果: 输出 '极客时间'

为什么会这样?

  1. bar() 函数在全局作用域声明
  2. 函数的作用域链在声明时就确定了
  3. bar() 在自己的作用域找不到 myName
  4. 沿着作用域链向上找,在全局找到了 myName = '极客时间'

对应代码的执行过程:虽然bar在foo内部调用,但它的作用域链仍然指向全局

屏幕截图 2026-02-01 153655.png

屏幕截图 2026-02-01 152036.png 关键理解点:

  • 作用域链是静态的,由代码书写位置决定
  • 函数的作用域链在声明时确定,与调用位置无关

2.2 词法作用域图示

为了更直观地理解,让我们看一个多层嵌套的例子:

// 对应屏幕截图 2026-02-01 153521.png 的代码
let count = 1;
function main(){
    let count = 2;
    function bar(){
        let count = 3;
        function foo(){
            let count = 4;
            console.log(count); // 输出4
        }
        foo();
    }
    bar();
}
main();

展示多层函数嵌套时的词法作用域链结构

屏幕截图 2026-02-01 153521.png 作用域链结构:

  • foo 的作用域:foobarmain → 全局
  • 查找 count 时,先从最近的开始找

三、复杂场景:块级作用域与执行上下文

3.1 复杂示例分析

现在让我们看一个更复杂的例子,理解执行上下文中的变量环境和词法环境:

// 文件:2.js
function bar () {
    var myName = '极客世界';
    let test1 = 100;
    if(1){
        let myName = 'Chrome 浏览器';
        console.log(test);  // 问题:这里输出什么?
    }
}
function foo () {
    var myName = '极客邦';
    let test = 2;
    {
        let test = 3;
        bar();
    }
}
var myName = '极客时间';
let myAge = 10;
let test = 1;
foo();

运行结果: 输出 1

执行过程分析:

  1. foo() 被调用,创建执行上下文
  2. foo() 中调用 bar(),创建 bar() 的执行上下文
  3. bar() 中查找 test 变量:
    • bar() 自己的作用域找不到
    • 沿着作用域链向上找(bar 在全局声明)
    • 在全局作用域找到 test = 1

对应代码的执行上下文结构:展示调用栈中的变量环境和词法环境

屏幕截图 2026-02-01 155540.png 关键知识点:

  • 变量环境:存储 var 声明的变量
  • 词法环境:存储 let/const 声明的变量,支持块级作用域
  • outer:指向外部的词法环境,形成作用域链

四、闭包:函数的"专属背包"

4.1 闭包基础

闭包是JavaScript中非常重要的概念,让我们通过一个具体的例子来理解:

// 文件:3.js
function foo(){
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = {
        getName: function(){
            console.log(test1)
            return myName
        },
        setName: function(newName){
            myName = newName
        }   
    }
    return innerBar  // 关键:返回包含函数的对象
}
var bar = foo()      // foo执行完毕,执行上下文出栈
bar.setName("极客邦") // 但依然能访问foo的变量
bar.getName()        // 输出:1 和 "极客邦"
console.log(bar.getName()) // 再次验证

神奇的现象:

  1. foo() 执行完毕后,它的执行上下文应该被销毁
  2. bar.setName()bar.getName() 依然能访问 foo() 中的变量
  3. 这是因为形成了闭包

屏幕截图 2026-02-01 162418.png

4.2 闭包的工作原理

闭包就像一个"专属背包",即使函数执行完毕,它依然背着这个背包:

展示foo函数执行完毕后,闭包如何保留变量

屏幕截图 2026-02-01 163232.png 闭包的形成条件:

  1. 函数嵌套函数
  2. 内部函数被外部引用(通过return、赋值等方式)
  3. 内部函数使用了外部函数的变量

4.3 闭包的实际执行过程

setName 函数执行时:

展示setName函数执行时如何访问闭包中的变量

屏幕截图 2026-02-01 164549.png 内存管理注意事项:

function createClosure() {
    var bigData = new Array(1000000).fill('*'); // 大数据
    
    return {
        useData: function() {
            console.log(bigData[0]); // 引用bigData
        }
    };
}

var closure = createClosure();
// 即使createClosure执行完毕,bigData也不会被回收
// 因为闭包还在引用它

五、V8引擎如何管理作用域

5.1 执行上下文栈(调用栈)

V8引擎使用"调用栈"来管理函数的执行:

function first() {
    console.log("执行first");
    second();  // 调用second
    console.log("first结束");
}

function second() {
    console.log("执行second");
    third();   // 调用third
    console.log("second结束");
}

function third() {
    console.log("执行third");
}

first();

调用栈的变化:

  1. first() 入栈

  2. second() 入栈

  3. third() 入栈

  4. third() 出栈

  5. second() 出栈

  6. first() 出栈

展示调用栈的结构:全局执行上下文、foo执行上下文、bar执行上下文

屏幕截图 2026-02-01 152036.png

5.2 变量提升(Hoisting)

理解执行上下文还需要知道变量提升:

console.log(a); // 输出:undefined,而不是报错
var a = 10;

console.log(b); // 报错:Cannot access 'b' before initialization
let b = 20;

原因:

  • var 声明的变量在编译阶段被提升,初始值为 undefined
  • let/const 声明的变量也有提升,但在声明前访问会报错(暂时性死区)

六、实际开发中的应用

6.1 模块模式(使用闭包封装)

// 使用闭包实现模块化
const CounterModule = (function() {
    // 私有变量
    let count = 0;
    
    // 私有函数
    function log(message) {
        console.log(`[计数器]: ${message}`);
    }
    
    // 公开的接口
    return {
        increment: function() {
            count++;
            log(`增加后: ${count}`);
            return count;
        },
        decrement: function() {
            count--;
            log(`减少后: ${count}`);
            return count;
        },
        getCount: function() {
            return count;
        },
        reset: function() {
            count = 0;
            log("已重置");
            return count;
        }
    };
})();

// 使用模块
CounterModule.increment(); // [计数器]: 增加后: 1
CounterModule.increment(); // [计数器]: 增加后: 2
console.log(CounterModule.getCount()); // 2
// CounterModule.count // 错误:无法直接访问私有变量

6.2 解决循环中的闭包问题

常见问题:

// 问题代码
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 都输出3
    }, 100);
}

解决方案1:使用let(推荐)

for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 输出0, 1, 2
    }, 100);
}

解决方案2:使用立即执行函数

for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j); // 输出0, 1, 2
        }, 100);
    })(i);
}

七、练习题与深度思考

练习题1:作用域链查找

var x = 10;
function outer() {
    console.log(x); // 输出什么?
    var x = 20;
    function inner() {
        console.log(x); // 输出什么?
    }
    inner();
}
outer();

练习题2:闭包应用

function createMultiplier(multiplier) {
    return function(number) {
        return number * multiplier;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 输出什么?
console.log(triple(5)); // 输出什么?

练习题3:综合考察

var a = 1;
function test() {
    console.log(a);
    var a = 2;
    console.log(a);
    function inner() {
        console.log(a);
        let a = 3;
        console.log(a);
    }
    inner();
}
test();

答案分析:

  1. 练习题1:输出 undefined20(变量提升)
  2. 练习题2:输出 1015(闭包记忆参数)
  3. 练习题3:输出 undefined2undefined3

八、调试技巧与学习建议

8.1 使用Chrome DevTools调试

  1. 查看调用栈

    • 打开Sources面板
    • 设置断点
    • 查看Call Stack面板
  2. 查看作用域

    • 在Scope面板查看当前作用域的变量
    • 区分Local、Closure、Global作用域
  3. 跟踪变量变化

    • 使用Watch面板添加监视表达式
    • 使用console.log输出关键信息

8.2 学习建议

  1. 从简单开始:先理解1.js这样的简单例子
  2. 动手调试:使用我提供的代码和截图对照理解
  3. 逐步深入:从作用域链到闭包,再到执行上下文
  4. 多写多练:自己编写测试代码,观察执行结果

九、总结

通过本文的学习,你应该掌握了:

  1. 作用域链的查找规则

    • 函数的作用域链在声明时确定
    • 查找变量时,从内向外沿着作用域链查找
  2. 闭包的工作原理

    • 函数"背"着它的外部变量
    • 即使外部函数执行完毕,内部函数仍能访问外部变量
  3. 执行上下文的结构

    • 变量环境(var)
    • 词法环境(let/const)
    • outer指向外部环境
  4. 实际应用场景

    • 模块化开发
    • 数据封装
    • 解决循环中的异步问题

记住这几个核心要点:

  • 作用域链是静态的,看代码书写位置
  • 闭包是函数的"记忆背包"
  • 多调试、多实践才能真正理解

希望这篇文章能帮助你彻底理解JavaScript的作用域链和闭包!如果有任何问题,欢迎在评论区讨论。


我的学习心得: 在学习这些概念时,最大的收获就是多调试、多画图。通过Chrome DevTools一步步跟踪代码执行,观察调用栈的变化,才能真正理解这些抽象的概念。建议你也动手试试我提供的代码示例,相信会有很大的收获!