Advanced JavaScriptFeatured

JavaScript Proxy and Reflect API: Metaprogramming Power

Master JavaScript's Proxy and Reflect APIs for powerful metaprogramming. Learn to intercept operations, create reactive objects, and implement advanced patterns.

By JavaScript Document Team
proxyreflectmetaprogrammingadvancedes6

The Proxy and Reflect APIs, introduced in ES6, provide powerful metaprogramming capabilities in JavaScript. They allow you to intercept and customize fundamental operations on objects, opening up new possibilities for reactive programming, validation, and more.

Understanding Proxy

A Proxy wraps another object (the target) and intercepts operations performed on it. You define handler functions (traps) that run when specific operations occur.

Basic Proxy Creation

const target = {
  name: 'John',
  age: 30,
};

const handler = {
  get(target, property, receiver) {
    console.log(`Getting ${property}`);
    return target[property];
  },

  set(target, property, value, receiver) {
    console.log(`Setting ${property} = ${value}`);
    target[property] = value;
    return true;
  },
};

const proxy = new Proxy(target, handler);

// Using the proxy
console.log(proxy.name); // Logs: Getting name, John
proxy.age = 31; // Logs: Setting age = 31

Common Proxy Traps

Property Access Traps

const user = {
  name: 'Alice',
  _id: '123',
  email: 'alice@example.com',
};

const secureUser = new Proxy(user, {
  // get trap
  get(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error(`Access denied to private property '${prop}'`);
    }
    return target[prop];
  },

  // set trap
  set(target, prop, value) {
    if (prop.startsWith('_')) {
      throw new Error(`Cannot set private property '${prop}'`);
    }
    if (prop === 'email' && !value.includes('@')) {
      throw new Error('Invalid email format');
    }
    target[prop] = value;
    return true;
  },

  // has trap (for 'in' operator)
  has(target, prop) {
    if (prop.startsWith('_')) {
      return false;
    }
    return prop in target;
  },

  // deleteProperty trap
  deleteProperty(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error(`Cannot delete private property '${prop}'`);
    }
    delete target[prop];
    return true;
  },
});

console.log(secureUser.name); // Alice
console.log('_id' in secureUser); // false
// secureUser._id                    // Error: Access denied

Function Traps

// apply trap for function calls
function sum(...args) {
  return args.reduce((a, b) => a + b, 0);
}

const trackedSum = new Proxy(sum, {
  apply(target, thisArg, argumentsList) {
    console.log(`Called with args: ${argumentsList}`);
    const result = target.apply(thisArg, argumentsList);
    console.log(`Result: ${result}`);
    return result;
  },
});

trackedSum(1, 2, 3); // Logs call info, returns 6

// construct trap for 'new' operator
class User {
  constructor(name) {
    this.name = name;
  }
}

const TrackedUser = new Proxy(User, {
  construct(target, args, newTarget) {
    console.log(`Creating user: ${args[0]}`);
    return new target(...args);
  },
});

const user = new TrackedUser('Bob'); // Logs: Creating user: Bob

Real-World Proxy Patterns

1. Reactive Objects (Like Vue.js)

function createReactive(target, onChange) {
  const handlers = {
    get(target, property, receiver) {
      const value = Reflect.get(target, property, receiver);

      // Make nested objects reactive too
      if (value && typeof value === 'object') {
        return createReactive(value, onChange);
      }

      return value;
    },

    set(target, property, value, receiver) {
      const oldValue = target[property];
      const result = Reflect.set(target, property, value, receiver);

      if (oldValue !== value) {
        onChange(property, value, oldValue);
      }

      return result;
    },

    deleteProperty(target, property) {
      const oldValue = target[property];
      const result = Reflect.deleteProperty(target, property);

      if (result) {
        onChange(property, undefined, oldValue);
      }

      return result;
    },
  };

  return new Proxy(target, handlers);
}

// Usage
const state = createReactive(
  {
    user: {
      name: 'Alice',
      settings: {
        theme: 'dark',
      },
    },
    count: 0,
  },
  (prop, newVal, oldVal) => {
    console.log(`${prop} changed from ${oldVal} to ${newVal}`);
  }
);

state.count = 1; // count changed from 0 to 1
state.user.name = 'Bob'; // name changed from Alice to Bob
state.user.settings.theme = 'light'; // theme changed from dark to light

