JavaScript 标签模板:raw 保留了反斜杠,插值为什么仍然会执行
给字符串前面加上 String.raw,路径里的反斜杠就能保留下来。于是有人进一步推断:模板中的所有东西都变成了原样文本,美元符号花括号里的调用也不会发生。这个推断会让配置生成器出现意外副作用,因为“原始片段”与“插值求值”属于两个步骤。
标签模板调用的第一个参数是一组静态片段,后面的参数才是表达式产生的值。标签函数可以返回字符串,也可以返回普通对象。我们先让它返回观察记录,不做拼接,就能看清这几类输入各自经历了什么。
把三份输入分别留住
将代码保存为 demo.mjs,用 node demo.mjs 运行。本文在 Node.js v24.19.0 验证。所有数据都在内存中,计数器只用于证明插值函数是否被调用;这里的“原始”指源码静态片段的表示,不是整个文件的原始字节。
AI概念示意图:表现本文的抽象关系,并非软件截图或真实运行结果。
import assert from 'node:assert/strict';
let calls = 0;
const value = () => { calls++; return 7; };
function inspect(parts, ...values) {
return { cooked: [...parts], raw: [...parts.raw], values };
}
const seen = inspect`A\n${value()}B`;
assert.deepEqual(seen.cooked, ['A\n', 'B']);
assert.deepEqual(seen.raw, ['A\\n', 'B']);
assert.deepEqual(seen.values, [7]);
assert.equal(calls, 1);
const joined = String.raw`A\n${value()}B`;
assert.equal(joined, 'A\\n7B');
assert.equal(calls, 2);
const inserted = String.raw`${'X\nY'}`;
assert.equal(inserted, 'X\nY');
function badEscape(parts) {
assert.equal(parts[0], undefined);
assert.equal(parts.raw[0], '\\unicode');
return 'checked';
}
assert.equal(badEscape`\unicode`, 'checked');
console.log(JSON.stringify(seen));
console.log(JSON.stringify(joined), calls);
console.log('tagged-template checks passed');raw 改变片段的解释,不取消插值
第一组断言中,cooked 的首段包含真实换行,raw 的首段包含反斜杠与字母 n。数值七则留在 values 数组,类型仍是数字。说明标签先收到拆开的材料,如何组合由函数决定,并不是先拼成一个完整字符串再交给标签分析。
第二次调用换成 String.raw,静态片段里的反斜杠得到保留,但 calls 仍然从一增加到二。输出里的七来自一次真实函数调用。因此不要把有副作用的表达式放进模板,再寄希望于 raw 阻止它执行。
第三个断言把一个已经包含换行的字符串作为插值送进去,得到的仍是换行。String.raw 不会回头重写插值内部的内容,也不会把真实换行替换成可见的反斜杠序列。调试时可使用 JSON.stringify 显示控制字符,避免终端换行造成误读。
异常转义与输出格式要分别负责
最后一组在带标签模板中写入无效转义。对应 cooked 片段为 undefined,raw 片段仍可读取。这是标签模板允许自定义解释规则的一个入口。若你的标签需要正常字符串片段,应主动拒绝 undefined,不能默认每个片段都有字符串方法。
同样的无效转义如果放入普通、不带标签的模板,会在语法分析阶段出错。实验特意把它留在标签之后,不能只删除函数名来“简化”代码。这个差别适合用来排查为什么一段模板在某个库中能用,搬出库后却无法运行。
实现模板工具时,可以先保留片段和参数,再按用途编码。HTML、SQL、命令行参数各有自己的结构规则;保留反斜杠不等于为这些目的地完成转义。本文的 inspect 只是观察器,不应直接升级为执行命令或拼接查询的通用接口。
验收自己的标签时,至少放入静态反斜杠、插值中的真实换行、数值参数和有计数效果的表达式。逐项确认结果与调用次数,比只看最终字符串“像不像”更容易找到处理阶段放错的位置。
还有一个容易混淆的入口是普通字符串变量。把已经构造好的字符串传给普通函数调用,并不会让源码重新获得标签模板的切片信息。若工具需要依赖静态片段与动态参数的分离,应让调用方式本身保留这条边界,而不是事后从拼接结果里猜测。


