从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!

2023-01-24 0 557

译者:T5450LX1

把思维能力变革为标识符的控制技术写手

从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!

序言

所以十多年了,你与否还在探讨javascript旋量群呢? 旋量群那个基本概念基本上也是任何人后端辩手单厢必修的难题!

因此认知javascript旋量群也是迈入高阶后端开发技师的必经之地!

也多于认知了旋量群基本原理和运行机制就可以写下更加安全可靠和典雅的javascript标识符

所以你与否自学javascript好久了但旋量群还没搞懂呢? 旋量群很晦涩难懂吗? 或许你把旋量群那个概念想象得太过神奇! 今天就来揭秘javascript旋量群 一个后端开发经久不衰的话题!

自学条件

这里我也特别说明一下旋量群其实牵扯的东西还是有点多,涉及到以下JS知识点:

函数的执行上下文环境(Execution context of function)

变量对象(Variable object)

活动对象(Active object)

作用域(scope)

作用域链(scope chain)

所以如果你对以上所涉及到的知识点还没清楚,所以建议补一下,我以后也会慢慢提及, 否则认知旋量群就会出现歧义!

到底什么是旋量群?

概述

旋量群比较书面化的解释是: 一个拥有许多变量和绑定了这些变量的环境的表达式,因此通常是一个函数, 而这些变量也是该表达式的一部分。我想如果你是一个零基础的小白, 所以估计不出意外的话应该完全不能认知这句话! 没关系想搞懂我们接着往下看…

所以我们首先来看一段JS标识符

//函数定义functionouterTest() {varnum=0;functioninnerTest() {++numconsole.log(num); }returninnerTest;}//调用varfn1=outerTest();fn1();fn1();fn1();

运行结果

从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!

以上就是一个旋量群的经典案例, 我们慢慢来分析!

其实你会发现以上这段JS标识符有两个特点:

1、innerTest函数嵌套在outerTest函数的内部

2、outerTest函数的返回值就是innerTest函数

所以有人就会说函数嵌套函数就是旋量群 其实这样子说是不严谨的!

基本原理分析

接着之前的那一段JS标识符 我们来看一张图

从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!

标识符分析

当在执行完var fn1 = outerTest();之后,变量fn1实际上是指向了函数innerTest

所以接下来如果再执行fn1()就会改变num变量的值, 当然那个过程通常懂一点程序执行流程也可以分析出来!

关键不同的是之后继续执行fn1()输出的却是num变量累加之后的结果! 你肯定想知道为什么会累加!对吧!

首先因为函数innerTest引用了函数outerTest内部的变量或者数据,再然后重点来了:

当一个局部函数或匿名函数被定义的时候,所以它的作用域链也会被初始化,因此虽然有的时候局部函数即便是没被调用,但是它会执行一个动作: 就是复制一份父函数的作用域链, 因此再将此作用域链的第0位插入该未调用函数的变量对象,等到该函数被调用了就激活为活动对象

如果实在你还无法认知这里的【作用域链】,所以你可以认知为是一种描述路径的术语, 沿着该路径可以找到需要的变量值!

再次回到旋量群的基本概念上来, 也就是当一个子函数引用了父级函数的某个变量或数据,所以 旋量群其实就产生了

因此那个变量或数据的生命周期始终能保持使用,就能间接保持原构父级函数 在内存中的变量对象不会消失

所以尽管outerTest()函数已经调用结束, 但是子函数却始终能引用到该父级函数中的变量的值,因此该变量值只能通这种方法来访问!

即使再次调用相同的outerTest()函数,但只会生成相对应的变量对象,新的变量对象只是对应新的值, 和上次那次调用的是各自独立的!

如图

从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!

简而言之 在嵌套在父级函数内部的子函数被定义时,因此也引用了父级函数的数据时就产生了旋量群

需要重点注意的是: 一个旋量群内对变量的修改,不会影响到另外一个旋量群中的变量

以上案例就是在outerTest函数执行完并返回后,旋量群使得JS中的的垃圾回收机制GC(Garbage collection)不会收回outerTest函数所占用的资源,这里指的资源是它的变量对象, 因为outerTest函数的内部函数innerTest的执行一直需要依赖outerTest函数中的变量或者其他数据。这就是对旋量群产生和特性最直白通俗的描述!

所以现在回过头来再次认知为什么每次调用fn1()函数 变量num会累加? 看下面这张图!

如图

从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!

