闭包

javascript闭包有些神秘。在工作中经常用到,甚至有时候在使用闭包的时候没有意识到使用了闭包。

概念


执行上下文环境

在Javascript中,执行上下文环境分为以下2类:

1.全局代码—全局环境,代码首次执行时的环境。

2.函数代码—函数环境,函数执行时函数内部的环境。

调用函数

1.javascript创建一个自己的执行上下文环境。

2.执行上下文环境包含它自己的变量。

3.执行上文环境放到执行栈。执行栈用来跟踪代码在执行过程中执行的位置。

函数执行结束

当函数遇到return声明或一个关闭括号}时,函数执行结束。

1.函数执行上下文环境从执行栈弹出。

2.函数返回return值。如果没有return,返回undefined。

3.函数执行上下文环境被销毁。当前上下文环境的所有变量被销毁。

简单例子


代码如下所示。

执行过程:

1.第4行。定义一个全局变量a,赋值3。

2.第5~8行。定义一个全局变量addTwo,赋值一个函数。

3.第9行。定义一个变量b,执行addTwo函数,执行后的结果赋值给b。

4.在全局执行上下文环境内存中找到变量addTwo和变量a,把a的值作为参数传给函数addTwo,准备执行函数。

5.切换执行上下文环境。创建一个新的局部执行上下文环境,并且压入调用栈。

6.在执行上下文环境中定义变量x。因为3被传递给arguments,变量x被赋值3。

7.接下来,在执行上下文环境中声明一个变量ret,赋值undefined。

8.第6行。执行加操作。从当前执行环境找到变量x,值为3。并且第2个操作数是2。加操作的执行结果赋值给ret。

9.第7行。返回ret的值5,函数结束。

10.第7~8行。函数执行结束,对应的执行上下文环境销毁。变量x和变量ret从内存中销毁。执行环境从调用栈中弹出并且返回值。第6行把返回值赋值给b。

11.第10行。输出出5。

scope


在本例子中,有一个当前局部执行上下文环境和一个全局执行上下文环境。javascript查找变量值:如果在当前局部执行上下文环境中没有找到,则到调用它的环境中查找。依次向上查找,直到全局执行上下文环境(如果全局执行上下文环境中也没有,值为undefined)。

1.第4行。在全局执行环境声明变量val1,赋值2。

2.第5~8行。声明变量multiplyThis,赋值一个函数。

3.第9行。在全局执行环境中声明一个变量multiiplied。

4.从全局环境内存中获取变量multiplyThis,传递6给arguments,执行函数。

5.新的函数调用 = 创建新的执行环境。创建一个新的当前局部执行环境。

6.在当前执行环境,声明变量n,赋值6。

7.第6行。在当前执行环境,声明变量ret。

8.第6行。执行相乘操作。两个操作数是n和val1。查找n变量,在当前执行环境找到n变量,值为6。查找val1变量,在当前执行环境中没有val1变量。进而查找调用环境,调用环境是全局执行环境。在全局执行环境找到变量val1,值为2。

9.两个数相乘6*2=12。赋值12给ret。

10.返回ret变量值。当前执行上下文环境销毁,变量ret和n被销毁。因为val1属于全局执行环境,val1没有被销毁。

11.第9行。12被赋值给multiplied。

12.最后,第20行。输出multiplied的值。 从这个例子中可以看出,函数能够访问调用者的执行环境的变量。

返回一个函数的函数


下面让我们看一下返回一个函数的例子,分析一下它的执行过程。

1.第3行。在全局执行环境声明一个变量val,赋值7。

2.第5~11行。在全局执行环境定义一个createAdder变量,赋值一个函数。第3~7行定义函数addNumbers。

3.第12行。在全局执行环境定义adder变量,赋值undefined。

4.第12行。在全局执行环境内存中找到函数createAdder,然后调用函数。

5.调用函数createAdder。一个新的当前局部执行上下文环境被创建。把新的执行环境压入调用栈。

6.到第6~9行。在当前执行环境,声明一个新的变量,即函数addNumbers。

7.第10行。返回addNumers指向的函数对象。然后从调用栈中弹出当前执行环境。

8.return以后,当前执行环境被销毁。同时addNumers变量被销毁。函数对象一直是存在的,return这个函数对象并且赋值给变量adder。

9.第13行。在全局执行环境定义一个变量num,赋值undefind。

10.下一步需要执行函数。执行变量名为adder的函数。在全局执行环境中找到这个变量,这个变量指向的函数有两个参数。

11.提取参数值,用于在调用函数时传递正确的arguments。第一个参数是7,第2个参数是8。

12.执行adder函数。一个新的当前执行上下文环境被创建。在当前执行环境中两个变量a,b被创建。分别被赋值arguments的7和8。

13.第17行。在当前执行环境声明一个变量ret。

14.第17行。执行加操作。两个操作数分别是a的值和b的值。相加的结果赋值给ret变量。

