正如标题所述,JavaScript闭包对我来说一直是个谜。我阅读过很多篇相关文章,我在工作中也使用了闭包,有时候我自己使用了闭包却不自知。最近我参加了一个讲座,在那儿终于有人给我解释清楚了。本文中我也将尝试用他们的方法来解释闭包。
在开始之前
ASP站长网在理解闭包之前,需要先理解一些概念,执行上下文就是其中的一个。
有篇文章很好地解释了执行上下文,以下内容引用自这篇文章:
在运行JavaScript代码时,它的运行环境是非常重要的,运行环境可能是如下几种中的一种:
全局代码——首次执行代码的默认环境。
函数代码——每当执行流程进入函数体时。
(...)
(...),我们将执行上下文定义当前代码的执行环境或作用域。
换句话说,当我们启动程序时,我们从全局执行上下文开始。我们在全局执行上下文中声明一些变量,这些变量为全局变量。当程序调用函数时,会发生以下几个步骤:
JavaScript创建一个新的本地执行上下文。
本地执行上下文将拥有自己的变量集。
新的执行上下文被抛到执行栈上。我们可以将执行栈视为一种用于跟踪程序执行位置的机制。
函数会在遇到return语句或结束括号}时结束执行,并发生以下情况:
本地执行上下文从执行栈中跳出。
函数将返回值发送给调用上下文。调用上下文是调用此函数的执行上下文,它可以是全局执行上下文或另一个本地执行上下文。调用上下文将负责处理返回值,返回值可以是对象、数组、函数、布尔值或其他任何东西。如果函数没有return语句,则返回undefined。
本地执行上下文被销毁,这个很重要。在本地执行上下文中声明的所有变量都将被删除,它们不再可用,这就是为什么它们被称为局部变量。
一个很基础的例子
在开始进入闭包之前,让我们看一下下面这段代码。
1: let a = 3
2: function addTwo(x) {
3: let ret = x + 2
4: return ret
5: }
6: let b = addTwo(a)
7: console.log(b)
为了理解JavaScript引擎的工作原理,让我们详细介绍一下。
在第1行,我们在全局执行上下文中声明一个新变量a,并将它的值赋为数字3。
在第2行到第5行,我们在全局执行上下文中声明了一个名为addTwo的新变量,并为其分配了一个函数定义,{}之间的内容被分配给了addTwo。函数内部的代码不会被执行,只是存储在变量中以备将来使用。
第6行,我们在全局执行上下文中声明了一个新变量,并将其标记为b。声明变量后,它的值为undefined。
接下来,仍然是第6行,我们看到了一个赋值运算符。我们准备为变量b分配一个新值。接下来,我们看到一个被调用的函数。当你看到一个变量后面跟着圆括号(...)时,就表示在调用一个函数。从函数返回的任何内容都将被分配给变量b。
但首先我们需要调用被标记为addTwo的函数。JavaScript将在其全局执行上下文内存中查找名为addTwo的变量。它找到了,也就是在步骤2(或第2-5行)中定义的那个。变量addTwo包含了一个函数定义。请注意,变量a被作为参数传递给该函数。JavaScript在其全局执行上下文内存中搜索变量a,找到它,发现它的值为3,然后将数值3作为参数传递给该函数。准备执行该函数。
现在切换执行上下文,创建了一个新的本地执行上下文,我们将其命名为“addTwo执行上下文”。执行上下文被推送到调用栈。我们在本地执行上下文中做的第一件事是什么?
你可能会想说,“在本地执行上下文中声明了一个新的变量ret”。但其实不是这样的,我们首先需要查看函数的参数。在本地执行上下文中声明了一个新变量x,又因为3被作为参数传递进来,所以变量x被赋值为3。
下一步:在本地执行上下文中声明新的变量ret,其值设置为undefined。(第3行)
仍然是第3行,需要执行一个加法运算。首先,我们需要x的值,JavaScript会尝试查找变量x,它首先查看本地执行上下文。它找到了,值为3。第二个操作数是数值2,加法的结果(5)被赋给变量ret。
第4行,我们返回变量ret的内容。在本地执行上下文中进行另一个查找。ret包含值5。函数返回数值5,函数结束执行。
第4-5行,函数结束执行,本地执行上下文被销毁,变量x和ret被清除,它们不再存在。上下文弹出调用栈,返回值被返回到调用上下文。在这种情况下,调用上下文就是全局执行上下文,因为函数addTwo是从全局执行上下文中调用的。
现在我们从在步骤4中暂停的位置继续。返回值(数值5)被分配给变量b。
在第7行,变量b的内容会在控制台中打印出来。在这个例子中,数值为5。
对于一个非常简单的程序来说,这样的解释显得太过冗长,但我们甚至都还没有提到闭包。我保证会说到那里,但首先我们需要说一些其他的。
词法作用域
我们需要了解词法作用域的某些方面,看下面的例子。
1: let val1 = 2
2: function multiplyThis(n) {
3: let ret = n * val1
4: return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)
在本地执行上下文和全局执行上下文中都有一些变量。JavaScript的一个复杂之处在于它的变量查找过程。如果它在本地执行上下文中找不到变量,就会在调用上下文中查找。如果没有在调用上下文中找到,最后会在全局执行上下文查找。如果还是没有找到,那它就是undefined。
在全局执行上下文中声明一个新变量val1,并赋值为2。
第2-5行,声明一个新变量multiplyThis,并将一个函数定义赋给它。
第6行,在全局执行上下文中声明一个新变量multiplied。
从全局执行上下文内存中获取变量multiplyThis,并将其作为函数执行。将数值6作为参数传递进去。
新函数调用就是新的执行上下文。创建一个新的本地执行上下文。
在本地执行上下文中,声明变量n,并赋值为6。
第3行,在本地执行上下文中声明变量ret。
第3行,用两个操作数执行乘法运算:变量n和val1的内容。在本地执行上下文中查找变量n,我们在步骤6中声明了它,它的内容是数值6。在本地执行上下文中查找变量val1,本地执行上下文中没有标记为val1的变量。我们从调用上下文中查找,调用上下文也就是全局执行上下文。让我们在全局执行上下文中查找val1。是的,它就在那里。它在步骤1中定义,值为数值2。
第3行,将两个操作数相乘,并将结果指定给变量ret。6 * 2 = 12。ret现在是12。
返回ret变量,本地执行上下文及其变量ret和n被销毁。变量val1不会被销毁,因为它是全局执行上下文的一部分。
回到第6行,在调用上下文中将数值12分配给变量multiplied。
第7行,在控制台中显示变量multiplied的值。
所以在这个例子中,我们需要记住一个函数可以访问在其调用上下文中定义的变量,这种现象的正式名称是词法作用域。
返回函数的函数
在第一个示例中,函数addTwo返回一个数值。请记住,函数可以返回任何内容。让我们看一个返回函数的函数的示例,因为这对理解闭包来说很重要。
1: let val = 7
2: function createAdder() {
3: function addNumbers(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return addNumbers
8: }
9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)
让我们来逐步分析它的执行过程。
大型站长资讯类网站! https://www.0792zz.cn