Proxy and Reflect: Complete Semantics of 13 Traps and Reactive Infrastructure
Chapter 4: Proxy and Reflect โ Complete Semantics of 13 Traps and the Reactive Infrastructure
In the ECMAScript specification, all object operations ultimately reduce to calls to 13 "internal methods." Proxy's design intercepts precisely these 13 internal methods โ not JavaScript's syntax level โ which means regardless of how complex your code is, every operation on an object can be precisely intercepted.
Core Question: What does each of Proxy's 13 traps intercept? Why must Reflect be used alongside it? How does Vue 3 build its reactivity system on this mechanism?
After reading this chapter, you will understand:
- The complete semantics of all 13 traps and the JavaScript operations each one corresponds to
- Reflect's design motivation: how the Receiver parameter solves
thisbinding problems in inheritance scenarios - Vue 3's two sets of Proxy handlers for reactive objects: regular objects vs Map/Set/WeakMap/WeakSet
Level 1 ยท What You Need to Know (1-3 Years Experience)
4.1 ECMAScript's 13 Internal Methods
From the perspective of a JavaScript engine, every object implements internal methods defined by the ECMAScript specification. These internal methods are not directly callable from JavaScript code โ they are mechanisms the engine uses internally when processing various JavaScript operations.
| Internal Method | Corresponding JavaScript Operation |
|---|---|
[[GetPrototypeOf]] |
Object.getPrototypeOf(obj) or obj.__proto__ |
[[SetPrototypeOf]] |
Object.setPrototypeOf(obj, proto) |
[[IsExtensible]] |
Object.isExtensible(obj) |
[[PreventExtensions]] |
Object.preventExtensions(obj) |
[[GetOwnProperty]] |
Object.getOwnPropertyDescriptor(obj, key) |
[[DefineOwnProperty]] |
Object.defineProperty(obj, key, desc) |
[[HasProperty]] |
key in obj |
[[Get]] |
obj.key or obj[key] |
[[Set]] |
obj.key = value or obj[key] = value |
[[Delete]] |
delete obj.key |
[[OwnPropertyKeys]] |
Object.keys(obj), Object.getOwnPropertyNames(obj) |
[[Call]] |
func() or func.call() |
[[Construct]] |
new func() |
Proxy's 13 traps map one-to-one to these 13 internal methods:
const handler = {
getPrototypeOf(target) {}, // [[GetPrototypeOf]]
setPrototypeOf(target, proto) {}, // [[SetPrototypeOf]]
isExtensible(target) {}, // [[IsExtensible]]
preventExtensions(target) {}, // [[PreventExtensions]]
getOwnPropertyDescriptor(target, key) {}, // [[GetOwnProperty]]
defineProperty(target, key, desc) {}, // [[DefineOwnProperty]]
has(target, key) {}, // [[HasProperty]]
get(target, key, receiver) {}, // [[Get]]
set(target, key, value, receiver) {}, // [[Set]]
deleteProperty(target, key) {}, // [[Delete]]
ownKeys(target) {}, // [[OwnPropertyKeys]]
apply(target, thisArg, args) {}, // [[Call]]
construct(target, args, newTarget) {} // [[Construct]]
};
This correspondence is not accidental โ it's Proxy's design philosophy: interception at the JavaScript semantic level, not the syntax level. Regardless of what syntax you use to access an object property (obj.key, obj['key'], Reflect.get(obj, 'key')), it ultimately triggers the [[Get]] internal method, which triggers the get trap.
4.2 The 5 Most Commonly Used Traps in Detail
get(target, key, receiver)
const handler = {
get(target, key, receiver) {
console.log(`Reading ${String(key)}`);
return Reflect.get(target, key, receiver);
}
};
const obj = new Proxy({ name: 'Vue' }, handler);
// All ways that trigger the get trap
obj.name; // "Reading name"
obj['name']; // "Reading name"
'name' in obj; // Does NOT trigger get (triggers has trap)
Object.keys(obj); // Does NOT trigger get (triggers ownKeys trap)
set(target, key, value, receiver)
const handler = {
set(target, key, value, receiver) {
console.log(`Setting ${String(key)} = ${value}`);
const result = Reflect.set(target, key, value, receiver);
return result; // Must return true/false indicating whether the set succeeded
}
};
has(target, key)
const handler = {
has(target, key) {
console.log(`Checking whether ${String(key)} exists`);
return key in target;
}
};
const obj = new Proxy({}, handler);
'name' in obj; // Triggers has trap: "Checking whether name exists"
deleteProperty(target, key)
const handler = {
deleteProperty(target, key) {
console.log(`Deleting ${String(key)}`);
return Reflect.deleteProperty(target, key);
}
};
const obj = new Proxy({ name: 'Vue' }, handler);
delete obj.name; // Triggers deleteProperty trap
ownKeys(target)
const handler = {
ownKeys(target) {
console.log('Enumerating ownKeys');
return Reflect.ownKeys(target);
}
};
const obj = new Proxy({ a: 1, b: 2 }, handler);
Object.keys(obj); // Triggers ownKeys
Object.getOwnPropertyNames(obj); // Triggers ownKeys
Object.getOwnPropertySymbols(obj); // Triggers ownKeys
for (const key in obj) { /* ... */ } // Triggers ownKeys (and has)
4.3 Why Reflect Is Required
You may have noticed that all the trap implementations above call Reflect.get(target, key, receiver) rather than directly using target[key]. This difference is critical โ it produces fundamentally different behavior in inheritance scenarios.
Wrong approach (without Reflect):
const parent = {
get value() {
return this._value;
}
};
const parentProxy = new Proxy(parent, {
get(target, key) {
return target[key]; // Wrong! Incorrect this binding
}
});
const child = Object.create(parentProxy);
child._value = 42;
console.log(child.value); // Returns undefined, not 42!
Why does it return undefined? When child.value is accessed:
childdoesn't have avalueproperty, looks up the prototype chain- Finds
parentProxy, triggers thegettrap - The
gettrap executestarget[key], i.e.,parent['value'] - Executes
parent'svaluegetter, withthisbound toparent(notchild) parent._valueisundefined(_valueis defined onchild, notparent)
Correct approach (using Reflect):
const parentProxy = new Proxy(parent, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver); // receiver = child (original caller)
}
});
console.log(child.value); // Correctly returns 42
The receiver parameter in Reflect.get(target, key, receiver): inside a getter, this is bound to receiver rather than target. This ensures that this inside the getter always points to the original caller (child), not the proxied object (parent).
The role of Reflect's receiver parameter:
Access chain for child.value:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
child โโprototype chainโโโบ parentProxy โโtrapโโโบ parent.value getter
Without Reflect (wrong):
this = target = parent โ parent._value = undefined โ
With Reflect (correct):
this = receiver = child โ child._value = 42 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
4.4 Special Handling for Map/Set
Proxy intercepts property accesses on objects, but Map and Set data is not accessed through properties โ it's accessed through method calls (map.get(key), map.set(key, value), set.add(value), etc.). This means:
const map = new Map([['key', 'value']]);
const proxy = new Proxy(map, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
}
});
proxy.get('key'); // TypeError: Method Map.prototype.get called on incompatible receiver
Why the error? proxy.get first triggers the get trap, which returns Map.prototype.get. Then this method is called with proxy as this. But Map.prototype.get internally uses [[MapData]] (an internal slot), which only exists on real Map instances, not on Proxy objects.
Vue 3 solves this by providing specialized handlers for collection types, binding this back to the original object in the get trap:
// Vue 3's collection handler core approach (simplified)
const collectionHandlers = {
get(target, key, receiver) {
// For collection methods, return a version bound to the original target
if (key === 'get') {
return function(mapKey) {
track(target, TrackOpTypes.GET, mapKey);
return target.get(mapKey); // Call with target, not proxy
};
}
if (key === 'set') {
return function(mapKey, value) {
const hadKey = target.has(mapKey);
const result = target.set(mapKey, value);
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, mapKey, value);
} else {
trigger(target, TriggerOpTypes.SET, mapKey, value);
}
return result;
};
}
// ... other methods
}
};
Level 2 ยท How It Works Under the Hood (3-5 Years Experience)
4.5 Complete Semantics Table for All 13 Traps
| Trap | Intercepted Operations | Return Value Requirement | Usage in Vue 3 |
|---|---|---|---|
get |
obj.key, obj[key], Reflect.get() |
Any value | track dependencies |
set |
obj.key = v, Reflect.set() |
Boolean | trigger updates |
has |
key in obj |
Boolean | track (v-if scenarios) |
deleteProperty |
delete obj.key |
Boolean | trigger delete events |
ownKeys |
Object.keys(), for...in, etc. |
Array of strings/Symbols | track ownKeys |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor() |
Descriptor or undefined | Rarely used |
defineProperty |
Object.defineProperty() |
Boolean | Rarely used |
getPrototypeOf |
Object.getPrototypeOf(), instanceof |
Object or null | Rarely used |
setPrototypeOf |
Object.setPrototypeOf() |
Boolean | Rarely used |
isExtensible |
Object.isExtensible() |
Boolean | Rarely used |
preventExtensions |
Object.preventExtensions() |
Boolean | Rarely used |
apply |
func(), func.call(), func.apply() |
Any value | Not used for object proxying |
construct |
new func() |
Object | Not used for object proxying |
4.6 Vue 3's Two Sets of Proxy Handlers
Vue 3's reactivity system uses different handlers for two categories of objects:
Reactive object classification:
reactive(obj)
โ
โโโ Plain objects/arrays โ mutableHandlers
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ get โ track โ
โ โ set โ trigger โ
โ โ deleteProperty โ trigger โ
โ โ has โ track โ
โ โ ownKeys โ track โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโ Map/Set/WeakMap/WeakSet โ mutableCollectionHandlers
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ get โ returns wrapped method โ
โ .get() โ track + original get โ
โ .set() โ original set + trigger โ
โ .has() โ track + original has โ
โ .add() โ original add + trigger โ
โ .delete() โ original delete + trigger โ
โ .clear() โ original clear + trigger โ
โ .forEach() โ track + original forEach โ
โ .size โ track + original size โ
โ [Symbol.iterator] โ track โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
4.7 Lazy Proxy Mechanism for Nested Objects
Vue 3 does not recursively proxy all nested objects when reactive() is called. Instead, it uses a lazy proxy strategy:
const raw = {
user: {
profile: {
name: 'Vue',
settings: {
theme: 'dark'
}
}
}
};
const state = reactive(raw);
// At this point: only raw itself is proxied
// raw.user, raw.user.profile, raw.user.profile.settings are all NOT proxied yet
const user = state.user;
// Now: triggers get trap, detects raw.user is an object, creates a Proxy for it and returns it
// raw.user is now proxied
const profile = state.user.profile;
// Now: first accesses state.user (triggers get, returns Proxy for raw.user)
// Then accesses .profile (triggers get, detects raw.user.profile is an object, creates Proxy for it)
// raw.user.profile is now also proxied
Memory optimization effect of this mechanism:
Deep nested object, initial access:
raw (proxied)
โโโ .user (not proxied)
โโโ .profile (not proxied)
โโโ .settings (not proxied)
Only layers accessed in templates/code get proxied:
If template only accesses state.user.profile.name:
raw (proxied)
โโโ .user (proxied) โ created when state.user is accessed
โโโ .profile (proxied) โ created when state.user.profile is accessed
โโโ .settings (NOT proxied!) โ never accessed, never proxied
Vue 3 also maintains two WeakMaps to avoid duplicate proxying:
// packages/reactivity/src/reactive.ts
const reactiveMap = new WeakMap<Target, any>() // Cache for reactive results
const shallowReactiveMap = new WeakMap<Target, any>()
const readonlyMap = new WeakMap<Target, any>()
const shallowReadonlyMap = new WeakMap<Target, any>()
function reactive(target: object) {
// Already a readonly proxy, return directly
if (isReadonly(target)) {
return target;
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap // Used for caching
);
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// Check cache: this target has already been proxied
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy; // Return cached Proxy directly
}
const proxy = new Proxy(target, /* ... handlers */);
proxyMap.set(target, proxy); // Cache
return proxy;
}
This ensures that calling reactive() on the same object multiple times returns the same Proxy instance.
4.8 Proxy Invariants
Proxy's traps must obey invariants defined in the ECMAScript specification. If a trap's return value violates these rules, the JavaScript engine throws a TypeError:
Invariant examples:
// Invariant 1: getPrototypeOf must return an object or null
const proxy = new Proxy({}, {
getPrototypeOf() {
return 42; // TypeError! Violates invariant
}
});
// Invariant 2: if the target is non-extensible, ownKeys must contain all target's own keys
const nonExtensible = Object.preventExtensions({ a: 1 });
const proxy2 = new Proxy(nonExtensible, {
ownKeys() {
return ['b']; // TypeError! Violates invariant ('a' must be in the result)
}
});
// Invariant 3: if target.key is non-configurable non-writable, get must return the actual value
const obj = {};
Object.defineProperty(obj, 'fixed', {
value: 42,
configurable: false,
writable: false
});
const proxy3 = new Proxy(obj, {
get(target, key) {
if (key === 'fixed') return 99; // TypeError! Violates invariant
return Reflect.get(target, key);
}
});
These invariants ensure that Proxy cannot violate JavaScript's fundamental object semantics, preventing code from using Proxy to deceive the engine.
Level 3 ยท Design Documents and Source Code (Senior Developers)
4.9 Full Implementation of mutableHandlers in Vue 3
// packages/reactivity/src/baseHandlers.ts (core portions)
// Factory function for the get trap
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// Handle special internal keys (ReactiveFlags)
if (key === ReactiveFlags.IS_REACTIVE) return !isReadonly
if (key === ReactiveFlags.IS_READONLY) return isReadonly
if (key === ReactiveFlags.IS_SHALLOW) return shallow
if (key === ReactiveFlags.RAW) {
// How toRaw() works: getting the original object via __v_raw key
if (receiver === (isReadonly ? (shallow ? shallowReadonlyMap : readonlyMap)
: (shallow ? shallowReactiveMap : reactiveMap)).get(target)
) {
return target
}
return
}
const targetIsArray = isArray(target)
if (!isReadonly) {
// Special array methods (includes, indexOf, lastIndexOf need to track each index)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
}
const res = Reflect.get(target, key, receiver)
// Skip tracking for built-in Symbols (Symbol.iterator, Symbol.toPrimitive, etc.)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// Track dependency
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// shallowReactive: don't recursively proxy
if (shallow) return res
// ref auto-unwrapping (refs inside arrays are NOT auto-unwrapped)
if (isRef(res)) {
return targetIsArray && isIntegerKey(key) ? res : res.value
}
// Lazy proxy: nested objects get proxied only when accessed
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
// set trap implementation
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (!shallow) {
const isOldValueReadonly = isReadonly(oldValue)
if (!isShallow(value) && !isReadonly(value)) {
// Get raw values to avoid nested reactive object interference
oldValue = toRaw(oldValue)
value = toRaw(value)
}
// Important: if target is not an array, old value is a ref, new value is not
// directly update ref.value (preserving the ref's reactivity)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) {
return false
} else {
oldValue.value = value
return true
}
}
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length // Array: check if within bounds
: hasOwn(target, key) // Object: check if key already exists
const result = Reflect.set(target, key, value, receiver)
// Key: only trigger when target is the raw object of receiver
// This prevents duplicate triggering from proxies in the prototype chain
if (target === toRaw(receiver)) {
if (!hadKey) {
// New property: trigger ADD operation
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// Modified property: trigger SET operation (only when value changed)
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
4.10 Source Code Analysis of Collection Type Handlers
// packages/reactivity/src/collectionHandlers.ts (core portions)
// Reactive wrapper for Map.prototype.get
function get(this: MapTypes, key: unknown, isReadonly = false, isShallow = false) {
const target = (this as any)[ReactiveFlags.RAW] // Get the original Map
const rawKey = toRaw(key) // Get the raw version of the key
if (key !== rawKey) {
// If key itself is reactive, track both versions
!isReadonly && track(target, TrackOpTypes.GET, key)
}
!isReadonly && track(target, TrackOpTypes.GET, rawKey)
const { has } = getProto(target)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
if (has.call(target, key)) {
return wrap(target.get(key)) // Nested objects also get made reactive
} else if (has.call(target, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== this) {
target.get(key)
}
}
// Reactive wrapper for Map.prototype.set
function set(this: MapTypes, key: unknown, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value) // New key
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue) // Modified key
}
return this
}
4.11 Implementation of arrayInstrumentations
Array methods includes, indexOf, lastIndexOf don't trigger reactive tracking in their original implementations. Vue 3 needs to handle them specially:
// packages/reactivity/src/baseHandlers.ts
const arrayInstrumentations: Record<string, Function> = {}
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) // Get original array
// Traverse array, tracking dependency on each index
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// Search using original method first
const res = method.apply(arr, args)
if (res === -1 || res === false) {
// Search failed, try again with raw version of arguments
// (arguments may themselves be reactive objects, need raw version for comparison)
return method.apply(arr, args.map(toRaw))
} else {
return res
}
}
})
// Methods that modify array length also need special handling
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
pauseTracking() // Pause dependency tracking (avoid tracking loops during mutation)
const res = method.apply(this, args)
resetTracking() // Resume dependency tracking
return res
}
})
Level 4 ยท Edge Cases and Traps (Everyone Should Read)
Trap 1: The in Operator Triggers has Trap, Affecting Reactive Tracking
import { reactive, watchEffect } from 'vue';
const state = reactive({ name: 'Vue', age: 18 });
watchEffect(() => {
// Using the in operator here
if ('name' in state) {
console.log('Has name property');
}
});
// Adding a new property
state.newProp = 'hello';
// watchEffect re-executes! Even though we only checked 'name'
// Why? The in operator triggers the has trap, and the has trap tracks ownKeys
// When a new property is added, ownKeys changes, and all effects that tracked ownKeys re-execute
Root cause: In Vue 3, the has trap tracks the operation. When a new property is added to an object, it triggers effects that tracked ownKeys. This is correct behavior (you need to know whether a key exists, and should recheck when new keys appear), but it can cause unexpected re-renders.
Trap 2: Operations on Objects Retrieved via toRaw Don't Trigger Reactivity
import { reactive, toRaw, watchEffect } from 'vue';
const state = reactive({ count: 0 });
const raw = toRaw(state);
watchEffect(() => {
console.log(state.count); // Tracks state.count
});
// Correct: modify through the proxy, triggers update
state.count++; // Triggers watchEffect
// Wrong: modify through raw, does NOT trigger update
raw.count++; // watchEffect does NOT execute!
// But raw.count did change โ the reactive system just wasn't notified
console.log(raw.count); // 2
console.log(state.count); // Also 2 (raw is the original object, state is its proxy)
Root cause: toRaw returns the original object behind the Proxy. Modifications to the raw object don't go through the Proxy, don't trigger the set trap, don't trigger trigger, and don't notify any effects. But the modification is real (raw and state point to the same data) โ the next time you access through state, you'll see the updated value.
Trap 3: Proxy Cannot Reactively Track New Properties on Non-Extensible Objects
import { reactive } from 'vue';
const obj = Object.freeze({ count: 0 });
const state = reactive(obj); // Creates a Proxy, but modifications silently fail
// In strict mode, this throws TypeError
// In non-strict mode, modifications are silently ignored
state.count = 1; // In non-strict mode: silent failure, state.count remains 0
state.newProp = 1; // Same silent failure
Vue prints a warning for frozen objects in DEV mode:
[Vue warn]: Set operation on key "count" failed: target is readonly.
Practical scenario: JSON objects received from a server are usually fine, but if you Object.freeze() the received data (which some libraries do to prevent accidental mutation) and then pass it to reactive(), you'll encounter this issue.
Trap 4: Map/Set Reactive Tracking Only Tracks Accessed Keys
import { reactive, watchEffect } from 'vue';
const map = reactive(new Map([['a', 1], ['b', 2]]));
watchEffect(() => {
console.log(map.get('a')); // Only tracks key 'a'
});
map.set('b', 99); // Does NOT trigger! watchEffect only tracked 'a', not 'b'
map.set('a', 99); // Triggers! 'a' was tracked
map.delete('a'); // Triggers! 'a' was deleted
// Special case: effects tracking size trigger on any key change
watchEffect(() => {
console.log(map.size); // Tracks size
});
map.set('c', 3); // Triggers! size changed (2 โ 3)
map.set('a', 99); // Does NOT trigger! size didn't change (still 3, 'a' already existed)
Root cause: Vue 3's Map reactive tracking is precise to the key level. map.get('a') only tracks key 'a'. Operations on other keys don't trigger this effect. This is the correct behavior of precise tracking โ avoiding unnecessary re-renders.
Chapter Summary
-
Proxy's 13 traps correspond to ECMAScript's 13 internal methods โ not JavaScript's syntax constructs. This means regardless of what syntax is used (
obj.key,obj['key'],Reflect.get()), the same trap is triggered. Interception is at the semantic level, not the syntax level. -
Reflect is the companion design for Proxy traps, with its core value in the
receiverparameter. In inheritance scenarios,Reflect.get(target, key, receiver)ensures thatthisinside a getter is bound to the original caller (receiver) rather than the proxied object (target), solving the incorrectthisbinding thattarget[key]produces in prototype chains. -
Vue 3 uses different Proxy handlers for plain objects and collection types:
mutableHandlersfor plain objects and arrays, using the 5 traps get/set/deleteProperty/has/ownKeys directly;collectionHandlersfor Map/Set/WeakMap/WeakSet, because these types' data is accessed through method calls, requiring method interception in the get trap and wrapping withthisbound back to the original object. -
Lazy proxying of nested objects is one of the keys to Vue 3's 55% initialization speed improvement: calling
reactive()only proxies the top-level object; nested objects get a Proxy created only when actually accessed. Combined with WeakMap caching, this ensures only one Proxy instance is ever created per object. -
Proxy has invariant constraints โ you cannot return arbitrary values from traps: ECMAScript enforces rules like "a non-configurable non-writable property's get must return the actual value" and "a non-extensible object's ownKeys must include all existing keys." Violations throw TypeError. Understanding these invariants helps avoid hard-to-debug errors when implementing custom Proxies.