极客时间已完结课程限时免费阅读

10 | 闭包: 理解了原理,它就不反直觉了

10 | 闭包: 理解了原理,它就不反直觉了-极客时间

10 | 闭包: 理解了原理,它就不反直觉了

讲述:宫文学

时长14:32大小13.30M

在讲作用域和生存期时,我提到函数里的本地变量只能在函数内部访问,函数退出之后,作用域就没用了,它对应的栈桢被弹出,作用域中的所有变量所占用的内存也会被收回。
但偏偏跑出来闭包(Closure)这个怪物。
在 JavaScript 中,用外层函数返回一个内层函数之后,这个内层函数能一直访问外层函数中的本地变量。按理说,这个时候外层函数已经退出了,它里面的变量也该作废了。可闭包却非常执着,即使外层函数已经退出,但内层函数仿佛不知道这个事实一样,还继续访问外层函数中声明的变量,并且还真的能够正常访问。
不过,闭包是很有用的,对库的编写者来讲,它能隐藏内部实现细节;对面试者来讲,它几乎是前端面试必问的一个问题,比如如何用闭包特性实现面向对象编程?等等。
本节课,我会带你研究闭包的实现机制,让你深入理解作用域和生存期,更好地使用闭包特性。为此,要解决两个问题:
函数要变成 playscript 的一等公民。也就是要能把函数像普通数值一样赋值给变量,可以作为参数传递给其他函数,可以作为函数的返回值。
要让内层函数一直访问它环境中的变量,不管外层函数退出与否。
我们先通过一个例子,研究一下闭包的特性,看看它另类在哪里。

闭包的内在矛盾

来测试一下 JavaScript 的闭包特性:
/**
* clojure.js
* 测试闭包特性
* 作者:宫文学
*/
var a = 0;
var fun1 = function(){
var b = 0; // 函数内的局部变量
var inner = function(){ // 内部的一个函数
a = a+1;
b = b+1;
return b; // 返回内部的成员
}
return inner; // 返回一个函数
}
console.log("outside: a=%d", a);
var fun2 = fun1(); // 生成闭包
for (var i = 0; i< 2; i++){
console.log("fun2: b=%d a=%d",fun2(), a); //通过fun2()来访问b
}
var fun3 = fun1(); // 生成第二个闭包
for (var i = 0; i< 2; i++){
console.log("fun3: b=%d a=%d",fun3(), a); // b等于1,重新开始
}
在 Node.js 环境下运行上面这段代码的结果如下:
outside: a=0
fun2: b=1 a=1
fun2: b=2 a=2
fun3: b=1 a=3
fun3: b=2 a=4
观察这个结果,可以得出两点:
内层的函数能访问它“看得见”的变量,包括自己的本地变量、外层函数的变量 b 和全局变量 a。
内层函数作为返回值赋值给其他变量以后,外层函数就结束了,但内层函数仍能访问原来外层函数的变量 b,也能访问全局变量 a。
这样似乎让人感到困惑:站在外层函数的角度看,明明这个函数已经退出了,变量 b 应该失效了,为什么还可以继续访问?但是如果换个立场,站在 inner 这个函数的角度来看,声明 inner 函数的时候,告诉它可以访问 b,不能因为把 inner 函数赋值给了其他变量,inner 函数里原本正确的语句就不能用了啊。
其实,只要函数能作为值传来传去,就一定会产生作用域不匹配的情况,这样的内在矛盾是语言设计时就决定了的。我认为,闭包是为了让函数能够在这种情况下继续运行所提供的一个方案。这个方案有一些不错的特点,比如隐藏函数所使用的数据,歪打正着反倒成了一个优点了!
在这里,我想补充一下静态作用域(Static Scope)这个知识点,如果一门语言的作用域是静态作用域,那么符号之间的引用关系能够根据程序代码在编译时就确定清楚,在运行时不会变。某个函数是在哪声明的,就具有它所在位置的作用域。它能够访问哪些变量,那么就跟这些变量绑定了,在运行时就一直能访问这些变量。
看一看下面的代码,对于静态作用域而言,无论在哪里调用 foo() 函数,访问的变量 i 都是全局变量:
int i = 1;
void foo(){
println(i); // 访问全局变量
}
foo(); // 访问全局变量
void bar(){
int i = 2;
foo(); // 在这里调用foo(),访问的仍然是全局变量
}
我们目前使用的大多数语言都是采用静态作用域的。playscript 语言也是在编译时就形成一个 Scope 的树,变量的引用也是在编译时就做了消解,不再改变,所以也是采用了静态作用域。
反过来讲,如果在 bar() 里调用 foo() 时,foo() 访问的是 bar() 函数中的本地变量 i,那就说明这门语言使用的是动态作用域(Dynamic Scope)。也就是说,变量引用跟变量声明不是在编译时就绑定死了的。在运行时,它是在运行环境中动态地找一个相同名称的变量。在 macOS 或 Linux 中用的 bash 脚本语言,就是动态作用域的。
静态作用域可以由程序代码决定,在编译时就能完全确定,所以又叫做词法作用域(Lexcical Scope)。不过这个词法跟我们做词法分析时说的词法不大一样。这里,跟 Lexical 相对应的词汇可以认为是 Runtime,一个是编写时,一个是运行时。
用静态作用域的概念描述一下闭包,我们可以这样说:因为我们的语言是静态作用域的,它能够访问的变量,需要一直都能访问,为此,需要把某些变量的生存期延长。
当然了,闭包的产生还有另一个条件,就是让函数成为一等公民。这是什么意思?我们又怎样实现呢?