2. Validation Proxy

function createValidator(target, schema) {
  return new Proxy(target, {
    set(target, property, value) {
      const validator = schema[property];

      if (!validator) {
        throw new Error(`Unknown property: ${property}`);
      }

      if (validator.type && typeof value !== validator.type) {
        throw new TypeError(`${property} must be ${validator.type}`);
      }

      if (validator.validator && !validator.validator(value)) {
        throw new Error(`Invalid value for ${property}`);
      }

      if (validator.transform) {
        value = validator.transform(value);
      }

      return Reflect.set(target, property, value);
    },
  });
}

// Schema definition
const userSchema = {
  name: {
    type: 'string',
    validator: (v) => v.length > 0,
  },
  age: {
    type: 'number',
    validator: (v) => v >= 0 && v <= 150,
  },
  email: {
    type: 'string',
    validator: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
    transform: (v) => v.toLowerCase(),
  },
};

const user = createValidator({}, userSchema);
user.name = 'John'; // OK
user.email = 'JOHN@EXAMPLE.COM'; // Stored as john@example.com
// user.age = '30';              // TypeError: age must be number
// user.age = 200;               // Error: Invalid value for age

3. Property Case Conversion

function caseInsensitiveProxy(target) {
  return new Proxy(target, {
    get(target, property) {
      if (typeof property === 'string') {
        property = property.toLowerCase();
      }
      return Reflect.get(target, property);
    },

    set(target, property, value) {
      if (typeof property === 'string') {
        property = property.toLowerCase();
      }
      return Reflect.set(target, property, value);
    },

    has(target, property) {
      if (typeof property === 'string') {
        property = property.toLowerCase();
      }
      return Reflect.has(target, property);
    },
  });
}

const headers = caseInsensitiveProxy({});
headers['Content-Type'] = 'application/json';
console.log(headers['content-type']); // application/json
console.log(headers['CONTENT-TYPE']); // application/json

4. Array Negative Indexing

function createArrayWithNegativeIndex(arr) {
  return new Proxy(arr, {
    get(target, property) {
      if (!isNaN(property)) {
        const index = Number(property);
        if (index < 0) {
          return target[target.length + index];
        }
      }
      return Reflect.get(target, property);
    },

    set(target, property, value) {
      if (!isNaN(property)) {
        const index = Number(property);
        if (index < 0) {
          target[target.length + index] = value;
          return true;
        }
      }
      return Reflect.set(target, property, value);
    },
  });
}

