定义

闭包在 MDN 上定义如下:

闭包 (closure) 是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

emmmm…. what? 一般来说很难理解这种定义,因为有些术语是编程语言理论的一部分。所以下面会介绍一些其它内容来补全字面意思背后的定义。

当我们在讨论闭包时,它不仅仅是 JavaScript 这门语言里的独有实现,而是所有把函数作为头等公民的语言使用闭包这一技术的概念。这类语言有:JavaScriptPythonRubyHaskellRustKotlinGroovySwift 等等。

值得一提的是 Java 语言本身不支持闭包的概念,但自 Java 8 以来,通过引入 lambda 表达式函数式接口,Java 提供了类似闭包的功能。Lambda 表达式可以捕获并使用其定义时的环境变量,类似于其他编程语言中的闭包。

术语

在介绍闭包之前,先介绍有关编程语言理论里有关的术语:

  • 标识符 (Identifier):命名的实体的唯一符号实体指的是变量数据类型函数模块等等。

    let a = 10;  // "a" 是一个标识符。
    function add() {}  // "add" 是一个标识符。
    class MyClass {}  // "MyClass" 是一个标识符。
    
  • 声明 (Declaration):声明用于定义一个标识符,并分配一个存储位置

    let a; // 声明了一个变量 a。即给 a 标识符指定一个内存位置。
    
  • 名称绑定 (name binding)实体标识符的关联。程序运行之前进行的绑定称为静态绑定,在程序运行时执行的名称绑定称为动态绑定

  • 词法作用域 (lexical scope):也称为静态作用域 (static scope),在进行静态名称绑定时就确定的作用域。而程序运行时确定的作用域则称为动态作用域 (dynamic scope)。

自由变量

自由变量 (free variables) 在某个函数或表达式中被引用,但在该函数或表达式内部未被绑定的变量。换句话说,自由变量是在某个作用域内部引用但未在该作用域内定义的变量。

特点

  1. 未绑定:自由变量在当前作用域内未被声明或定义。
  2. 依赖于外部作用域:自由变量的值取决于其被定义的外部作用域。
  3. 可以是函数参数或外部变量:自由变量可以是函数参数,也可以是外部作用域中的变量

示例

考虑下面的 Python 代码:

def outer_function():
    x = 10
    def inner_function(y):
        return x + y
    return inner_function

closure = outer_function()
print(closure(5))

在这个例子中,xinner_function 的自由变量。虽然 xouter_function 中定义,但它在 inner_function 中被引用,且没有在 inner_function 内部定义。

因此,x 被视为自由变量,其值在 inner_function 被调用时由 outer_function 提供。

自由变量与绑定变量的区别:

  • 自由变量:在当前作用域内被引用但未在该作用域内定义的变量。
  • 绑定变量:在当前作用域内定义并且可以在该作用域内被引用的变量。

在某些上下文中,自由变量和绑定变量也被称为自由标识符绑定标识符

作用域规则和块语句

大部分语言 (例:C 和 Java) 使用静态作用域,也就是编译器在编译期就决定了实体环境。

作用域规则是基于程序结构的,一个声明作用域由该声明出现的位置隐含地决定的,有些语言也通过 publicprivateprotected 等关键字显式控制。

块语句

块 (block) 是一种语句,一个块包含一个声明的序列,然后再跟着一个语句序列。通常用花括号 {} 包围这些声明和语句。

块语法允许嵌套,这种嵌套特性称为块结构 (block structure)。

closure.svg

let a = 1;
let b = 1;
{
  let b = 2;
  {
    let a = 3;
    console.log('A:',a,b); // A: 3 2
  }
  {
    let b = 4;
    console.log('B:',a,b); // B: 1 4
  }
  console.log('C:',a,b); // C: 1 2
}
console.log('D:', a,b); // D: 1 1

词法环境

通常词法环境的极简定义是将其定义为作用域中所有变量绑定的集合,这也是任何语言中的闭包都必须捕获的内容。

在命令式语言中,变量绑定到内存中可以存储值的相对位置。尽管绑定的相对位置在运行时不会更改,但绑定位置中的值可以更改。在此类语言中,由于闭包捕获绑定,因此对变量的任何操作,无论是否从闭包完成,都在同一相对内存位置上执行。这通常称为“通过引用”捕获变量。

什么是闭包?

闭包也有时被叫做词法闭包 (lexical closure) 或函数闭包 (function close),首先是闭包是在编译期对语言做语义分析时使用的名称绑定技术,再者该技术是应用于函数上的。

数据存储结构上闭包是将函数与声明该函数的词法环境 (lexical environment) 一起存储的记录 (record)。

闭包的概念是在 1960 年代开发的,用于对 λ 演算中的表达式进行机械计算,并于 1970 年首次作为 PAL 编程语言中的语言功能全面实现,以支持词法作用域头等函数 (first-class function)。

支持闭包的编程语言

闭包是一种特性,使得函数能够捕获并存储其定义时的环境,从而在函数外部也能访问这些环境变量。许多现代编程语言都支持闭包,以下是一些主要的支持闭包的编程语言及其示例:

  1. JavaScript:JavaScript 中的函数可以访问其定义时的环境,即使在函数外部调用时也能访问这些变量。

    function outerFunction(x) {
        return function innerFunction(y) {
            return x + y;
        };
    }
    
    const closure = outerFunction(10);
    console.log(closure(5));  // 输出: 15
    
  2. Swift:Swift 的闭包可以捕获和存储其定义时的变量。

    func outerFunction(x: Int) -> (Int) -> Int {
        return { y in
            return x + y
        }
    }
    
    let closure = outerFunction(x: 10)
    print(closure(5))  // 输出: 15
    
  3. Kotlin:Kotlin 支持闭包,通过 lambda 表达式和匿名函数实现。

    fun outerFunction(x: Int): (Int) -> Int {
        return { y: Int -> x + y }
    }
    
    val closure = outerFunction(10)
    println(closure(5))  // 输出: 15
    
  4. Rust:Rust 支持闭包,通过匿名函数和捕获环境变量实现。

    fn outer_function(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x + y
    }
    
    let closure = outer_function(10);
    println!("{}", closure(5));  // 输出: 15
    
  5. Haskell:Haskell 是一种纯函数式编程语言,闭包是其核心特性之一。

    outerFunction :: Int -> (Int -> Int)
    outerFunction x = \y -> x + y
    
    main = do
        let closure = outerFunction 10
        print (closure 5)  -- 输出: 15
    

这些语言中的闭包具有以下共同特性

  1. 环境捕获:闭包可以捕获定义时环境变量,并在函数外部访问这些变量。
  2. 持久化环境:即使在定义闭包的作用域外,闭包仍然保持对环境变量的引用。
  3. 高阶函数:支持高阶函数,即函数作为参数传递给其他函数,或从其他函数返回。

闭包使得编程更加灵活,尤其在处理回调、事件处理、异步编程和函数式编程时,能够简化代码并提高代码复用性和可读性。

参考资料:

> https://en.wikipedia.org/wiki/Closure_(computer_programming)