函数作为一等公民

在 JavaScript 和 Python 等语言里,函数可以像数值一样使用,比如给变量赋值、作为参数传递给其他函数,作为函数返回值等等。这时,我们就说函数是一等公民。
作为一等公民的函数很有用,比如它能处理数组等集合。我们给数组的 map 方法传入一个回调函数,结果会生成一个新的数组。整个过程很简洁,没有出现啰嗦的循环语句,这也是很多人提倡函数式编程的原因之一:
var newArray = ["1","2","3"].map(
fucntion(value,index,array){
return parseInt(value,10)
})
那么在 playscript 中,怎么把函数作为一等公民呢?
我们需要支持函数作为基础类型,这样就可以用这种类型声明变量。但问题来了,如何声明一个函数类型的变量呢?
在 JavaScript 这种动态类型的语言里,我们可以把函数赋值给任何一个变量,就像前面示例代码里的那样:inner 函数作为返回值,被赋给了 fun2 和 fun3 两个变量。
然而在 Go 语言这样要求严格类型匹配的语言里,就比较复杂了:
type funcType func(int) int // Go语言,声明了一个函数类型funcType
var myFun funType // 用这个函数类型声明了一个变量
它对函数的原型有比较严格的要求:函数必须有一个 int 型的参数,返回值也必须是 int 型的。
而 C 语言中函数指针的声明也是比较严格的,在下面的代码中,myFun 指针能够指向一个函数,这个函数也是有一个 int 类型的参数,返回值也是 int:
int (*myFun) (int); //C语言,声明一个函数指针
playscript 也采用这种比较严格的声明方式,因为我们想实现一个静态类型的语言:
function int (int) myFun; //playscript中声明一个函数型的变量
写成上面这样是因为我个人喜欢把变量名称左边的部分看做类型的描述,不像 Go 语言把类型放在变量名称后面。最难读的就是 C 语言那种声明方式了,竟然把变量名放在了中间。当然,这只是个人喜好。
把上面描述函数类型的语法写成 Antlr 的规则如下:
functionType
: FUNCTION typeTypeOrVoid '(' typeList? ')'
;
typeList
: typeType (',' typeType)*
;
在 playscript 中,我们用 FuntionType 接口代表一个函数类型,通过这个接口可以获得返回值类型、参数类型这两个信息:
package play;
import java.util.List;
/**
* 函数类型
*/
public interface FunctionType extends Type {
public Type getReturnType(); //返回值类型
public List<Type> getParamTypes(); //参数类型
}
试一下实际使用效果如何,用 Antlr 解析下面这句的语法:
function int(long, float) fun2 = fun1();
它的意思是:调用 fun1() 函数会返回另一个函数,这个函数有两个参数,返回值是 int 型的。
我们用 grun 显示一下 AST,你可以看到,它已经把 functionType 正确地解析出来了:
目前,我们只是设计完了语法,还要实现运行期的功能,让函数真的能像数值一样传来传去,就像下面的测试代码,它把 foo() 作为值赋给了 bar():
/*
FirstClassFunction.play 函数作为一等公民。
也就是函数可以数值,赋给别的变量。
支持函数类型,即FunctionType。
*/
int foo(int a){
println("in foo, a = " + a);
return a;
}
int bar (function int(int) fun){
int b = fun(6);
println("in bar, b = " + b);
return b;
}
function int(int) a = foo; //函数作为变量初始化值
a(4);
function int(int) b;
b = foo; //函数用于赋值语句
b(5);
bar(foo); //函数做为参数
运行结果如下:
in foo, a = 4
in foo, a = 5
in foo, a = 6
in bar, b = 6
运行这段代码,你会发现它实现了用函数来赋值,而实现这个功能的重点,是做好语义分析。比如编译程序要能识别赋值语句中的 foo 是一个函数,而不是一个传统的值。在调用 a() 和 b() 的时候,它也要正确地调用 foo() 的代码,而不是报“找不到 a() 函数的定义”这样的错误。
实现了一等公民函数的功能以后,我们进入本讲最重要的一环:实现闭包功能。

