document.all 是浏览器中一个非常特殊的存在——它是 HTML DOM 中最早的集合对象之一,却因为历史原因被设计成"不可检测"(undetectable)的对象。它的特殊性在于:
typeof document.all返回'undefined'document.all == undefined返回truedocument.all === undefined返回false它既支持数组索引访问
document.all[0],又支持函数调用document.all(0)两种访问方式返回相同的结果
本文将深入探讨如何通过 Node.js C++ 插件(Addon)技术,在 Node.js 环境中完美模拟这个特殊对象。
一、技术背景
1.1 Node.js Addon 是什么?
Node.js Addon 是用 C/C++ 编写的动态链接库,通过 Node.js 的 N-API 或 V8 API 与 JavaScript 交互。它允许我们在 Node.js 中调用高性能的 C++ 代码,或实现 JavaScript 无法直接完成的功能。
1.2 V8 引擎的核心概念
在实现过程中,我们需要了解几个 V8 的核心概念:
Isolate:V8 引擎的隔离实例,每个线程拥有独立的 Isolate
Context:JavaScript 执行上下文,包含全局对象和内置函数
ObjectTemplate:对象模板,用于创建具有相同属性和行为的对象
FunctionTemplate:函数模板,用于创建可调用的对象
InternalField:对象的内部存储字段,用于存储 C++ 数据
二、核心技术点解析
2.1 不可检测对象(Undetectable Object)
V8 提供了 MarkAsUndetectable() 方法,可以让对象在 JavaScript 中表现为"不可检测":
Local<ObjectTemplate> desc = ObjectTemplate::New(isolate);
desc->MarkAsUndetectable();这个标记会影响:
typeof运算符返回'undefined'在布尔上下文中转换为
false==比较时与undefined和null相等
2.2 函数调用模拟
通过 SetCallAsFunctionHandler 可以让对象像函数一样被调用:
desc->SetCallAsFunctionHandler(CallHandler);当 JavaScript 代码执行 document.all(0) 时,会触发 CallHandler 回调。
2.3 属性拦截器(Property Interceptor)
V8 的拦截器机制让我们可以拦截对对象属性的读写操作:
索引属性拦截器(Indexed Property Handler)
desc->SetHandler(v8::IndexedPropertyHandlerConfiguration(
IndexedGetterCallback, // 获取属性时的回调
IndexedSetterCallback, // 设置属性时的回调
nullptr, // 查询回调
nullptr, // 删除回调
nullptr, // 枚举回调
nullptr, // 定义回调
Local<Value>() // 数据
));命名属性拦截器(Named Property Handler)
desc->SetHandler(v8::NamedPropertyHandlerConfiguration(
NamedGetterCallback, // 获取属性时的回调
nullptr, // 设置回调
nullptr, // 查询回调
nullptr, // 删除回调
nullptr, // 枚举回调
nullptr, // 定义回调
Local<Value>() // 数据
));2.4 数据存储策略
使用 InternalField 存储数据,确保在拦截器回调中能够访问到:
// 设置内部字段数量
desc->SetInternalFieldCount(1);
// 存储数据
result->SetInternalField(0, elementsArr);
// 读取数据
Local<Value> elementsVal = self->GetInternalField(0);三、完整实现代码
#include <node.h>
#include <v8.h>
#include <string>
#include <vector>
using namespace v8;
// 获取 length 属性
static void LengthGetter(Local<Name> property, const PropertyCallbackInfo<Value>& info) {
Isolate *isolate = info.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
// 检查是否为 "length"
Local<String> lengthStr = String::NewFromUtf8(isolate, "length").ToLocalChecked();
if (property->StrictEquals(lengthStr)) {
Local<Object> self = info.Holder();
Local<Value> elementsVal = self->GetInternalField(0);
if (!elementsVal.IsEmpty() && elementsVal->IsArray()) {
Local<Array> elementsArr = elementsVal.As<Array>();
info.GetReturnValue().Set(Number::New(isolate, elementsArr->Length()));
} else {
info.GetReturnValue().Set(Number::New(isolate, 0));
}
return;
}
// 对于其他命名属性,返回 undefined
info.GetReturnValue().SetUndefined();
}
// 索引属性的 Getter 回调
static void IndexedGetterCallback(uint32_t index, const PropertyCallbackInfo<Value>& info) {
Isolate *isolate = info.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Object> self = info.Holder();
Local<Value> elementsVal = self->GetInternalField(0);
if (!elementsVal.IsEmpty() && elementsVal->IsArray()) {
Local<Array> elementsArr = elementsVal.As<Array>();
if (index < (uint32_t)elementsArr->Length()) {
MaybeLocal<Value> element = elementsArr->Get(context, index);
if (!element.IsEmpty()) {
info.GetReturnValue().Set(element.ToLocalChecked());
return;
}
}
}
info.GetReturnValue().SetUndefined();
}
// 索引属性的 Setter 回调
static void IndexedSetterCallback(uint32_t index, Local<Value> value, const PropertyCallbackInfo<Value>& info) {
Isolate *isolate = info.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Object> self = info.Holder();
Local<Value> elementsVal = self->GetInternalField(0);
if (!elementsVal.IsEmpty() && elementsVal->IsArray()) {
Local<Array> elementsArr = elementsVal.As<Array>();
// 如果索引超出长度,扩展数组
if (index >= (uint32_t)elementsArr->Length()) {
// 设置数组长度,用 undefined 填充空位
for (uint32_t i = elementsArr->Length(); i <= index; i++) {
elementsArr->Set(context, i, Undefined(isolate)).Check();
}
}
elementsArr->Set(context, index, value).Check();
info.GetReturnValue().Set(value);
return;
}
// 如果 internal field 不是数组,创建一个新数组
Local<Array> newArr = Array::New(isolate);
for (uint32_t i = 0; i <= index; i++) {
newArr->Set(context, i, Undefined(isolate)).Check();
}
newArr->Set(context, index, value).Check();
self->SetInternalField(0, newArr);
info.GetReturnValue().Set(value);
}
// 获取索引对应的元素(用于函数调用)
static void GetIndexElement(const FunctionCallbackInfo<Value>& args) {
Isolate *isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Object> self = args.Holder();
if (args.Length() > 0 && args[0]->IsInt32()) {
int index = args[0].As<Int32>()->Value();
Local<Value> elementsVal = self->GetInternalField(0);
if (!elementsVal.IsEmpty() && elementsVal->IsArray()) {
Local<Array> elementsArr = elementsVal.As<Array>();
if (index >= 0 && index < (int)elementsArr->Length()) {
MaybeLocal<Value> element = elementsArr->Get(context, index);
if (!element.IsEmpty()) {
args.GetReturnValue().Set(element.ToLocalChecked());
return;
}
}
}
}
args.GetReturnValue().SetUndefined();
}
// 当作为函数调用时的处理器
static void CallHandler(const FunctionCallbackInfo<Value>& args) {
GetIndexElement(args);
}
void CreateUndetectable(const FunctionCallbackInfo<Value> &args)
{
Isolate *isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<ObjectTemplate> desc = ObjectTemplate::New(isolate);
desc->SetInternalFieldCount(1);
desc->MarkAsUndetectable();
desc->SetCallAsFunctionHandler(CallHandler);
// 设置命名属性拦截器(用于 length 属性)
desc->SetHandler(v8::NamedPropertyHandlerConfiguration(
LengthGetter, // getter
nullptr, // setter
nullptr, // query
nullptr, // deleter
nullptr, // enumerator
nullptr, // definer
Local<Value>() // data
));
// 设置索引属性拦截器(包含 getter 和 setter)
desc->SetHandler(v8::IndexedPropertyHandlerConfiguration(
IndexedGetterCallback, // getter
IndexedSetterCallback, // setter
nullptr, // query
nullptr, // deleter
nullptr, // enumerator
nullptr, // definer
Local<Value>() // data
));
MaybeLocal<Object> obj = desc->NewInstance(context);
if (!obj.IsEmpty()) {
Local<Object> result = obj.ToLocalChecked();
// 初始化空的元素数组
Local<Array> elementsArr = Array::New(isolate);
// 如果传入了数组参数,使用传入的数组
if (args.Length() > 0 && args[0]->IsArray()) {
elementsArr = args[0].As<Array>();
}
result->SetInternalField(0, elementsArr);
args.GetReturnValue().Set(result);
}
}
// 初始化
void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "undetectable", CreateUndetectable);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)四、编译
不同版本的 Node.js 使用的 V8 API 可能有差异。我当前的环境配置如下:
nodejs v20.19.3
node-gyp v11.4.2
python v3.9.18
由于是编译c++代码,所以我们还需要配置c++环境,可以通过安装Visual Studio的方式来安装c++环境
我们新建一个文件夹,文件夹中新建三个文件分别是native.cc、index.js以及binding.gyp
native.cc用来存放我们编写的c++代码index.js用来存放我们的测试代码binding.gyp用来存放编译配置的文件
{
"targets": [
{
"target_name": "native", # 编译后的模块名称
"sources": [ "native.cc" ] # 编译的源文件
}
]
}五、测试
我们在index.js文件中编写我们的测试代码
const path = require("path");
// 导入我们的node插件
const addon = require(path.join(__dirname, "build/Release/native.node"));
const elements = [
{ name: "test1" },
{ name: "test2" },
{ name: "test3" }
];
document = {
all: new addon.undetectable(elements)
}
console.log(document.all); // {}
console.log(typeof document.all); // undefined
console.log(document.all == undefined); // true
console.log(document.all === undefined); // false
console.log(document.all[0] === document.all(0)); // true
console.log(document.all.length); // 3深入探索 Node.js C++ 插件:模拟浏览器 document.all 对象
http://8.134.212.132:8090/archives/shen-ru-tan-suo-node.js-c-cha-jian-mo-ni-liu-lan-qi-document.all-dui-xiang
评论