JavaScript闭包简易指南

本文最后更新于:2021年3月30日 下午

JavaScript闭包简易指南(翻译)

在学习闭包过程之中,发现了medium中一篇比较好理解的外国博客。为方便学习交流,在下初略翻译搬运,并在自己的理解上进行修改和添加了自己的内容.

本文适合初步了解闭包,只从作用域和函数执行过程两个角度简易解释闭包
深入认识闭包还要从设计模式、应用场景、浏览器垃圾回收机制和词法作用域(待更新)

原文连接


javascript中的闭包是让许多开发者头疼的东西。在下面的文章中,我将清楚地解释何为闭包,并使用简单的范例来阐述要点。

什么是闭包?

闭包是 JavaScript 中的一项功能,其中内部函数可以访问外部(封闭)函数的变量
就是函数和函数变量所处作用域的混合

即形成 范围链(即作用域链,个人认为范围链更容易理解,)。

闭包有三个范围链:

  • 它有权访问自己的范围(作用域)——在其大括号{}之间定义的变量
  • 它有权访问外部函数的变量
  • 它可以访问全局变量

小白看到这些行话有可能不太理解

(可自行查阅MDN中的js作用域、变量等知识点)

为什么要用闭包?

简单的闭包例子

看看一个简单闭包的范例:

1
2
3
4
5
6
7
8
9
function outer() {   
var b = 10;
function inner() {

var a = 20;
console.log(a+b);
}
return inner;
}

这里有两个功能:

  • 具有变量b的外部函数outer,返回值是inner
  • 具有变量a的内部函数inner,在其内部可以访问outer的变量b

b的作用范围被限制在函数outer内,a则被限制在inner

现在调用函数,并赋值存储到变量上。

然后,第二次调用同样的过程

1
2
3
4
5
6
7
8
9
10
function outer() {   
var b = 10;
function inner() {
var a = 20;
console.log(a+b);
}
return inner;
}
var X = outer(); //outer() invoked the first time
var Y = outer(); //outer() invoked the second time

一行一行来康康outer函数第一次执行过程

  1. 变量b被创建,它的作用域是outer之内,此时赋值b=10

  2. 这一行开始是函数声明

  3. inner结束后是return inner,发现返回的是个函数,此时返回整个inner函数体
    [注意return并不会执行inner函数,只有inner被调用才执行]

  4. 被返回的内容被存储在变量x中, 即 x =

    1
    2
    3
    4
    function inner(){
    var a = 20;
    console.log(a + b);
    }
  5. 函数执行结束, outer作用域内的所有变量都被释放,不再存在

这个过程中第四步最为重要,要注意一旦函数结束执行,函数范围内的任何变量都将消失

函数执行时间 === 函数内变量生存周期

既然这样, 当尝试调用X时,outer函数的执行已经结束,只在执行时存在变量b此时已经被释放,那么属于outer的变量binner中的console.log(a + b)就会报错, b应该是undefined

当函数第二次执行,此时赋值给变量Y,重复上述过程
……

第二次执行的重点是,这时的变量都是全新的,同样符合:
函数执行时间 === 函数内变量生存周期

现在回到代码,看看XYouter函数执行后都返回同样的函数给变量XY,那么XY也理所当然是函数:

1
2
3
//按f12在控制台中打印x和y的类型:
console.log(typeof(X)); //X is of type function
console.log(typeof(Y)); //Y is of type function

执行XY:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function outer() {
var b = 10;
function inner() {
var a = 20;
console.log(a+b);
}
return inner;
}
var X = outer();
var Y = outer();
//end of outer() function executions
X(); // X() invoked the first time
X(); // X() invoked the second time
X(); // X() invoked the third time

Y(); // Y() invoked the first time

不难看出实际上我们都在执行inner

来一步一步看看执行过程:

  1. 创建变量a = 20
  2. JavaScript 现在尝试执行 。这里是事情变得有趣的地方。JavaScript 知道存在变量a, 因为第一步刚刚创建它。
    但是,根据变量生命周期,属于outerb不再存在。
    outer函数早在我们调用inner之前就完成了执行,因此函数范围内的任何变量都不再存在
  3. ……