实现我们自己的闭包机制

在这之前,我想先设计好测试用例,所以先把一开始提到的那个 JavaScript 的例子用 playscript 的语法重写一遍,来测试闭包功能:
/**
* clojure.play
* 测试闭包特性
*/
int a = 0;
function int() fun1(){ //函数的返回值是一个函数
int b = 0; //函数内的局部变量
int inner(){ //内部的一个函数
a = a+1;
b = b+1;
return b; //返回内部的成员
}
return inner; //返回一个函数
}
function int() fun2 = fun1();
for (int i = 0; i< 3; i++){
println("b = " + fun2() + ", a = "+a);
}
function int() fun3 = fun1();
for (int i = 0; i< 3; i++){
println("b = " + fun3() + ", a = "+a);
}
代码的运行效果跟 JavaScript 版本的程序是一样的:
b = 1, a = 1
b = 2, a = 2
b = 3, a = 3
b = 1, a = 4
b = 2, a = 5
b = 3, a = 6
这段代码的 AST 我也让 grun 显示出来了,并截了一部分图,你可以直观地看一下外层函数和内层函数的关系:
现在,测试用例准备好了,我们着手实现一下闭包的机制。
前面提到,闭包的内在矛盾是运行时的环境和定义时的作用域之间的矛盾。那么我们把内部环境中需要的变量,打包交给闭包函数,它就可以随时访问这些变量了。
在 AST 上做一下图形化的分析,看看给 fun2 这个变量赋值的时候,发生了什么事情:
简单地描述一下给 fun2 赋值时的执行过程:
先执行 fun1() 函数,内部的 inner() 函数作为返回值返回给调用者。这时,程序能访问两层作用域,最近一层是 fun1(),里面有变量 b;外层还有一层,里面有全局变量 a。这时是把环境变量打包的最后的机会,否则退出 fun1() 函数以后,变量 b 就消失了。
然后把内部函数连同打包好的环境变量的值,创建一个 FunctionObject 对象,作为 fun1() 的返回值,给到调用者。
给 fun2 这个变量赋值。
调用 fun2() 函数。函数执行时,有一个私有的闭包环境可以访问 b 的值,这个环境就是第二步所创建的 FunctionObject 对象。
最终,我们实现了闭包的功能。
在这个过程中,我们要提前记录下 inner() 函数都引用了哪些外部变量,以便对这些变量打包。这是在对程序做语义分析时完成的,你可以参考一下 ClosureAnalyzer.java 中的代码:
/**
* 为某个函数计算闭包变量,也就是它所引用的外部环境变量。
* 算法:计算所有的变量引用,去掉内部声明的变量,剩下的就是外部的。
* @param function
* @return
*/
private Set<Variable> calcClosureVariables(Function function){
Set<Variable> refered = variablesReferedByScope(function);
Set<Variable> declared = variablesDeclaredUnderScope(function);
refered.removeAll(declared);
return refered;
}
下面是 ASTEvaluator.java 中把环境变量打包进闭包中的代码片段,它是在当前的栈里获取数据的:
/**
* 为闭包获取环境变量的值
* @param function 闭包所关联的函数。这个函数会访问一些环境变量。
* @param valueContainer 存放环境变量的值的容器
*/
private void getClosureValues(Function function, PlayObject valueContainer){
if (function.closureVariables != null) {
for (Variable var : function.closureVariables) {
// 现在还可以从栈里取,退出函数以后就不行了
LValue lValue = getLValue(var);
Object value = lValue.getValue();
valueContainer.fields.put(var, value);
}
}
}
你可以把测试用例跑一跑,修改一下,试试其他闭包特性。

