一文帮你搞懂JavaScript的闭包机制!

0 阅读8分钟

📖前言

在阅读了由KYLE SIMPSON创作的《你不知道的JavaScript上卷》的闭包部分后,我深刻地认识到了闭包的概念,在此书写一篇帮助理解闭包的文章,其中凝括了这本书里对闭包的知识讲解和结合deepseek的一些思考,相信你看完后,一定也能对闭包很熟悉!



🔍一个基础例子

下面我们来看一段代码

function foo(){
    var a = 2;
    
    function bar(){
    console.log(a);
    }
    
    return bar;
}

var a = 3;

var baz = foo()

baz(); //2 

foo函数的返回值是bar函数,赋值给了baz,所以baz也就是bar函数,当baz(bar)调用时,输出了函数里定义的var a = 2。想想这是为什么?

bar的词法作用域是foo函数的作用域,可是你会发现咱们执行bar(baz就是bar)时作用域是全局的作用域呢!但这时,bar输出的还是词法作用域,也就是foo函数作用域里面的变量a!想想这是为什么?

其实这些都是闭包的效果!



🧩闭包到底是什么

一句话概括:闭包是函数和其词法环境的结合,既包含结构(函数+变量),也包含行为(维持变量的引用),它是JavaScript的核心机制!

我们来分析一下上面这个例子:

闭包使得在上面这个例子bar是在自己定义的词法作用域以外的地方执行,但仍然持有对定义的词法作用域的引用。因此,在几位秒后变量baz被调用(调用的就是bar),不出意料它可以访问定义时的词法作用域,因此它也可以如预期般访问foo中的变量a。

上面这个例子是我为了体现闭包的特性而特意书写的,它看起来有的点刻意,但实际上我们身边处处存在闭包,比如咱们经常写的setTimeout,请看下面这个例子

function wait(message){
    
    setTimeout( function timer(){
        console.log(message);
    },1000)
}

wait("Hello,closure");

这里将一个内部函数(名为timer)传递给setTimeout(..), timer具有涵盖wait(..)作用域的闭包,因此还保有对变量message的引用

wait(..)执行1000毫秒后,它的内部作用域并不会消失,timer函数依然保有wait(..)作用域的闭包.

实际上,在定时器,时间监听器.Ajax请求,跨窗口通信,Web Works或者其它的异步或同步任务中,只要使用了回调函数,你就能发现闭包的使用!!!




✨闭包的神奇之处--阻止垃圾回收

好了,通过以上的介绍,我相信你已经清楚地知道了闭包是什么了,能够知道闭包无处不在,下来我们将要探讨一下闭包的的神奇之处--阻止函数的垃圾回收。

首先,在函数执行之后,通常将函数的整个内部作用域销毁,为了不浪费空间,会有引擎的垃圾回收器用来释放不再使用的内存空间。

function foo(){
    var a = 2;
    
    function bar(){
    console.log(a);   // bar 捕获了 foo 的作用域(形成闭包)

    }
    
    return bar;
}

var a = 3;

var baz = foo()  //执行foo,理论上foo的作用域应该被回收
 
baz(); //输出2 (闭包阻止了GC回收foo的作用域)

还是这个例子,按理来说,咱们在var baz = foo()执行完后,foo()的整个内部作用域就会被销毁,可是闭包的神奇之处正是可以阻止这件事的发生,事实上foo()的作用域仍然存在,未被回收,这是为什么呢?原来是有bar()在使用.

拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够不被回收,一直存活.所以当掉调用baz,baz()时,它当然可以访问该作用域内的变量a!


下面来看一个没有闭包的函数案例

function noClosureExample() {
  var temp = "I should be garbage collected";
  console.log(temp); // 无内部函数引用 temp
}

noClosureExample(); // 执行后,temp 会被 GC 回收(无闭包引用)

这个普通函数没有闭包的存在,所以它在执行后,自然而然的就会被回收啦!


🚀闭包的应用:模块化

