QuickJS 嵌入式 JavaScript 引擎完全教程 / 01 - QuickJS 概述
QuickJS 概述
1.1 QuickJS 简介
QuickJS 是由法国程序员 Fabrice Bellard 于 2019 年 7 月发布的轻量级 JavaScript 引擎。Bellard 以开发 FFmpeg、QEMU、TinyCC 等传奇项目闻名,QuickJS 体现了他一贯的工程哲学:用最少的代码实现最多的功能,且不依赖外部库。
核心特性一览
| 特性 | 说明 |
|---|---|
| 完整 ES2023 支持 | 包括 Proxy、Symbol、Promise、async/await、Module 等 |
| 零外部依赖 | 仅需标准 C 库(libc) |
| 极小体积 | x86_64 上约 210KB 静态编译 |
| 字节码编译 | 支持预编译为字节码,加速加载 |
| 垃圾回收 | 引用计数 + 循环检测 |
| 数学大数 | 内置 BigInt 和 BigDecimal 支持 |
| 262 测试套件 | 通过率超过 95% |
// QuickJS 最小示例:在 C 中执行 JavaScript
#include "quickjs.h"
#include <stdio.h>
int main() {
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
const char *code = "1 + 2";
JSValue result = JS_Eval(ctx, code, strlen(code), "<input>", 0);
if (!JS_IsException(result)) {
printf("Result: %d\n", JS_VALUE_GET_INT(result));
}
JS_FreeValue(ctx, result);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
1.2 ES2023 特性支持
QuickJS 几乎完整实现了 ECMAScript 2023 规范。以下是主要特性的支持情况:
语言特性
| ES 特性 | QuickJS | 说明 |
|---|---|---|
let / const | ✅ | 块级作用域 |
| 箭头函数 (Arrow Function) | ✅ | () => {} |
| 模板字符串 (Template Literal) | ✅ | `hello ${name}` |
| 解构赋值 (Destructuring) | ✅ | { a, b } = obj |
| 展开运算符 (Spread) | ✅ | ...arr |
class 语法 | ✅ | 包括私有字段 #field |
Promise | ✅ | 完整实现 |
async / await | ✅ | 异步编程 |
Symbol | ✅ | 包括 Well-known Symbols |
Proxy / Reflect | ✅ | 元编程 |
Map / Set / WeakMap / WeakSet | ✅ | 集合类型 |
Iterator / Generator | ✅ | function* / yield |
for...of | ✅ | 迭代协议 |
import / export | ✅ | ES Module |
BigInt | ✅ | 任意精度整数 |
BigDecimal | ✅ | 任意精度十进制(Stage 3 提案) |
?? (Nullish Coalescing) | ✅ | 空值合并 |
?. (Optional Chaining) | ✅ | 可选链 |
globalThis | ✅ | 全局对象 |
Array.prototype.at() | ✅ | 负索引访问 |
Object.hasOwn() | ✅ | ES2022 新增 |
Top-level await | ✅ | 模块顶层 await |
RegExp 命名捕获组 | ✅ | (?<name>...) |
Array.prototype.findLast() | ✅ | ES2023 新增 |
Array.prototype.findLastIndex() | ✅ | ES2023 新增 |
Object.groupBy() / Map.groupBy() | ⚠️ | ES2024 提案,未完全支持 |
Temporal | ❌ | Stage 3 提案,未实现 |
内置对象
// QuickJS 支持的内置对象示例
// 1. Promise 与 async/await
async function fetchData() {
const result = await Promise.resolve(42);
return result * 2;
}
// 2. Proxy
const handler = {
get(target, prop) {
console.log(`Accessing ${String(prop)}`);
return target[prop];
}
};
const proxy = new Proxy({ x: 1, y: 2 }, handler);
console.log(proxy.x); // 输出: Accessing x \n 1
// 3. Generator
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
console.log(
Array.from({ length: 10 }, () => fib.next().value)
); // [0,1,1,2,3,5,8,13,21,34]
// 4. BigInt
const big = 9007199254740993n;
console.log(big + 1n); // 9007199254740994n
// 5. ES2023 Array 方法
const arr = [1, 2, 3, 4, 5];
console.log(arr.findLast(x => x % 2 === 0)); // 4
console.log(arr.findLastIndex(x => x % 2 === 0)); // 3
QuickJS 独有的扩展
QuickJS 还提供了一些规范之外的实用扩展:
// 1. console.log(非标准,但广泛使用)
console.log("hello", 42, [1,2,3]);
// 2. print 函数(QuickJS 特有)
print("QuickJS says hello!");
// 3. 脚本参数(通过 scriptArgs)
// 运行: qjs script.js arg1 arg2
scriptArgs.forEach(arg => print(arg));
// 4. 文件读写(需要 os 和 std 模块)
import * as std from "std";
import * as os from "os";
const content = std.loadFile("data.txt");
if (content !== null) {
print(content);
}
// 5. Worker(Web Worker 兼容 API)
const w = new Worker("worker.js");
w.postMessage({ data: 42 });
1.3 QuickJS 与 V8 的对比
V8 是 Google 开发的高性能 JavaScript 引擎,用于 Chrome 浏览器和 Node.js。两者的设计目标有本质差异。
架构对比
┌──────────────────────────────────────────────────────────┐
│ V8 架构 │
├──────────────────────────────────────────────────────────┤
│ 源代码 → Parser → AST → Ignition(字节码解释器) │
│ ↓ │
│ Sparkplug(基线编译器) │
│ ↓ │
│ Maglev(中层优化编译器) │
│ ↓ │
│ TurboFan(优化 JIT 编译器) │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ QuickJS 架构 │
├──────────────────────────────────────────────────────────┤
│ 源代码 → Parser → AST → 字节码编译器 → 字节码 │
│ ↓ │
│ 解释器循环执行 │
│ (可选: 预编译为二进制字节码) │
└──────────────────────────────────────────────────────────┘
详细对比表
| 维度 | QuickJS | V8 |
|---|---|---|
| 编译策略 | 解释执行(字节码) | JIT 多层编译 |
| 执行速度 | 较慢(约 V8 的 1/20-1/50) | 极快 |
| 启动速度 | 极快 | 较慢(JIT 预热) |
| 内存占用 | 极低(<1MB) | 高(10MB+) |
| 二进制体积 | ~210KB | ~10MB+ |
| 外部依赖 | 无 | ICU、zlib 等 |
| 适合场景 | 嵌入式、轻量脚本 | 桌面/服务端高性能 |
| GC 策略 | 引用计数 + 循环检测 | 分代标记-清除 |
| ES Module | ✅ | ✅ |
| WebAssembly | ❌(部分实验) | ✅ |
| 调试接口 | 基础 | 完整(DevTools) |
| 平台支持 | 几乎所有 C 编译器支持的平台 | 主流桌面/移动 |
代码性能对比
// fibonacci.js — 典型的递归性能测试
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
const start = Date.now();
const result = fib(35);
const elapsed = Date.now() - start;
console.log(`fib(35) = ${result}, time: ${elapsed}ms`);
典型运行时间对比(fib(35),单次运行):
| 引擎 | 执行时间 | 备注 |
|---|---|---|
| QuickJS | ~3500ms | 解释执行 |
| V8 (Node.js) | ~80ms | JIT 优化后 |
| V8 (首次运行) | ~200ms | JIT 预热前 |
| Duktape | ~8000ms | ES5.1 引擎 |
| Lua | ~1500ms | LuaJIT ~30ms |
注意: QuickJS 的设计目标不是与 V8 竞争执行速度。在嵌入式场景中,启动速度、内存占用和二进制体积往往比运行速度更重要。
1.4 QuickJS 与 Duktape 的对比
Duktape 是另一个流行的嵌入式 JavaScript 引擎,两者经常被放在一起比较。
对比表
| 维度 | QuickJS | Duktape |
|---|---|---|
| ES 规范版本 | ES2023(完整) | ES5.1(几乎完整) |
| Promise | ✅ | ❌ |
| async/await | ✅ | ❌ |
| ES Module | ✅ | ❌ |
| class | ✅ | ❌(ES5 模拟) |
| Symbol | ✅ | ❌ |
| Proxy | ✅ | ❌ |
| Generator | ✅ | ❌ |
| BigInt | ✅ | ❌ |
| 二进制体积 | ~210KB | ~300KB |
| 性能 | 较快 | 较慢 |
| 维护状态 | 活跃 | 低活跃(2022年后少更新) |
| 文档质量 | 中等 | 优秀 |
| 社区生态 | 增长中 | 成熟 |
选型建议
需要现代 JavaScript 特性?
├── 是 → QuickJS
│ ├── 需要 async/await? → QuickJS
│ ├── 需要 ES Module? → QuickJS
│ └── 需要 class? → QuickJS
└── 否(仅 ES5.1 即可)
├── 已有 Duktape 项目? → 继续使用 Duktape
└── 新项目? → 推荐 QuickJS(更好的未来兼容性)
1.5 适用场景详解
场景 1:嵌入式设备脚本引擎
在 MCU(如 STM32、ESP32)上运行 JavaScript 配置脚本或业务逻辑。
// 在嵌入式设备上执行传感器数据处理脚本
const char *process_script = R"JS(
function processData(rawValues) {
const filtered = rawValues
.filter(v => v > 0 && v < 4096)
.map(v => (v / 4096) * 3.3); // 转换为电压值
const avg = filtered.reduce((a, b) => a + b, 0) / filtered.length;
return {
voltage: Math.round(avg * 1000) / 1000,
count: filtered.length,
status: avg > 2.5 ? 'HIGH' : 'NORMAL'
};
}
)JS";
场景 2:游戏脚本引擎
// 游戏 NPC 行为脚本
class NPCBehavior {
constructor(npc) {
this.npc = npc;
this.state = 'idle';
this.patrolPoints = [];
this.currentTarget = 0;
}
update(dt) {
switch (this.state) {
case 'idle':
this.idleBehavior(dt);
break;
case 'patrol':
this.patrolBehavior(dt);
break;
case 'chase':
this.chaseBehavior(dt);
break;
}
}
idleBehavior(dt) {
// 检测玩家距离
const dist = this.npc.distanceToPlayer();
if (dist < 5.0) {
this.state = 'chase';
} else if (this.npc.idleTime > 3.0) {
this.state = 'patrol';
}
}
patrolBehavior(dt) {
const target = this.patrolPoints[this.currentTarget];
this.npc.moveToward(target, dt);
if (this.npc.distanceTo(target) < 0.5) {
this.currentTarget =
(this.currentTarget + 1) % this.patrolPoints.length;
}
}
}
场景 3:安全沙箱
// 用户提交的代码在沙箱中执行
// 限制:不能访问文件系统、网络、进程等
function untrustedUserCode(data) {
// 用户可以自由使用 JavaScript 语法
const result = data
.filter(item => item.active)
.map(item => ({
id: item.id,
score: item.value * 1.5,
label: `${item.name} (${item.value})`
}))
.sort((a, b) => b.score - a.score);
return result.slice(0, 10);
}
场景 4:配置语言
// 用 JavaScript 替代 JSON/TOML 作为配置语言
// 优势:支持注释、计算、条件逻辑
export default {
server: {
host: process.env.HOST || '0.0.0.0',
port: parseInt(process.env.PORT) || 8080,
workers: Math.max(1, os.cpus().length - 1),
},
database: {
url: 'postgres://localhost/mydb',
pool: { min: 2, max: 10 },
},
features: {
// 支持注释!
enableBeta: process.env.NODE_ENV === 'development',
cacheTimeout: 60 * 60 * 24, // 可以计算
}
};
1.6 QuickJS 的限制
在选择 QuickJS 之前,需要了解它的设计限制:
不适合的场景
| 场景 | 原因 | 推荐替代 |
|---|---|---|
| 高并发 Web 服务 | 无 JIT,计算密集型任务慢 | Node.js (V8)、Deno |
| 大型前端项目 | 无 DOM API、无浏览器生态 | 浏览器内建引擎 |
| 需要原生 WebAssembly | 仅部分实验性支持 | V8、SpiderMonkey |
| 需要完整调试工具 | 调试支持有限 | V8 (DevTools) |
已知的规范兼容性缺口
// 以下在 QuickJS 中不可用或行为可能不同
// 1. Temporal API(未实现)
// const now = Temporal.Now.plainDateTimeISO(); // ❌
// 2. Atomics / SharedArrayBuffer(未实现)
// Atomics.store(buffer, 0, 42); // ❌
// 3. WebAssembly(实验性,需编译时启用)
// const module = new WebAssembly.Module(bytes); // ⚠️
// 4. FinalizationRegistry(未实现)
// const registry = new FinalizationRegistry(...); // ❌
// 5. 性能敏感的正则表达式
// QuickJS 的正则引擎是回溯式的,某些模式可能慢于 V8
1.7 发展历史与社区
里程碑
| 日期 | 事件 |
|---|---|
| 2019-07 | QuickJS 首次发布,支持 ES2019 |
| 2019-12 | 添加 BigInt、BigDecimal、ES Module 支持 |
| 2020-03 | 通过 Test262 75% 测试 |
| 2020-09 | 性能优化,添加快速数组模式 |
| 2021-04 | 支持 ES2021,通过 Test262 90%+ |
| 2022-09 | 支持 ES2022,私有类字段 |
| 2023-03 | 支持 ES2023,Array.findLast |
| 2024-01 | 社区维护的 GitHub 版本活跃更新 |
| 2024-06 | 性能持续改进,新增尾调用优化 |
| 2025-01 | 社区贡献的内存优化和平台扩展 |
相关项目生态
| 项目 | 说明 |
|---|---|
| quickjs-emscripten | 在浏览器/WebAssembly 中运行 QuickJS |
| go-quickjs | Go 语言绑定 |
| quickjs-rs | Rust 语言绑定 |
| QuickJS-Android | Android 平台集成 |
| quickjspp | C++ 封装库 |
| liquidsoap | 使用 QuickJS 作为脚本引擎 |
1.8 本章小结
| 要点 | 说明 |
|---|---|
| QuickJS 是什么 | 由 Fabrice Bellard 开发的小型 ES2023 JavaScript 引擎 |
| 核心优势 | 极小体积、零依赖、完整 ES2023 支持 |
| 与 V8 区别 | 不追求执行速度,专注于嵌入和轻量 |
| 与 Duktape 区别 | 支持现代 JS 特性,更活跃的维护 |
| 最佳场景 | 嵌入式、游戏脚本、安全沙箱、配置语言 |