体验一下函数式编程

现在,我们已经实现了闭包的机制,函数也变成了一等公民。不经意间,我们似乎在一定程度上支持了函数式编程(functional programming)。
它是一种语言风格,有很多优点,比如简洁、安全等。备受很多程序员推崇的 LISP 语言就具备函数式编程特征,Java 等语言也增加了函数式编程的特点。
函数式编程的一个典型特点就是高阶函数(High-order function)功能,高阶函数是这样一种函数,它能够接受其他函数作为自己的参数,javascript 中数组的 map 方法,就是一个高阶函数。我们通过下面的例子测试一下高阶函数功能:
/**
LinkedList.play
实现了一个简单的链表,并演示了高阶函数的功能,比如在javascript中常用的map功能,
它能根据遍历列表中的每个元素,执行一个函数,并返回一个新的列表。给它传不同的函数,会返回不同的列表。
*/
//链表的节点
class ListNode{
int value;
ListNode next; //下一个节点
ListNode (int v){
value = v;
}
}
//链表
class LinkedList{
ListNode start;
ListNode end;
//添加新节点
void add(int value){
ListNode node = ListNode(value);
if (start == null){
start = node;
end = node;
}
else{
end.next = node;
end = node;
}
}
//打印所有节点内容
void dump(){
ListNode node = start;
while (node != null){
println(node.value);
node = node.next;
}
}
//高阶函数功能,参数是一个函数,对每个成员做一个计算,形成一个新的LinkedList
LinkedList map(function int(int) fun){
ListNode node = start;
LinkedList newList = LinkedList();
while (node != null){
int newValue = fun(node.value);
newList.add(newValue);
node = node.next;
}
return newList;
}
}
//函数:平方值
int square(int value){
return value * value;
}
//函数:加1
int addOne(int value){
return value + 1;
}
LinkedList list = LinkedList();
list.add(2);
list.add(3);
list.add(5);
println("original list:");
list.dump();
println();
println("add 1 to each element:");
LinkedList list2 = list.map(addOne);
list2.dump();
println();
println("square of each element:");
LinkedList list3 = list.map(square);
list3.dump();
运行后得到的结果如下:
original list:
2
3
5
add 1 to each element:
3
4
6
square of each element:
4
9
25
高阶函数功能很好玩,你可以修改程序,好好玩一下。

课程小结

闭包这个概念,对于初学者来讲是一个挑战。其实,闭包就是把函数在静态作用域中所访问的变量的生存期拉长,形成一份可以由这个函数单独访问的数据。正因为这些数据只能被闭包函数访问,所以也就具备了对信息进行封装、隐藏内部细节的特性。
听上去是不是有点儿耳熟?封装,把数据和对数据的操作封在一起,这不就是面向对象编程嘛!一个闭包可以看做是一个对象。反过来看,一个对象是不是也可以看做一个闭包呢?对象的属性,也可以看做被方法所独占的环境变量,其生存期也必须保证能够被方法一直正常的访问。
你看,两个不相干的概念,在用作用域和生存期这样的话语体系去解读之后,就会很相似,在内部实现上也可以当成一回事。现在,你应该更清楚了吧?

一课一思

