Proxy and Reflect are powerful metaprogramming features introduced in ES6. This article covers the usage and practical applications of these concepts.

Proxy Basics

Creating Proxies

// Basic syntax
const target = { name: 'Alice', age: 25 };
const handler = {
  get(target, prop, receiver) {
    console.log(`Accessing property: ${prop}`);
    return target[prop];
  }
};

const proxy = new Proxy(target, handler);
proxy.name;  // Output: Accessing property: name, Returns: 'Alice'

// Empty handler (transparent proxy)
const transparentProxy = new Proxy(target, {});
transparentProxy.name;  // 'Alice' (direct access to original)

Revocable Proxies

// Create revocable proxy
const { proxy, revoke } = Proxy.revocable(target, handler);

proxy.name;  // Works normally

// Revoke the proxy
revoke();

// Access after revocation throws error
proxy.name;  // TypeError: Cannot perform 'get' on a proxy that has been revoked

Common Traps

get Trap

const handler = {
  get(target, prop, receiver) {
    // Return default value for missing properties
    if (!(prop in target)) {
      return `Property ${prop} does not exist`;
    }
    return target[prop];
  }
};

const obj = new Proxy({ name: 'Alice' }, handler);
obj.name;     // 'Alice'
obj.unknown;  // 'Property unknown does not exist'

// Implement negative array indices
const arr = new Proxy([1, 2, 3, 4, 5], {
  get(target, prop, receiver) {
    const index = Number(prop);
    if (index < 0) {
      prop = target.length + index;
    }
    return target[prop];
  }
});

arr[-1];  // 5
arr[-2];  // 4

set Trap

// Data validation
const validator = {
  set(target, prop, value, receiver) {
    if (prop === 'age') {
      if (typeof value !== 'number') {
        throw new TypeError('Age must be a number');
      }
      if (value < 0 || value > 150) {
        throw new RangeError('Age must be between 0 and 150');
      }
    }
    target[prop] = value;
    return true;  // Indicates success
  }
};

const person = new Proxy({}, validator);
person.age = 25;    // OK
person.age = -1;    // RangeError
person.age = 'old'; // TypeError

has Trap

// Hide private properties
const handler = {
  has(target, prop) {
    if (prop.startsWith('_')) {
      return false;  // Hide properties starting with _
    }
    return prop in target;
  }
};

const obj = new Proxy({ name: 'Alice', _secret: 123 }, handler);
'name' in obj;     // true
'_secret' in obj;  // false (exists but hidden)

deleteProperty Trap

// Prevent deleting certain properties
const handler = {
  deleteProperty(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error(`Cannot delete private property ${prop}`);
    }
    delete target[prop];
    return true;
  }
};

const obj = new Proxy({ name: 'Alice', _id: 1 }, handler);
delete obj.name;  // Success
delete obj._id;   // Error: Cannot delete private property _id

ownKeys Trap

