Category: reference
JavaScript Proxy & Reflect
Proxy ดักจับ operations บน object (get, set, delete) และ Reflect ให้ default behavior — ใช้สร้าง reactive data, validation, lazy loading
สารบัญ
Proxy คืออะไร
Proxy ห่อ object ต้นฉบับ แล้วให้เรากำหนด “traps” ที่ intercept operations ต่างๆ ก่อนที่จะส่งต่อไปยัง object จริง
const handler = {
get(target, prop, receiver) { /* intercept property read */ },
set(target, prop, value, receiver) { /* intercept property write */ },
deleteProperty(target, prop) { /* intercept delete */ },
has(target, prop) { /* intercept 'in' operator */ },
apply(target, thisArg, args) { /* intercept function call */ },
};
const proxy = new Proxy(target, handler);
Validation — ตรวจ type ก่อน set
function createTypedObject(schema) {
return new Proxy({}, {
set(target, prop, value) {
const expectedType = schema[prop];
if (!expectedType) throw new Error(`Unknown property: ${String(prop)}`);
if (typeof value !== expectedType) {
throw new TypeError(`${String(prop)} must be ${expectedType}, got ${typeof value}`);
}
target[prop] = value;
return true; // ✓ set ต้อง return true
},
get(target, prop) {
if (!(prop in target)) return undefined;
return target[prop];
},
});
}
const user = createTypedObject({ name: 'string', age: 'number' });
user.name = 'Alice'; // ✓
user.age = 30; // ✓
user.age = '30'; // ❌ TypeError: age must be number
user.email = 'x'; // ❌ Error: Unknown property
Reactive Data (Vue 3-style)
function reactive(obj) {
const subscribers = new Map();
function subscribe(prop, fn) {
if (!subscribers.has(prop)) subscribers.set(prop, new Set());
subscribers.get(prop).add(fn);
return () => subscribers.get(prop).delete(fn); // unsubscribe
}
function notify(prop) {
subscribers.get(prop)?.forEach((fn) => fn());
}
const proxy = new Proxy(obj, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
if (result) notify(String(prop));
return result;
},
});
return { proxy, subscribe };
}
const { proxy: state, subscribe } = reactive({ count: 0 });
subscribe('count', () => console.log('count changed:', state.count));
state.count++; // log: "count changed: 1"
state.count++; // log: "count changed: 2"
Lazy Loading — load ข้อมูลตอน access
function lazyLoad(loader) {
let cache = null;
return new Proxy({}, {
get(_, prop) {
if (!cache) {
console.log('Loading...');
cache = loader(); // โหลดครั้งแรกที่ access
}
return cache[prop];
},
});
}
const config = lazyLoad(() => {
// อ่านไฟล์หรือ API ตอน access ครั้งแรก
return JSON.parse(fs.readFileSync('./config.json', 'utf8'));
});
// ยังไม่โหลด ณ จุดนี้
const dbUrl = config.DATABASE_URL; // โหลดตอนนี้ครั้งเดียว
const apiKey = config.API_KEY; // ใช้ cache แล้ว
Default Values
// Return default value สำหรับ property ที่ไม่มีอยู่
const withDefaults = (obj, defaults) =>
new Proxy(obj, {
get(target, prop) {
return prop in target ? target[prop] : defaults[prop];
},
});
const config = withDefaults(
{ port: 8080 },
{ host: 'localhost', port: 3000, debug: false }
);
config.port // 8080 (จาก target)
config.host // 'localhost' (จาก defaults)
config.debug // false (จาก defaults)
Reflect — Default Behavior
Reflect ให้ default operations ที่เหมือน object ปกติ ใช้คู่กับ Proxy เพื่อ “forward” หลังจาก trap ทำงาน
// ✓ ใช้ Reflect.get แทน target[prop] ตรงๆ
// เพราะ Reflect รองรับ receiver (prototype chain) ถูกต้อง
get(target, prop, receiver) {
console.log(`Reading: ${String(prop)}`);
return Reflect.get(target, prop, receiver); // ✓
}
// เทียบกับ target[prop] ตรงๆ — อาจผิดถ้ามี getter ใน prototype
get(target, prop, receiver) {
return target[prop]; // ❌ อาจ break getter ที่ใช้ this
}
// Reflect methods correspond to Proxy traps:
Reflect.get(target, prop, receiver) // → get trap
Reflect.set(target, prop, value, receiver) // → set trap
Reflect.has(target, prop) // → has trap
Reflect.deleteProperty(target, prop) // → deleteProperty trap
Reflect.apply(target, thisArg, args) // → apply trap
Reflect.construct(target, args, newTarget) // → construct trap
Reflect.ownKeys(target) // → ownKeys trap
Logging / Debug Proxy
function createLogger(obj, label = 'obj') {
return new Proxy(obj, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value !== 'function') {
console.log(`[${label}] get ${String(prop)} = ${JSON.stringify(value)}`);
}
return value;
},
set(target, prop, value, receiver) {
console.log(`[${label}] set ${String(prop)} = ${JSON.stringify(value)}`);
return Reflect.set(target, prop, value, receiver);
},
});
}
const user = createLogger({ name: 'Alice', age: 30 }, 'user');
user.name; // [user] get name = "Alice"
user.age = 31; // [user] set age = 31
Revocable Proxy
// Proxy ที่ยกเลิกได้ — หลังจาก revoke ทุก operation throw TypeError
const { proxy, revoke } = Proxy.revocable({ secret: 42 }, {
get(target, prop) {
return Reflect.get(target, prop);
},
});
proxy.secret; // 42
revoke();
proxy.secret; // ❌ TypeError: Cannot perform 'get' on a proxy that has been revoked
ข้อจำกัด
// ❌ Proxy ไม่สามารถ intercept private fields (#)
class Person {
#name = 'Alice';
getName() { return this.#name; }
}
const p = new Proxy(new Person(), {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
});
p.getName(); // ✓ ทำงาน (method call ผ่าน Proxy)
// แต่ #name เข้าไม่ได้ตรงๆ
// ❌ performance overhead มากกว่า plain object
// ❌ ไม่ทำงานกับ Map, Set, WeakMap — ต้องใช้ custom wrapper