Table of Contents:

静态作用域和动态作用域规则

词法作用域(也叫静态作用域)的函数中遇到既不是形参也不是函数内部定义的局部变量的变量时,去函数定义时的环境中查询。
动态域的函数中遇到既不是形参也不是函数内部定义的局部变量的变量时,到函数调用时的环境中查。

所谓的作用域就是变量的可见范围。
静态作用域是指声明的作用域是根据程序正文在编译时就确定的,有时也称为词法作用域。
而在采用动态作用域的语言中,程序中某个变量所引用的对象是在程序运行时刻根据程序的控制流信息来确定的。

大多数现在程序设计语言都是采用静态作用域规则,而只有为数不多的几种语言采用动态作用域规则,包括APL、Snobol和Lisp的早期方言。而采用静态作用域的语言中,基本都是最内嵌套作用域规则:为找到某个标识符由内向外去找,若都找不到则报错

设想标识符x出现在某个函数体中,而x又不是在该函数体内定义的,那么x的值必然依赖于该函数外部的某个声明。那么在程序运行时,程序要查找标识符x所引用的对象则必须依据该语言所采用的作用域规则。

静态作用域和动态作用域的一个重要区别在于:
静态作用域规则查找一个变量声明时依赖的是源程序中块之间的静态关系;
而动态作用域规则依赖的是程序执行时的函数调用顺序
说的具体点,就是静态作用域查找的是距离当前作用域最近的外层作用域中同名标识符的声明,而动态作用域则是查找最近的活动记录中的同名标识符声明。

用静态作用域的概念描述一下闭包,我们可以这样说:因为我们的语言是静态作用域的,它能够访问的变量,需要一直都能访问,为此,需要把某些变量的生存期延长。
其实,闭包就是把函数在静态作用域中所访问的变量的生存期拉长,形成一份可以由这个函数单独访问的数据。正因为这些数据只能被闭包函数访问,所以也就具备了对信息进行封装、隐藏内部细节的特性。

产生闭包的条件

1.语言为静态作用域
2.函数为first class,且作为返回值返回
3.只要函数引用upvalue(非全局变量),upvalue不会被销毁

lua闭包

我们再来看一段代码:

function newCounter()
     local i = 0
     return function () -- 匿名函数
          i = i + 1
          return i
     end
end

-- 相当于每次调用一下就会产生一个对象,这种面向对象的写法很是trick
c1 = newCounter()
print(c1())
print(c1())

“非局部的变量”这是一个非常很重要的概念,它可以理解为不是在局部作用范围内定义的一个变量,同时,它又不是一个全局变量,也就是大家说的 upvalue,由于有了这样的一种变量的存在,就成全了Lua中的闭包。这种变量主要应用在嵌套函数匿名函数里。我们都知道,可以在Lua的函数中再定义函数,也就是内嵌函数,内嵌函数可以访问外部函数已经创建的所有局部变量,而这些变量就被称为该内嵌函数的upvalue,upvalue实际指的是变量而不是值,这些变量可以在内部函数之间共享,比如以下代码:

function Fun1()
     local iVal = 10          -- upvalue  
     function InnerFunc1()     -- 内嵌函数
          print(iVal)          --
     end
 
     function InnerFunc2()     -- 内嵌函数
          iVal = iVal + 10
     end
 
     return InnerFunc1, InnerFunc2
end
 
-- 将函数赋值给变量,此时变量a绑定了函数InnerFunc1, b绑定了函数InnerFunc2
local a, b = Fun1()
-- 调用a
a()          -->10
-- 调用b
b()          -->在b函数中修改了upvalue iVal
-- 调用a打印修改后的upvalue
a()          -->20

上述这段简单的代码,就验证了在内嵌函数中是共享upvalue的,

闭包实现

闭包=内部函数+引用环境(或者upvalue)。具体的表现就是子函数可以使用父函数中的局部变量。

上述代码中这个返回的匿名函数就是闭包的组成部分中的函数;引用环境就是变量 i 所在的环境,引用环境只要还存在引用的话内存就不会被释放掉。所以它存在的位置不会是在栈区。全局区和静态区也不可能。那只会是堆区了。
底层的实现可以是引用环境可以作为一个结构+配合一个函数指针

如果用C语言来实现闭包,显然函数指针是不够的,它不能保存自由变量的状态。
我们可以用一个struct或者map结构来保存闭包的环境,每次调用闭包时,都会自动将它作为额外的参数,这样就会保存、更新状态。闭包就是(函数指针,结构体指针)的二元组

实际上,闭包只是在形式和表现上像函数,但实际上是比函数多了一个东西。我们都知道,函数就是一些可执行语句的组合体,这些代码语句在函数被定义后就确定了,并不会再执行时发生变化,所以函数只有一个实例。而闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

闭包作用

1. 可以模拟类,进行更好的封装。可以看到闭包是数据和行为的结合体,就好比C++中的类,这样就使得闭包具有较好的抽象能力。
2. 在某些场合下,我们需要记住某次调用完成以后数据的状态,就好比C++中的static类型的变量,每次调用完成以后,static类型的变量并不会被清除。使用闭包就可以很好的完成该功能。
3. 可以把局部变量驻留在内存中,可以避免使用全局变量(也就是封装性,只有内部函数可以访问变量)
4. python中的装饰器,传进来一个函数,然后对其装饰一番再返回。

闭包注意点

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成内存泄露。

Javascript的闭包

一、变量的作用域
要理解闭包,首先必须理解Javascript特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。
Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。
另一方面,在函数外部自然无法读取函数内的局部变量。

二、如何从外部读取局部变量?
在函数的内部,再定义一个函数,并且返回这个函数

function f1(){
    n=999;
    function f2(){
      alert(n); // 999
    }
    return f2;
}

三、闭包的概念
闭包就是能够读取其他函数内部变量的函数。
闭包就是将函数内部和函数外部连接起来的一座桥梁。

四、闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

可模拟对象

function f1(){
  var n=999;
  nAdd=function(){n+=1} //这也是个闭包 ,函数内部的函数-->去操作函数中的局部变量
  function f2(){
    alert(n);
  }
  return f2;
}
var result=f1(); //f1()执行后,n并没有被销毁,还在引用呢
result(); // 999
nAdd();
result(); // 1000

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

function ClassA(){
    var a = 4;
    this.get_a = function(){
        return a;
    };
    this.set_a = function(value){
        a = value;
    };
}
var v = new ClassA();
v.get_a()    //结果为4
v.set_a(1);
v.get_a()   //结果为1
alert(v.a); //显示undefined

闭包小结

闭包有函数属性,也有对象属性。
因为是函数,所以可以被调用;因为是对象,所以可以保存状态。 因为闭包是对象,所以一般在堆上分配,内存交给GC处理(滥用闭包可能导致内存泄露)