// Filter enumerable properties
const handler = {
  ownKeys(target) {
    // Hide properties starting with _
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
};

const obj = new Proxy(
  { name: 'Alice', age: 25, _secret: 'hidden' },
  handler
);

Object.keys(obj);        // ['name', 'age']
Object.values(obj);      // ['Alice', 25]
Object.entries(obj);     // [['name', 'Alice'], ['age', 25]]

apply Trap

// Function call interception
const handler = {
  apply(target, thisArg, args) {
    console.log(`Calling function with args: ${args}`);
    return target.apply(thisArg, args);
  }
};

function sum(a, b) {
  return a + b;
}

const proxySum = new Proxy(sum, handler);
proxySum(1, 2);  // Output: Calling function with args: 1,2, Returns: 3

// Function execution timing
const timedHandler = {
  apply(target, thisArg, args) {
    const start = performance.now();
    const result = target.apply(thisArg, args);
    const end = performance.now();
    console.log(`Execution time: ${end - start}ms`);
    return result;
  }
};

construct Trap

// Intercept new operator
const handler = {
  construct(target, args, newTarget) {
    console.log(`Creating instance with args: ${args}`);
    return new target(...args);
  }
};

class Person {
  constructor(name) {
    this.name = name;
  }
}

const ProxyPerson = new Proxy(Person, handler);
const p = new ProxyPerson('Alice');
// Output: Creating instance with args: Alice

Reflect API

Reflect Basics

// Reflect methods correspond one-to-one with Proxy traps
const obj = { name: 'Alice', age: 25 };

// Property operations
Reflect.get(obj, 'name');           // 'Alice'
Reflect.set(obj, 'age', 26);        // true
Reflect.has(obj, 'name');           // true
Reflect.deleteProperty(obj, 'age'); // true

// Define property
Reflect.defineProperty(obj, 'city', {
  value: 'New York',
  writable: true
});

// Get property descriptor
Reflect.getOwnPropertyDescriptor(obj, 'name');

// Get prototype
Reflect.getPrototypeOf(obj);

// Set prototype
Reflect.setPrototypeOf(obj, null);

Using Reflect in Proxy

// Recommended: use Reflect in Proxy to forward operations
const handler = {
  get(target, prop, receiver) {
    console.log(`Accessing: ${prop}`);
    // Use Reflect to maintain correct this binding
    return Reflect.get(target, prop, receiver);
  },

  set(target, prop, value, receiver) {
    console.log(`Setting: ${prop} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
};

// Importance of receiver parameter
const parent = new Proxy({
  get name() {
    return this._name;
  },
  _name: 'Parent'
}, handler);

const child = {
  __proto__: parent,
  _name: 'Child'
};

// Using Reflect.get(target, prop, receiver)
// receiver ensures this in getter points to correct object
child.name;  // 'Child' (not 'Parent')

Reflect vs Traditional Methods

// Reflect.get vs obj[prop]
const obj = { name: 'Alice' };
Reflect.get(obj, 'name');  // 'Alice'
obj['name'];               // 'Alice' (same effect)

// Reflect.set returns boolean for success/failure
const frozen = Object.freeze({ x: 1 });
Reflect.set(frozen, 'x', 2);  // false (set failed)
// frozen.x = 2;              // Silent fail or throws in strict mode

// Reflect.defineProperty returns boolean
const success = Reflect.defineProperty(obj, 'age', { value: 25 });
// vs Object.defineProperty throws on failure

// Reflect.has vs in operator
Reflect.has(obj, 'name');  // true
'name' in obj;             // true

// Reflect.deleteProperty vs delete
Reflect.deleteProperty(obj, 'name');  // true (returns success)
delete obj.name;                      // true (can't distinguish success from non-existent)

Practical Applications

Reactive System

// Vue 3 style reactivity implementation
function reactive(target) {
  const handlers = {
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);
      track(target, prop);  // Collect dependencies
      // Recursively proxy nested objects
      if (typeof value === 'object' && value !== null) {
        return reactive(value);
      }
      return value;
    },

    set(target, prop, value, receiver) {
      const oldValue = target[prop];
      const result = Reflect.set(target, prop, value, receiver);
      if (oldValue !== value) {
        trigger(target, prop);  // Trigger updates
      }
      return result;
    }
  };

  return new Proxy(target, handlers);
}

// Simplified dependency tracking
let activeEffect = null;
const targetMap = new WeakMap();

function track(target, prop) {
  if (activeEffect) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()));
    }
    let deps = depsMap.get(prop);
    if (!deps) {
      depsMap.set(prop, (deps = new Set()));
    }
    deps.add(activeEffect);
  }
}

function trigger(target, prop) {
  const depsMap = targetMap.get(target);
  if (depsMap) {
    const deps = depsMap.get(prop);
    if (deps) {
      deps.forEach(effect => effect());
    }
  }
}

Data Validation

// Create validated object
function createValidated(target, schema) {
  return new Proxy(target, {
    set(target, prop, value, receiver) {
      const validator = schema[prop];
      if (validator && !validator(value)) {
        throw new Error(`Property ${prop} validation failed`);
      }
      return Reflect.set(target, prop, value, receiver);
    }
  });
}

const userSchema = {
  name: v => typeof v === 'string' && v.length > 0,
  age: v => typeof v === 'number' && v >= 0 && v <= 150,
  email: v => /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(v)
};

const user = createValidated({}, userSchema);
user.name = 'Alice';       // OK
user.age = 25;             // OK
user.email = 'test@example.com';  // OK
user.age = -1;             // Error: Property age validation failed

Access Logging

// Log all property access
function createLogger(target, name = 'Object') {
  return new Proxy(target, {
    get(target, prop, receiver) {
      console.log(`[GET] ${name}.${prop}`);
      return Reflect.get(target, prop, receiver);
    },

    set(target, prop, value, receiver) {
      console.log(`[SET] ${name}.${prop} = ${JSON.stringify(value)}`);
      return Reflect.set(target, prop, value, receiver);
    },

    deleteProperty(target, prop) {
      console.log(`[DELETE] ${name}.${prop}`);
      return Reflect.deleteProperty(target, prop);
    }
  });
}

const user = createLogger({ name: 'Alice' }, 'user');
user.name;           // [GET] user.name
user.age = 25;       // [SET] user.age = 25
delete user.age;     // [DELETE] user.age

Caching Proxy

// Function result caching
function memoize(fn) {
  const cache = new Map();

  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);

      if (cache.has(key)) {
        console.log('Cache hit');
        return cache.get(key);
      }

      console.log('Computing result');
      const result = Reflect.apply(target, thisArg, args);
      cache.set(key, result);
      return result;
    }
  });
}

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoFib = memoize(fibonacci);
memoFib(40);  // Computing result (slow)
memoFib(40);  // Cache hit (instant)

Singleton Pattern

// Implement singleton using Proxy
function createSingleton(ClassName) {
  let instance = null;

  return new Proxy(ClassName, {
    construct(target, args, newTarget) {
      if (!instance) {
        instance = Reflect.construct(target, args, newTarget);
      }
      return instance;
    }
  });
}

class Database {
  constructor(connection) {
    this.connection = connection;
    console.log('Creating database connection');
  }
}

const SingletonDB = createSingleton(Database);

const db1 = new SingletonDB('mysql://...');
const db2 = new SingletonDB('postgres://...');  // Won't create new instance

db1 === db2;  // true

Immutable Objects

// Deep freeze object
function deepFreeze(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);
      if (typeof value === 'object' && value !== null) {
        return deepFreeze(value);
      }
      return value;
    },

    set() {
      throw new Error('Object is read-only');
    },

    deleteProperty() {
      throw new Error('Cannot delete property from read-only object');
    }
  });
}

const config = deepFreeze({
  api: {
    baseUrl: 'https://api.example.com',
    timeout: 5000
  }
});

config.api.baseUrl = 'xxx';  // Error: Object is read-only

Important Considerations

Performance

// Proxy has performance overhead
// Avoid excessive use in performance-critical paths

// Bad practice: complex proxy in hot path
const arr = new Proxy(largeArray, complexHandler);
for (let i = 0; i < arr.length; i++) {
  arr[i];  // Every access goes through proxy
}

// Better: get raw data when needed
const rawData = getOriginalData(arr);
for (let i = 0; i < rawData.length; i++) {
  rawData[i];  // Direct access
}

this Binding Issues

// Some built-in objects may have this binding issues
const map = new Map();
const proxyMap = new Proxy(map, {});

// This will throw because Map methods need correct this
// proxyMap.set('key', 'value');  // TypeError

// Solution: bind this
const proxyMap2 = new Proxy(map, {
  get(target, prop, receiver) {
    const value = Reflect.get(target, prop, receiver);
    if (typeof value === 'function') {
      return value.bind(target);
    }
    return value;
  }
});

proxyMap2.set('key', 'value');  // Works correctly

Best Practices Summary

Proxy and Reflect Best Practices:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Proxy Usage                                       │
│   ├── Data validation and type checking            │
│   ├── Implement reactive systems                   │
│   ├── Access control and logging                   │
│   └── Create immutable data structures             │
│                                                     │
│   Reflect Usage                                     │
│   ├── Forward operations in Proxy                  │
│   ├── Get success/failure as boolean               │
│   ├── Maintain correct receiver binding            │
│   └── Replace some Object static methods           │
│                                                     │
│   Considerations                                    │
│   ├── Be aware of performance overhead             │
│   ├── Handle this binding issues                   │
│   ├── Consider revocable proxies                   │
│   └── Design trap handlers carefully               │
│                                                     │
└─────────────────────────────────────────────────────┘
TrapInterceptsReflect Method
getProperty readReflect.get()
setProperty assignmentReflect.set()
hasin operatorReflect.has()
deletePropertydelete operatorReflect.deleteProperty()
applyFunction callReflect.apply()
constructnew operatorReflect.construct()

Master Proxy and Reflect to unlock JavaScript metaprogramming capabilities.