无论什么语言,咱们总要学习做用域(或生命周期)的概念,好比常见的称呼:全局变量、包变量、模块变量、本地变量、局部变量等等。无论如何称呼这些做用域的范围,实现它们的目的都同样:python
可是不一样语言的做用域规则不同,虽然学个简单的基础就足够应用,由于咱们有编程规范:(1)尽可能避免名称冲突;(2)加上相似于local的修饰符尽可能缩小生效范围;(3)放进代码块,等等。可是真正去细心验证做用域的生效机制却并不是易事(我学Python的时候,花了很长时间细细验证,学perl的时候又花了很长时间细细验证),但能够确定的是,理解本文的词法做用域规则(Lexical scoping)和动态做用域规则(dynamic scoping),对学习任何语言的做用域规则都有很大帮助,这两个规则是各类语言都宏观通用的。shell
很简单的一段bash下的代码:编程
x=1 function g(){ echo "g: $x" ; x=2; } function f(){ local x=3 ; g; echo "f: $x"; } # 输出2仍是3 f # 输出1仍是3? echo $x # 输出1仍是2?
对于bash来讲,上面输出的分别是3(g函数中echo)、2(f函数中的echo)和1(最后一行echo)。可是一样语义的代码在其它语言下获得的结果安全
可能就不同(分别输出一、3和2,例如perl中将local替换为my)。bash
这牵扯到两种贯穿全部程序语言的做用域概念:词法做用域(相似于C语言中static)和动态做用域。词法做用域和"词法"这个词真的没什么关系,反而更应该称之为"文本段做用域"。要区别它们,只须要回答"函数out_func中嵌套的内层函数in_func可否看见out_func中的环境"。多线程
对于上面的bash代码来讲,假如这段代码是适用于全部语言的伪代码:闭包
local x=3
的设置,因而g的echo会输出全局变量x=1
,而后设置x=2
,由于它没有加上做用域修饰符,而g又是全局内的函数,因此x将修改全局做用域的x值,使得最后的echo输出2,而f中的echo则输出它本身文本段中的local x=3。因此整个流程输出1 3 2
local x=3
的设置,因此g的echo会输出3。g中设置x=2后,仅仅只是在f的内层嵌套函数中设置,因此x=2对g文本段和f文本段(由于g是f的一部分)均可见,但对f文本段外部不可见,因此f中的echo输出2,最后一行的echo输出1。因此整个流程输出3 2 1
因为bash实现的是动态做用域规则。因此,输出的是3 2 1
。对于perl来讲,my修饰符实现词法做用域规则,local修饰符实现动态做用域规则。app
例如,使用my修饰符的perl程序:异步
#!/usr/bin/perl $x=1; sub g { print "g: $x\n"; $x=2; } sub f { my $x=3; g(); print "f: $x\n"; } # 词法做用域 f(); print "$x\n";
执行结果:编程语言
[fairy@fairy:/perlapp]$ perl scope2.pl g: 1 f: 3 2
使用local修饰符的perl程序:
#!/usr/bin/perl $x=1; sub g { print "g: $x\n"; $x=2; } sub f { local $x=3; g(); print "f: $x\n"; } # 动态做用域 f(); print "$x\n";
执行结果:
[fairy@fairy:/perlapp]$ perl scope2.pl g: 3 f: 2 1
有些语言只支持一种做用域规则,特别是那些比较现代化的语言,而有些语言支持两种做用域规则(正如perl语言,my实现词法变量做用域规则,local实现动态做用域规则)。相对来讲,词法做用域规则比较好控制整个流程,还能借此实现更丰富的功能(如最典型的"闭包"以及高阶函数),而动态做用域因为让变量生命周期"没有任何深度"(回想一下shell脚本对函数和做用域的控制,很是傻瓜化),比较少应用上,甚至有些语言根本不支持动态做用域。
1.引用(reference):数据对象和它们的名称
前文所说的可见、不可见、变量是否存在等概念,都是针对变量名(或其它名称,如函数名、列表名、hash名)而言的,和变量的值无关。名称和值的关系是引用(或指向)关系,赋值的行为就是将值所在的数据对象的引用(指针)交给名称,让名称指向这个内存中的这个数据值对象。以下图:
2.一级函数(first-class functions)和高阶函数(high-order functions)
有些语言认为函数就是一种类型,称之为函数类型,就像变量同样。这种类型的语言能够:
$ref_func=\&myfunc
表示将函数myfunc的引用赋值给$ref_func
,那么$ref_func
也指向这个函数。myfunc(func1)
就将func1做为myfunc的参数。
-print
、-exec {}\;
选项实现的功能看做是其它的函数(请无视它是否真的是函数),这些选项对应的函数是find函数的参数,每当find函数找到符合条件的文件名时,就执行-print
函数输出这个文件名function myfunc(){ ...return func1 }
。其实,实现上面三种功能的函数称之为一级函数或高阶函数,其中高阶函数至少要实现上面的2和3。一级函数和高阶函数并无区分的必要,但若是必定要区分,那么:
3.自由变量(free variable)和约束变量(bound variable)
这是一组数学界的术语。
在计算机程序语言中,自由变量是指函数中的一种特殊变量,这种变量既不在本函数中定义,也不是本函数的参数。换句话说,多是外层函数中定义的但却在内层函数中使用的,因此自由变量经常和"非本地变量"(non-local variable,熟悉Python的人确定知道)互用。例如:
function func1(x){ var z; function func2(y){ return x+y+z # x和z既不是func2内部定义的,也不是func2的参数,因此x和z都是自由变量 } return func1 }
自由变量和约束变量对应。所谓约束变量,是指这个变量以前是自由变量,但以后会对它进行赋值,将自由变量绑定到一个值上以后,这个变量就成为约束变量或者称为绑定变量。
例如:
function func1(x){ var m=20 # 对func2来讲,这是自由变量,对其赋值,因此m变成了bound variable var z function func2(y){ z=10 # 对自由变量z赋值,z变成bound variable return m+x+y+z # m、x和z都是自由变量 } return func1 } ref_func=func1(3) # 对x赋值,x变成bound variable
回调函数一开始是C里面的概念,它表示的是一个函数:
也就是说,将一个函数(B)做为参数传递给另外一个函数(A),但A执行完后,再自动调用B。因此这种回调函数的概念也称为"call after"。
可是如今回调函数已经足够通用化了。通用化的回调函数定义为:将函数B做为另外一个函数A的参数,执行到函数A中某个地方的时候去调用B。和原来的概念相比,再也不是函数A结束后再调用,而是咱们本身定义在哪一个地方调用。
例如,Perl中的File::Find
模块中的find函数,经过这个函数加上回调函数,能够实现和unix find命令相同的功能。例如,搜索某个目录下的文件,而后print输出这个文件名,即find /path xxx -print
。
#!/usr/bin/perl use File::Find; sub print_path { # 定义一个函数,用于输出路径名称 print "$File::Find::name\n"; } $callback = \&print_path; # 建立一个函数引用,名为$callback,因此perl是一种支持一级函数的语言 find( $callback,"/tmp" ); # 查找/tmp下的文件,每查找到一个文件,就执行一次$callback函数
这里传递给find函数的$callback
就是一个回调函数。几个关键点:
$callback
做为参数传递给另外一个find()函数(因此find()函数是一个高阶函数)$callback
函数。固然,若是find是咱们本身写的程序,就能够由咱们本身定义在什么地方去调用$callback
$callback
不是咱们主动调用的,而是由find()函数在某些状况下(每查找到一个文件)去调用的回调就像对函数进行填空答题同样,根据咱们填入的内容去复用填入的函数从而实现某一方面的细节,而普通函数则是定义了就只能机械式地复用函数自己。
之因此称为回调函数,是由于这个函数并不是由咱们主观地、直接地去调用,而是将函数做为一个参数,经过被调用者间接去调用这个函数参数。本质上,回调函数和通常的函数没有什么区别,可能只是由于咱们定义一个函数,却历来没有直接调用它,这一点感受上有点奇怪,因此有人称之为"回调函数",用来统称这种间接的调用关系。
回调函数能够被多线程异步执行。
计算机中的闭包概念是从数学世界引入的,在计算机程序语言中,它也称为词法闭包、函数闭包。
闭包简单的、通用的定义是指:函数引用一个词法变量,在函数或语句块结束后(变量的名称消失),词法变量仍然对引用它的函数有效。在下一节还有关于闭包更严格的定义(来自wiki)。
看一个python示例:函数f中嵌套了函数g,并返回函数g
def f(x): def g(y): return x + y return g # 返回一个闭包:有名称的函数(高阶函数的特性) # 将执行函数时返回的闭包函数赋值给变量(高阶函数的特性) a = f(1) # 调用存储在变量中闭包函数 print (a(5)) # 无需将闭包存储进临时变量,直接一次性调用闭包函数 print( f(1)(5) ) # f(1)是闭包函数,由于没有将其赋值给变量,因此f(1)称为"匿名闭包"
上面的a是一个闭包,它是函数g()的一个实例。f()的参数x能够被g访问,在f()返回g函数后,f()就退出了,随之消失的是变量名x(注意是变量名称x,变量的值在这里还不必定会消失)。当将闭包f(1)赋值给a后,原来x指向的数据对象(即数值1)仍被a指向的闭包函数引用着,因此x对应的值1在x消失后仍保存在内存中,只有当名为a的闭包被消除后,原来x指向的数值1才会消失。
闭包特性1:对于返回的每一个闭包g()来讲,不一样的g()引用不一样的x对应的数据对象。换句话说,变量x对应的数据对象对每一个闭包来讲都是相互独立的
例以下面获得两个闭包,这两个闭包中持有的自由变量虽然都引用相等的数值1,但两个数值是不一样数据对象,这两个闭包也是相互独立的:
a=f(1) b=f(1)
闭包特性2:对于某个闭包函数来讲,只要这不是一个匿名闭包,那么闭包函数能够一直访问x对应的数据对象,即便名称x已经消失
可是
a=f(1) # 有名称的闭包a,将一直引用数值对象1 a(3) # 调用闭包函数a,将返回1+3=4,其中1是被a引用着的对象,即便a(3)执行完了也不放开 a(3) # 再次调用函数a,将返回4,其中1和上面一条语句的1是同一个数据对象 f(1)(3) # 调用匿名的闭包函数,数据对象1在f(1)(3)执行完就消失 f(1)(3) # 调用匿名的闭包函数,和上面的匿名闭包是相互独立的
最重要的特性就在于上面执行的两次a(3):将词法变量的生命周期延长,但却足够安全。
看下面perl程序中的闭包函数,能够更直观地看到结果。
sub how_many { # 定义函数 my $count=2; # 词法变量$count return sub {print ++$count,"\n"}; # 返回一个匿名函数,这是一个匿名闭包 } $ref=how_many(); # 将闭包赋值给变量$ref how_many()->(); # (1)调用匿名闭包:输出3 how_many()->(); # (2)调用匿名闭包:输出3 $ref->(); # (3)调用命名闭包:输出3 $ref->(); # (4)再次调用命名闭包:输出4
上面将闭包赋值给$ref
,经过$ref
去调用这个闭包,则即便how_many中的$count
在how_many()执行完就消失了,但$ref
指向的闭包函数仍然在引用这个变量,因此屡次调用$ref
会不断修改$count
的值,因此上面(3)和(4)先输出3,而后输出改变后的4。而上面(1)和(2)的输出都是3,由于两个how_many()函数返回的是独立的匿名闭包,在语句执行完后数据对象3就消失了。
注意,严格定义的闭包和前面通俗定义的闭包结果上是不同的,通俗意义上的闭包并不必定符合严格意义上的闭包。
关于闭包更严格的定义,是一段谁都看不懂的说明(来自wiki)。以下,几个关键词我加粗显示了,由于重要。
闭包是一种在支持一级函的编程语言中可以将词法做用域中的变量名称进行绑定的技术。在操做上,闭包是一种用于保存函数和环境的记录。这个环境记录了一些关联性的映射,将函数的每一个自由变量与建立闭包时所绑定名称的值或引用相关联。经过闭包,就算是在做用域外部调用函数,也容许函数经过闭包拷贝他们的值或经过引用的方式去访问那些已经被捕获的变量。
我知道这段话谁都看不懂,因此简而言之一下:一个函数实例和一个环境结合起来就是闭包。这个所谓的环境,决定了这个函数的特殊性,决定了闭包的特性。
仍是上面的python示例:函数f中嵌套了函数g,并返回函数g
def f(x): def g(y): return x + y return g # 返回一个闭包:有名称的函数 # 将执行函数时返回的闭包函数赋值给变量 a = f(1)
上面的a是一个闭包,它是函数g()的一个实例。f()的参数x能够被g访问,对于g()来讲,这个x不是g()内部定义的,也不是g()的参数,因此这个x对于g来讲是一个自由变量(free variable)。虽然g()中持有了自由变量,可是g()函数自身不是闭包函数,只有在g持有的自由变量x和传递给f()函数的x的值(即f(1)中的1)进行绑定的时候,才会从g()建立一个闭包函数,这表示闭包函数开始引用这个自由变量,而且这个闭包一直持有这个变量的引用,即便f()已经执行完毕了。而后在f()中return这个闭包函数,由于这个闭包函数绑定了(引用)自由变量x,这就是闭包函数所在的环境。
环境对闭包来讲很是重要,是区别普通函数和闭包的关键。若是返回的每一个闭包不是独立持有属于本身的自由变量,而是全部闭包都持有彻底相同的自由变量,那么闭包虽然仍可称为闭包,但和普通函数却没有区别了。例如:
def f(x): x=3 def g(y): return x + y return g a = f(1) b = f(3)
在上面的示例中,x虽然是自由变量,但却在g()的定义以前就绑定了值(前文介绍过,它称为bound variable),使得闭包a和闭包b持有的再也不是自由变量,而是值对象彻底相同的绑定变量,其值对象为3,a和b这个时候其实没有任何区别(虽然它们是不一样对象)。换句话说,有了闭包a就彻底没有必要再定义另外一个功能上彻底相同的闭包b。
在函数复用性的角度上来讲,这里的a和普通函数没有任何区别,都只是简单地复用了函数体。而真正严格意义上的闭包,除了复用函数体,还复用它所在的环境。
可是这样一种状况,对于通俗定义的闭包来讲,返回的g()也是一个闭包,但在严格定义的闭包中,这已经不算是闭包。
再看一个示例:将自由变量x放在g()函数定义文本段的后面。
def f(y): return x+y x=1 def g(z): x=3 return f(z) print(g(1)) # 输出2,而不是4
首先要说明的是,python在没有给任何做用域修饰符的时候实现的词法做用域规则,因此上面return f(z)
中的f()看见的是全局变量x(由于f()定义在全局文本段中),而不是g()中的x=3。
回到闭包问题上。上面f()持有一个自由变量x,这个f(z)的文本定义段是在全局文本段中,它绑定的自由变量x是全局变量(声明并初始化为空或0),可是这个变量以后赋值为1了。对于g()中返回的每一个f()所在的环境来讲,它持有的自由变量x一开始都是不肯定的,可是后来都肯定为1了。这种状况也不能称之为闭包,由于闭包是在f()对自由变量进行绑定时建立的,而这个时候x已是固定的值对象了。
回调函数、闭包和匿名函数其实没有必然的关系,但由于不少书上都将匿名函数和回调函数、闭包放在一块儿解释,让人误觉得回调函数、闭包须要经过匿名函数实现。实际上,匿名函数只是一个有函数定义文本段,却没有名称的函数,而闭包则是一个函数的实例加上一个环境(严格意义上的定义)。
对于闭包和匿名函数来讲,仍然以python为例:
def f(x): def g(y): return x + y return g # 返回一个闭包:有名称的函数 def h(x): return lambda y: x + y # 返回一个闭包:匿名函数 # 将执行函数时返回的闭包函数赋值给变量 a = f(1) b = h(1) # 调用存储在变量中闭包函数 print (a(5)) print (b(5))
对于回调函数和匿名函数来讲,仍然以perl的find函数为例:
#!/usr/bin/perl use File::Find; $callback = sub { print "$File::Find::name\n"; }; # 建立一个匿名函数以及它的引用 find( $callback,"/tmp" ); # 查找/tmp下的文件,每查找到一个文件,就执行一次$callback函数
匿名函数让闭包的实现更简洁,因此不少时候返回的闭包函数就是一个匿名函数实例。