前言网上关于闭包的文章五花八门,有的只是一点点,有的是CV党,有的只讲了一部分。就我而言,要想理解闭包,你需要掌握词法环境(或ES中的变量对象)、执行上下文和调用堆栈、(词法)作用域等。幸运的是,我们在前面已经整理了这些articles几块内容:词法作用域、词法环境和执行上下文。接下来,让我们揭开闭包的面纱,看看埃及艳后七世的一句话解释,闭包是一个绑定到执行环境的函数。它利用了词法作用域和嵌套函数的特点,当内部函数引用外部函数作用域下的变量,并且内部函数在全局环境下是可访问的时,就形成了闭包的定义。各个高手winter对闭包的定义:闭包其实只是一个绑定了执行环境的函数闭包和普通函数的区别在于它承载了一个执行环境,就像人在里面需要自带吸氧设备一样一个外星空间,这个功能也有一个生活在程序中的环境。JavaScript中闭包对应的概念是“函数”。函数嵌套时,内层函数引用外层函数作用域下的变量,内层函数在全局环境下是可访问的,形成一个闭包黑客和画家Closure(lexicalclosure)可以引用变量的函数由包含此函数的代码定义。MDN闭包是可以访问自由变量的函数。那么什么是自由变量呢?自由变量是在函数中使用的变量,但既不是函数参数也不是函数局部变量。由此,我们可以看出一个闭包有两部分:闭包=一个函数+一个函数可以访问的自由变量现代JavaScript教程闭包是当一个函数记住它的外部变量并且可以访问它们的时候《你不知道的 JavaScript》闭包是一种自然基于词法作用域编写代码的结果,你甚至不需要有意识地创建闭包来利用它们。闭包在您的代码中随处创建和使用。你缺少的是随意识别、拥抱和影响闭包的心理环境调用外部函数返回内部函数后,即使外部函数已经执行,内部函数引用的变量外部函数仍然存储在内存中。我们称这些变量的集合为闭包原则。开头提到,想要理解闭包,首先要理解三个知识点:词法作用域、词法环境、执行上下文。我们快速梳理一下:词法作用域:这里说的是函数作用域,它是由函数的声明位置决定的。功能范围有一个特点。函数内的变量不能在函数外访问,函数外的变量可以访问函数内的词法环境:在代码编译阶段记录变量声明和函数声明。函数声明形参的集合执行上下文:调用函数时带来的所有信息。包括词法环境,变量环境,还有this,我们讲三者的结合,会有:一段代码在执行过程中分为两个阶段,编译阶段和执行阶段(JavaScript是一种解释型的language,会逐行执行程序,但还是会执行。有两个阶段,两者相差几微秒)“变量提升”会发生在编译阶段,作用域是确定的,并生成词法环境。词法环境包括环境记录器和外部环境。环境记录器记录自由变量,外部指向父作用域。词法环境的环境记录器收集var、function等变量。变量环境的环境记录器收集let、const、class等变量。执行阶段创建一个执行上下文,其中包括词法环境、变量环境和this,并确定作用域链。除此之外,词法环境、变量环境等执行上下文在编译阶段就已确定。其中词法环境中的变量var和function会被提升和初始化。变量环境中的变量虽然被提升了,但是不会被初始化;虽然两者的外部相同,但它们都指向父作用域。当代码要访问一个变量时,会先在自己的作用域(即内部词法环境)中搜索该变量,然后沿着外层的父作用域(外部环境),再搜索外部环境,依此类推,直到全局词法环境,称为作用域链,并在调用函数时得到确认。我们用一个例子来解释闭包:functionfoo(){vara=1;变量b=2;返回函数bar(){console.log(a++);};}varbaz=foo();巴兹();在执行任何代码之前,首先创建一个全局执行上下文,并将其压入调用栈,然后创建一个词法环境,注册函数声明foo和变量声明baz(此时处于编译阶段)。由于全局词法环境没有外部引用,所以箭头指向空代码开始执行,执行foo(),创建foo()的函数执行上下文,压入调用栈,然后开始执行中的代码foo函数,即函数中的编译阶段,创建foo的词法环境,注册函数声明bar和变量声明a,b。它的外部指向父作用域——当全局作用域代码执行到函数bar时,bar的词法环境被创建。它没有变量,外部指向父作用域foo。所有函数都会记住它们“出生”时创建它们的词法环境所有函数都有一个[[Environment]]隐藏属性,它持有对创建函数的词法环境的引用。我们说过作用域与创建的地方有关,与调用的地方无关。调用函数foo()后,弹出调用栈,foo中的函数bar和变量b随着foo出栈被释放,因为函数bar的结果赋给了全局变量baz,baz是等价的到多个隐藏属性[[Environment]],指向父作用域foo,bar引用foo作用域下的变量a,所以变量a不能释放。因此,baz.[[Environment]]有一个对{a:0}词法环境的引用[[Environment]]在函数创建时被设置并永久保存以调用函数baz(),创建baz()的执行上下文,将其压入调用栈并在执行代码之前(编译阶段)创建一个新的词法环境,其外层指向baz.[[Environment]],即父作用域foo。当它寻找变量a时,它首先在它自己的词法环境中寻找它。如果找不到它,它会沿着外部到达其父范围。在foo词法环境中找到变量a,在变量所在的词法环境中更新变量。这样,在调用baz之后,由于baz始终存在于全局词法环境中,其隐藏属性[[Environment]]总是指向foo函数中的a变量(即使foo函数已经被销毁),当baz被调用时再次,baz()会被压入调用栈,生成一个新的bar的词法环境,它的外层还是指baz.[[Environment]],也就是不知道是不是baz的词法环境上图中的foo已经解释清楚了。简单地说:闭包是一个有自己变量的函数。首先先嵌套函数,内层函数引用外层函数的变量。由于词法作用域的性质,作用域和词法环境在定义函数的时候就已经确定了,所以即使调用了外层函数,外层函数中的变量也不会被垃圾回收,因为内层函数已经赋值给了Global变量,因为变量存在,所以外层函数的变量不会被释放。内部逻辑是:内层函数赋值给全局变量,内层函数引用外层函数变量,所以外层函数变量不会被释放。简而言之:Closure=function+freevariable闭包的本质就是函数在执行时会被压入执行栈中执行,执行完会被弹出栈。但是作用域成员由于外部引用不能释放,所以内部函数仍然可以访问外部函数的成员functioncheckAge(min){returnfunction(age){//functionreturnsage>min;//引用外部函数Variable}}//ES6语法会更简洁constcheckAge=min=>age=>age>minconstcheckAge18=checkAge(18)checkAge18(lucy.age)闭包的函数试想一下,什么问题关闭解决了吗?如果没有闭包,你需要自己实现一个应用,写很多页面,引入一些库和框架,自己写一些实用函数。当它们在一个(全局)作用域时,就会发生变量冲突(虽然它们可以通过命名约定来解决,但是在实际开发中难免会遇到命名相同的问题)假设你定义了一个变量a和一个函数b,并将它们直接写在全局环境中。这个模块实现了函数A,现在我们的程序需要开发函数B,如果你还想用变量a和函数b的标识符来表示,那就尴尬了,因为已经在函数A上用过了,不能再用了。如果不使用变量a和函数b标识符你可能会想到c和d这两个名字,但是当你调试的时候发现这两个标识符也被其他实用函数使用了。命名冲突的原因是因为它们具有相同的功能。域下已存在相同的变量名。解决上述问题,必须从作用域入手——一个模块应该有自己的作用域,以保证模块的正常运行。全局范围肯定不好。我们只使用函数函数域就可以做到这一点。所以闭包实际上是一种利用函数作用域实现的变量保护机制。它的作用是保护模块中的变量。即在函数作用域内写好代码后,对外暴露要使用的变量return语句functionouter(){vara='私有变量,只能在outer中使用';functioninner(){console.log('我是outer中的私有函数,只能在outer中使用');}返回函数closureOuter(){inner();控制台日志(一);}}varbar=outer();bar();//我是outer中的私有函数,只能在outer中使用整个过程。由于作用域特性,外层函数不能访问内层函数的变量。所以函数outer不能使用变量ainsideouter和函数inner。但是如果我们在调用函数outer的时候给bar赋值,那么返回的就是函数outer中的函数closureOuter。此时bar就是函数closureOuter,函数closureOuter因为词法环境可以访问变量a和函数inner。当bar被调用时,函数clousureOuter被执行,inner被执行并且变量a被打印出来。我们也可以理解outer是一个对外暴露closureOuter的模块。外界调用外部模块,可以使用外部变量,但不能对内部变量做任何事情。修改(保护变量)所以闭包的作用是:闭包可以创建一个函数的私有变量,这个变量在函数执行后不会被垃圾回收机制回收。而且这样可以在某些场景下保护变量闭包的优缺点和误区优点保护私有变量,避免全局变量污染使这些变量一直存在于内存中(优点)缺点一直存在于内存中(也是缺点)网上说闭包会不会导致内存泄漏?这是错误的。内存泄漏是指您不使用但仍然占用内存空间的变量。但是闭包中的变量就是我们需要的变量。怎么能说是内存泄漏呢?闭包的应用闭包有两种应用场景,一种是作为返回值,一种是作为参数。你熟悉吗?这不就是为什么我们说函数是Function中的第一批公民吗?函数的性质允许它们作为值返回并作为参数传递。所以所有函数本质上都是闭包(只有一个例外,就是newFunction语法,它的[[Environment]]不指向当前的词法环境,而是指向全局环境)因为返回值非常类似于下面的例子,也是我们平时看到最多的闭包形式,外层函数返回内层函数functionfoo(){vara=1;返回函数bar(){console.log(a)}}varbaz=foo();baz();//1作为参数传递functionfoo(){vara=1;functionbar(){console.log(a)}baz(bar)}functionbaz(fn){fn()}baz(foo)//1PS:也许这个例子更能说明闭包。foo函数作为参数传递给baz函数。虽然是在baz函数中执行的,但是可以像我们平时开发一样访问foo函数中的变量(即a,自由变量),无意中使用了各种闭包私有实例变量functionPerson(name,age,like){return{toString(){return`${name}${age}${like}`}}}constjohnny=newPerson('johnny',28,'sayhi')console.log(johnny.toString())toString形成一个closure函数式编程functionadd(a){returnfunction(b){returna+b}}add(2)(3)//5个面向事件的编程定时器、事件监听器、Ajax请求、跨窗口通信、WebWorkers或任何异步,只要使用回调函数,它实际上是使用闭包//timerfunctionwait(message){setTimeout(functiontimer(){console.log(message);},1000);}wait(“您好,关闭!”);//message是wait函数的变量,但是被timer函数引用,形成闭包//调用wait后,wait函数被压入调用栈,message被赋值,timer任务调用,然后弹出,1000ms后回调函数timer被压入调用栈,因为引用了消息,所以可以打印出消息//事件监听leta=1;letbtn=document.getElementById('btn');btn.addEventListener('click',functioncallback(){console.log(a);});//变量a被回调函数引用,形成一个闭包//和定时器一样,事件监听属于函数传参形成的闭包封装addEventListener函数有两个参数,一个是事件名,一个是事件监听器callbackfunction//调用事件监听函数,将addEventListener压入调用栈,词法环境中有click、callback等变量,并且因为callback是一个函数,所以有一个作用,形成一个域函数,引用一个变量.然后弹出调用栈,当用户点击时,回调函数被触发,回调函数被压入调用栈,a沿着作用域链查找,找到全局作用域中的变量a,并打印//AJAXleta=1;fetch("/api").then(functioncallback(){console.log(a)})//只要事件监听器是回调函数,函数中引入了变量,就形成了一个闭包。可以说在JavaScript中,所有的函数都是天然的闭包(除了newFunction的特例)。模块化使用闭包模拟私有方法varCounter=(function(){varprivateCounter=0;functionchangeBy(val){privateCounter+=val;}return{increment:function(){changeBy(1);},decrement:function(){changeBy(-1);},value:function(){returnprivateCounter;}}})();console.log(Counter.value());/*记录0*/Counter.increment();Counter.increment();console.log(Counter.value());/*记录2*/Counter.decrement();console.log(Counter.value());/*logs1*/Examplesource:MDNReacthooks在React的功能组件中,我们会使用hooks来控制组件状态,但是也有闭包造成的闭包陷阱。下面的例子:functionProfilePage(props){constshowMessage=()=>{alert('Followed'+props.user);};consthandleClick=()=>{setTimeout(showMessage,3000);};return(