const arr = createArrayWithNegativeIndex([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4
arr[-1] = 10;
console.log(arr); // [1, 2, 3, 4, 10]

The Reflect API

Reflect provides methods that mirror proxy traps, offering a cleaner way to perform object operations.

Why Use Reflect?

const obj = { a: 1 };

// Old way
try {
  Object.defineProperty(obj, 'b', {
    value: 2,
    writable: false,
  });
} catch (e) {
  console.error(e);
}

// New way with Reflect
const success = Reflect.defineProperty(obj, 'b', {
  value: 2,
  writable: false,
});

if (!success) {
  console.error('Failed to define property');
}

// Reflect methods return booleans instead of throwing

Common Reflect Methods

const obj = { name: 'John', age: 30 };

// Reflect.get
console.log(Reflect.get(obj, 'name')); // John

// Reflect.set
Reflect.set(obj, 'city', 'New York');
console.log(obj.city); // New York

// Reflect.has
console.log(Reflect.has(obj, 'age')); // true

// Reflect.deleteProperty
Reflect.deleteProperty(obj, 'age');
console.log(obj.age); // undefined

// Reflect.ownKeys
console.log(Reflect.ownKeys(obj)); // ['name', 'city']

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

const person = Reflect.construct(Person, ['Alice']);
console.log(person.name); // Alice

// Reflect.apply
function greet(greeting) {
  return `${greeting}, ${this.name}!`;
}

const user = { name: 'Bob' };
console.log(Reflect.apply(greet, user, ['Hello'])); // Hello, Bob!

Advanced Patterns

Revocable Proxy

const target = {
  data: 'sensitive',
};

const { proxy, revoke } = Proxy.revocable(target, {
  get(target, property) {
    console.log(`Accessing ${property}`);
    return target[property];
  },
});

console.log(proxy.data); // Accessing data, sensitive

// Revoke access
revoke();

// Any operation on proxy now throws
try {
  console.log(proxy.data);
} catch (e) {
  console.error('Proxy revoked'); // This executes
}

Membrane Pattern

function createMembrane(target) {
  const proxyCache = new WeakMap();
  let revoked = false;

  function wrap(obj) {
    if (revoked) {
      throw new Error('Membrane revoked');
    }

    if (obj === null || typeof obj !== 'object') {
      return obj;
    }

    if (proxyCache.has(obj)) {
      return proxyCache.get(obj);
    }

    const proxy = new Proxy(obj, {
      get(target, property) {
        if (revoked) throw new Error('Membrane revoked');
        return wrap(Reflect.get(target, property));
      },

      set(target, property, value) {
        if (revoked) throw new Error('Membrane revoked');
        return Reflect.set(target, property, unwrap(value));
      },
    });

    proxyCache.set(obj, proxy);
    return proxy;
  }

  function unwrap(obj) {
    if (proxyCache.has(obj)) {
      return (
        [...proxyCache.entries()].find(([_, proxy]) => proxy === obj)?.[0] ||
        obj
      );
    }
    return obj;
  }

  return {
    proxy: wrap(target),
    revoke() {
      revoked = true;
    },
  };
}

// Usage
const obj = {
  user: {
    name: 'Alice',
    permissions: ['read', 'write'],
  },
};

const { proxy, revoke } = createMembrane(obj);
console.log(proxy.user.name); // Alice

revoke();
// Now all access is revoked
// proxy.user.name // Error: Membrane revoked

Observable Pattern

class Observable {
  constructor(target) {
    this.observers = new Map();

    return new Proxy(target, {
      set: (target, property, value) => {
        const result = Reflect.set(target, property, value);
        this.notify(property, value);
        return result;
      },
    });
  }

  subscribe(property, callback) {
    if (!this.observers.has(property)) {
      this.observers.set(property, new Set());
    }
    this.observers.get(property).add(callback);

    // Return unsubscribe function
    return () => {
      const callbacks = this.observers.get(property);
      if (callbacks) {
        callbacks.delete(callback);
      }
    };
  }

  notify(property, value) {
    const callbacks = this.observers.get(property);
    if (callbacks) {
      callbacks.forEach((callback) => callback(value));
    }
  }
}

// Usage
const state = new Observable({
  count: 0,
  user: null,
});

const unsubscribe1 = state.subscribe('count', (value) => {
  console.log(`Count changed to: ${value}`);
});

const unsubscribe2 = state.subscribe('user', (value) => {
  console.log(`User changed to:`, value);
});

state.count = 1; // Count changed to: 1
state.count = 2; // Count changed to: 2
state.user = { name: 'Bob' }; // User changed to: { name: 'Bob' }

unsubscribe1();
state.count = 3; // No log (unsubscribed)

Performance Considerations

// Performance test
const iterations = 1000000;

// Direct access
const directObj = { value: 0 };
console.time('Direct');
for (let i = 0; i < iterations; i++) {
  directObj.value = i;
}
console.timeEnd('Direct');

// Proxy access
const proxyObj = new Proxy(
  { value: 0 },
  {
    set(target, prop, value) {
      target[prop] = value;
      return true;
    },
  }
);

console.time('Proxy');
for (let i = 0; i < iterations; i++) {
  proxyObj.value = i;
}
console.timeEnd('Proxy');

// Proxies are slower but the overhead is usually acceptable
// for the functionality they provide

Best Practices

  1. Use Reflect in trap handlers for consistency and proper behavior
  2. Return appropriate values from traps (especially boolean traps)
  3. Consider performance when using proxies in hot code paths
  4. Document proxy behavior as it can make code harder to understand
  5. Use revocable proxies for temporary access control
  6. Cache nested proxies to avoid creating multiple proxies for the same object

Conclusion

Proxy and Reflect APIs are powerful tools for metaprogramming in JavaScript. They enable patterns like reactive programming, validation, access control, and more. While they add some complexity and performance overhead, they provide elegant solutions to problems that would otherwise require much more code or be impossible to implement.