ClojureScript 编译器核心原理
2026/3/23
ClojureScript 编译器 函数式编程
ClojureScript 编译器核心原理
从源码层面理解 ClojureScript 如何将 Lisp 编译为 JavaScript。
编译流程概览
ClojureScript 的编译流程是经典的三阶段设计:
CLJS 源码 → Reader → AST (analyzer) → JavaScript (compiler)
| 阶段 | 输入 | 输出 | 职责 |
|---|---|---|---|
| Reader | 字符串 | 数据结构 | 词法分析,处理宏 |
| Analyzer | 数据结构 | AST | 语义分析,类型推断 |
| Compiler | AST | JavaScript | 代码生成 |
核心编译器文件:src/main/clojure/cljs/compiler.cljc(约 74KB)
emit* 多方法:编译的分发核心
ClojureScript 使用 Clojure 的**多方法(multimethod)**实现编译分发:
(defmethod emit* :if [...]) ; 条件表达式
(defmethod emit* :fn [...]) ; 函数定义
(defmethod emit* :def [...]) ; 变量定义
(defmethod emit* :invoke [...]) ; 函数调用
(defmethod emit* :let [...]) ; let 绑定
(defmethod emit* :loop [...]) ; loop/recur
(defmethod emit* :recur [...]) ; 尾递归
(defmethod emit* :try [...]) ; 异常处理
(defmethod emit* :throw [...]) ; 抛出异常
(defmethod emit* :new [...]) ; 构造对象
每个 AST 节点类型有对应的 emit* 实现。这种设计让编译器可扩展——添加新节点类型只需定义新的多方法实现。
if 表达式编译
ClojureScript 的 if 需要处理 truthy/falsey 语义:
源码位置
compiler.cljc 第 687-702 行
实现逻辑
(defmethod emit* :if
[{:keys [test then else env unchecked]}]
(let [context (:context env)]
;; 1. 常量折叠优化
(if (and (constant? test) (not unchecked))
(if (truthy? test)
(emit then) ; 条件永远为真,只编译 then
(emit else)) ; 条件永远为假,只编译 else
;; 2. 正常编译
(do
(emit-wrap context
(emits "((cljs.core.truth_(" test ")) ? " then " : " else ")"))))))
生成的 JavaScript
// 表达式上下文(需要返回值)
(cljs.core.truth_(test) ? then : else)
// 语句上下文(不需要返回值)
if(cljs.core.truth_(test)){
then
} else {
else
}
关键点
- 常量折叠:如果 test 是常量,编译时直接选择分支,零运行时开销
- truth_ 包装:Clojure 的 truthy 语义(nil 和 false 为假,其他为真)与 JavaScript 不同,需要包装
- unchecked 优化:如果用户标记
^boolean类型,跳过 truth_ 包装
多元函数编译
ClojureScript 支持同名函数多个 arity(参数个数不同),这是 Lisp 的经典特性。
单一 arity
(defn add [x y] (+ x y))
生成简单的 JavaScript 函数:
var add = function(x, y) {
return (x + y);
};
多 arity
(defn greet
([name] (str "Hello, " name))
([name greeting] (str greeting ", " name)))
生成 switch 分发:
(function() {
var greet = null;
// 1 参数版本
var greet__1 = function(name) {
return ["Hello, ", cljs.core.str(name)].join('');
};
// 2 参数版本
var greet__2 = function(name, greeting) {
return [greeting, ", ", cljs.core.str(name)].join('');
};
// 分发函数
greet = function(name, greeting) {
switch(arguments.length) {
case 1: return greet__1.call(this, name);
case 2: return greet__2.call(this, name, greeting);
}
throw new Error('Invalid arity: ' + arguments.length);
};
// 附加 arity 信息(允许直接调用)
greet.cljs$core$IFn$_invoke$arity$1 = greet__1;
greet.cljs$core$IFn$_invoke$arity$2 = greet__2;
return greet;
})()
关键设计
- 闭包封装:整个结构包在 IIFE 中,避免污染全局
- arity 缓存:每个 arity 版本附加到函数对象上(
cljs$core$IFn$_invoke$arity$N) - 直接调用优化:知道 arity 时可以直接调用特定版本,避免 switch 开销
变参函数编译
ClojureScript 支持变参函数(variadic functions):
(defn sum [x & rest]
(apply + x rest))
生成:
var sum = function(x) {
// 将剩余参数转为 IndexedSeq
var rest = new cljs.core.IndexedSeq(
Array.prototype.slice.call(arguments, 1), // 从第 2 个参数开始
0,
null
);
return cljs.core.apply.cljs$core$IFn$_invoke$arity$3(
cljs.core._PLUS_,
x,
rest
);
};
为什么是 IndexedSeq?
Clojure 的序列是惰性的,IndexedSeq 是对 JavaScript 数组的惰性包装。这保持了 Clojure 的语义,同时避免了不必要的数组复制。
编译上下文
编译器根据上下文生成不同风格的代码:
| 上下文 | 含义 | 生成的代码 |
|---|---|---|
:expr | 表达式 | 需要返回值,用三元运算符等 |
:statement | 语句 | 不需要返回值,用 if/else 语句 |
:return | 返回 | 需要 return 语句 |
示例
(if test then else)
// :expr 上下文
(cljs.core.truth_(test) ? then : else)
// :statement 上下文
if(cljs.core.truth_(test)){ then } else { else }
// :return 上下文
return (cljs.core.truth_(test) ? then : else);
辅助函数
munge:符号转换
Clojure 符号允许 - 和 .,但 JavaScript 不允许。munge 函数处理转换:
| Clojure 符号 | JavaScript 标识符 |
|---|---|
my-fn | my_fn |
str.length | str.length |
class | class$(保留字加 $) |
emit-wrap:上下文包装
(defn emit-wrap [context body]
(case context
:expr (emits "(" body ")")
:return (emits "return " body ";")
:statement body))
emits / emitln
(emits "a" "b" "c") ; 输出 "abc"(不换行)
(emitln "a" "b" "c") ; 输出 "abc\n"(换行)
loop/recur:零开销尾递归
ClojureScript 的 loop/recur 编译为真正的循环,无函数调用开销:
(loop [x 0]
(if (< x 10)
(recur (inc x))
x))
生成:
(function() {
while(true) {
if((x < 10)) {
var G__x = (x + 1);
x = G__x;
continue;
} else {
return x;
}
break;
}
})()
关键点
- while(true):无限循环
- continue:
recur变成continue,更新绑定后继续 - break:出口条件触发时跳出
- 零开销:无函数调用,纯循环
let 编译
let 编译为 IIFE 或直接赋值,取决于上下文:
(let [x 1
y 2]
(+ x y))
// 表达式上下文
(function() {
var x = 1;
var y = 2;
return (x + y);
})()
// 语句上下文
var x = 1;
var y = 2;
(x + y);
实用技巧
1. 查看编译输出
(require '[cljs.compiler :as comp])
(comp/emit '(if true 1 2))
;; => "((cljs.core.truth_(true))?1:2)"
2. 理解命名规范
G__前缀:编译器生成的临时变量$后缀:保留字转义_替换:连字符转义
3. 性能提示
- 标记
^boolean类型可以跳过 truth_ 包装 - 单 arity 函数比多 arity 更快
loop/recur比递归函数调用更高效
小结
ClojureScript 编译器的核心设计:
- 多方法分发 - 可扩展的 AST 处理
- 上下文感知 - 根据上下文生成不同代码
- 零开销抽象 -
loop/recur编译为循环 - 语义保持 - truthy/falsey、多元函数等语义在 JavaScript 中正确实现
理解编译器原理有助于:
- 写出更高效的 ClojureScript 代码
- 调试编译问题
- 贡献编译器改进
参考资料
- ClojureScript 源码
- compiler.cljc - 核心编译器
- analyzer.cljc - AST 分析器
- ClojureScript 官方文档