Proxy 有哪些东西是无法拦截的?深入解释 Reflect 与陷阱盲区

556 阅读3分钟

JavaScript 中的 Proxy 是一把极其强大的API,允许你劫持对象的读取、赋值、删除、函数调用等等。但它并不是“无所不能”。在某些边界场景下,Proxy 是拦截不了的,甚至有些你以为能拦的操作,背后根本就没调用 proxy。

本篇将系统梳理 Proxy 中所有 不能拦截 的行为,配合 Reflect 机制一同解析,避免在项目中对 Proxy 抱有不切实际的幻想。


Proxy 可以拦截的操作(简略复习)

先来看 Proxy 到底可以劫持哪些操作。根据 MDN 的定义,常见的 trap 包括:

  • get:属性读取
  • set:属性赋值
  • has:in 操作符
  • deleteProperty:delete 操作
  • ownKeys:Object.keys() / Reflect.ownKeys()
  • getPrototypeOf / setPrototypeOf:原型访问与设置
  • apply / construct:函数调用与 new 操作

但接下来我们重点讲不能劫持的。


❌ Proxy 无法拦截的行为

1. typeof proxy

const p = new Proxy(function () {}, {})
console.log(typeof p) // 'function'

无论你怎么设置 trap,typeof 操作始终不会触发任何 trap(包括 applyconstruct),这是语言层面的保留行为。

typeof 是静态操作,不触发运行时 hook。


2. 原始值转换:String(proxy)Number(proxy)+proxy

const p = new Proxy({
  toString() {
    return 'proxied'
  }
}, {
  get(target, key) {
    console.log('get', key)
    return target[key]
  }
})

console.log(String(p)) // 'proxied'

虽然你可能能拦住 toString 的访问,但你无法拦住整个“类型强转行为”。像 +proxyNumber(proxy)== null 等比较运算,底层通过 ToPrimitive 转换,不触发 trap。

除非你拦截的是一个函数调用的属性访问阶段,否则基本无解。


3. instanceof 操作符(如果目标不是构造函数)

const target = {}
const proxy = new Proxy(target, {})

console.log(proxy instanceof Object) // true

因为 instanceof 是基于 proxy.[[Prototype]] 来查找构造链的,除非你修改其 getPrototypeOf,否则也无法改变。

更重要的是:

  • 如果目标不是 callable,instanceof 也不会触发任何 trap。
  • 要改变 instanceof 的行为,只能返回一个特殊的构造器对象。

4. with 作用域链内部绑定

const p = new Proxy({ secret: 42 }, {
  has(t, k) {
    console.log('has', k)
    return true
  }
})

with (p) {
  console.log(secret) // 42,但不会触发 get!
}

使用 with 时,虽然触发了 has trap(用于作用域绑定判断),但属性读取是直接绑定原始对象的内部值,不走 get trap!

这是一个非常冷门的行为,通常只有你在 try-catch + sandbox 时才会踩到。


5. Object.prototype.toString.call(proxy)[object X] 不可自定义

Object.prototype.toString.call(new Proxy({}, {}))
// [object Object] —— 无法改变

即便你设置了 Symbol.toStringTag,但 proxy 仍然受限于宿主对象类型。

const proxy = new Proxy({
  [Symbol.toStringTag]: 'Custom'
}, {})

console.log(Object.prototype.toString.call(proxy)) // [object Object] 而不是 [object Custom]

6. JSON.stringify 无法被完全拦截

const proxy = new Proxy({
  toJSON() {
    return 'hello'
  }
}, {
  get(t, k) {
    console.log('get', k)
    return t[k]
  }
})

console.log(JSON.stringify(proxy)) // "hello",触发了 toJSON,但无法完全控制字符串化

虽然你可以拦截 toJSON,但无法拦截 JSON.stringify 的整体行为逻辑。


🤖 Reflect 是干什么用的?为啥 trap 里都要写 Reflect?

Proxy 的 trap 中,正确的写法通常长这样:

get(target, key, receiver) {
  return Reflect.get(target, key, receiver)
}

而不是:

get(target, key) {
  return target[key]
}

这是因为 Reflect 保留了原始对象操作的行为语义,确保原型链正确、上下文正确(特别是 setter / getter 中的 this)。

最常见的错误是:使用 target[key] 触发了意外的 getter / proxy 嵌套,导致递归。

所以,除非你明确要修改行为,否则建议所有 trap 都用 Reflect.xxx


你还有什么操作你试过 Proxy ,但“没反应”?

📌 你可以继续看我的系列文章