有许许多多的应用都会利用闭包的强大威力,下面我们一起来研究其中最强大的一个:模块


📦模块是什么

在JavaScript中, 模块是一个独立的代码单元,它将变量、函数、类等封装起来,通过导出和导入 机制与其它模块交互,模块的核心目的是实现代码复用、作用域隔离和依赖管理。

下面的代码是ES的模块实现的案例。

// math.js(模块定义)
var mathModule = (function() {
  // 私有变量(外部无法访问)
  var PI = 3.14;

  // 公共方法
  function sum(a, b) {
    return a + b;
  }

  // 暴露公共接口
  return {
    sum: sum,
    PI: PI
  };
})();

闭包的体现:在mathModule中,IIFE 执行后,其内部作用域(包含 PI 和 sum)本应被销毁,但由于返回的对象中引用了 sum 和 PI,这些变量被闭包保留,供外部通过 mathModule 访问。

模块模式必须具备两个必要条件:

1.必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例) 2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态


🔒模块的私有性:

模块的私有性是指:未被返回的变量(如 IIFE 内其他未暴露的变量)会被真正销毁,无法从外部访问,这就是模块的私有性

var counterModule = (function() {
  var count = 0; // 私有变量

  function increment() {
    count++;
  }

  function getCount() {
    return count;
  }

  return { increment, getCount }; // 只暴露部分接口
})();

counterModule.increment();
console.log(counterModule.getCount()); // 1
console.log(counterModule.count); // undefined(无法直接访问私有变量)

increment 和 getCount 函数内部引用了 count,因此 JavaScript 会保留 count 的引用。但是count 被定义在 IIFE 内部,外部无法直接修改或读取它,只有通过 increment() 和 getCount() 才能操作 count,这就是闭包实现的数据封装,从而模块可以利用这个特性来形成其私有性。


🆕ES6的模块机制

在ES5中,JS没有原生的模块API,开发者只能通过IIFE(立即执行函数)、闭包、命名空间等方式模拟模块化,太过复杂的同时还会存在手动管理依赖和全局污染风险等问题,于是ES6,引入了原生模块语法,为模块增加了一级语法支持。

在ES6 通过模块系统进行加载时,ES6会将文件当作独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员。

具体语法如下:

1. 导出模块(export

// math.js
export const PI = 3.14; // 命名导出
export function sum(a, b) { return a + b; } // 命名导出
export default class Calculator { // 默认导出(每个模块仅限一个)
  add(a, b) { return a + b; }
}

2. 导入模块(import

// app.js
import Calculator, { PI, sum } from './math.js'; // 混合导入(默认 + 命名)
console.log(sum(PI, 2)); // 5.14
const calc = new Calculator();
console.log(calc.add(1, 2)); // 3

3. 动态导入(按需加载)

// 动态加载模块(返回 Promise)
import('./math.js').then(module => {
  console.log(module.sum(1, 2)); // 3
});

以下是两者的对比表格:

方案ES5(模拟)ES6(原生模块)
实现方式IIFE + 闭包export / import 语法
作用域手动管理(闭包)文件即模块(自动隔离)
依赖管理手动(<script> 标签顺序)静态分析(编译时确定依赖)
私有性通过闭包隐藏变量默认隔离,支持显式导出
优化无 Tree Shaking支持 Tree Shaking



🎯总结

本文主要介绍了闭包的概念与闭包的重要应用:模块,其中具体介绍了闭包到底是什么,闭包能够在底层阻止函数作用域被回收,模块是什么,模块的私有性,以及在ES之后模块新的原生支持的使用方式,这些概念不论是在深入理解JS还是应对大厂的面试,都是非常之重要的,希望大家能够理清这些概念,读者也强烈建议各位可以取阅读《你不知道的JavaScript》这本经典著作,感谢你看到这里,祝福你能够在JavaScript的学习上收获满满!!!

🌇结尾

本文部分内容参考KYLE SIMPSON的《你不知道的JavaScript(上卷)

感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)

作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。