编译过程

Clojure 语言本身是编译到 JVM Bytecode, 而 ClojureScript 则是编译到 JavaScript.

Macros

Clojure(Script) 编译过程大概经历三个阶段:

  • 读取: 经字符串, 将 Macro 进行展开
  • 分析: 基于读取的符号构建 AST
  • 生成: 产生目标输出, 比如编译到 JavaScript

编译过程 - 图1

这个过程对于 Clojure 和 ClojureScript 来说类似, 细节参考 Compilation Pipeline.

比如这样一段 ClojureScript 代码, 参考 Mike 的文章:

  1. (defn f [long-name]
  2. (let [process (fn [foo]
  3. (str "abc" "def"
  4. (+ 1 2 3) foo))]
  5. (process long-name)))

通过 ClojureScript 的编译器会被编译成:

  1. function foo$core$g(long_name) {
  2. var process = function(foo__$1) {
  3. return ["abc", "def",
  4. cljs.core.str.cljs$core$IFn$_invoke$arity$1(1 + 2 + 3),
  5. cljs.core.str.cljs$core$IFn$_invoke$arity$1(foo__$1)
  6. ].join("");
  7. };
  8. return process.call(null, long_name);
  9. }

Google Closure Compiler

对于 JavaScript 而言, 在上线之前仍然需要进行优化, 比如前端常用 Uglify 优化代码体积.
在 cljs 社区, 普遍使用 Google Closure Compiler 进行优化, 可以做一些更深层的优化, 可以得到:

  1. function(a) { return ["abcdef",
  2. cljs.core.str.cljs$core$IFn$_invoke$arity$1(6),
  3. cljs.core.str.cljs$core$IFn$_invoke$arity$1(a)].join("");
  4. }

可以得到代码被进一步简化了, 甚至部分的代码被预先做了计算.

cljs 编译器生成的代码可读性并不是那么好, 在使用有些函数的情况下会更加难读.
cljs 生成的代码本身针对 Google Closure Compiler 优化, 并且严重依赖 Dead Code Elimination 功能来控制体积.

Google Closure Compiler 主要有 4 个编译选项:

  • :none 不做任何优化, 不合并文件
  • :whitespace 去除空白, 合并文件
  • :simple 合并文件, 重命名局部变量
  • :advanced 合并文件, 去除无用代码, 压缩混淆代码

由于 Google Closure Compiler 基于 Java 实现, 所以大部分 cljs 编译过程依赖 JVM.
比如 Lein 和 Boot 就基于 JVM 环境运行, 而 shadow-cljs 会调用系统的 JVM.

另一种办法是把 cljs 的编译器整个编译到 JavaScript, 同时使用 Closure Compiler 的 JavaScript 版本.
这种情况当中使用的 cljs 称为 Self-hosted ClojureScript, 有时也叫 Bootstraped ClojureScript.
比如 Planck 和 Lumo 就用了 Self-hosted ClojureScript, 可以使用 Node.js 来编译 cljs.
使用 Self-hosted cljs 相对而言冷启动较快, 但编译速度较慢, 适合在 REPL 中使用.
不过在 Self-hosted cljs 当中某些语言特性的细节会不同, 比如 Macro 的用法等等.