函数
Source: Dev.to
截图
我构建的
提交 3da669d、930014e、5adb148 和 9d534f3
我理解的内容
1️⃣ 更新语法
- 优先级 – 函数调用现在拥有最高的优先级,位于一元运算规则之上。
- 可调用对象 – 引入了
Callable类(实际为LoxCallable),用于判断一个实体是否可以被调用。- 示例:字符串不可调用,而类和函数(用户自定义或原生)是可调用的。
- 如果 被调用者 没有实现
LoxCallable,解释器会抛出运行时错误。
- 柯里化 / 链式调用 – 解析器现在允许类似
(( "(" arguments? ")" )*的结构,从而支持function(arg)(arg)(arg)这样的调用。

2️⃣ 参数
- 参数个数检查 – 解释器会验证提供的实参数量是否与函数定义的形参数量相匹配。

3️⃣ 原生函数
- 在 Java 中实现了
clock()原生函数。 - 它是实现了
LoxCallable的匿名类,并绑定到解释器全局环境中的标识符clock。 - 返回当前系统时间(秒),可用于基准测试。
4️⃣ 函数声明
- 遇到
fun时会创建一个可调用对象(LoxCallable),并将其绑定到函数名所在的新环境中。 - 函数的形参在该局部环境中绑定。
- 每次函数调用也会创建一个全新的环境,从而支持递归。
- 当函数体执行完毕后,局部环境被销毁,执行返回到调用者的环境。
5️⃣ 返回
- 解释器会查找
return语句,后面可以跟一个表达式。 - 如果没有提供表达式,则隐式返回
nil。 - 当执行
return时,解释器必须立即展开当前调用栈。 - 这通过一个简单的异常类
Return来实现,该异常携带返回值。 - 调用点用
try‑catch包裹函数执行,捕获该异常;如果未抛出异常,函数正常结束并返回nil。
6️⃣ 作用域
- 之前 Lox 使用动态作用域:被调用函数的作用域始终是全局环境,而不是函数定义时所在的环境。
- 这导致函数在定义环境已经结束后,无法访问在定义时捕获的变量。
- 解决办法是捕获词法环境(函数创建时存在的环境),而不是在调用点再进行名称解析。
示例说明问题(以及修复)
fun getName(name) {
var person = name;
fun greet() {
print "Hello, " + person + "!";
}
return greet;
}
var momo = getName("Momo");
var chutney = getName("Chutney");
momo(); // 应该打印 "Hello, Momo!"
chutney(); // 应该打印 "Hello, Chutney!"
- 修复前 由于
person在全局作用域中查找,会抛出错误。 - 实现词法作用域后 每个返回的
greet闭包都会正确捕获各自的person变量,产生预期的输出。
Source: …
闭包、作用域与解析器
1. 闭包示例
function getName(name) {
var person = "Hello, " + name + "!";
return function () {
console.log(person);
};
}
var momo = getName("Momo"); // Returns a function that closes over `person`
momo(); // → “Hello, Momo!”
function chutney() {
console.log("Hello, Chutney!");
}
chutney(); // → “Hello, Chutney!”
2. 为什么第一个示例会失败
- 当
getName("Momo")执行完毕时,变量person会因为函数作用域结束而 被销毁。 - 当随后调用
momo()时,它会尝试在其父作用域中寻找person,此时父作用域已经是 全局作用域(其中并不存在person)。→ 错误!
3. 词法(静态)作用域
- 我们通过 词法作用域 来解决这个问题。
- 当函数 声明 时,它会 闭合(捕获)其周围的环境。
- 内部函数持有对外部环境的 活跃引用,即使外部函数已经返回,垃圾回收器也不会销毁该环境。
- 当内部函数随后 调用 时,捕获的环境成为它的 父作用域,而不是全局作用域。
4. 解析器
通过词法作用域实现闭包会引入另一个问题:
- 闭包持有对表示当前作用域的 可变 映射的活跃引用。
- 该作用域的捕获视图可能会因同一块中后续代码的执行而改变,而这 不应该发生。
因为 Lox 采用 词法/静态作用域,变量的使用必须在整个程序执行期间始终解析到 相同的声明,即使该变量随后被重新声明或重新定义。
示例
var a = "one";
{
fun showA() {
print a;
}
showA(); // → "one"
var a = "two";
showA(); // Should still print "one", but would print "two" without a resolver
}
a属于 全局作用域,值为"one"。showA创建了一个闭包,捕获了它声明所在的 块/环境。- 第一次调用
showA()时,块中找不到a,于是查找继续到全局作用域 → 打印"one"。 - 在同一块中执行
var a = "two"重新声明a后,可变的作用域导致第二次调用showA()打印"two"——这是一种错误行为。
解析器如何修复
解析器(在解析器之后、解释器之前加入)使用 持久化数据结构:
- 原始数据结构不能被直接篡改。
- 任何修改都会创建一个 新结构,该结构包含原始信息以及此次修改。
这类似于 区块链:每个新块引用前一个块的哈希,同时包含新的变更。
当解析器处理一个块并看到变量被 使用(在其声明之后),它会进行一次 静态遍历:
Scope 0 (function's scope): a is not found
Scope 1 (block scope): a is not found (var a = "two" hasn't been defined yet)
Scope 2 (global scope): a is found
- 因为变量在离最内层作用域 2 步之外被找到,解析器会在该使用位置标注数字 2。
- 在运行时,解释器利用这个跳数直接跳到正确的声明处,避免了可变作用域带来的意外。
5. 接下来会做什么?
我们将继续保持 Classy! 💅
(接下来是类与继承。)
6. 随想
过去几个月里,我有幸目睹了一些最奇妙、最令人困惑的事物。感觉自己仿佛经历了人生的全部光谱,正如莎士比亚的 七个阶段。在某种程度上,这让我变得非常谦卑。
很长一段时间,我害怕放手——放手我的信念、兴趣,甚至一些梦想——因为我担心会失去自我。现在我明白,就像函数一样,拥有一套不可协商的模板是可以的,在此基础上我们仍然可以…
生活不断添加它自己的“implementations”。(我真的必须使用那个类比——请见谅。)
变化和不可预测性不再显得如此令人生畏,也许(大约 0.05 %)它们也有好处。我想 c’est la vie。

