RSS Feed

Posts Tagged ‘javascript’

  1. 闭包,词法作用域和lambda函数

    November 8, 2011 by xudifsd

    在学习编程时最重要的是了解程序解析器或编辑器的运行过程,有很多解析器模型可以用于模拟这些过程,在学习支持lambda函数的语言时有一个非常好用的解析器模型——环境解析模型,这个模型在《SICP》中提到,这方法对于学习lambda解析很有效。几乎所有支持lambda函数的都可以使用这种方法来模拟解析器运行,目前我知道的支持lambda函数的语言有JavaScript, Lisp和Haskell。

    环境

    想要理解环境解析方法就必须先理解环境。环境就类似于存储键值对的通用仓库,所有对变量值的查找都是在环境中查找,而环境则是由更小的帧对象组成,如何理解呢?下图显示了几个由帧组成的环境

    环境和帧

    上图有三个帧,分别为1、2、3,其中a、b、c分别指向这些帧,1是最底层的帧,2和3都指向帧1,用继承的语言来说就是帧1是帧2和帧3的父帧。从a来看,变量x、y的值都为100;从b来看x为2,y为100,z为5,x是2而不是100是由于从b看时帧2中的x会屏蔽帧1中的x,而从b也能找到y就是因为帧2指向帧1;同理从c来看x为100,y为10而z为100。这样我们可以总结出在环境中查找变量的规则:如果在当前帧没有找到变量的值,找当前帧的父帧,直到找值到或者当前帧没有指向其他帧。这种环境由帧组成的方法提供了很大的便利,这些便利可以从后面的讨论中看出。

    函数对象

    在任何支持lambda函数的语言中,函数都可以看作一个函数对象,这意味着函数是有属性的,他们有两个属性:函数代码和环境,函数代码就是定义函数时写的函数代码,而环境就是定义函数时的环境。函数对象可以用以下图形表示:

    从上图可以看出一个函数的结构,左边的菱形保存的是函数定义的代码(虽然在实现时出于效率的考虑不可能保存代码的文本,但这样可以集中思考),右边保存指向环境的指针。每次创建一个函数都会创建一个这样的函数对象。这与其他语言如C语言不同的就是支持lambda的语言都支持在运行时动态创建函数,这样创建的函数会捕捉到创建它时的环境,同时也允许将函数作为参数或返回值,这就是为什么在这些语言中函数被称为顶级对象(First Class Object)。

    环境解析的方法

    环境解析方法有两个规则,一个针对函数的定义,也就是lambda函数,另一个针对函数的调用。规则如下:

    1. 函数定义时会捕捉定义时的环境。
    2. 函数运行在它被定义的环境中。

    我们可以通过例子和图来模拟环境解析的方法,假设有如下代码:

    (define (make-acc base)
    	(lambda ()
    		(set! base (+ 1 base))
    		base))

    上面的代码定义了一个累加器,它接受一个参数作为累加器的基数,返回一个从这个基数进行累加的函数。定义了这个函数之后得到如下环境:

    在这之后运行如下代码:

    (define from-100 (make-acc 100))

    这会产生如下的环境:

    环境解析方法2

    这个图非常形象地描述了环境解析方法的规则,make-acc是个lambda函数,调用该函数会新建一个环境帧2,这个环境帧包括的就是形参和实参的对应关系,这个帧的父帧就是make-acc函数属性中的环境,这确保了函数中的自由变量也能得到正确的引用,在创建环境帧之后会运行lambda表达式,这会定义一个新的函数,而这个函数的环境属性就指向刚刚创建的这个环境,如果之后再调用from-100时from-100会在环境2上运行而不是环境1上运行,它会修改帧2中的base变量,让其自增1然后返回自增后的值。这种函数在定义的环境中运行而不是在调用它的环境中运行的特点就是词法作用域。而这样的特性也就产生了最为优美的闭包。在这个简单的例子中也产生了一个闭包:只有from-100函数能访问创建该函数时给的参数base,其他任何函数都无法访问base,这有些类似面向对象语言中的private属性,但这完全是通过函数的闭包特性来实现的,而不需要动摇整个语言,增加语言解析器的复杂度。