46 KiB
native addon API 使用规范
本文件用于网页端 Node.js 补环境阶段。每次进入补环境阶段、修改 native-protect.js / addon helper、创建或修复 WebAPI 对象模型、处理 document.all、navigator.plugins / navigator.mimeTypes、集合对象、私有状态或构造函数行为时必须读取。
选择 isolated-vm 框架时的 xbs 规则
如果用户选择 isolated-vm 补环境框架,必须使用随包魔改 xbs isolated-vm,并读取 references/xbs-isolated-vm-api.md。此时 WebAPI native-first 能力来自 Context 内的 window.xbs / globalThis.xbs,不要把本文件所述旧 addon.node 桥接进 isolated-vm。xbs API 名称与本文件主要 API 基本一致:createNativeFunction、createGetter、createSetter、createProtoChains、createNativeCollection、getMimeTypesAndPlugins、createUndetectable 等;补环境约束仍然是原型链、描述符、访问器、toString、集合对象和特殊对象优先走 native API。
Skill 硬性规则
- addon-first 是默认硬性基线:进入补环境阶段先加载 / 记录 addon,可用时优先调用 addon API。ABI 不兼容时不得直接降级;先读取
node-version-recovery.md,提示 addon 兼容 Node.js v25.8.2,检测 nvm 并征得用户同意是否安装 / 切换兼容 Node。只有用户拒绝切换、当前平台缺失、切换后仍失败、明确要求不使用 addon 或 API 调用失败时,才降级为NativeProtect/ JS fallback。 - 新代码优先新版 API:构造函数、原型链、别名、实例工厂、原型方法和静态方法优先使用
createProtoChains(descriptors);禁止把旧式createProtoChains(name, chain)或createNativeObject(tag, proto, properties)写成新代码主路径。 - WebAPI 方法必须 native-like:普通方法优先
createNativeFunction(false, name, length, callback);构造函数优先createProtoChains或必要时createNativeFunction(true, ...);getter / setter 优先createGetter/createSetter。 - 集合对象优先 addon 集合 API:
HTMLCollection、NodeList、PluginArray、MimeTypeArray、DOMTokenList、StyleSheetList等浏览器集合对象,addon 可用时优先使用createNativeCollection或getMimeTypesAndPlugins,不要手写普通数组 / 普通对象作为主路径。 - plugins / mimeTypes 是强约束:补
navigator.plugins和navigator.mimeTypes时,addon 可用必须优先使用getMimeTypesAndPlugins([config]);如果真实浏览器样本与内置 PDF 插件数据不同,传入 config 生成样本一致的插件和 MIME 类型。只有 addon 不可用或用户明确禁用 addon 时,才允许最小 JS fallback,并记录差异。 - 特殊对象优先 native 能力:
document.all优先createUndetectable;需要 named / indexed property interceptor 的高级对象可用createInterceptor;最终能表达为真实构造函数和集合结构时,仍应迁移到真实对象模型。 - 内部状态优先 V8 Private:对象内部状态优先使用
setPrivate/getPrivate/hasPrivate/deletePrivate,不要把状态直接挂成可枚举属性;addon 不可用时才用WeakMapfallback。 - 注册表管理只用于隔离和重置:
getProtoChainRegistry、deleteProtoChainRegistryEntry、clearProtoChainRegistry主要用于测试隔离、环境重置或分阶段初始化管理;正式补环境应尽量一次性规划对象模型,避免运行中反复删除注册表。 - 错误行为必须采样:
constructorBehavior、callBehavior、constructorErrorMessage、callErrorMessage、illegalConstructor等配置要以目标浏览器采样为准,不要泛化写死Illegal constructor。 - 禁止本机强依赖:Skill 文档和最终产物不得写入本机绝对路径;addon 默认从
assets/native-addon/<platform>-<arch>/addon.node加载,用户自定义路径只能作为运行参数或环境变量。
WebAPI 默认映射
| 场景 | addon-first API |
|---|---|
| 构造函数、原型链、别名、实例工厂 | createProtoChains(descriptors) |
| 普通 WebAPI 方法 | createNativeFunction(false, name, length, callback) |
| getter / setter | createGetter(name, 0, callback) / createSetter(name, 1, callback) |
document.all / HTMLDDA 近似 |
createUndetectable(callback, handlers) |
navigator.plugins / navigator.mimeTypes |
getMimeTypesAndPlugins(config) |
HTMLCollection / NodeList / 类数组集合 |
createNativeCollection(options) |
| named / indexed property interceptor | createInterceptor(options) |
| 内部状态 | setPrivate / getPrivate / hasPrivate / deletePrivate |
| 构造函数注册表隔离 | getProtoChainRegistry / deleteProtoChainRegistryEntry / clearProtoChainRegistry |
| 浏览器 TypeError | throwTypeError(message) |
降级记录要求
如果发生 NativeProtect / JS fallback,必须在 notes、阶段报告和最终总结写明:
- addon 加载路径或随包加载结果。
- 失败 API、错误信息、Node 版本、平台和 ABI。
- fallback 覆盖范围和已知差异。
- 是否为用户明确豁免。
- 如果是 ABI 不兼容,记录是否已提示 nvm + Node.js v25.8.2 恢复流程、用户选择、重新检测结果,以及为何最终仍需 fallback。
native 能力缺口处理
当 addon 可加载但当前 API 无法表达目标浏览器行为时,不得把问题简单降级为 NativeProtect / JS fallback。先确认是否属于 API 用法错误:已有 createProtoChains、createNativeFunction、createGetter、createSetter、createNativeCollection、getMimeTypesAndPlugins、createUndetectable、createInterceptor、private slot、注册表管理或 throwTypeError 可以覆盖时,必须先用这些 API 修复。
确认当前 addon API 仍无法覆盖时,按 references/native-capability-gap.md 输出能力缺口报告。报告中必须给出:
- 真实浏览器采样行为。
- 纯 JS fallback 的失败点。
- 当前 addon API 的失败点或缺失能力。
- 如果用户选择 isolated-vm,同时给出当前 xbs API 的失败点或缺失能力。
- 建议新增或增强的 addon API 名称、参数、返回值、浏览器语义和保护点。
- 最小行为测试用例。用户更新 addon.node 后必须先让该测试通过,才能继续把该点写成已解决。
示例:如果 document.all 需要同时满足 typeof document.all === "undefined"、document.all.length、document.all.item()、Object.prototype.toString.call(document.all) === "[object HTMLAllCollection]",而当前 createUndetectable 只能覆盖部分行为,则输出建议新增 createHTMLDDACollection(options) 或增强 createUndetectable() 的能力,而不是继续用状态化 getter 或普通对象硬凑。
项目对外 API 使用说明
本文档说明当前 Node.js native addon 对外可用 API、常用配置项、返回值、示例和注意事项。文档基于当前项目源码 main.cc、undetect.cc、test.js 与当前构建产物的可见导出整理。
加载方式示例:
const addon = require("./build/Release/addon");
1. API 总览
| API | 推荐状态 | 主要用途 |
|---|---|---|
createNativeFunction |
推荐 | 创建 native-like 函数或构造函数,Function.prototype.toString 显示为 [native code]。 |
createGetter |
推荐 | 创建 native-like getter 函数,例如 function get userAgent() { [native code] }。 |
createSetter |
推荐 | 创建 native-like setter 函数,例如 function set value() { [native code] }。 |
createNativeObject |
兼容 / 旧式 | 创建单个构造函数、实例对象和嵌套父级原型链。新代码更建议使用 createProtoChains。 |
createProtoChains |
推荐 | 批量创建构造函数、原型链、构造函数继承、别名、实例工厂、原型方法和静态方法。 |
getProtoChainRegistry |
推荐 | 查看 createProtoChains 的全局构造函数 / 别名注册表。 |
deleteProtoChainRegistryEntry |
推荐 | 删除注册表中的构造函数或别名,并清理指向该构造函数的别名。 |
clearProtoChainRegistry |
推荐 | 清空 createProtoChains 注册表,适合测试隔离或环境重新初始化。 |
getPrivate |
推荐 | 读取 V8 Private 私有字段。 |
setPrivate |
推荐 | 写入 V8 Private 私有字段。 |
hasPrivate |
推荐 | 判断对象上是否存在指定 V8 Private 私有字段。 |
deletePrivate |
推荐 | 删除对象上的指定 V8 Private 私有字段。 |
createInterceptor |
高级 | 创建带 V8 named / indexed property interceptor 的对象。 |
createNativeCollection |
推荐 | 通用浏览器集合对象工厂,可创建 HTMLCollection、NodeList、PluginArray 类似对象。 |
getMimeTypesAndPlugins |
可用 | 创建浏览器风格 navigator.mimeTypes / navigator.plugins;支持默认数据和参数化配置。 |
createUndetectable |
高级 | 创建 V8 undetectable 对象,常用于近似 document.all / HTMLDDA 行为。 |
throwTypeError |
辅助 | 从 native addon 抛出 TypeError。 |
hello |
历史调试 | 当前源码未导出,正式补环境逻辑不要依赖。 |
2. 通用注意事项
- API 都是 native 函数:导出函数自身的
Function.prototype.toString.call(api)通常为function xxx() { [native code] }。 - 参数类型要严格:多数 API 在参数类型不满足时会直接不返回结果或抛错,调用前建议自行校验。
- 构造回调参数差异:
createProtoChains/createNativeObject的构造回调总会把isNew作为第一个参数传入。createNativeFunction(true, ...)只有在new调用时才把isNew作为第一个参数传入;普通调用不会自动补这个布尔值。
- 回调参数已按实际长度传递:
createProtoChains/createNativeFunction内部会按调用参数数量动态构造参数数组;仍建议 WebAPI 形参保持和真实浏览器.length一致。 - 命名全局注册:
createProtoChains内部维护构造函数模板与别名注册表,同名构造函数会跨调用复用。请使用稳定且不冲突的名称;测试或重新初始化时可用注册表管理 API 清理。
3. createNativeFunction(isConstructor, name, length, callback)
创建一个 native-like 函数或构造函数。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
isConstructor |
boolean |
是 | true 表示允许 new,false 表示不可构造。 |
name |
string |
是 | 函数名 / 构造函数名。 |
length |
number |
是 | 函数形参长度,会影响 .length。 |
callback |
function |
是 | 实际执行逻辑。 |
行为
isConstructor === true:- 允许
new Fn()。 - 构造调用时,
callback的第一个参数是isNew === true。 - 构造调用最终返回
this,即使callback返回其他值。 - 普通调用
Fn()也允许,但不会自动传入isNew。
- 允许
isConstructor === false:- 只能普通调用。
new fn()会抛出不可构造相关错误。
示例
const XBS = addon.createNativeFunction(true, "XBS", 0, function (isNew) {
if (isNew) this.value = 111;
});
console.log(new XBS().value); // 111
console.log(XBS.toString()); // function XBS() { [native code] }
const add = addon.createNativeFunction(false, "add", 2, function (a, b) {
return a + b;
});
console.log(add(1, 2)); // 3
console.log(add.toString()); // function add() { [native code] }
4. createGetter(name, length, callback)
创建 native-like getter 函数。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
string |
是 | getter 对应属性名。 |
length |
number |
是 | 当前实现会校验该参数,但实际 getter .length 固定为 0。 |
callback |
function |
是 | getter 实际逻辑。this 为访问该属性的对象。 |
示例
class Navigator {}
Object.defineProperty(Navigator.prototype, "userAgent", {
get: addon.createGetter("userAgent", 0, function () {
if (!(this instanceof Navigator)) {
addon.throwTypeError("Illegal invocation");
}
return "Mozilla/5.0";
}),
enumerable: true,
configurable: true,
});
const getter = Object.getOwnPropertyDescriptor(Navigator.prototype, "userAgent").get;
console.log(getter.toString()); // function get userAgent() { [native code] }
5. createSetter(name, length, callback)
创建 native-like setter 函数。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
string |
是 | setter 对应属性名。 |
length |
number |
是 | 当前实现会校验该参数,但实际 setter .length 固定为 1。 |
callback |
function |
是 | setter 实际逻辑。this 为访问该属性的对象。 |
示例
const state = new WeakMap();
class Location {}
Object.defineProperty(Location.prototype, "href", {
get: addon.createGetter("href", 0, function () {
return state.get(this) || "";
}),
set: addon.createSetter("href", 1, function (value) {
state.set(this, String(value));
}),
enumerable: true,
configurable: true,
});
6. createNativeObject(options)
旧式单对象创建 API。它会创建一个构造函数、一个实例对象,并可通过嵌套 parent 创建父级构造函数和原型链。
配置项
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string |
无 | 构造函数名,必填。 |
length |
number |
无 | 构造函数 .length,必填。 |
constructor |
function |
无 | 构造回调,必填。第一个参数固定为 isNew。 |
isReadOnlyPrototype |
boolean |
false |
使构造函数 .prototype 属性只读。 |
isImmutableProto |
boolean |
false |
使构造函数的 prototype 对象原型不可更改。 |
isImmutableInstanceProto |
boolean |
false |
使创建出的实例对象原型不可更改。 |
parent |
object |
无 | 父级描述符,结构与当前 options 类似,可继续嵌套。 |
返回值
{
instance: object,
constructor: Function,
prototypeChains?: Function[]
}
示例
const result = addon.createNativeObject({
name: "Window",
length: 0,
constructor(isNew) {
if (isNew) throw new TypeError("Failed to construct 'Window': Illegal constructor");
throw new TypeError("Illegal constructor");
},
isReadOnlyPrototype: true,
isImmutableProto: true,
isImmutableInstanceProto: true,
parent: {
name: "EventTarget",
length: 0,
constructor(isNew) {
if (!isNew) throw new TypeError("Please use the 'new' operator");
},
isReadOnlyPrototype: true,
isImmutableProto: true,
},
});
const window = result.instance;
const Window = result.constructor;
console.log(window instanceof Window); // true
建议
新功能优先使用 createProtoChains,因为它支持字符串父级引用、构造函数父级、跨调用复用、别名和实例工厂。
7. createProtoChains(descriptors)
批量创建构造函数、原型链和实例相关对象,是当前推荐的构造函数 / 原型链 API。
基本调用
const chain = addon.createProtoChains([
{
name: "EventTarget",
length: 0,
constructor(isNew) {
if (!isNew) {
throw new TypeError(
"Failed to construct 'EventTarget': Please use the 'new' operator"
);
}
},
},
{
name: "Node",
length: 0,
constructor(isNew) {},
prototypeParent: "EventTarget",
},
]);
const { EventTarget, Node } = chain;
console.log(new Node() instanceof EventTarget); // true
普通构造函数描述符
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string |
无 | 构造函数名,必填。 |
constructor |
function |
无 | 构造回调,必填。第一个参数固定为 isNew。 |
length |
number |
0 |
构造函数 .length。 |
prototypeParent |
string |
无 | 原型父级。设置后会使 Child.prototype -> Parent.prototype。 |
parent |
string |
无 | prototypeParent 的旧字段别名。 |
constructorParent |
string / null |
默认同 prototypeParent |
构造函数对象自身的父级。设置后会使 Child -> Parent,用于静态属性继承。传 null 可显式禁用默认继承。 |
readOnlyPrototypeProperty |
boolean |
false |
使构造函数 .prototype 属性只读。 |
isReadOnlyPrototype |
boolean |
false |
readOnlyPrototypeProperty 的旧字段别名。 |
immutablePrototypeObject |
boolean |
false |
使 Constructor.prototype 的 [[Prototype]] 不可更改。 |
isImmutableProto |
boolean |
false |
immutablePrototypeObject 的旧字段别名。 |
immutableInstancePrototype |
boolean |
false |
使通过模板创建的实例对象 [[Prototype]] 不可更改。 |
isImmutableInstanceProto |
boolean |
false |
immutableInstancePrototype 的旧字段别名。 |
hasToStringTag |
boolean |
true |
是否在 Constructor.prototype 上设置 Symbol.toStringTag。 |
toStringTag |
string |
name |
Symbol.toStringTag 的值。 |
internalClassName |
string |
无 | 写入 addon 内部类型名。即使没有 Symbol.toStringTag,也能让 addon 创建的实例在 Object.prototype.toString.call(instance) 中显示指定标签。 |
constructorBehavior |
"allow" / "illegal" / "throw" |
"allow" |
控制 new Constructor() 的 native 层行为。illegal 与 throw 都会在进入 JS 回调前抛出 TypeError。 |
callBehavior |
"allow" / "illegal" / "throw" |
"allow" |
控制 Constructor() 直接调用的 native 层行为。 |
constructorErrorMessage |
string |
自动生成 | constructorBehavior 抛错时使用的错误信息。 |
callErrorMessage |
string |
"Illegal constructor" |
callBehavior 抛错时使用的错误信息。 |
illegalConstructor |
boolean |
false |
便捷配置;为 true 时同时把 constructorBehavior 和 callBehavior 设置为 "illegal"。 |
prototypeMethods |
NativeMember[] |
[] |
批量定义到 Constructor.prototype 的 native-like 方法。 |
staticMethods |
NativeMember[] |
[] |
批量定义到构造函数自身的 native-like 静态方法。 |
aliases |
string[] |
[] |
为当前构造函数注册一个或多个别名,别名导出的是同一个函数对象。 |
instanceFactoryName |
string |
无 | 创建实例工厂函数的导出名。用于公开构造函数不能 new,但内部仍要创建实例的场景。 |
instanceInitializer |
function |
无 | factory 创建实例后调用,this 指向实例,factory 参数会原样传入。必须配合 instanceFactoryName 使用。 |
isCreateInstance |
boolean |
false |
旧式立即创建实例开关。 |
instanceName |
string |
无 | 旧式实例导出名,配合 isCreateInstance: true 使用。 |
alias-only 描述符
用于给已有构造函数注册别名。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
string |
是 | 别名导出名。 |
aliasOf |
string |
是 | 指向的真实构造函数名或已有别名。 |
alias-only 描述符不能定义 constructor、length、父级、不可变选项、toStringTag、aliases、实例或 factory 相关字段。
返回值
返回一个普通对象,包含:
- 所有本次创建或解析到的构造函数:
result[name]。 - 所有可导出的别名:
result[alias],值与真实构造函数严格相等。 - 使用旧式
isCreateInstance + instanceName创建的实例。 - 使用
instanceFactoryName创建的 factory 函数。
prototypeParent 与 constructorParent
const { Base, Child } = addon.createProtoChains([
{ name: "Base", length: 0, constructor() {} },
{ name: "Child", length: 0, constructor() {}, prototypeParent: "Base" },
]);
Object.getPrototypeOf(Child.prototype) === Base.prototype; // true
Object.getPrototypeOf(Child) === Base; // true,因为 constructorParent 默认同 prototypeParent
prototypeParent控制实例链:new Child() instanceof Base。constructorParent控制构造函数对象链:Child.__proto__ === Base,用于静态属性继承。- 如果只想继承实例链,不想继承构造函数对象链:
addon.createProtoChains([
{ name: "Base", length: 0, constructor() {} },
{
name: "Child",
length: 0,
constructor() {},
prototypeParent: "Base",
constructorParent: null,
},
]);
只读与不可变选项区别
| 选项 | 影响对象 | 效果 |
|---|---|---|
readOnlyPrototypeProperty |
构造函数的 .prototype 属性 |
.prototype 属性不可写。 |
immutablePrototypeObject |
Constructor.prototype 对象 |
不能通过 Object.setPrototypeOf(Constructor.prototype, ...) 改它的原型。 |
immutableInstancePrototype |
创建出的实例对象 | 不能通过 Object.setPrototypeOf(instance, ...) 改实例原型。 |
Symbol.toStringTag
默认会设置:
Constructor.prototype[Symbol.toStringTag] === name;
Object.prototype.toString.call(new Constructor()); // [object name]
关闭:
{ name: "NoTag", constructor() {}, hasToStringTag: false }
自定义:
{ name: "URL", constructor() {}, toStringTag: "URL" }
内部 [[Class]] / 无 Symbol.toStringTag 标签
有些浏览器对象没有暴露 Symbol.toStringTag,但 Object.prototype.toString.call(obj) 仍然需要返回指定标签。此时可以关闭 hasToStringTag,并使用 internalClassName:
const { createDocument } = addon.createProtoChains([
{
name: "Document",
constructor() {},
hasToStringTag: false,
internalClassName: "Document",
instanceFactoryName: "createDocument",
},
]);
const document = createDocument();
console.log(document[Symbol.toStringTag]); // undefined
console.log(Object.prototype.toString.call(document)); // [object Document]
实现注意:
internalClassName使用 V8 Private 保存,不会成为普通 JS 可枚举属性。- addon 会安装 native-like 的
Object.prototype.toString包装函数;普通对象、null、undefined会回退原始行为。 - 如果同时设置了
Symbol.toStringTag和internalClassName,实际检测结果应以目标浏览器样本为准,建议二选一保持结构清晰。
构造函数行为模式
constructorBehavior 和 callBehavior 用于在 native 层提前控制公开构造函数能否被 new 或直接调用。适合“公开构造函数不可用,但内部 factory 可以创建实例”的 DOM 场景。
const { Document, createDocument } = addon.createProtoChains([
{
name: "Document",
constructor() {
throw new Error("不会执行到这里");
},
constructorBehavior: "throw",
constructorErrorMessage: "Failed to construct 'Document': Illegal constructor",
callBehavior: "throw",
callErrorMessage: "Illegal constructor",
instanceFactoryName: "createDocument",
},
]);
try { new Document(); } catch (err) { console.log(err.message); }
try { Document(); } catch (err) { console.log(err.message); }
const document = createDocument();
console.log(document instanceof Document); // true
行为说明:
allow:允许调用并进入constructorJS 回调。illegal/throw:在 native 层抛TypeError,不会执行constructorJS 回调。illegalConstructor: true是快捷写法,等价于公开构造和直接调用都走非法构造路径。- 错误信息建议来自目标浏览器采样,不要随意泛化。
批量定义原型方法和静态方法
prototypeMethods 与 staticMethods 的元素结构如下:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string |
无 | 方法名。 |
callback |
function |
无 | 实际执行逻辑。也兼容字段名 value。 |
length |
number |
0 |
方法 .length。 |
writable |
boolean |
true |
descriptor 的 writable。 |
enumerable |
boolean |
true |
descriptor 的 enumerable。 |
configurable |
boolean |
true |
descriptor 的 configurable。 |
const { URL } = addon.createProtoChains([
{
name: "URL",
constructor(isNew, href) {
if (isNew) this.href = String(href);
},
prototypeMethods: [
{
name: "toJSON",
length: 0,
enumerable: false,
callback() {
return this.href;
},
},
],
staticMethods: [
{
name: "canParse",
length: 1,
callback(value) {
return typeof value === "string" && value.length > 0;
},
},
],
},
]);
const url = new URL("https://example.com/");
console.log(url.toJSON()); // https://example.com/
console.log(URL.canParse("/")); // true
console.log(url.toJSON.toString()); // function toJSON() { [native code] }
别名:aliases 与 aliasOf
适合模拟 URL / webkitURL 这类同一构造函数多个名字的场景。
const { URL, webkitURL } = addon.createProtoChains([
{
name: "URL",
aliases: ["webkitURL"],
length: 0,
constructor() {},
},
]);
console.log(webkitURL === URL); // true
console.log(webkitURL.name); // URL
也可以单独注册:
const first = addon.createProtoChains([
{ name: "URL", length: 0, constructor() {} },
]);
const second = addon.createProtoChains([
{ name: "webkitURL", aliasOf: "URL" },
]);
console.log(second.webkitURL === first.URL); // true
实例工厂:instanceFactoryName / instanceInitializer
用于“公开构造函数不能被 new,但内部仍需要创建一个合法实例”的场景。
const { Document, createDocument } = addon.createProtoChains([
{
name: "Document",
length: 0,
constructor(isNew) {
if (isNew) {
throw new TypeError("Failed to construct 'Document': Illegal constructor");
}
throw new TypeError("Illegal constructor");
},
instanceFactoryName: "createDocument",
instanceInitializer(url) {
this.url = url;
},
},
]);
try {
new Document();
} catch (err) {
console.log(err.message); // Failed to construct 'Document': Illegal constructor
}
const doc = createDocument("https://example.com/");
console.log(doc instanceof Document); // true
console.log(doc.url); // https://example.com/
特点:
- factory 通过 native
InstanceTemplate创建实例,不调用公开构造函数回调。 - factory 自身不可
new。 instanceInitializer的this是新实例。- factory 名不能和构造函数名、别名、旧式
instanceName或其他 factory 重名。
旧式立即创建实例
仍然可用,但新代码更推荐使用实例工厂。
const { Window, window } = addon.createProtoChains([
{
name: "Window",
length: 0,
constructor(isNew) {
if (isNew) throw new TypeError("Illegal constructor");
},
isCreateInstance: true,
instanceName: "window",
},
]);
console.log(window instanceof Window); // true
跨调用复用与冲突规则
- 同名构造函数会复用已注册的
FunctionTemplate/Function。 - 已注册别名也会参与后续父级解析。
- 别名不能和真实构造函数名冲突。
aliasOf必须指向当前调用中存在或之前已注册的构造函数 / 别名。- 对已经创建并发布过的构造函数,不建议后续再尝试改变它自身的父级或原型继承关系;请尽量一次性完成该构造函数的结构定义。
注册表管理 API
createProtoChains 会把构造函数模板、构造函数对象和别名写入内部注册表,便于后续跨调用复用父级和别名。新增了三个管理 API:
| API | 参数 | 返回值 | 说明 |
|---|---|---|---|
getProtoChainRegistry() |
无 | { constructors: string[], aliases: object } |
查看当前已注册构造函数名和别名映射。 |
deleteProtoChainRegistryEntry(name) |
name: string |
boolean |
删除指定构造函数或别名。删除构造函数时,也会清理指向它的别名。 |
clearProtoChainRegistry() |
无 | boolean |
清空全部构造函数和别名注册。 |
addon.createProtoChains([
{
name: "URL",
aliases: ["webkitURL"],
constructor() {},
},
]);
console.log(addon.getProtoChainRegistry());
// { constructors: ["URL"], aliases: { webkitURL: "URL" } }
addon.deleteProtoChainRegistryEntry("URL");
addon.clearProtoChainRegistry();
注意:
- 这些 API 主要用于测试隔离、环境重置或分阶段初始化管理。
- 删除注册表不会主动销毁 JS 中已经拿到的构造函数引用,只是不再让后续
createProtoChains通过注册表复用它。 - 正式补环境中建议一次性规划好对象模型,减少运行中清理注册表。
8. V8 Private 私有槽 API
基于 V8 Private::ForApi 给对象保存不可通过普通 JS 属性枚举访问的私有值。
参数
| API | 参数 | 返回值 |
|---|---|---|
setPrivate |
object: object, key: string, value: any |
boolean,写入是否成功。 |
getPrivate |
object: object, key: string |
保存的值;不存在时通常为 undefined。 |
hasPrivate |
object: object, key: string |
boolean,指定私有槽是否存在。 |
deletePrivate |
object: object, key: string |
boolean,删除是否成功。 |
示例
const obj = {};
addon.setPrivate(obj, "internalState", { ok: true });
console.log(obj.internalState); // undefined
console.log(addon.hasPrivate(obj, "internalState")); // true
console.log(addon.getPrivate(obj, "internalState")); // { ok: true }
addon.deletePrivate(obj, "internalState");
console.log(addon.hasPrivate(obj, "internalState")); // false
console.log(addon.getPrivate(obj, "internalState")); // undefined
9. getMimeTypesAndPlugins([config])
创建一组浏览器风格插件对象,通常用于补 navigator.mimeTypes 和 navigator.plugins。不传参数时使用项目内置 PDF 插件数据;传入 config 时按配置生成插件和 MIME 类型。
返回值
{
mimeTypes: MimeTypeArrayLike,
plugins: PluginArrayLike,
PluginArray: Function,
MimeTypeArray: Function,
MimeType: Function,
Plugin: Function
}
当前内置数据
mimeTypes.length === 2application/pdftext/pdf
plugins.length === 5PDF ViewerChrome PDF ViewerChromium PDF ViewerMicrosoft Edge PDF ViewerWebKit built-in PDF
参数化配置
type MimePluginConfig = {
plugins?: Array<{
name: string;
filename?: string;
description?: string;
mimeTypes?: Array<{
type: string;
suffixes?: string;
description?: string;
}>;
}>;
};
配置规则:
plugins不是数组时按空数组处理。- 每个 plugin 会生成一个
Plugin实例,并写入name、filename、description、length。 - 每个 MIME 配置会生成一个
MimeType实例,并写入type、suffixes、description、enabledPlugin。 - 返回的
plugins与mimeTypes都由createNativeCollection生成,支持length、数字索引、item()、namedItem()、迭代和Symbol.toStringTag。
const result = addon.getMimeTypesAndPlugins({
plugins: [
{
name: "Custom PDF Viewer",
filename: "custom-pdf-viewer",
description: "Custom Portable Document Format",
mimeTypes: [
{
type: "application/x-custom-pdf",
suffixes: "cpdf",
description: "Custom PDF",
},
],
},
],
});
console.log(result.plugins.length); // 1
console.log(result.mimeTypes.length); // 1
console.log(result.plugins[0].name); // Custom PDF Viewer
console.log(result.plugins.namedItem("Custom PDF Viewer") === result.plugins[0]); // true
console.log(result.mimeTypes[0].enabledPlugin === result.plugins[0]); // true
支持行为
mimeTypes.item(index)/plugins.item(index)。mimeTypes.namedItem(name)/plugins.namedItem(name)。- 数字索引访问和名称访问。
Plugin.prototype[Symbol.iterator]近似使用数组迭代器。PluginArray、MimeTypeArray、MimeType、Plugin构造函数都会模拟非法构造:直接调用或new都会抛出 TypeError。
示例
const {
mimeTypes,
plugins,
PluginArray,
MimeTypeArray,
MimeType,
Plugin,
} = addon.getMimeTypesAndPlugins();
Object.defineProperty(navigator, "mimeTypes", {
value: mimeTypes,
enumerable: true,
configurable: true,
});
Object.defineProperty(navigator, "plugins", {
value: plugins,
enumerable: true,
configurable: true,
});
console.log(mimeTypes.item(0) === mimeTypes[0]);
console.log(mimeTypes.namedItem("application/pdf"));
try { new Plugin(); } catch (err) { console.log(err.message); }
注意
当前源码中 MimeType 的部分 className / Symbol.toStringTag 拼写为 MimeTpye,如果目标检测非常严格,需要按实际输出决定是否修正源码或在 JS 层额外处理。
10. createInterceptor(options)
创建一个带 V8 named / indexed property interceptor 的对象。它适合做受控的“虚拟属性 + backing target”对象,能拦截读取、写入、查询、删除、枚举和属性描述符。
参数
type InterceptorOptions = {
target?: object;
handlers?: {
getter?: (target: object, property: string | number | symbol) => any;
setter?: (target: object, property: string | number | symbol, value: any) => any;
query?: (target: object, property: string | number | symbol) => number;
deleter?: (target: object, property: string | number | symbol) => boolean;
enumerator?: (target: object) => Array<string | number>;
definer?: (target: object, property: string | number | symbol, descriptor: object) => any;
descriptor?: (target: object, property: string | number | symbol) => PropertyDescriptor;
};
immutablePrototype?: boolean;
internalClassName?: string;
};
handler 返回约定
| handler | 返回值约定 |
|---|---|
getter |
返回 { value } 表示拦截并返回该值;返回 { intercept: false } 表示放弃拦截并回落到 target。 |
setter |
返回 { intercept: true, value } 可改写写入 target 的值;返回 { intercept: false } 可放弃 native 拦截。 |
descriptor |
返回标准属性描述符对象。 |
enumerator |
返回枚举属性名数组;indexed enumerator 内部只接受可转成 uint32 的索引。 |
query |
可返回 V8 属性特性数字;未返回时会尝试从 descriptor 或 target 推导。 |
deleter |
返回布尔值可作为删除结果。 |
definer |
返回 false 或 { intercept: false } 可放弃拦截,否则 descriptor 会定义到 target。 |
示例
const target = { existing: 1 };
const obj = addon.createInterceptor({
target,
internalClassName: "MagicInterceptor",
handlers: {
getter(targetObject, property) {
if (property === "virtual") return { value: 42 };
return { intercept: false };
},
setter(targetObject, property, value) {
return { intercept: true, value: String(value) };
},
descriptor(targetObject, property) {
if (property === "virtual") {
return {
value: 42,
writable: true,
enumerable: true,
configurable: true,
};
}
},
enumerator() {
return ["virtual"];
},
},
});
console.log(obj.virtual); // 42
console.log(obj.existing); // 1
obj.created = 123;
console.log(target.created); // "123"
console.log(Object.keys(obj)); // ["virtual"]
console.log(Object.prototype.toString.call(obj)); // [object MagicInterceptor]
注意:createInterceptor 适合高级探测和特殊对象;最终 WebAPI 若能用真实对象、描述符和 createProtoChains 表达,仍优先使用真实结构。
11. createNativeCollection(options)
通用浏览器集合对象工厂,用于创建 HTMLCollection、NodeList、PluginArray、MimeTypeArray 等“有 length、数字索引、名称索引、item、namedItem、可迭代”的集合对象。
参数
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string |
"NativeCollection" |
集合构造函数名。 |
items |
`Array<any | { name?: string, value: any }>` | [] |
itemMethod |
boolean |
true |
是否在 prototype 上安装 item(index)。 |
namedItemMethod |
boolean |
true |
是否在 prototype 上安装 namedItem(name)。 |
indexedAccess |
boolean |
true |
是否定义数字索引属性。 |
namedAccess |
boolean |
true |
是否定义名称属性。 |
namedEnumerable |
boolean |
false |
名称属性是否可枚举。 |
iterable |
boolean |
true |
是否安装 Symbol.iterator。 |
hasToStringTag |
boolean |
true |
是否设置 Symbol.toStringTag。 |
toStringTag |
string |
同 name |
Symbol.toStringTag 的值。 |
internalClassName |
string |
无 | 不暴露 Symbol.toStringTag 时使用的内部对象标签。 |
immutableInstancePrototype |
boolean |
false |
是否使集合实例原型不可改。 |
返回值
{
collection: object;
constructor: Function;
[name: string]: Function;
}
示例
const first = { id: 1 };
const second = { id: 2 };
const result = addon.createNativeCollection({
name: "HTMLCollection",
items: [
{ name: "first", value: first },
{ name: "second", value: second },
],
hasToStringTag: false,
internalClassName: "HTMLCollection",
});
const collection = result.collection;
const HTMLCollection = result.HTMLCollection;
console.log(result.constructor === HTMLCollection); // true
console.log(collection.length); // 2
console.log(collection[0] === first); // true
console.log(collection.first === first); // true
console.log(collection.item(1) === second); // true
console.log(collection.namedItem("second") === second);
console.log([...collection]); // [first, second]
console.log(Object.prototype.toString.call(collection)); // [object HTMLCollection]
try { new HTMLCollection(); } catch (err) { console.log(err.message); }
12. createUndetectable(callback[, handlers])
创建一个 V8 MarkAsUndetectable() 对象,适合模拟 document.all 这类特殊对象。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
callback |
function |
是 | 对象被当作函数调用时执行。this 为内部 target。 |
handlers |
object |
否 | 属性拦截处理器集合。 |
handler 列表
| handler | 调用签名 | 说明 |
|---|---|---|
getter |
(target, property) |
属性读取前调用。返回普通对象(例如 {})时会继续从内部 target 读取同名属性;返回 { intercept: false } 表示放弃 native 拦截并回落到外层对象默认读取。当前 addon 中如果只定义 setter 而不定义 getter,读取刚写入的属性可能得到 undefined,因此需要读写内部 target 属性时必须显式提供 getter() { return {}; }。 |
setter |
(target, property, value) |
属性写入时调用。可返回 { intercept: true, value } 改写保存值,或 { intercept: false, value } 放弃拦截。 |
query |
(target, property) |
用于属性存在性 / 属性特性查询。 |
deleter |
(target, property) |
删除属性时调用。返回布尔值可直接作为删除结果。 |
enumerator |
(target) |
枚举属性名。返回数组时使用该数组,否则使用 target 自有属性。 |
definer |
(target, property, descriptor) |
Object.defineProperty 时调用。返回 false 或 { intercept: false } 可放弃拦截。 |
descriptor |
(target, property) |
Object.getOwnPropertyDescriptor 时调用。返回 descriptor 可覆盖默认结果。 |
示例:近似 document.all
const all = addon.createUndetectable(function () {
return undefined;
});
Object.defineProperty(document, "all", {
value: all,
enumerable: false,
configurable: true,
});
示例:带属性拦截
const special = addon.createUndetectable(
function (...args) {
return undefined;
},
{
getter(target, property) {
return {};
},
setter(target, property, value) {
return { intercept: true, value: String(value) };
},
descriptor(target, property) {
if (property === "hidden") {
return { value: 1, writable: true, enumerable: false, configurable: true };
}
},
}
);
special.foo = 123;
console.log(special.foo); // "123"
13. throwTypeError(message)
从 native addon 抛出 TypeError。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
message |
string |
是 | 错误消息。 |
示例
function assertIllegalInvocation(thisValue, Ctor) {
if (!(thisValue instanceof Ctor)) {
addon.throwTypeError("Illegal invocation");
}
}
如果没有传参,会抛出:the number of parameters must be at least one.
如果第一个参数不是字符串,会抛出:the first parameter require is a string.
14. hello()
历史调试 API。当前源码中 NODE_SET_METHOD(exports, "hello", Method); 已注释,当前重新构建后的导出列表不包含 hello。请不要在正式补环境逻辑中依赖它。
15. 推荐组合用法
创建普通 native-like 方法
Storage.prototype.getItem = addon.createNativeFunction(false, "getItem", 1, function (key) {
return storageMap.get(String(key)) ?? null;
});
更推荐配合 Object.defineProperty:
Object.defineProperty(Storage.prototype, "getItem", {
value: addon.createNativeFunction(false, "getItem", 1, function (key) {
return storageMap.get(String(key)) ?? null;
}),
writable: true,
enumerable: true,
configurable: true,
});
创建不可直接构造但可内部实例化的 DOM 构造函数
const { Document, createDocument } = addon.createProtoChains([
{
name: "Document",
length: 0,
constructor(isNew) {
if (isNew) throw new TypeError("Failed to construct 'Document': Illegal constructor");
throw new TypeError("Illegal constructor");
},
readOnlyPrototypeProperty: true,
immutablePrototypeObject: true,
immutableInstancePrototype: true,
instanceFactoryName: "createDocument",
instanceInitializer(url) {
Object.defineProperty(this, "URL", {
value: String(url),
enumerable: true,
configurable: true,
});
},
},
]);
const document = createDocument("https://example.com/");
创建带别名的构造函数
const { URL, webkitURL } = addon.createProtoChains([
{
name: "URL",
aliases: ["webkitURL"],
length: 1,
constructor(isNew, url) {
if (!isNew) throw new TypeError("Class constructor URL cannot be invoked without 'new'");
this.href = String(url);
},
},
]);
console.log(URL === webkitURL); // true
16. 建议实践
- 优先使用
createProtoChains管理构造函数、原型链、别名、实例工厂、原型方法和静态方法。 - 对 WebAPI 关键属性统一使用
Object.defineProperty/Object.defineProperties明确描述符。 - 对不能被脚本直接构造的 DOM 类型,使用
constructor抛错 +instanceFactoryName内部创建实例。 - 需要
URL/webkitURL这类别名时,使用aliases或aliasOf,不要复制两个函数。 - 避免在多个阶段重复定义同一个构造函数名称;如果必须跨调用复用,请确保第一次创建时已经设置好继承与不可变选项。
createNativeObject保留给旧代码;新代码尽量迁移到createProtoChains。- 对
document.all优先用createUndetectable,JS fallback 很难完整模拟 HTMLDDA 行为。 - 对浏览器集合对象优先用
createNativeCollection,避免手写普通数组或普通对象导致描述符、迭代器和toStringTag不一致。 - 对需要临时拦截访问的高级对象可用
createInterceptor,但最终可表达为真实结构时仍应迁移到真实对象模型。 - 测试之间需要隔离构造函数注册状态时,使用
clearProtoChainRegistry()或deleteProtoChainRegistryEntry(name)。
17. 快速自检代码
const addon = require("./build/Release/addon");
console.log(Object.keys(addon).sort());
const f = addon.createNativeFunction(false, "demo", 0, () => 1);
console.log(f());
console.log(f.toString());
const { A, createA } = addon.createProtoChains([
{
name: "A",
constructor(isNew) {
if (isNew) throw new TypeError("Illegal constructor");
},
instanceFactoryName: "createA",
},
]);
console.log(createA() instanceof A);