思考一下我在开头提到的那个面试题:如何用闭包做类似面向对象的编程?
其实,我在课程中提供了一个 closure-mammal.play 的示例代码,它完全用闭包的概念实现了面向对象编程的多态特征。而这个闭包的实现,是一种更高级的闭包,比普通的函数闭包还多了一点有用的特性,更像对象了。我希望你能发现它到底不同在哪里,也能在代码中找到实现这些特性的位置。
你能发现,我一直在讲作用域和生存期,不要嫌我啰嗦,把它们吃透,会对你使用语言有很大帮助。比如,有同学非常困扰 JavaScript 的 this,我负责任地讲,只要对作用域有清晰的了解,你就能很容易地掌握 this。
那么,关于作用域跟 this 之间的关联,如果你有什么想法,也欢迎在留言区分享。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友,特别是分享给那些还没搞清楚闭包的朋友。
本节课的示例代码放在了文末,供你参考。
playscript-java(项目目录): 码云 GitHub
PlayScript.java(入口程序): 码云 GitHub
PlayScript.g4(语法规则): 码云 GitHub
ASTEvaluator.java(解释器,找找闭包运行期时怎么实现的): 码云 GitHub
ClosureAnalyzer.java(分析闭包所引用的环境变量):码云 GitHub
RefResolver.java(在这里看看函数型变量是怎么消解的): 码云 GitHub
closure.play(演示基本的闭包特征): 码云 GitHub
closure-fibonacci.play(用闭包实现了斐波那契数列计算):码云 GitHub
closure-mammal.play(用闭包实现了面向对象特性,请找找它比普通闭包强在哪里):码云 GitHub
FirstClassFunction.play(演示一等公民函数的特征):码云 GitHub
LinkedList.play(演示了高阶函数 map):码云 GitHub
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 5

提建议

上一篇
09 | 面向对象:实现数据和方法的封装
下一篇
11 | 语义分析(上):如何建立一个完善的类型系统?
unpreview
 写留言

