Description
前言
几乎每个前端JSER都会碰到闭包、函数式语言、高阶函数等概念,而大多数社区的回答都有点含糊不清。本文从词法作用域的角度来解析什么是闭包,并对程序语言中的一些术语(terms)进行解释。
作用域
要讨论词法作用域,必须先理解什么是作用域。
首先我先摘录一段wikipedia在computer science条目下的解释
The term "scope" is used to refer to the set of all entities that are visible
or names that are valid within a portion of the program or at a given point in a program,
which is more correctly referred to as context or environment.
通俗点就是说:在程序的某个节点上的作用域指的是,该代码节点能够阅读到的所有实体(entity),也被称为上下文或者执行环境。
(注:entity简单来讲就是由标识符代表的代码和变量)
作用域的表现形式
以上我们提到作用域的讨论依赖具体的程序节点,这个程序节点可以细分为如下两块。
- 源代码的文本片段(area of text)
- 源代码的节点运行时(runtime)
如果是第一种情况,我们就称它为lexical scope(词法作用域)
如果是第二种情况,就称它为dynamic scope(动态作用域)
概念性的东西确立后,我们就可以讨论词法作用域和动态作用域了。
词法作用域和动态作用域的执行方式
对于词法作用域而言,程序在某个节点上运行的时候。
变量查找先从该节点所属的函数(或代码块)开始,如果找不到,则往上面一级的函数(或者代码块)开始查找,直到根作用域。
对于动态作用域而言,由于作用域依赖于runtime,程序在某个节点上运行的时候。
变量查找按照执行栈(call stack)进行查找,变量先在执行函数里面查找,如果找不到则往调用该执行函数的栈里面查找。
可以看到的是,两种作用域形式首先都会在当前函数作用域下寻找变量。
举个直观的例子,以bash脚本(使用dynamic scoping)为例
$ x=1
$ function g () { echo $x ; x=2 ; }
$ function f () { local x=3 ; g ; }
$ f
$ echo $x
对于如上的代码,分析下执行结果。
如果采用dynamic scoping来分析的话,变量查找依赖于运行的执行栈。
首先函数f执行,由于g在函数f中被执行,因此g中的x访问的是f中的本地变量,打印出来3并修改f中的局部变量x为2。
然后程序运行echo $x来说,访问的是全局作用域下的x,因此打印出来的是1。最终结果打印出来的是3和1
如果采用的是lexical scoping的话,则g在f中执行,访问的是全局变量x,因此打印出来1,同时修改全局变量x为2,因此echo $x打印出来的为2。最终结果打印出来1和2.
由这个简单的例子,大家其实可以看清楚两种作用域下不同的scope方式。
下面我们就可以说说词法作用域下的FP(函数式编程)下的闭包closure了。
函数式编程语言与闭包
开篇先理清楚FP、closure、lexical scoping的联系。
** 闭包是词法作用域在函数式编程语言的集中体现。 **
** 在实践上,闭包就是函数和上下文的绑定。 **
** 闭包允许闭包内的函数,访问闭包创建时拷贝的上下文变量(值或者引用) **
举个直观的例子,以JavaScript为例
function startAt(x)
let fnY = (y)=> x + y ;
return fnY;
let closure1 = startAt(1)
let closure2 = startAt(5)
基本学过JS的都能够知道closure1(3)返回4,closure2(3)返回8。
由于fnY在startAt函数的词法作用域里面,因此startAt(1)在执行后,返回一个闭包。
这个闭包拷贝了变量x(值或引用),因此fnY在执行的时候,直接可以获取到变量x的值。
需要补充说明的是,closure1和closure2返回的是不同的闭包,这种不同体现在上下文变量的拷贝不同,导致了fnY函数在执行的时候,阅读到的上下文不同,而产生不同的执行结果
闭包在语言中的实现方式
要实现闭包,在数据结构选型方面,肯定不是线性stack,因为闭包在执行时,仍应该保持绑定上下文的不变,而不是去阅读对应的执行环境。而线性(stack)显然无法满足要求。
实际上对于大部分拥有闭包的语言,程序语言采用的是堆(heap)的形式存储上下文non-local变量。 也正因为如此,这些语言基本自带GC(垃圾回收)。
闭包在github上也有很多的C语言实现,我的基本思路如下
根据function的嵌套关系,在单向链表中存储函数执行的context>context>context。
每次碰到要创建context的时候,则复制链表之前的数据作为当前函数执行的闭包环境。
变量引用的时候,则沿着当前的闭包环境单向链表不断向上寻找即可。
总结
这篇文章是我根据wikiPedia及查阅了相关资料的一个小总结。
对于词法作用域的阐述参考了较多的wikipedia,而对于closure的理解则参考了MDN及chrome dev tool给出的实验数据。
citation引用
wikipedia在computer science条目下的解释
entity
wiki对闭包的解释
MDN closure
Activity
KMBaby-zyl commentedon Nov 14, 2016
这篇闭包我是服的
闭包这个名字 换成“ javascript的词法作用域” 的话对jser初学者应该会更好理解一点。
slashhuang commentedon Nov 14, 2016
@KMBaby-zyl
恩,其实今天看wikiPedia的时候,感觉到它对closure的讲解有些地方并不是很清楚。
主要的一点原因是闭包在不同的语言下有不同的实现方式。
所以这篇文章就不仅仅针对JS了,包括其他函数式编程语言,如python也是这种机制。
不过类似python这种老牌的语言,可能整体而言比JS的实现的规范要更加严格点
evelynlab commentedon Nov 21, 2016
词法作用域的函数中遇到既不是形参也不是函数内部定义的局部变量的变量时,去函数定义时的环境中查询。
动态域的函数中遇到既不是形参也不是函数内部定义的局部变量的变量时,到函数调用时的环境中查。
--知乎上看到这么句话才顿悟。
词法作用域(静态作用域)是在书写代码或者说定义时确定的,而动态作用域是在运行时确定的。
词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用,其作用域链是基于运行时的调用栈的。
--这个解释的也很好。
slashhuang commentedon Nov 21, 2016
@Yuqy 赞清扬老师来访