定义:闭包是由函数和与其相关的引用环境组合而成的实体
引用环境:指在程序执行中的某个点所有处于活跃状态的约束所组成的集合。其中的约束是指一个变量的名字和其所代表的对象之间的联系。
那么为什么要把引用环境与函数组合起来呢?这主要是因为在支持嵌套作用域的语言中,有时不能简单直接地确定函数的引用环境。这样的语言一般具有这样的特性:
- 函数是一阶值(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值
- 函数可以嵌套定义,即在一个函数内部可以定义另一个函数。
这些概念上的解释很难理解,显然一个实际的例子更能说明问题。Lua 语言的语法比较接近伪代码,我们来看一段 Lua 的代码:
清单 1. 闭包示例1
function make_counter() local count = 0 function inc_count() count = count + 1 return count
end
return inc_countendc1 = make_counter()c2 = make_counter()print(c1())print(c2())
在这段程序中,函数 inc_count 定义在函数 make_counter 内部,并作为 make_counter 的返回值。变量 count 不是 inc_count 内的局部变量,按照最内嵌套作用域的规则,inc_count 中的 count 引用的是外层函数中的局部变量 count。接下来的代码中两次调用 make_counter() ,并把返回值分别赋值给 c1 和 c2 ,然后又依次打印调用 c1 和 c2 所得到的返回值。
这里存在一个问题,当调用 make_counter 时,在其执行上下文中生成了局部变量 count 的实例,所以函数 inc_count 中的 count 引用的就是这个实例。但是 inc_count 并没有在此时被执行,而是作为返回值返回。当 make_counter 返回后,其执行上下文将失效,count 实例的生命周期也就结束了,在后面对 c1 和 c2 调用实际是对 inc_count 的调用,而此处并不在 count 的作用域中,这看起来是无法正确执行的。
上面的例子说明了把函数作为返回值时需要面对的问题。当把函数作为参数时,也存在相似的问题。下面的例子演示了把函数作为参数的情况。
清单 2. 闭包示例2
function do10times(fn) for i = 0,9 do fn(i) end end sum = 0 function addsum(i) sum = sum + i end do10times(addsum) print(sum)
这里我们看到,函数 addsum 被传递给函数 do10times,被并在 do10times 中被调用10次。不难看出 addsum 实际的执行点在 do10times 内部,它要访问非局部变量 sum,而 do10times 并不在 sum 的作用域内。这看起来也是无法正常执行的。
这两种情况所面临的问题实质是相同的。在这样的语言中,如果按照作用域规则在执行时确定一个函数的引用环境,那么这个引用环境可能和函数定义时不同。要想使这两段程序正常执行,一个简单的办法是在函数定义时捕获当时的引用环境,并与函数代码组合成一个整体。当把这个整体当作函数调用时,先把其中的引用环境覆盖到当前的引用环境上,然后执行具体代码,并在调用结束后恢复原来的引用环境。这样就保证了函数定义和执行时的引用环境是相同的。这种由引用环境与函数代码组成的实体就是闭包。当然如果编译器或解释器能够确定一个函数在定义和运行时的引用环境是相同的(注3),那就没有必要把引用环境和代码组合起来了,这时只需要传递普通的函数就可以了。现在可以得出这样的结论:闭包不是函数,只是行为和函数相似,不是所有被传递的函数都需要转化为闭包,只有引用环境可能发生变化的函数才需要这样做。
再次观察上面两个例子会发现,代码中并没有通过名字来调用函数 inc_count 和 addsum,所以他们根本不需要名字。以第一段代码为例,它可以重写成下面这样:
清单 3. 闭包示例3
function make_counter() local count = 0 return function() count = count + 1 return count end end c1 = make_counter() c2 = make_counter() print(c1()) print(c2())
这里使用了匿名函数。使用匿名函数能使代码得到简化,同时我们也不必挖空心思地去给一个不需要名字的函数取名字了。
上面简单地介绍了闭包的原理,更多的闭包相关的概念和理论请参考参考资源中的"名字,作用域和约束"一章。
一个编程语言需要哪些特性来支持闭包呢,下面列出一些比较重要的条件:
- 函数是一阶值;
- 函数可以嵌套定义;
- 可以捕获引用环境,并
- 把引用环境和函数代码组成一个可调用的实体;
- 允许定义匿名函数;
这些条件并不是必要的,但具备这些条件能说明一个编程语言对闭包的支持较为完善。另外需要注意,有些语言使用与函数定义不同的语法来定义这种能被传递的"函数",如 Ruby 中的 Block。这实际上是语法糖,只是为了更容易定义匿名函数而已,本质上没有区别。
借用一个非常好的说法来做个总结:对象是附有行为的数据,而闭包是附有数据的行为。
闭包的表现形式
虽然建立在相似的思想之上,各种语言所实现的闭包却有着不同的表现形式,下面我们来看一下闭包在一些常用语言中的表现形式。
JavaScript 中的闭包
JavaScript(ECMAScript)不是通用编程语言,但却拥有较大的用户群体,而 Ajax 的流行也使更多的人关注 JavaScript。虽然在进行 DOM 操作时容易引发循环引用问题,但 JavaScript 语言本身对闭包的支持还是很好的,下面是一个简单的例子:
清单 4. JavaScript
function addx(x) { return function(y) {return x+y;}; } add8 = addx(8); add9 = addx(9); alert(add8(100)); alert(add9(100));
Ruby 中的闭包
随着 Ruby on Rails 的走红,Ruby 无疑是时下炙手可热的语言之一,Ruby 吸取了很多其他语言的优点,是非常优秀的语言,从这一点来看,很难说清是 Rails 成就了 Ruby 还是 Ruby 成就了 Rails。
Ruby 使用 Block 来定义闭包,Block 在 Ruby 中十分重要,几乎到处都可以看到它的身影,下面的代码就展示了一个 Block:
清单 5. Ruby
sum = 0 10.times{|n| sum += n} print sum
10.times 表示调用对象10的 times 方法(注5),紧跟在这个调用后面的大括号里面的部分就是Block。所谓 Block 是指紧跟在函数调用之后用大括号或 do/end 括起来的代码,Block 的开始部分(左大括号或 do)必须和函数调用在同一行。Block 也可以接受参数,参数列表必须用两个竖杠括起来放在最前面。Block 会被作为它前面的函数调用的参数,而在这个函数中可以使用关键字 yield 来调用该 Block。在这个例子中,10.times 会以数字0到9为参数调用 Block 10次。
Block 实际上就是匿名函数,它可以被调用,可以捕获上下文。由于语法上要求 Block 必须出现在函数调用的后面,所以 Block 不能直接作为函数的的返回值。要想从一个函数中返回 Block,必须使用 proc 或 lambda 函数把 Block 转化为对象才行。
相关推荐
此外,它还能帮助我们识别潜在的社交规律,例如邓巴数理论——一个人能维持稳定社会关系的人数上限。 总之,通过对"CollegeMsg"数据集中的三元闭包进行验证,我们可以深入洞察社交网络的结构和动态,这对于理解人际...
本文将介绍一个在JavaScript经常会拿来讨论的话题 —— 闭包(closure)。 闭包其实已经是个老生常谈的话题了; 有大量文章都介绍过闭包的内容, 尽管如此,这里还是要试着从理论角度来讨论下闭包, 看看ECMAScript...
总结起来,离散数学实验中的Warshall算法实验不仅锻炼了我们对离散数学理论知识的应用,也提升了我们的编程实践能力。通过亲手实现并运行程序,我们能更深入地理解传递闭包的计算过程,这对于进一步学习和研究计算机...
本资源是“计算理论基础”第二版的课后答案,主要涵盖了第二章——有穷自动机的内容。 有穷自动机(Finite Automaton,简称FA)是计算理论中最基本的模型之一,用于模拟执行简单决策任务的机器。它们的名字来源于...
数据库中的“范式”是指规范化理论,它是关系数据库设计的基础,用于优化数据库结构,减少数据冗余和提高数据一致性。范式分析主要包括求最小依赖集、求闭包和求候选键这三个方面。 首先,求最小依赖集的目标是找到...
7. **高级话题**:深入理解JavaScript,还需要掌握闭包——一种函数可以访问并操作其外部作用域变量的特性;原型链——理解JavaScript的继承机制;异步编程——包括回调函数、Promise和async/await;以及模块化——...
此外,还将探讨闭包——JavaScript中的一个高级概念,它是实现模块化和封装的关键工具。 数组和字符串处理在JavaScript中扮演着重要角色。书中会详细讲解数组的方法,如push、pop、slice等,以及字符串的常用方法,...
离散数学是计算机科学的基础课程,它主要研究离散而非连续的数学结构...无论是对于计算机专业的本科生,还是准备计算机专业研究生入学考试或计算机等级考试的考生,这本《离散数学——习题与解析》都是宝贵的参考资料。
正则语言的闭包性质表明,它们对并集、交集和星号(*)运算具有封闭性,即正则语言通过这些运算的组合依旧能保持正则性。而正则语言与pumping定理的关系,则进一步揭示了正则语言的结构特征,以及如何通过该定理来...
数据库系统原理中的候选键算法是关系数据库理论中的一个重要概念,主要用来确定关系模式中的最小超键,即不能被更小的属性集合所替代的关键字。候选键能够唯一标识表中的每一行,且不存在冗余属性。以下是关于候选键...
这份“离散数学复习题——期末复习”资料,显然是为帮助学生备考离散数学期末考试而精心准备的,具有极高的学习价值。以下是基于这个主题的详细知识点讲解: 1. **逻辑基础**: - **命题逻辑**:包括基本的逻辑...
此外,还会讲解到作用域、闭包等进阶主题,这些都是编写高效、可维护代码的关键。 DOM(Document Object Model)编程是JavaScript应用的重要领域。DOM是HTML和XML文档的结构化表示,JavaScript通过DOM API可以查找...
介绍 本章我们将介绍在JavaScript里大家经常来讨论的话题 —— 闭包(closure)。闭包其实大家都已经谈烂了。尽管如此,这里还是要试着从理论角度来讨论下闭包,看看ECMAScript中的闭包内部究竟是如何工作的。 正如...
总之,"IOS应用源码——吉他应用.zip"涵盖了iOS应用开发的多个方面,包括编程语言、界面设计、多媒体处理、音乐理论应用以及用户交互等。对于希望学习iOS开发或者对吉他教育应用感兴趣的开发者来说,这是一个宝贵的...
深入学习JavaScript,需要理解闭包——一个函数及其相关的引用环境组合而成的实体,它是JavaScript中的强大特性,常用于模块化、私有变量和异步编程。此外,还需掌握异步编程的概念,如回调函数、Promise和async/...
关系的闭包、函数的性质、函数的复合和逆函数等内容在此部分讲解。 7. **形式语言和自动机**:这部分涉及到正则表达式、正规集、有限状态自动机等,是编译原理和形式语言理论的基础。 8. **组合优化**:如旅行商...
此外,源码可能还涉及了Swift语言特性,如协议、扩展、闭包等,以及UIKit框架的相关知识,如Storyboard布局、Auto Layout、动画效果等。通过深入研究这个项目,开发者可以学习到如何将不同功能模块有效地整合在一起...
为了实现这种效果,开发者可能还运用了Swift编程语言的一些特性,如可选链、闭包、GCD(Grand Central Dispatch)进行异步操作,以确保动画不影响主线程的响应。 总的来说,"IOS应用源码——像舞台帷幕打开的效果...
书中将详细介绍JavaScript的基本语法,包括变量声明、数据类型(如字符串、数字、布尔值、数组、对象等)、控制结构(如条件语句、循环语句)、函数的定义和调用,以及作用域和闭包等核心概念。这些基础知识是编写...