美文网首页Theory
[Theory] A closure is nothing bu

[Theory] A closure is nothing bu

作者: 何幻 | 来源:发表于2020-06-24 15:42 被阅读0次

以前写的一篇文章

1. 找出定义

在解释新概念时,很多人喜欢类比比喻,似乎只有这样,才能让抽象的概念更容易理解一些,
然而非常不幸,这些不恰当的比喻,却经常会造成误解
让我们自以为如此,实则还是对力量一无所知

在讨论 closure(闭包)这个概念时,可能一百个前端会有一百二十种认识。
那么到底 closure 是个什么东西呢?
我们还是要回到定义,用最原始的笨方法去理解它。

先看维基百科上是怎么说明的吧,

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions.

粗略的翻译一下,
在编程语言中, closure(闭包)或者称词法闭包或函数闭包,
是一种与编程语言实现相关的技术,它在支持first-class function的语言中可以实现词法作用域

除此之外,在《EOPL》第80页,我们还看到,

These data structures are often called closures, because they are self-contained: they contain everything the procedure needs in order to be applied. We sometimes say the procedure is closed over or closed in its creation environment.

这样就比较清晰了,closure(闭包)原来是一种数据结构,它包含了函数调用所需要的一切信息。

2. 理解实现

那么 closure 到底是一种怎样的数据结构呢?
刚好我在这篇文章中有过一些介绍:词法作用域和闭包

; closure in the lexical-scope

(struct closure 
  (param body env))

这是一段 Racket 代码,使用 struct 定义了一个数据结构。
包含了3个字段,parambodyenv
这就是 closure(闭包)了。

它是怎么使用的呢?

