闭包:JavaScript中最难部分?一文带你拿捏它!

0 阅读4分钟

引言

如果你正在学习 JavaScript ,那么闭包这个概念你一定绕不开。它是 JavaScript 中最核心、最强大的特性之一,也是面试中最高频的考点。那么,为什么闭包这么重要?

1. 它是理解 JavaScript 运行机制的关键

闭包的本质是函数能够记住并访问其词法作用域,即使函数在其定义之外被调用。

2. 它是模块化编程的基础

在 ES6 模块系统出现之前,闭包是实现数据私有化和模块化的主要方式。即使在今天,闭包仍然是编写高质量 JavaScript 代码的必备技能。

在深入闭包之前,我们需要先了解作用域链的概念,因为它是闭包的基础。

作用域链

1. 什么是作用域链?

JavaScript 在预编译时会创建一个调用栈,然后在调用栈中加入执行上下文,执行上下文中又有 变量环境词法环境。每一个执行上下文的变量环境中都存在一个 outer 指针,用来指向外部的执行上下文。当 v8引擎 在查找一个变量时,在当前执行上下文中没有找到,就会顺着 outer 所指向的那个执行上下文查找,以此类推,直到找到全局为止,我们把这个查找的链条叫做 作用域链

image.png

2. 作用域链的形成

当前执行上下文→ outer → 外部执行上下文 → outer → 全局执行上下文

闭包

1. 闭包是什么?

  • 一个函数执行完毕后,它的执行上下文会被销毁
  • 根据作用域的查找规则,内部函数一定可以访问外部函数中的变量

那么当一个外部函数的内部函数被拿到外部函数之外来执行,哪怕外部函数执行完毕了,被内部函数引用的那部分变量依然需要保留,我们把这部分变量的集合称之为闭包。

2. 闭包的形成条件

function foo(){
    let a = 1;// 外部函数变量
    
    function bar(){
        console.log(a);//内部函数引用外部变量
    }
    return bar;//内部函数被拿到外部执行
}

const fn = foo();//fn就是一个闭包
fn();

image.png

foo执行上下文预编译完成后被删除,但其词法环境中的变量 a 被引用了,所以留下一个闭包,闭包中保留下 a = 1 。所以 闭包 = bar函数 + a 变量所在的词法环境。

3.闭包的生命周期

创建:外部函数执行时

存活:内部函数被外部引用期间

销毁:内部函数不再被引用时

闭包的优缺点

优点

1.数据私有化:封装变量,防止全局污染

2.模块化代码:创建独立的模块

3.记忆状态:保存函数执行的上下文

缺点 1.内存泄漏:变量不被释放 2.性能问题:过度使用会增加内存负担

闭包的使用场景

1.数据私有化

function add() {
    let count = 0 //私有变量,外部无法直接访问
    return function() {
        count++
        return count
    }
}
//使用示例
const bar = add()
console.log(bar())// 1
console.log(bar())// 2
console.log(bar())// 3
console.log(add.count);//undefined (无法直接访问)

2.解决 var 循环问题

var arr = []

for(var i = 1; i <= 5; i++) {
    arr.push(function(){
        console.log(i);//输出: 5,5,5,5,5
    })
}

for(var n = 0; n <arr.length; n++) {
    arr[n]()
}

输出:5 5 5 5 5

原因:var 声明的 i 是全局变量,循环结束时 i = 5,定时器回调执行时访问的是同一个 i。

解决方案1:使用闭包

var arr = []

for(var i = 1; i <= 5; i++) {
    function fn(j){//创建独立作用域
        arr.push(function(){
        console.log(j);//输出:1,2,3,4,5
    })
    }
    fn(i)//将当前 i 作为参数传入
   
    
}

for(var n = 0; n <arr.length; n++) {
    arr[n]()
}

原理:闭包捕获了每次循环的 j 值,每个闭包都有独立的 j ,互不影响

解决方案2:使用 let (ES6)

var arr = []

for(let i = 1; i <= 5; i++) {
    arr.push(function(){
        console.log(i);//输出:1,2,3,4,5
    })
}

for(var n = 0; n <arr.length; n++) {
    arr[n]()
}

原理:let 在循环中会为每次迭代创建新的绑定

总结

闭包是 JavaScript 中函数能够记住并访问其创建时词法作用域的特性。它源于作用域链的查找规则和 JavaScript 的垃圾回收机制,是实现数据私有化、模块化和状态保存的核心技术。