精选留言(19)

  • 刘強
    2019-10-27
    有些东西研究的透彻以后,你就会不由自主的成为哲学家了。

    作者回复: 我这几年对复杂系统有关的理论很感兴趣,曾经在校友的一次聚会上分享了一个主题,其中的主要意思,就是从科学甚至可以推导出哲学,印证古老智慧。

    共 3 条评论
    13
  • 独钓寒江雪
    2020-01-15
    闭包的产生(以JavaScript为例): 1. 因为JavaScript是静态作用域的,所以它内部环境中需要的变量在编译时就确定了,运行时不会改变; 2. 因为JavaScript中,函数是一等公民,可以被调用,可以作为参数传递,可以赋值给变量,也可以作为函数返回值,所以它的运行时环境很容易变化; 3. 当函数作为参数返回时,其外层函数中的变量已经从调用栈弹出,但是我们必须让函数可以访问到它需要的变量,因此运行时的环境和定义时的作用域之间就产生矛盾; 4. 所以我们把内部环境中需要的变量,打包交给内层函数(闭包函数),它就可以随时访问这些变量了,就形成了闭包。
    展开

    作者回复: 总结得非常好!Great!

    11
  • nil
    2019-10-17
    记得第一次遇到闭包是在学习python得时候,方式刚觉这个玩意好牛逼。后来随着对其理解的深入,闭包完全带有面向对象的意思,外层函数通过函数参数的形式给内部函数创建运营期变量,这个运行期作用的变量和oop中的成员变量有相似的味道。通过这一讲,对闭包的实现原理有了进一步的理解,原来闭包不反人类,设计还相当巧妙😁

    作者回复: 非常好! 遇到看似不正常的东西的时候,其实就是让认知深化的契机。

    共 3 条评论
    11
  • 沉淀的梦想
    2019-09-06
    闭包如果引用的是外部函数中的局部变量,直接把这个变量从栈中复制一份到FunctionObject里面就可以了,但是如果应用了全局变量的话,感觉必须要引用全局变量本身,这样才能自己的修改体现在全局变量中。老师代码中是如何实现这个的呢?

    作者回复: 非常好,你注意到了这个细节。 实际上,我在运行时用到了一个小技巧。首先是按照作用域查找变量,这个时候就会找到那个全局变量。在作用域里找不到的时候,再到FunctionObject中去找。所以,其实运行期里,全局变量存了两份。一份是在顶层的栈桢里,一份在FunctionObject里。只不过后者不起作用罢了。 当存在多层函数嵌套的时候,上面的算法可以根据运行时所在的作用域,访问正确的变量。 这个方法有些偷懒,因为毕竟FunctionObject里冗余了一份,浪费空间了。你也可以找其他机制来实现。只要支持闭包的原理就行! 你可以参考ASTEvaluator.java中的getLValue()方法,里面有注释,说了这个思路。

    共 6 条评论
    8
  • 2019-10-05
    怎么看起来像: 闭包变量,就是在语义分析时,为闭包函数生成的static变量。

    作者回复: 但只对这个闭包有用。再调用一次函数,新生成一个闭包,就会再生成另一个变量。

    共 2 条评论
    5
  • Smallfly
    2019-09-22
    我今天发现 IntelliJ 全家桶支持 ANTLR 插件,可以集成在编译器里直接查看生成的 AST。

    作者回复: 那更方便了!

    6
  • Tao
    2020-02-23
    JavaScript 函数中 this ,如果是一个函数是对象调用比如 obj.foo(),那么foo中这个this就是当前对象obj 如果这个foo当作普通函数调用如: var bar =obj.foo bar() 这个时候this就不是obj这个对象了,非严格模式下this此时是全局对象 window
    展开

    作者回复: 从语言的设计角度讲,对象的方法和函数没有什么区别。 比如,java程序在调用方法时,会在第一个参数中传递对象引用,这就是this。比如一个void foo(int a)函数,实际会接收到两个参数。

    3
  • sugar
    2020-04-16
    既然聊到JavaScript,我还是想问一句 宫老师真的觉得js的this设计不是败笔吗?感觉在es规范迭代的多个版本中,形成了一个相当沉重的历史包袱。严格模式下,语言制定者(或者说当时有权修改语言规范的人)就对this的含义做了修改。就像前面评论的那位所说,this在非严格模式指向window,而如果开启严格模式这里又是报错了...
    2
  • dbo
    2020-03-16
    其实,只要函数能作为值传来传去,就一定会产生作用域不匹配的情况,这样的内在矛盾是语言设计时就决定了的。 不理解这句话,为什么函数作为值传来传去会产生作用域不匹配的情况,考试能解释下吗?谢谢。

    作者回复: 有一个概念,叫做词法作用域(Lexical Scope),又叫做静态作用域。也就是变量的声明和使用的关系(变量消解),是在编译期就可以确定的,是完全由变量在源代码中的位置决定的。 对于闭包的情况,内部函数使用了外部函数的变量。但这个内部函数在运行时又被传到其他地方去使用。那么声明这个函数时所依赖的变量,在实际运行时,没有办法从上一级的栈帧里去获取,这就是矛盾的地方。

    3
  • 曾经瘦过
    2019-09-25
    个人理解: JavaScript是 静态作用域 但是JavaScript中this 是动态作用域

    作者回复: 应该说,this本来就用来指代当前作用域的。对象就是一个作用域。所以this总在变是应该的。this不需要我们在代码里去声明,它是一个内在的机制。 动态作用域,是指我们在代码里显式声明的变量,其值不是声明时的作用域里的值,而是运行环境的作用域里的值。

    共 3 条评论
    2
  • 沉淀的梦想
    2019-09-07
    /* 这是针对函数可能是一等公民的情况。这个时候,函数运行时的作用域,与声明时的作用域会不一致。 我在这里设计了一个“receiver”的机制,意思是这个函数是被哪个变量接收了。要按照这个receiver的作用域来判断。 */ else if (frame.object instanceof FunctionObject){ FunctionObject functionObject = (FunctionObject)frame.object; if (functionObject.receiver != null && functionObject.receiver.enclosingScope == f.scope) { frame.parentFrame = f; break; } } 不是很理解老师的这个receiver机制,能举个例子吗?
    展开

    作者回复: 看一下closure.play示例程序: int a = 0; function int() fun1(){ int b = 0; //函数内的局部变量 int inner(){ //内部的一个函数 a = a+1; b = b+1; return b; //返回内部的成员 } return inner; //返回一个函数 } function int() fun2 = fun1(); 这时候,fun2是个变量,这个变量就是fun1()中的inner()函数的receiver。这个时候,inner()函数的运行时坐在的作用域是fun2这个变量的。 receiver这个机制是我创造的,不用拘泥于这种实现方式。只要能够实现闭包的原理,就都可以。

    共 2 条评论
    2
  • Geek_d0aef1
    2019-09-06
    想问个没有技术含量的问题,想确认下,antlr 自动生成的代码只有4个,其他都是自己手动写的?

    作者回复: 你运行antlr命令的时候,通过带不同的参数,会生成数量不等的java文件。工具生成的头上都带有注释,说明是Antlr生成的。 在playscript-java项目中,应该是有6个。1个lexer,1个parser,2个是支持listener,2个是支持visitor的。 其他是手动写的:-)

    2
  • Nail
    2020-07-09
    我用的是 ts,从 7 开始,发现很难跟上了。举个例子,比如在实现 visitor 时的很多方法都和 java 的不一样,也没有找到对应的文档。想请问有没有缓解的办法

    作者回复: TS实现visitor也很简单。一个办法,是让Antlr给你生成一下,然后你可以借鉴一下。Antlr支持TS。

    2
  • xxx
    2021-03-06
    现在代码确实是看不太懂了,就读读思想把。感觉保存变量那块术语其实就是 Capture,然后大部分语言在这里是允许返回的函数修改capture到的变量的,但Java不可以,所以还得用数组绕过。
  • VictorLee
    2020-09-15
    那个是闭包面试经典题,另外js中的this实现了类似的动态作用域机制,其确定有一个优先级

    作者回复: Great!

  • Geek_179681
    2020-06-25
    老师好,C语言里用函数指针作为参数传递也能实现本节中 LinkedList 中 map 的作用,可以认为它们是类似的东西吗?比如说函数能作为参数传递的底层也是传递函数地址? 另外一个问题是,C 和 C++ 中没有函数嵌套定义的问题,所以是否也就不存在闭包的概念,即不存在在函数中返回另一个函数的操作场景?

    作者回复: 没错,C语言中的函数指针是一种数据类型,可以用来作为参数传递或赋值,从而实现函数式编程的一些优点。 在把函数作为参数传递方面,每种语言的实现机制是不一样的。比如,对于Python而言,函数也是一个对象,跟一个整数、一个字符串是一样的。Python的运行时会根据这个对象里的信息,找到一个具体的函数地址(机器码地址)去执行,或者去解释执行一段字节码。我在《编译原理实战课》对上述机制做了剖析。 关于最后一个问题,C和C++的标准规范里是不支持函数的嵌套定义的。但是,由于C语言具有高度的灵活性,它其实可以模拟出闭包的效果来,你可以在网络上搜索一下其实现机制。

  • Aaaaaaaaaaayou
    2020-02-10
    老师,有个问题问一下,代码中的 LValue 类型是干嘛用的,为什么有些节点的返回值是 Object,有些是 LValue

    作者回复: LValue的意思是左值,也就是可以出现在赋值符号左边的值。它得是一个变量的引用(或C语言中变量的地址),这样才能修改变量的值。 与之对应的,是右值。右值可以仅仅是一个值。也就是示例代码中的Object。 左值可以当做右值来用,而右值不能当做左值来用。

    1
  • Sun Fei
    2019-09-07
    精彩。

    作者回复: :-)

  • 茶底
    2019-09-05
    老师什么时候开始讲lex和yacc啊

    作者回复: lex和yacc都没计划讲。因为这些工具都差不多。掌握原理后,用哪个应该都没问题。 lex(或flex)比较简单,所以会用Antlr一定也会用lex。 yacc(或bison)是LR算法的,我们讲完LR算法以后,你理解这个工具的原理应该也没啥问题。