(define (eval-exp exp env)
  (handle-decision-tree 
   `((,is-symbol? ,eval-symbol)     
     (,is-self-eval-exp? ,eval-self-eval-exp)     
     (,is-list?      
      ((,is-lambda? ,eval-lambda)       
       (,is-function-call-list? ,eval-function-call-list))))   
   exp env))

; ......

(define (eval-lambda exp env)  
  (display "eval-lambda\n")  
  (let ((param (caadr exp))        
        (body (caddr exp)))    
    (closure param body env)))

; ......

(define (eval-function-call-list exp env)  
  (display "eval-function-call-list\n")  
  (let* ((clos (eval-exp (car exp) env))         
         (arg (eval-exp (cadr exp) env))

         (body (closure-body clos))
         (lexical-env (closure-env clos))
         (param (closure-param clos))

         (frame (create-frame)))

    (extend-frame frame param arg)

    (let ((executing-env (extend-env lexical-env frame)))
      (eval-exp body executing-env))))

以上代码 eval-exp 可用来求值任意表达式,我们会进行判断,
如果这个表达式是 函数定义 is-lambda?,我们就调用 eval-lambda
如果这个表达式是 函数调用 is-function-call-list?,我们就调用 eval-function-call-list

函数定义的逻辑非常简单,只是把 parambody 和当前 env 存到 closure 中返回就行了,
之后函数调用的时候,再从 closure 中把这个 env 拿出来,用实参 extend 一下,再在这个环境中求值函数体。

所以,closure(闭包)的原理一下子就清晰了,
closure 只是一种技术手段,让函数在调用时,能拿到它被定义时的环境(env)。

3. 回到现实

我们用JavaScript来理解一下,找找感觉。
先来看看,如果函数调用时拿不到定义时的环境会怎样?

3.1 环境(env)和栈帧(frame)

首先,我们知道,函数的调用链路是先调用后返回的,即,
如果 foo 中调用了 bar,则 bar 返回后, foo 才会返回。

foobar 两者很可能有相同的实参,

function foo(x) {
    bar(2);
}
function bar(x) { }

foo(1);

所以需要花点心思考虑 env 该怎么表示。

经过一番考虑之后,我们决定用一个数组表示 env
env 中的元素为一个一个对象(称为栈帧 frame),对象中存着变量名和值之间的绑定关系。

env = [frame1, frame2, ...]
frame = {key1: value1, key2: value2, ...}

具体说来就是,(序号1,2,表示时间顺序)

const a = 1;   // 1.[{a: 1}]
const b = 2;   // 2.[{a: 1, b: 2}]

函数调用时会创建一个新的栈帧,并将实参与值的绑定关系写入这个栈帧中。

const a = 1;              // 1. [{a: 1}]
const b = 2;              // 2. [{a: 1, b: 2}]

const f = function (x) {  // 3. [{a: 1, b: 2, f: <function>}]
                          // 5. 创建一个空的frame,加入env尾部,[{a: 1, b: 2, f: <function>}, { }]
                          // 6. 将实参与值的绑定关系写入frame中,[{a: 1, b: 2, f: <function>}, {x: 3}]
}

f(3);                     // 4. 调用前:[{a: 1, b: 2, f: <function>}]
                          // 7. 调用后:弹栈把栈帧销毁,又回到了[{a: 1, b: 2, f: <function>}]

3.2 动态作用域

这样处理是最容易想到的办法,我们似乎并不需要使用 closure。然而

const f = function () {      // 1. [{f: <function>}]
                             // 3. 创建新栈帧,[{f: <function>}, { }]

    const a = 1;             // 4. 将局部变量写入当前栈帧,[{f: <function>}, {a: 1}]
    const g = function () {  // 5. [{f: <function>}, {a: 1, g: <function>}]
                             // 11. 创建新栈帧,[{f: <function>, g: <function>, a: 2}, { }]
        return a;            // 12. 在env中从右到左(栈顶)查找a的值,a = 2,函数返回 2
    }

    return g;                // 6. 函数返回
}

const g = f();               // 2. 调用前:[{f: <function>}]
                             // 7. 调用后:弹栈,又回到了[{f: <function>}]
                             // 8. 将返回值赋值给g,[{f: <function>, g: <function>}]
const a = 2;                 // 9. 变量写入当前栈帧,[{f: <function>, g: <function>, a: 2}]
const r = g();               // 10. 调用前:[{f: <function>, g: <function>, a: 2}]
                             // 13. 调用后:弹栈,又回到了[{f: <function>, g: <function>, a: 2}]
                             // 14. 将返回值赋值给r,[{f: <function>, g: <function>, a: 2, r: 2}]

我们看到,最终 r 的值为 2,而不是离 return a; 最近的 const a = 1;
这就很容易令人困惑,当我们想知道 return a;a 是什么值的时候,还要关心 g 是怎么被调用的。
g 在调用之前 const a = 2; 那么 g 就会返回 2,在调用之前 const a = 3;,则会返回 3

同样的写法 g(),居然会有不同的结果,这样很容易出事。
因此,这种简易的 env 实现,逐渐被人们所抛弃了。

取而代之,人们通过 closure 来保存函数在定义时的环境,并在那个环境中求值函数体。

3.3 静态(词法)作用域

下面我们再看看使用 closure,我们可以做到什么,

const f = function () {      // 1. [{f: <closure>}]
                             // 3*. (从closure中获取f定义时的环境:第1步的)创建新栈帧,[{f: <closure>}, { }]

    const a = 1;             // 4. 将局部变量写入当前栈帧,[{f: <closure>}, {a: 1}]
    const g = function () {  // 5. [{f: <closure>}, {a: 1, g: <closure>}]
                             // 11*. (从closure中获取f定义时的环境:第5步的)创建新栈帧,[{f: <closure>}, {a: 1, g: <closure>}, { }]
        return a;            // 12*. 在env中从右到左(栈顶)查找a的值,a = 1,函数返回 1
    }

    return g;                // 6. 函数返回
}

const g = f();               // 2. 调用前:[{f: <closure>}]
                             // 7*. 调用后(恢复成调用前的环境):[{f: <closure>}]
                             // 8. 将返回值赋值给g,[{f: <closure>, g: <closure>}]
const a = 2;                 // 9. 变量写入当前栈帧,[{f: <closure>, g: <closure>, a: 2}]
const r = g();               // 10. 调用前:[{f: <closure>, g: <closure>, a: 2}]
                             // 13*. 调用后(恢复成调用前的环境):[{f: <closure>, g: <closure>, a: 2}]
                             // 14*. 将返回值赋值给r,[{f: <closure>, g: <closure>, a: 2, r: 1}]

不妨重点关注带 * 号的步骤,
在第 3 步和第 11 步中,我们并没有像之前的例子那样,直接操作函数调用时的环境,
而是从 closure 中取出了函数在定义时的环境。

11 步最为明显,它取出了第 5 步时的环境
因此,再从 env 中找 a 值的时候,我们找到了 const a = 1; 的值。
这个 const a = 1; 是离 return a; 最近的,在代码(词法)范围内距离最近的。

因此,我们就说变量 a 是具有静态(词法)作用域的。

With lexical scope, a name always refers to its (more or less) local lexical environment.

4. 小结

本文介绍了什么是 closure,以及它在编程语言实现中扮演着的角色,
接着,我们看到了动态作用域和静态(词法)作用域的区别,
相信这样理解的话,我们应对 closure 的认识更加深刻了吧。


参考

Wikipedia: Closure (computer programming)
Wikipedia: lexical scope
Essentials of Programming Languages
词法作用域和闭包

相关文章

网友评论

    本文标题:[Theory] A closure is nothing bu

    本文链接:https://www.haomeiwen.com/subject/wzirfktx.html