闭包(一)浅谈闭包的由来与 JS中的表现

487 阅读5分钟

写这篇文章的目的是希望大家能够深入的理解闭包。闭包已经成为了开发者手中不可或缺的工具之一。闭包是由函数式编程衍生而来,但是闭包和函数有着本质性的区别

闭包的表现形式

Closure 是为了在实现 λ-calculus 时搞定作用域问题而搞出来的,是函数式编程及其核心思想"Lambda计算法"的必备设定。但随着各门高级语言的发展,都引入了闭包这个特性。在我的看法里闭包已经独立与函数之外了。

在computer programming中这样命名闭包

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. Operationally, a closure is a record storing a function together with an environment.The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.

人们喜欢把一切数据化,这和OOP 就有关系了。

  • 对于搞数学或者抽象的人来说,程序 = 算法 + 数据结构
  • 对于 CPU 来说,程序 = 指令 + 参数
  • 对于面向过程的设计来说,程序 = 处理的过程 + 处理的数据
  • 对于面向对象的设计来说,程序 = 对象的方法 + 对象的属性 -上面的组成中,都有一个相对偏小"操作/动作/行为",一个偏向数据。因为我们实际解决问题的也是这样子的,动作 + 数据。

如何把数据动作化?

  • c 语言:函数指针
  • C++:函数指针(兼容 C)、函数对象、lambda、模板语法......
  • java:匿名类、lambda
  • Python:一切都是对象
  • Javascript:不知道说啥 ......

例举处这些,并不是用来对比语言特性的,而是要说明:怎么样把动作数据化,lambda 并不是唯一和必要的、函数指针、封装成对象...都可以做到。闭包时真正解决什么问题的?

函数与过程,在不同场景有这不同的含义。我此时仅围绕特定场景来解释:函数是对动作的一种描述,而过程是执行中的动作。虽然不是很准确,但是可以这样理解:把执行中的动作和执行时的上下文相关数据,整个数据化,就是闭包

在各门语言的开发社区中,对于闭包有以下两种说法主流的说法:

  • 闭包是在其词法上下文中引用自由变量的函数
  • 闭包是函数+引用环境

虽然有些咬文嚼字,但在某种意义上这是两种完全对立的观点。一方认为闭包是函数,另一方认为闭包是函数和引用环境组成的Object。从另一个角度来说闭包,闭包只是在形式和表现上像函数,函数是一些可执行的代码,这些代码在函数被定义后就确定了,不会再执行时发生变化,所以函数只有一个实例。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。把函数于引用环境组合起来,这主要是因为在支持嵌套作用域的语言中,存在不能简单直接的确定函数的引用环境,这样的语言一般具有这样的特性:

  • 函数是 first-class value
  • 函数可以嵌套定义

一门编程语言需要哪些特性来支持闭包呢?

  • 函数是一阶值
  • 函数可以嵌套定义
  • 可以捕获引用环境,并吧引用环境和函数代码组成一个可调用的实体
  • 允许定义匿名函数

这些条件并不是必要的,但是具备这些条件能说明这门编程语言对闭包的支持比较完善。另外,有些语言使用与函数定义不同的语法来定义这种被传递的"函数",比如 Ruby 中的 Block。实际上,这是语法糖,只是为了更容易定义匿名函数而已,本质上没有区别。

JavaScript 中的闭包

首先,MDN 中这样定义闭包:

函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构建成闭包。也就是说,闭包可以让你从函数内部访问外部函数作用域。在 JS 中,每当函数被创建,就会在函数生成时生成闭包

闭包,作为 JS 语言的核心之一。JS 闭包的本质源自两点:词法作用域、函数当做值来传递。闭包是一个能够访问其他函数作用域的函数,但这只是闭包实现的方式。对于闭包更加倾向于状态的解释,封闭了外部状态,函数记录并访所在的词法作用域,并保持对词法作用域的引用,便形成了闭包。 在 ES6之前的时代,闭包的常见用例是基于魔方私有方法的匿名函数/IIEF(立即调用函数表达式),这些方法不是 JS 所特有的。ES6之后引入let、const以及模块,很大程度上解决了 var 的局限性多导致的这种情况和其他类型的用例。IIFE 包括闭包,但不是闭包。匿名函数也不是闭包。

后续会对闭包在 go、swift/oc、Java等语言的表现做出对比和相关解释,来理解闭包。