QuickJS 嵌入式 JavaScript 引擎完全教程 / 03 - 基本使用
基本使用
3.1 REPL 交互模式
REPL(Read-Eval-Print Loop,读取-求值-打印 循环)是学习和调试 JavaScript 代码的最直接方式。
启动 REPL
# 启动交互式 REPL
./qjs
# 启动时加载标准库到全局
./qjs --std
启动后会看到:
QuickJS - Type "\h" for help
qjs >
REPL 命令
| 命令 | 说明 |
|---|---|
\h | 显示帮助信息 |
\q | 退出 REPL |
\x | 以十六进制显示结果 |
\d | 以十进制显示结果 |
\t | 以 typeof 显示结果 |
REPL 使用技巧
qjs > # 1. 多行输入
qjs > function greet(name) {
...... return `Hello, ${name}!`;
...... }
undefined
qjs > greet("QuickJS")
"Hello, QuickJS!"
qjs > # 2. 使用 Tab 补全
qjs > Math.P # 按 Tab
Math.PI Math.pow
qjs > # 3. 访问上一次结果(注意:QuickJS REPL 没有 _ 变量,需自己存储)
qjs > let last = 1 + 2
undefined
qjs > last * 2
6
qjs > # 4. 执行异步代码
qjs > let p = new Promise(resolve => resolve(42))
undefined
qjs > p.then(v => console.log(v))
42
[object Promise]
qjs > # 5. 加载模块(需要 --std 或手动 import)
qjs > let std = await import("std")
undefined
qjs > std.printf("formatted: %d\n", 42)
formatted: 42
14
3.2 脚本执行
基本脚本
// hello.js — 第一个 QuickJS 脚本
console.log("Hello from QuickJS!");
console.log("Arguments:", scriptArgs);
// 日期处理
const now = new Date();
console.log("Current time:", now.toISOString());
// 数组操作
const data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
const sorted = [...data].sort((a, b) => a - b);
console.log("Sorted:", sorted);
console.log("Sum:", data.reduce((a, b) => a + b, 0));
console.log("Average:", (data.reduce((a, b) => a + b, 0) / data.length).toFixed(2));
./qjs hello.js arg1 arg2
脚本模式 vs 模块模式
QuickJS 支持两种脚本加载模式:
# 脚本模式(默认)— 代码在全局作用域执行
./qjs --script script.js
# 模块模式 — 代码在模块作用域执行,支持 import/export
./qjs --module app.mjs
./qjs -m app.mjs
# 自动检测:.mjs 后缀或包含 import/export 语句时自动使用模块模式
脚本模式 vs 模块模式的区别:
| 特性 | 脚本模式 | 模块模式 |
|---|---|---|
| 作用域 | 全局 | 模块私有 |
this 指向 | 全局对象 | undefined |
import / export | ❌ | ✅ |
var 声明 | 挂载到全局 | 模块私有 |
顶层 await | ❌ | ✅ |
| 严格模式 | 需手动启用 | 默认启用 |
错误处理
// error_handling.js — 在 QuickJS 中处理错误
try {
// 故意触发错误
undefined_function();
} catch (e) {
console.log("Caught error:", e.message);
console.log("Stack trace:", e.stack);
}
// 捕获语法错误(需要在外部 try-catch 中处理 eval)
try {
eval("function {invalid syntax");
} catch (e) {
console.log("Syntax error caught:", e.message);
}
// 未捕获的 Promise 拒绝
// QuickJS 会打印警告
Promise.reject(new Error("unhandled rejection"));
3.3 模块系统
QuickJS 完整支持 ES Module(ESM),包括命名导出、默认导出、重导出等。
基本导入导出
// math_utils.js — 工具模块
// 命名导出
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export const PI = 3.141592653589793;
// 默认导出
export default class Calculator {
#history = [];
calculate(expr) {
const result = eval(expr);
this.#history.push({ expr, result });
return result;
}
getHistory() {
return [...this.#history];
}
}
// app.js — 主模块
// 默认导入
import Calculator from "./math_utils.js";
// 命名导入
import { add, multiply, PI } from "./math_utils.js";
// 别名导入
import { add as addition } from "./math_utils.js";
// 全部导入
import * as math from "./math_utils.js";
// 使用
const calc = new Calculator();
console.log(calc.calculate("2 + 3")); // 5
console.log(add(10, 20)); // 30
console.log(multiply(PI, 2)); // 6.28318...
console.log(addition(5, 7)); // 12
console.log(math.PI); // 3.14159...
# 以模块模式运行
./qjs -m app.js
重导出
// index.js — 模块聚合
export { add, multiply } from "./math_utils.js";
export { default as Calculator } from "./math_utils.js";
// 重导出全部
export * from "./math_utils.js";
3.4 标准模块
QuickJS 提供了两个内置模块:std 和 os,以及一个 os 子模块用于 Worker。
std 模块
import * as std from "std";
// 文件操作
const content = std.loadFile("data.txt");
if (content !== null) {
print("File content:", content);
} else {
print("Failed to read file, errno:", std.errno);
}
// 使用 FILE* API 进行更精细的文件操作
const file = std.open("output.txt", "w");
if (file) {
file.puts("Line 1\n");
file.puts("Line 2\n");
file.printf("Formatted: %d, %s\n", 42, "hello");
file.close();
}
// 标准流
std.out.puts("stdout output\n");
std.err.puts("stderr output\n");
// 临时文件
const tmpName = "/tmp/quickjs_test.txt";
std.saveFile(tmpName, "temporary data");
// 环境变量
print("HOME:", std.getenv("HOME"));
// 评估文件内容
const result = std.evalScript("1 + 2");
print("Eval result:", result);
// 获取最后一个错误信息
print("Error string:", std.strerror(std.errno));
os 模块
import * as os from "os";
// 系统信息
console.log("Platform:", os.platform); // "linux", "darwin", "win32"
console.log("Architecture:", os.arch); // "x64", "arm", "arm64"
// 时间
console.log("Timestamp (ms):", Date.now());
console.log("High-res timer:", os.now()); // 毫秒级高精度时间
// 定时器
const timer = os.setTimeout(() => {
print("Timer fired after 1 second!");
}, 1000);
// 清除定时器
// os.clearTimeout(timer);
// 周期定时器
let count = 0;
const interval = os.setInterval(() => {
count++;
print(`Interval tick ${count}`);
if (count >= 3) {
os.clearInterval(interval);
print("Interval cleared");
}
}, 500);
// 进程相关
print("PID:", os.pid);
// 信号处理(Unix)
if (os.platform !== "win32") {
os.signal(os.SIGINT, () => {
print("Caught SIGINT, exiting...");
std.exit(0);
});
}
// 文件系统操作
os.mkdir("/tmp/quickjs_test_dir", 0o755);
os.rename("/tmp/quickjs_test_dir", "/tmp/quickjs_renamed");
os.remove("/tmp/quickjs_renamed");
// 获取文件状态
const stat = os.stat("/etc/hostname");
if (stat) {
print("File size:", stat.size);
print("Is directory:", !!(stat.mode & os.S_IFDIR));
}
// 读取目录
const entries = os.readdir("/tmp");
print("Entries:", entries);
// 睡眠(阻塞)
print("Sleeping 100ms...");
os.sleep(100);
print("Awake!");
综合示例:文件处理管道
// file_pipeline.js — 使用 std 和 os 处理文件
import * as std from "std";
import * as os from "os";
function processLogFile(inputPath, outputPath) {
const input = std.loadFile(inputPath);
if (input === null) {
print(`Error: Cannot read ${inputPath}`);
return false;
}
const lines = input.split("\n");
const errors = lines
.filter(line => line.includes("ERROR"))
.map(line => {
const timestamp = line.substring(0, 24);
const message = line.substring(line.indexOf("ERROR") + 6);
return `[${timestamp}] ${message.trim()}`;
});
const output = errors.join("\n");
const result = std.saveFile(outputPath, output);
if (result < 0) {
print(`Error: Cannot write ${outputPath}`);
return false;
}
print(`Processed ${lines.length} lines, found ${errors.length} errors`);
return true;
}
// 主流程
const startTime = os.now();
processLogFile("app.log", "errors.txt");
const elapsed = os.now() - startTime;
print(`Completed in ${elapsed}ms`);
3.5 字节码编译与执行
字节码(Bytecode)编译可以将 JavaScript 源码预编译为二进制格式,提高加载速度并隐藏源码。
使用 qjsc 编译
# 编译为字节码文件
./qjsc -o myapp.qjsc myapp.js
# 编译为模块字节码
./qjsc -o mymodule.qjsc -m mymodule.js
# 执行字节码
./qjs myapp.qjsc
# 生成 C 嵌入文件
./qjsc -c -o bytecode.h -m myapp.js
在代码中编译字节码
// compile_bytecode.js — 使用 std 模块编译字节码
import * as std from "std";
// 注意:在 QuickJS 中,字节码编译主要通过 qjsc 命令行工具完成
// 这里展示的是使用 std.evalScript 执行预编译的字节码
// 保存字节码的典型流程:
// 1. 使用 qjsc -c -o bytecode.h script.js 生成字节数组
// 2. 在 C 程序中 #include "bytecode.h"
// 3. 使用 JS_ReadObject() 加载字节码
// 4. 使用 JS_EvalFunction() 执行
print("Bytecode compilation is primarily done via qjsc CLI tool.");
print("See Chapter 04 for C API bytecode loading.");
字节码的优势与限制
| 方面 | 说明 |
|---|---|
| 加载速度 | 跳过解析阶段,直接加载字节码 |
| 文件大小 | 字节码通常比源码小 30-50% |
| 源码保护 | 字节码不包含原始源码(但可反编译) |
| 版本绑定 | 字节码与 QuickJS 版本绑定,升级后需重新编译 |
| 可读性 | 二进制格式,不可直接阅读 |
3.6 脚本参数传递
通过 scriptArgs 获取参数
// args_demo.js
console.log("Script name:", scriptArgs[0]);
console.log("All arguments:", scriptArgs.slice(1));
// 简单的参数解析器
function parseArgs(args) {
const result = { files: [], options: {} };
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith("--")) {
const [key, value] = arg.slice(2).split("=");
result.options[key] = value || true;
} else if (arg.startsWith("-")) {
result.options[arg.slice(1)] = true;
} else {
result.files.push(arg);
}
}
return result;
}
const args = parseArgs(scriptArgs.slice(1));
console.log("Parsed:", JSON.stringify(args, null, 2));
./qjs args_demo.js --output=result.txt -v file1.js file2.js
输出:
Script name: args_demo.js
All arguments: ["--output=result.txt", "-v", "file1.js", "file2.js"]
Parsed: {
"files": ["file1.js", "file2.js"],
"options": {
"output": "result.txt",
"v": true
}
}
3.7 定时器与事件循环
QuickJS 提供了基本的定时器功能,但没有内置完整的事件循环(Event Loop)。
定时器使用
// timers.js
import * as os from "os";
print("Start");
os.setTimeout(() => {
print("Timeout 1: 100ms later");
}, 100);
os.setTimeout(() => {
print("Timeout 2: 50ms later");
}, 50);
// 注意:QuickJS 没有自动事件循环
// 定时器回调不会自动执行,需要手动驱动循环
// 在 qjs 中,exit 后的定时器可能不会执行
print("End (synchronous)");
重要提示: QuickJS 的
setTimeout/setInterval是由quickjs-libc.c提供的,并且在qjs命令行工具中,它会自动运行一个简单的事件循环直到所有定时器完成。但在嵌入式 C 代码中,你需要自己管理事件循环。
3.8 调试技巧
控制台输出
// 调试输出方法
console.log("Standard log");
console.warn("Warning message");
console.error("Error message");
// 格式化输出(QuickJS 支持简单格式化)
console.log("Number: %d, String: %s", 42, "hello");
// 打印对象
const obj = { name: "test", values: [1, 2, 3] };
console.log("Object:", JSON.stringify(obj, null, 2));
// 打印调用栈
function debugHere() {
try { throw new Error(); } catch(e) {
console.log("Stack:", e.stack);
}
}
debugHere();
性能测量
// perf_test.js
function measureTime(label, fn, iterations = 1000) {
const start = Date.now();
for (let i = 0; i < iterations; i++) {
fn();
}
const elapsed = Date.now() - start;
console.log(`${label}: ${elapsed}ms (${iterations} iterations, ${(elapsed/iterations).toFixed(3)}ms each)`);
}
measureTime("Array.push", () => {
const arr = [];
for (let i = 0; i < 1000; i++) arr.push(i);
});
measureTime("String concat", () => {
let s = "";
for (let i = 0; i < 1000; i++) s += "a";
});
measureTime("Array.join", () => {
const parts = [];
for (let i = 0; i < 1000; i++) parts.push("a");
parts.join("");
});
3.9 常见陷阱与注意事项
全局变量污染
// ❌ 不好:变量泄漏到全局
function bad() {
myVar = 42; // 缺少 let/const/var
}
// ✅ 好:使用严格模式或模块
"use strict";
function good() {
const myVar = 42;
}
数值精度
// QuickJS 使用双精度浮点数,与所有 JS 引擎相同
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// 使用 BigInt 处理大整数
const big = 9007199254740993n; // 超过 Number.MAX_SAFE_INTEGER
console.log(big); // 9007199254740993n
// 使用 BigDecimal 处理精确十进制
const price = 1.10d;
console.log(price * 3d); // 3.30d
同步阻塞
// 注意:os.sleep() 会阻塞整个线程
// 在嵌入式环境中,这可能会阻止其他任务执行
import * as os from "os";
// ❌ 不好:长时间阻塞
os.sleep(10000); // 10 秒
// ✅ 好:使用短时间的 sleep 或事件驱动
for (let i = 0; i < 100; i++) {
os.sleep(100); // 每次只休眠 100ms
// 可以在这里检查中断条件
}
3.10 本章小结
| 要点 | 说明 |
|---|---|
| REPL | 交互式 JavaScript 执行环境,使用 \h 查看帮助 |
| 脚本模式 | 默认模式,代码在全局作用域执行 |
| 模块模式 | 使用 -m 启用,支持 import/export |
| std 模块 | 文件操作、环境变量、标准流 |
| os 模块 | 系统信息、定时器、文件系统、信号 |
| 字节码 | 使用 qjsc 预编译,提高加载速度 |
| 调试 | 使用 console.log 和 Date.now() 测量性能 |