因为由于旋量群的存在使得函数outerTest返回后,函数outerTest中的num变量其实始终存在与内存中,这样每次执行fn1(),单厢找到内存中与之对应outerTest函数变量对象num变量进行累加1后,输出num的值

旋量群具体步骤总结

当执行函数outerTest的时候,outerTest函数会进入相应的执行上下文环境!

在创建函数outerTest执行环境的过程中,首先会为函数outerTest添加一个scope属性,即函数outerTest的作用域,其值就为函数outerTest中的作用域链scope chain

然后执行环境会创建一个活动对象(activation object)。活动对象也是当前被调用那个函数所拥有的一个对象,它是用来保存数据的, 它不能通过JS标识符直接访问, (如果你实在认知不了可以想象成一个抽象的对象)

创建完活动对象后,把该活动对象添加到outerTest函数作用域链中的最顶端,也就是图中的第0位,此时outerTest函数作用域链包含了两个对象:outerTest函数活动对象全局window变量对象也就是图中蓝色和绿色两个对象

然后在outerTest函数活动对象上添加一个arguments属性,它保存着调用outerTest函数时所传递的实际参数,当然我们这里并没传递任何人参数进来!

再然后把所有outerTest函数形参和内部的innerTest函数、以及num变量这些数据的引用也添加到outerTest函数活动对象上。

此时完成了函数innerTest的定义,因此如同第3步,函数innerTest作用域链以及innerTest函数的变量对象跟之前outerTest函数一样被初始化了, 所以到这里整个outerTest函数从定义到执行的步骤就完成了!

然后在外部 outerTest函数返回innerTest函数命名为fn1引用变量,又因为innerTest函数作用域链包含了对outerTest函数变量对象的引用,注意:此时outerTest函数已经调用结束,活动对象也变成了内存中滞留的变量对象,所以innerTest函数可以访问到outerTest函数中定义的所有变量和函数, 因此innerTest函数被外部的fn1所引用,函数innerTest又依赖函数outerTest,因此函数outerTest变量对象在返回后不会被JS垃圾回收机制GC(Garbage collection)销毁。

所以当fn1执行也相当于在执行函数innerTest时候也会像以上步骤一样。因此执行时innerTest函数作用域链包中含了3个对象:innerTest函数活动对象outerTest函数变量对象全局window变量对象, 也就是图中蓝色+绿色+紫色三个对象, 如果你觉得上图看不清楚所以就看下面这张图!

如图

从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!

当在innerTest函数中访问一个变量时,搜索顺序是先搜索自身的活动对象如果存在则返回

注意: 如果函数innerTest存在prototype原型对象,则在查找完自身的活动对象后, 会先查找自身的原型对象

如果不存在将继续搜索滞留在内存中outerTest函数变量对象,依次查找直到找到为止, 这就是JS中的数据查找机制 ,当然如果整个作用域链上都无法找到,则返回undefined

我们在认知旋量群的时候 重点也是在作用域链那个环节容易出错, 要知道函数的定义与执行的区别。

函数作用域是在函数定义时就已经确定,而不是在执行的时候确定, 这里引出了一个基本概念词法作用域

举个栗子

functionouter(num) { functioninner() {returnnum; }returninner;}varfn1=outer(1);console.log(fn1());

我们假设函数fn1作用域是在执行时,也就是console.log(fn1())确定的,所以此时fn1的作用域链是如下:

函数fn1的活动对象->console.log的活动对象->window对象,如果假设成立,所以输出值就必然是undefined

另一种假设也就是函数fn1的作用域是在定义时确定的,就是说fn1指向的inner函数在定义的时候就已经确定了作用域。所以在执行的时候,函数fn1的作用域链为如下:

函数fn1的活动对象->函数outer的变量对象->window对象,如果假设成立,所以输出值也就是1。

所以运行结果最终为1,说明了第2种假设是正确的,也就证明了函数的作用域确实是在定义那个函数的时候就已经确定了那个说法!

有人又会问如果我们不返回outerTest函数行不行呢? 答案肯定是不行的

因为outerTest函数执行完后,innerTest函数没被返回给外界,只是被outerTest函数所使用

因此函数outerTest函数innerTest互相使用, 但又不被外界使用,所以函数outerTest执行完毕之后就会被GC(Garbage collection)垃圾回收机制回收, 所以outerTest函数执行上下文环境也会被弹出call Stack, 内存中也不会在有outerTest函数所对应的变量对象了, 自然也无法继续保存值了!

从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!

旋量群的应用场景

应用场景1 标识符模块化

旋量群的应用场景主要是用于模块化