15.在函数中return ret。当前的执行环境被销毁,从调用栈中弹出。变量a,b和ret也被销毁。

16.返回值被赋值给sum变量。

17.输出sum的值是15。

和预期的一致,sun的值是15。通过这个例子我们可以看出,(1)一个变量指向函数对象,函数定义仅仅是一个对象。直到函数执行时,才是代码片段。(2)每当函数被调用时,一个临时的执行上下文环境被创建。当函数执行完后,这个执行环境销毁。

闭包


1.第4~11行。在全局执行环境创建一个变量createCounter,赋值一个函数。

2.第12行。在全局执行环境声明一个increment变量。

3.第12行。调用createCounter函数,给变量increment赋值函数的返回值。

4.第4~11行。调用函数。创建一个新的执行上下文环境。

5.第5行。在当前执行环境中,声明一个counter变量,赋值0。

6.第6~9行。在当前执行环境中,声明myFunction变量,赋值一个新的function。

7.第10行。返回myFunction变量指向的函数。当前执行环境被销毁。myFunction和counter被销毁。

8.第12行。createCounter函数返回的函数对象赋值给increment。myFuncrion引用的函数对象不再被myFuncrion引用,而是被全局执行环境的increment引用。

9.第13行。声明一个变量c1。

10.第13行。查找变量increment,然后执行这个函数。

11.创建一个新的执行环境。

12.第7行。counter = counter + 1。在当前执行环境没有找到变量counter,然后到全局执行环境中查找,没有找到。Javascript将会这样计算:counter = undefined + 1。最终,在当前执行环境声明一个变量counter,赋值1(undefined是0)。

13.第8行。返回counter的值1。销毁当前执行环境和变量counter。

14.回到第13行。返回值1赋值给c1。

15.第14行。重复10~14的步骤,c2也被赋值1。

16.第15行。重复10~14的步骤,c2也被赋值1。

17.第16行。输出c1,c2,c3的值。

实际上,代码执行后,执行结果是1,2和3。而不是,按照上面分析的结果1,1和1。increment函数能够记住counter的值。那么是怎么做到的呢?我们看另外一种机制:闭包。

闭包工作机制

当定义一个函数并且把这个函数赋值给一个变量时,存储了函数定义同时,也存储了一个闭包。闭包类似于一个背包。函数定义携带一个背包。背包中包含了函数创建时作用域范围内的所有变量。

1.第4~11行。在全局执行环境创建一个变量createCounter,赋值一个函数。

2.第12行。在全局执行环境声明一个increment变量。

3.第12行。需要调用createCounter函数,把函数的返回值赋值给increment变量。

4.第4~11行。调用函数。创建一个新的当前局部执行上下文环境。

5.第5行。在当前执行环境,定义一个counter变量,把0赋值给counter。

6.第6~9行。在当前执行环境,声明一个myFunction变量。同时创建一个闭包,闭包作为函数定义的一个组成部分。闭包包含当前作用域范围内的所有变量,在这个例子中,包含counter变量。

7.第10行。返回myFunction指向的函数。当前执行环境被销毁。myFunction和counter也被销毁。返回函数的定义和函数的闭包。

8.第12行。createCounter执行后返回函数,赋值给increment。increment现在包含一个函数定义(和闭包)。

9.第13行。声明一个变量c1。

10.第13行。查找increment,并且调用它。

11.创建一个新的执行上下文环境。

12.第7行。counter = counter + 1。从闭包中查找变量counter。在闭包中包含一个变量counter,它的值是0。在第7行后,counter的值被设置为1,并且存储在闭包中。闭包中包含的counter的值是1。

13.第8行。返回counter的值1,销毁当前执行环境。

14.返回到13行。返回的1赋值给c1。

15.第14行。重复10~14步。查找闭包时,当前闭包的counter值是1。在12步或第4行,counter值被设置。值加1并且存储在increment的闭包中。c2被赋值2。

16.第15行。重复10~14步,c3被赋值为3。

17.第16行。输出c1,c2和c3的值。

从上面的例子中,可以看出闭包是如何工作的。当定义一个函数时,函数包含函数的定义和闭包。闭包是在创建函数时在作用域内的所有变量的收集。 任何一个函数都有闭包。即使是在全局作用域的函数。在全局作用域的函数也创建一个闭包。由于在全局作用域创建函数,它们可以访问全局作用域的变量。在这里闭包概念不明显。

当一个函数返回一个函数时,闭包的概念明显的体现出来了。返回的函数能够访问不是全局作用域内的变量,访问的变量仅仅存在闭包中。

总结


闭包类似于一个背包。当一个函数被创建、被传递或被返回的时候,函数携带一个背包。背包中包含函数创建时函数所在的作用域的所有变量。





https://medium.com/dailyjs/i-never-understood-javascript-closures-9663703368e8

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容