ข้ามไปเนื้อหาหลัก

Category: reference

JavaScript Proxy & Reflect

Proxy ดักจับ operations บน object (get, set, delete) และ Reflect ให้ default behavior — ใช้สร้าง reactive data, validation, lazy loading

· อ่านประมาณ 4 นาที

สารบัญ

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