旋量群可以一定程度上保护函数内的变量安全可靠。

还是刚才的案例举例!

outerTest函数中的num变量多于innerTest函数就可以访问,而无法通过其他途径访问到,因此保护了num变量的安全可靠性, 所以旋量群模块化基本可以解决函数污染变量随意被修改难题!

比如说Java、php等语言中有支持将方法声明为私有,它们只能被同一个类中的其它方法所调用。

js是没这种原生支持的,但我们可以使用旋量群模拟私有方法

私有方法不仅仅有利于限制对标识符的访问权限, 还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了标识符的公共接口部分。

举个栗子

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()); /* 输出 1 */

如图

从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!

以上案例表现了如何使用旋量群来定义公共函数,并让它可以访问私有函数变量

IIFE匿名函数包含两个私有数据:名为 privateCounter 变量changeBy函数, 而这两项都无法在那个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数接口来进行访问!

increment()、decrement()、value()这三个公共函数是共享同一个作用域执行上下文环境的变量对象, 也就是旋量群也多亏 js作用域,它们都可以访问 privateCounter变量changeBy函数

应用场景2 在内存中保持变量数据一直不丢失!

还是以最开始的例子, 由于旋量群的影响,函数outerTestnum变量会一直存在于内存中,因此每次执行外部的fn1()时,单厢给num变量进行累加!

所以每累加一次也就是每调用一次fn1() 就会去内存中一层层寻找outerTest函数变量对象里面的num进行累加!

现在完全明白了旋量群了吧!

如果你真的认知了旋量群,所以下面那个案例就很容易去推理了,也非常经典 就是在事件循环中如何保留每一次循环的索引值!

标识符栗子

html标识符

<button>Button0</button><button>Button1</button><button>Button2</button><button>Button3</button><button>Button4</button>

js标识符

window.onload=function(){varbtns=document.getElementsByTagName(button);for(vari=0,len=btns.length; i<len; i++) {btns[i].onclick=function() {console.log(i); }}}

分析

通过执行该段标识符,其实你会发现不论点击哪个button按钮 ,均输出5,

如图

从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!

这是很多初学者 或者还没完全认知旋量群的朋友心中的困惑! 那今天就要跟你解开那个困惑了!

首先你要明白一点, onclick事件是被异步触发的,也就是等着用户事件被触发时,for循环其实早已结束!

此时变量 i 的值已经是5 所以当onlick事件函数顺着作用域链从内向外查找变量 i时,找到的值总是 5

也就是那个变量i已经在外层的变量对象中一直保存的都是最终值!

如果你想要每次都打印出所 对应的索引号 这里就要使用到旋量群了!

修改js标识符如下形式

window.onload=function(){varbtns=document.getElementsByTagName(button);for(vari=0, len=btns.length; i<len; i++) { (function(i) {btns[i].onclick=function() {console.log(i); } }(i)) }}

或者

window.onload=function(){varbtns=document.getElementsByTagName(button);for(vari=0, len=btns.length; i<len; i++) {functiontest(index){btns[index].onclick=function() {console.log(index); } }test(i) }}

这样一来每次循环的变量i值都被封闭起来,这样在事件函数执行时,会查找定义时的作用域链,那个作用域链里的变量i值是在每次循环中都被保留在对应的变量对象中,因此点击不同的button按钮会输出不同的变量i

如图

从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!

旋量群的缺陷

如果不是某些特定业务需求下, 尽量避免使用旋量群,因为旋量群在处理速度和内存消耗方面对脚本性能具有负面影响, 其会根据旋量群数量的多少而在内存中创建更多的变量对象, 最终可能会导致内存溢出 等情况!

当然通常最简单的解决办法就是: 解除对引用变量函数的使用

引用变量函数 = null;

我们可以将引用变量的值将其设置为null即可,js垃圾回收将会将其清除, 释放内存资源!

总结旋量群

1、当内部函数 在定义它的作用域的外部被引用(使用)时,就创建了该内部函数的旋量群 ,如果内部函数引用了位于父级函数的变量或者其他数据时,当父级函数调用完毕后,这些变量数据在内存不会被GC(Garbage collection)释放,因为旋量群它们被一直引用着!否则两者没交互就不会长久存在于内存中,所以在Chrome中的debug找不到旋量群

原因是因为词法作用域,也就是函数的作用域是其声明的作用域而不是执行调用时的作用域

从来都没有理解JS闭包?我非把你教会不可!看这一篇就够了大白话!
举报/反馈

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务