必须用 reflect.construct(target, args, newtarget) 透传 newtarget,否则子类原型链断裂、instanceof 失败;错误写法 return new target(...args) 会丢失 new.target 语义。

construct 拦截必须用 new.target 判断构造调用上下文
直接在 construct 陷阱里返回新实例是常见错误——它会绕过原类的原型链和初始化逻辑,导致 instanceof 失败、this 绑定异常、装饰器失效等问题。关键在于:必须用 Reflect.construct(target, args, newTarget) 并透传 newTarget(即 new.target),否则子类继承链就断了。
典型错误写法:return new target(...args) —— 这会丢失 new.target,子类调用时无法正确指向子类构造器。
- 正确做法始终使用
Reflect.construct(target, args, newTarget) - 若需修改参数,只改
args数组,不碰newTarget - 拦截器中访问
new.target必须从第三个参数取,不能从target推导
如何安全注入日志而不破坏实例化流程
日志埋点逻辑必须放在 Reflect.construct 调用前后,但不能包裹整个过程——否则异步上报会拖慢构造、甚至引发竞态(比如该实例刚创建就被另一个异步日志请求再次拦截)。同步打点是唯一稳妥方式。
- 构造前记录:类名、参数长度、是否为子类实例化(对比
target === newTarget) - 构造后记录:仅记录成功实例的
constructor.name和内存地址(obj.constructor可读,但避免调用toString()等可能触发副作用的方法) - 禁止在
construct中发起fetch或setTimeout,这会污染构造时序
示例片段:
FastAPI + Flask 混合部署最佳实践,解决路由定义、API 代理等常见问题,适用于同时运行 FastAPI API 与 Flask 前端的场景。
const handler = {
construct(target, args, newTarget) {
console.log('[construct-enter]', {
class: target.name,
isSubclass: target !== newTarget,
argCount: args.length
});
const instance = Reflect.construct(target, args, newTarget);
console.log('[construct-exit]', {
class: instance.constructor.name,
id: instance // 不做序列化,只留引用供后续弱映射关联
});
return instance;
}
};
Proxy 无法代理未被 new 调用的类——这是设计限制
construct 陷阱只在明确使用 new 关键字时触发。如果类被当作普通函数调用(如 MyClass()),或通过 Reflect.construct 显式绕过(且未传 newTarget),则该陷阱完全不生效。这意味着你无法靠 Proxy 拦截所有“类使用场景”,只能覆盖标准构造调用路径。
- 静态方法调用(
MyClass.staticMethod())不受影响,需另配get陷阱处理 - 箭头函数或普通函数即使命名以大写字母开头,也不会触发
construct - TypeScript 编译后的类仍走 JS 原生机制,不影响 Proxy 行为
真正零侵入的前提:类定义本身不能有 new 以外的暴露入口
如果业务代码存在工厂函数、单例缓存、依赖注入容器(如 InversifyJS)或类静态 create 方法,这些路径会完全绕过 construct 陷阱。Proxy 只能劫持语言层最底层的 new 操作,对上层封装无感知。
换句话说:只要有一处代码没走 new MyClass(),而是走 factory.create('MyClass') 或 MyClass.getInstance(),你的埋点就漏了。这类漏点比语法错误更难发现,因为运行时不会报错,只会静默缺失数据。










