JavaScript属性描述符与Proxy元编程—13种拦截陷阱详解|新宇宙博客 'name'
console
log
Object
getOwnPropertyDescriptors
描述符字段含义 字段 含义 默认值(defineProperty) 默认值(字面量) value 属性值 undefined 设定值 writable 能否修改 value false true enumerable 能否被枚举 false true configurable 能否删除/重新定义 false true get getter 函数 undefined — set setter 函数 undefined —
2. 数据描述符 vs 访问器描述符
const person = {};
Object .defineProperty (person, 'name' , {
value : 'Alice' ,
writable : true ,
enumerable : true ,
configurable : true
});
let _age = 25 ;
Object .defineProperty (person, 'age' , {
get ( ) { return _age; },
set (val ) {
if (typeof val !== 'number' || val < 0 ) throw new TypeError ('Invalid age' );
_age = val;
},
enumerable : true ,
configurable : true
});
person.age = 30 ;
console .log (person.age );
person.age = -1 ;
3. Object.defineProperty 深度用法
const CONFIG = {};
Object .defineProperty (CONFIG , 'API_URL' , {
value : 'https://api.example.com' ,
writable : false ,
enumerable : true ,
configurable : false
});
CONFIG .API_URL = 'hacked' ;
delete CONFIG .API_URL ;
Object .defineProperty (CONFIG , 'API_URL' , { value : 'new' });
class EventEmitter {
constructor ( ) {
Object .defineProperty (this , '_events' , {
value : {},
writable : true ,
enumerable : false ,
configurable : false
});
}
}
const emitter = new EventEmitter ();
console .log (Object .keys (emitter));
console .log (JSON .stringify (emitter));
console .log (emitter._events );
4. 对象冻结三级机制
const obj = { a : 1 , b : { c : 2 } };
Object .preventExtensions (obj);
obj.newProp = 'x' ;
obj.a = 10 ;
delete obj.a ;
const obj2 = { a : 1 };
Object .seal (obj2);
obj2.a = 10 ;
delete obj2.a ;
const obj3 = { a : 1 , nested : { b : 2 } };
Object .freeze (obj3);
obj3.a = 10 ;
obj3.nested .b = 20 ;
function deepFreeze (obj ) {
Object .freeze (obj);
for (const key of Object .getOwnPropertyNames (obj)) {
const val = obj[key];
if (val && typeof val === 'object' && !Object .isFrozen (val)) {
deepFreeze (val);
}
}
return obj;
}
5. Proxy 的 13 种陷阱
const handler = {
get (target, prop, receiver ) {},
set (target, prop, value, receiver ) {},
has (target, prop ) {},
deleteProperty (target, prop ) {},
ownKeys (target ) {},
getOwnPropertyDescriptor (target, prop ) {},
defineProperty (target, prop, descriptor ) {},
getPrototypeOf (target ) {},
setPrototypeOf (target, proto ) {},
isExtensible (target ) {},
preventExtensions (target ) {},
apply (target, thisArg, args ) {},
construct (target, args, newTarget ) {}
};
验证代理
function createValidatedObject (schema ) {
const data = {};
return new Proxy (data, {
set (target, prop, value ) {
const validator = schema[prop];
if (!validator) {
throw new Error (`Unknown property: ${prop} ` );
}
const { type, required, min, max, validate } = validator;
if (required && (value === null || value === undefined )) {
throw new TypeError (`${prop} is required` );
}
if (type && typeof value !== type) {
throw new TypeError (`${prop} must be ${type} , got ${typeof value} ` );
}
if (min !== undefined && value < min) {
throw new RangeError (`${prop} must be >= ${min} ` );
}
if (max !== undefined && value > max) {
throw new RangeError (`${prop} must be <= ${max} ` );
}
if (validate && !validate (value)) {
throw new Error (`${prop} validation failed` );
}
target[prop] = value;
return true ;
},
get (target, prop ) {
if (!(prop in schema)) {
throw new Error (`Unknown property: ${prop} ` );
}
return target[prop];
}
});
}
const user = createValidatedObject ({
name : { type : 'string' , required : true },
age : { type : 'number' , min : 0 , max : 150 },
email : { type : 'string' , validate : v => v.includes ('@' ) }
});
user.name = 'Alice' ;
user.age = 200 ;
6. Reflect API 与不变量
const loggingProxy = new Proxy ({}, {
get (target, prop, receiver ) {
console .log (`GET ${String (prop)} ` );
return Reflect .get (target, prop, receiver);
},
set (target, prop, value, receiver ) {
console .log (`SET ${String (prop)} = ${value} ` );
return Reflect .set (target, prop, value, receiver);
},
deleteProperty (target, prop ) {
console .log (`DELETE ${String (prop)} ` );
return Reflect .deleteProperty (target, prop);
}
});
console .log (Reflect .defineProperty ({}, 'x' , { value : 1 }));
class Parent {
get value () { return 'parent' ; }
}
class Child extends Parent {
get value () { return 'child' ; }
}
Proxy 不变量(Invariants)
const frozen = Object .freeze ({ x : 1 });
const proxy = new Proxy (frozen, {
get (target, prop ) {
return 'hacked' ;
}
});
console .log (proxy.x );
7. 可撤销 Proxy
const { proxy, revoke } = Proxy .revocable ({ data : 'secret' }, {
get (target, prop ) {
return target[prop];
}
});
console .log (proxy.data );
revoke ();
console .log (proxy.data );
function createSafeApi (realApi, timeout ) {
const { proxy, revoke } = Proxy .revocable (realApi, {});
setTimeout (revoke, timeout);
return proxy;
}
8. 实战案例
实战案例 1:Vue 3 响应式系统核心 const targetMap = new WeakMap ();
let activeEffect = null ;
function reactive (target ) {
return new Proxy (target, {
get (target, key, receiver ) {
track (target, key);
const result = Reflect .get (target, key, receiver);
if (typeof result === 'object' && result !== null ) {
return reactive (result);
}
return result;
},
set (target, key, value, receiver ) {
const oldValue = target[key];
const result = Reflect .set (target, key, value, receiver);
if (oldValue !== value) {
trigger (target, key);
}
return result;
},
deleteProperty (target, key ) {
const hadKey = key in target;
const result = Reflect .deleteProperty (target, key);
if (hadKey && result) {
trigger (target, key);
}
return result;
}
});
}
function track (target, key ) {
if (!activeEffect) return ;
let depsMap = targetMap.get (target);
if (!depsMap) targetMap.set (target, (depsMap = new Map ()));
let dep = depsMap.get (key);
if (!dep) depsMap.set (key, (dep = new Set ()));
dep.add (activeEffect);
}
function trigger (target, key ) {
const depsMap = targetMap.get (target);
if (!depsMap) return ;
const effects = depsMap.get (key);
effects?.forEach (effect => effect ());
}
实战案例 2:负索引数组 function createNegativeIndexArray (...items ) {
return new Proxy (items, {
get (target, prop, receiver ) {
const index = Number (prop);
if (Number .isInteger (index) && index < 0 ) {
prop = String (target.length + index);
}
return Reflect .get (target, prop, receiver);
},
set (target, prop, value, receiver ) {
const index = Number (prop);
if (Number .isInteger (index) && index < 0 ) {
prop = String (target.length + index);
}
return Reflect .set (target, prop, value, receiver);
}
});
}
const arr = createNegativeIndexArray (1 , 2 , 3 , 4 , 5 );
console .log (arr[-1 ]);
console .log (arr[-2 ]);
arr[-1 ] = 99 ;
console .log (arr[4 ]);
实战案例 3:自动 API 客户端 function createApiClient (baseUrl ) {
return new Proxy ({}, {
get (target, resource ) {
return new Proxy ({}, {
get (target, method ) {
return async function (params = {} ) {
const url = new URL (`${baseUrl} /${resource} ` );
switch (method) {
case 'list' :
Object .entries (params).forEach (([k, v] ) => url.searchParams .set (k, v));
return fetch (url).then (r => r.json ());
case 'get' :
return fetch (`${url} /${params.id} ` ).then (r => r.json ());
case 'create' :
return fetch (url, { method : 'POST' , body : JSON .stringify (params), headers : { 'Content-Type' : 'application/json' } }).then (r => r.json ());
case 'update' :
return fetch (`${url} /${params.id} ` , { method : 'PUT' , body : JSON .stringify (params), headers : { 'Content-Type' : 'application/json' } }).then (r => r.json ());
case 'delete' :
return fetch (`${url} /${params.id} ` , { method : 'DELETE' });
}
};
}
});
}
});
}
const api = createApiClient ('https://api.example.com' );
const users = await api.users .list ({ page : 1 });
const user = await api.users .get ({ id : 1 });
await api.users .create ({ name : 'Alice' , email : 'alice@test.com' });
9. 深度追问
Q1:Proxy 的性能开销有多大? V8 中 Proxy 调用比直接属性访问慢约 5-10 倍。但在 I/O 密集场景中可以忽略。关键点:Proxy 会阻止 V8 的内联缓存(IC)优化,因此在热路径中应避免使用。Vue 3 通过 markRaw 和 shallowReactive 提供了跳过 Proxy 的选项。
Q2:defineProperty vs Proxy 的根本区别? defineProperty 是对单个属性 的拦截,需要预先知道属性名。Proxy 是对整个对象 操作的拦截,可以捕获未知属性的访问。这就是 Vue 2(defineProperty)不能检测新属性添加,而 Vue 3(Proxy)可以的原因。
Q3:如何让 Proxy 对象通过 typeof 和 instanceof 检测? Proxy 是完全透明的——typeof proxy === typeof target,proxy instanceof Constructor 也会正确工作。但 proxy === target 为 false(它们是不同的对象)。对于 WeakMap/WeakSet 键,proxy 和 target 被视为不同的键。
10. 总结表格 元编程工具 粒度 拦截范围 性能 defineProperty单属性 get/set 高 Proxy整对象 13 种操作 低 Symbol协议定制 特定行为 高 Reflect辅助 API 转发操作 高
Proxy 陷阱 触发操作 典型用途 get obj.prop数据拦截/默认值 set obj.prop = val验证/响应式 has prop in obj隐藏属性 deleteProperty delete obj.prop保护属性 ownKeys Object.keys()过滤属性 apply fn()函数拦截 construct new Fn()工厂模式