*上面这个例子就是是要说明需要闭包的原因: 只有函数跑起来的时候变量才存在,那么怎么使用哪些其他本应消失的变量*?

为了解决这个矛盾,JavaScript设计出闭包.

闭 包

上例阐述了:
inner函数可以访问已经结束生命周期的outer函数中的变量b

用一句话来解释闭包:
一个函数在已封闭的函数执行时,保存并且能够访问范围链内的局部变量(即已封闭的函数的变量),此时产生的就是闭包

用轮子哥的话说(站在闭包的角度):
img

在我们的示例中,inner函数保留了执行outer函数时的值,并继续保留(称为’’封闭’’)

注意有个重点是:
inner只有在使用到了外部的局部变量时,范围链内才出现闭包

现在再理解以下范围链,注意到这条链中确实有变量b的值,因为bouter函数执行时将闭包的值封闭在闭包内。

所以语句console.log(a + b)才能够正确执行,得到想要结果

您可以通过向上述示例添加以下代码行来验证这一点:

1
2
3
4
5
6
7
8
9
10
function outer() {
var b = 10;
function inner() {
var a = 20;
console.log(a+b);
}
return inner;
}
var X = outer(); console.dir(X);
//use console.dir() instead of console.log()

通过控制台打印出函数执行路径:
1_WB0SdempUbLxhVLMS2DvDA

可以看到变量 b=10 保留在 inner中的
Scopes链→
→Closure闭包
→b : 10

现在回到文章开头提出的范围链并和闭包关联起来:

  • 它有权访问自己的范围(作用域)——变量a

  • 它有权访问外部函数的变量——变量b

  • 它可以访问全局变量

闭包的实际使用

使用闭包的核心要点,让我们通过添加三行代码展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function outer() {
var b = 10;
var c = 100;
function inner() {
var a = 20;
console.log("a= " + a + " b= " + b); a++;
b++;
}
return inner;

}
var X = outer(); // outer() invoked the first time
var Y = outer(); // outer() invoked the second time
//end of outer() function executions
X(); // X() invoked the first time
X(); // X() invoked the second time
X(); // X() invoked the third time

Y(); // Y() invoked the first time

运行这段代码,将在在控制台中看到以下输出:

1
2
3
4
a=20 b=10
a=20 b=11
a=20 b=12
a=20 b=10

让我们逐步检查此代码,看看到底发生了什么,并看到闭包的过程!

1
var X = outer();  // outer() invoked the first time

初始调用函数outer
这是调用的是outer,执行以下步骤:

  1. 变量b=10c=100被创建,作为参考命名为b1,c1
  2. inner函数被返回并且复制给变量X
    此时变量b被使用到,因此被inner封闭到它的范围链内,即产生了闭包,这个闭包包含一个变量b : 10
  3. outer函数完成执行,并且它的所有变量都不再存在。
    变量b也不存在,尽管闭包中保存了b的引用
1
var Y= outer();  // outer() invoked the second time

这时执行和X()一样的过程,并且创建全新的环境和变量…..

现在,让我们看看执行以下代码行时会发生什么:

1
2
3
4
5
X(); // X() invoked the first time
X(); // X() invoked the second time
X(); // X() invoked the third time

Y(); // Y() invoked the first time

首次调用

  1. 创建变量a=20
  2. a的值来自赋值语句,b的值来自闭包中保存的b1b=10
  3. a++、b++,自增
  4. X()完成执行,内部变量释放
    然而,b1作为闭包保留,所以继续存在。

第二次调用时

  1. 再次创建变量a=20
  2. 由于上一次执行b++b1的值增加了,故此时的b=11
  3. a++、b++,再次自增
  4. X()完成执行,内部变量释放
    然而,b1继续作为闭包保留,所以继续存在。

第三次调用时
同理,此时b=12

首次调用 Y() 时,

  1. 再次创建变量a=20
  2. a的值来自赋值语句,b的值来自闭包中保存的b1b=10
  3. a++、b++,自增
  4. Y()完成执行,内部变量释放
    然而,b1作为闭包保留,所以继续存在。

前面三次执行X()和最后一次执行Y(),区别在于不同的函数构成不同的闭包,是彼此独立的,不会相互影响

##自此就是关于闭包的初步了解,深入了解关注后续博文