Lua 中的闭包
封面来源:由博主个人绘制,如需使用请联系博主。
参考链接:
这几天看 Lua 感觉进了个大坑,这个大坑就是闭包。闭包这玩意很玄学,它很重要,又难以掌握,难以下一个准确的定义,但在用的时候又那么自然。本文在互联网上参考了很多文章和回答,上方只列举出一些出处,如果发现本文的论述和互联网上某些文章或回答极其相似,不用怀疑,是我抄的 ta。在此向那些被“借鉴”的作者致以崇高的敬意与衷心感谢。
1. 基本概念
闭包这一概念最初是在 JavaScript 中听到的,但身为后端开发的我并没有对此进行深入了解,也仅仅是听过这个名词罢了。
最近在看 Lua 时发现 Lua 在实现多状态迭代器时使用了闭包,因此也打算对此深入了解一下。
先看几个概念:
1.1 词法定界
词法定界:Lua 中函数可以访问函数之外定义的值,该值可以定义在其他函数内,也可以直接定义在文件之内。比如:
1 | local function fn1() |
1.2 一等公民
第一类值:我更喜欢叫它“一等公民”。在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量(比如可以认为 Java 8 及其之后函数是一等公民)。比如:
1 | local function fn3() |
1.3 闭包的定义
通过调用含有一个内部函数加上该外部函数持有的外部局部变量的外部函数产生的一个实例函数。比如:
1 | local function outerFunc() |
正因为闭包这个机制,在实现 Lua 的多状态迭代器是需要保存上一次调用的状态和下一次成功的状态,因此恰好可以使用闭包的机制来实现。
1 | local array = {"one", "two"} |
1.4 闭包的特点
1、让外部访问函数内部变量成为可能;
2、局部变量会常驻在内存中;
3、可以避免使用全局变量,防止全局变量污染;
4、有一块内存空间被长期占用,而不被释放。
注意第四点并 不一定会 内存泄漏,内存泄露是指用不到(或访问不到)的变量,依然占据着内存空间,不能被再次利用起来。闭包里的变量明显是我们需要的变量,因此这也就不是内存泄漏了。
如果闭包里面的变量是我们需要的变量,那这就不是内存泄漏了;但如果我就是闲得慌,整了一些无用的变量一直占着内存,这肯定是内存泄漏了。
依我看来,闭包和内存泄漏是没有内在联系的,只能说闭包 可能会 产生内存泄漏。
1.5 闭包的创建
闭包可以创建一个独立的环境,每个闭包里面的环境是独立的、互不干扰的。
每次外部函数执行时,外部函数的引用地址不同,都会重新创建一个新的地址。
凡是当前活动对象中有被内部子集引用的数据时,这个数据不会被删除,将会保留一根指针给内部活动对象。
2. 闭包的应用
先说概念:闭包找到的是同一地址中父级函数中对应变量最终的值。
请带着这个概念查看下面的每个例子:
例子一
1 | local function funA() |
例子二
1 | local function outerFunc() |
例子三
1 | local i = 0 |
例子四
1 | local function fn() |
例子五
1 | local function outerFun() |
例子六
1 | WINDOW = {}; |
例子七
1 | local function a() |
例子八
1 | local function f() |
例子九
1 | local tmp |
例子十
1 | local table = {"apple", "pear", "orange"} |
例子十一
1 | local function m1() |
例子十二
1 | local fn12 = (function () |
例子十三
1 | local function love1() |
例子十四
1 | local function fun14(n, o) |
例子十五
1 | local function fun15() |
例子十六
1 | local function fun16() |
例子十七
1 | local function fun17() |
场景十八
1 | V18 = {} |
场景十九
1 | local function fun19() |
怎么样,知道上面这些例子的最终结果是怎么来的吗?
不知道没关系,下面还有一节深入理解。
3. 深入理解闭包
3.1 给闭包下定义
《JavaScript高级程序设计》对闭包的解释
如果按照这个定义,那么像这样嵌套在 foo()
中的 bar()
函数就是闭包:
1 | function foo(){ |
《JavaScript权威指南》对闭包的解释
那这样一个包含变量函数也是一个闭包:
1 | function foo() { |
还有这样的解释
闭包是指在函数声明时的作用域以外的地方被调用的函数,在这时需要通过将该函数作为返回值或作为参数进行传递。比如作为返回值:
1 | function foo() { |
或者作为参数:
1 | function foo() { |
也就是说,只要将内部函数传递到所在的 词法作用域 以外,它都会持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包。
3.2 IIFE
IIFE 的全称是 Immediately Invoked Function Expression,译为:立即调用函数表达式。比如:
1 | var a = "mofan" |
那么它是闭包吗?
它在全局作用域中被定义,也在全局作用域中被调用,按照《JavaScript权威指南》对闭包的解释,它是闭包,但按照另外的解释似乎又不是。
除此之外,可以使用 IIFE 实现模块化编程,利用 window.fn = fn
来暴露接口,而这个 fn
就是闭包,IIFE 是一个包含闭包的函数调用:
1 | (function() { |
那 IIFE 究竟是不是呢?就看你的理解了。
3.3 作用域与作用域链
先以 JavaScript 为例,了解 全局作用域 的概念:
1、一对 <script>
标签里、或一个单独的 JS 文件里的 JS 代码,都是全局作用域;
2、全局作用域在页面打开时创建,页面关闭时销毁;
3、在全局作用域中有一个全局对象 window(代表的是一个浏览器的窗口,由浏览器创建),可以直接使用。
再了解 局部作用域 的概念:
1、一个函数内部就是一个局部作用域,其中定义的变量只在函数的内部起作用;
2、调用函数时创建函数作用域,函数执行完毕之后,函数作用域销毁;
3、每调用一次函数就会创建一个新的函数作用域,它们之间是相互独立的。
假设在 A 函数中创建 B 函数,在 B 函数中创建 C 函数,那么作用域链就是 C->B->A
,沿着这个链找自由变量。
将一个变量定义在 JS 的全局作用域中,由于作用域链的存在,函数内部能访问到外部上级作用域的变量。
3.4 究竟什么是闭包
在知乎的 什么是闭包 的问答中进行了激烈的讨论:
寸志 - 知乎 (zhihu.com) 大佬是这样说的
JavaScript 闭包的本质源自 词法作用域 和 函数当作值传递。
词法作用域是在写代码或定义时确定的,简单就是内部函数可以访问到函数外的变量。引擎通过数据结构和算法表示一个函数,当某个函数执行并按照词法作用域访问了外围的变量时,这些变量会被添加到对应的数据结构中。
在 Lua 中,就是我们最开始说的 词法定界。那么还记得之后的 一等公民 吗?
当函数作为返回值时,相当于返回了一个通道,这个通道可以访问这个词法作用域中的变量,即函数所需要的数据结构被保存了下来,数据结构中的值在外层函数执行时创建,外层函数执行完毕时理因被销毁,但由于内部函数作为值返回出去,使得这些值能保存下来,而且无法直接访问,必须通过返回的函数(体现了私有性)。
也就是说:函数在执行完毕后,返回了函数,或者将函数得以保存下来,也就形成了闭包。
换言之,闭包是词法作用域的体现。
Saviio - 知乎 (zhihu.com) 又是这样说的
闭包,一言以蔽之:一个持有外部环境变量的函数就是闭包。 闭包的三个关键点:
1、函数
2、自由变量
3、环境(作用域)
如何理解自由变量呢?一个 🌰:
1 | let a = "mofan" |
函数 fun()
捕获了外部作用域的变量 a
,由于 a
不属于函数 fun()
,因此变量 a
也被成为 自由变量。 按照我们的定义,上述例子也形成了闭包。
Saviio 的其他解惑:
Q1:使用上述的例子在浏览器中进行断点调试时,Scope 下面并不会出现 Closure,这与定义矛盾了?
A1: 浏览器引擎针对全局对象做了特殊优化,是工程实现的一部分。闭包是一个跨语言的概念,浏览器只是一种语言的实现环境,以某一实现对比整体概念是不妥的。从定义角度出发,一旦引用自由变量,立刻成为闭包,但浏览器引擎可以选择用某种手段优化掉这个函数,不让它成为闭包,进而导致 Scope 下面没有出现 Closure。
Q2:MDN 上说【闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。】这里说的局部变量是包括了全局作用域中的全局变量吗?如果不是,是不是表示 JS 的闭包和广义的闭包定义不一样?
A2:这是把实现优化和定义混在一起了。全局变量可以视为一种特殊的自由变量。因此在函数里访问了全局变量,则成为闭包。但对于引擎来说,闭包有额外代价和开销,引擎默认通过把全局变量封装进执行环境来达到即使不形成闭包也保证函数可以访问全局变量的目的,可以理解为全局变量是具体实现的一种优化。这也是为什么 Q1 中 Scope 下面没有出现 Closure 的原因。
Q3:按照定义说的“持有外部环境变量的函数叫闭包”,由于作用域链本身就包含了外部环境变量,因此所有函数都会持有外部环境变量,那函数使用的外部变量不是自由变量,这是否矛盾?
A3:准确地说,函数是持有了访问外部环境的路径,但不代表持有了环境里的变量,也就是说,如果没有在 inner 函数里使用 outer 的变量,解释器是可以把这个函数优化掉。
Q4:闭包和作用域链有什么关系呢?函数访问外部作用域的变量是很正常的事,靠的是作用域链,怎么能形成闭包呢?
A4:作用域链是实现闭包的一种方式或机制。闭包这种特性能够通过作用域链实现,闭包是作用域链的实现对象,不能使用一种实现方式来否认实现对象。
我的看法
我更偏向:一个持有外部环境变量的函数就是闭包。
在我的理解下,这是更为广泛的定义,而其他的定义恰好只是它的某些特殊情况,因此我更偏向这种。
当然,也能有不同的理解,毕竟谁也不是一定对的。
3.5 必须有 return?
很明显,闭包中并不一定含有 return
。
闭包与否,与函数是否持有外部环境变量有关。在使用闭包时经常 return
一个函数是为了能够更好得体现变量的私有性。
除此之外,从 2. 闭包的应用 - 例子十九 可以看出,对于函数嵌套,就算 return
了,也不一定非得 return
一个函数。
3.6 为什么要函数套函数
因为需要局部变量。
如果变量不放在函数里,那么这个变量可以在声明后在当前文件的任何位置被访问,这样就达不到闭包 隐藏变量 体现私有性的目的。
当然并不是说闭包一定是函数套函数。
3.7 闭包的作用
闭包常常用来间接访问一个变量,也就是说为了 隐藏一个变量 。
假设在设计一款游戏时,一个角色只有 3 条命,可以定义一个全局变量:
1 | A = 3 |
然后变量 A
可以在当前文件任何地方被访问,甚至将 A
修改为 100,又或者将 A
减少至 -100,这显然都是不行的。
因此不能让别人 直接访问 到这个变量。
采用局部变量的话别人又访问不到,因此可以暴露一些函数,让别人 间接访问 :
1 | V = {} |
这上面有两个地方出现了闭包。
如果以 Java 做比较,外层函数可以当做 Java 中的类,闭包就好像 Getter / Setter。或者说是,封装了一个私有变量,并向外暴露了一些方法。
当我们在写代码时并不知道所谓的闭包,只是按照自己的意图去编写,最终却编写出了闭包,这或许才是闭包真正的作用。