Scala.js 把 Scala 编译成 JavaScript 而非字节码,让前后端共享同一门语言、同一套领域模型。代价是要面对两套类型系统之间的"翻译损耗"。本节讲它的编译路线、与 JS 的互操作面、以及何时值得选它。
Scala 编译器前端把源码产出为中间表示(SJSIR),Scala.js 后端再发射 JavaScript。源码不变,后端换轨:
| JVM 路线 | Scala.js 路线 | |
|---|---|---|
| 产物 | JVM 字节码 | 优化的 JS(支持死码消除) |
| 库生态 | Maven 中央仓库 | 兼容库 + JS 生态 facade |
| 运行 | JVM | 浏览器与 Node |
| 典型用途 | 后端、大数据 | 前端、全栈共享模型 |
真正的高价值场景是领域模型与校验逻辑前后端共享:订单表单的校验规则写一遍,浏览器里即时提示,服务端再跑同一份代码兜底——校验漂移(前后端规则不一致)这类 bug 从结构上消失。
JS 是动态类型,Scala 是静态的,桥靠 facade(外观类型):
// 为 JS 库写类型外观(示意) @js.native @JSGlobal("localStorage") object LocalStorage extends js.Object: def getItem(key: String): String = js.native def setItem(key: String, value: String): Unit = js.native // Scala 侧静态调用,编译器检查键值类型 LocalStorage.setItem("token", jwt)
动态类型侧还有逃生门 js.Dynamic.global,但每个绕过类型系统的调用都在积累债务。Laminar、Tyrian 等纯 Scala 前端框架则让你完全写 Scala、零 facade 需求,代价是离开 React/Vue 生态。
所以我的判断线:已有 Scala 后端且领域逻辑密集的团队,Scala.js 收益明确(共享模型是硬收益);以前端为主、招 TS 工程师的项目,没有理由翻越这道山脊。

背景:想在网页里复用 Scala 写的温度逻辑,而不是用 JavaScript 重写一遍。三步走:
// build.sbt 里启用插件后 import org.scalajs.dom.document @main def front(): Unit = val input = document.getElementById("c").asInstanceOf[org.scalajs.dom.HTMLInputElement] val out = document.getElementById("out") document.getElementById("btn") .asInstanceOf[org.scalajs.dom.HTMLButtonElement] .onclick = _ => val c = input.value.toDoubleOption.getOrElse(0.0) out.textContent = s"${c}C = ${c * 9 / 5 + 32}F" // 与后端同一份核心逻辑
操作:核心转换函数不动,前端只加一层 DOM 绑定;结果:sbt fastLinkJS 产出 JS 包,页面直接运行。解读:Scala.js 把 Scala 编译成等价 JS 而非跑在虚拟机里,产物体积可控、可与 npm 生态互操作;业务规则一份代码两端复用,是它最大的卖点。变式:需要强类型前端选 Laminar 这类纯 Scala UI 框架;需要复用 React 生态则走Slinky。
Scala.js 目标是浏览器与 Node,产物是 JavaScript;Scala Native 目标是免 JVM 的原生可执行文件,产物经 LLVM 编译。两者共享"同一份业务代码、多个编译目标"的理念,选型只看部署环境:网页选前者,边缘设备与命令行小工具选后者。
import scala.scalajs.js @js.native @JSGlobal("Math") object JSMath extends js.Object: def max(a: Double, b: Double): Double = js.native @js.native trait ConsoleLike extends js.Object: def log(msg: String): Unit = js.native val m2: Double = JSMath.max(3.0, 7.0) // 直接吃 JS 全局对象 val dyn = scala.scalajs.js.Dynamic.global dyn.console.log("dynamic 通道") // 动态通道:无检查,慎用
@js.native 声明"这是对岸已有的东西",编译器不做实现只做类型对接;Dynamic 则完全绕开类型检查。纪律与 5.2 节的结构类型一致:能用静态门面就用静态,动态通道只留给无法预知形状的场景(如读任意 JSON),并且一出边界立刻转成强类型模型,别让 Any 长驱直入业务层。
体积控制是上生产前的必修课:fullLinkJS 会做死代码消除,只留真正用到的标准库分支,配合模块拆分与 sourcemap 管理,一个中型前端的 JS 产物可以压到几百 KB——先量后砍,别凭印象否决。