JavaScript Proxies and Reflect

JavaScript Proxies and the Reflect API are powerful features introduced in ES6 (ES2015) that allow developers to intercept and customize operations on objects. These features enable metaprogramming capabilities, making it possible to implement custom behaviors for fundamental operations like property lookup, assignment, enumeration, and function invocation.

Proxies

A Proxy object wraps another object (the target) and intercepts operations like property lookup, assignment, enumeration, and function invocation, optionally modifying their behavior.

Basic Syntax

// Syntax: new Proxy(target, handler)
const target = {
  message: 'Hello, World!'
};

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

const proxy = new Proxy(target, handler);

// Using the proxy
console.log(proxy.message); 
// Logs: "Getting property: message"
// Then logs: "Hello, World!"

Proxy Handlers (Traps)

Handlers define "traps" that intercept different operations on the target object. Here are the most commonly used traps:

Handler Trap Description Intercepted Operations
get Intercepts property access proxy.property, proxy['property']
set Intercepts property assignment proxy.property = value
has Intercepts the in operator 'property' in proxy
deleteProperty Intercepts the delete operator delete proxy.property
apply Intercepts function calls proxy(...args), proxy.call(), proxy.apply()
construct Intercepts new operator new proxy(...args)
getPrototypeOf Intercepts getting the prototype Object.getPrototypeOf(proxy)
setPrototypeOf Intercepts setting the prototype Object.setPrototypeOf(proxy)
isExtensible Intercepts Object.isExtensible() Object.isExtensible(proxy)
preventExtensions Intercepts Object.preventExtensions() Object.preventExtensions(proxy)
getOwnPropertyDescriptor Intercepts Object.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor(proxy, 'property')
defineProperty Intercepts Object.defineProperty() Object.defineProperty(proxy, 'property', descriptor)
ownKeys Intercepts Object.keys() and related methods Object.keys(proxy), for...in loops

Common Use Cases for Proxies

1. Validation

// User object with validation
function createUser(name, age) {
  const user = {
    name,
    age
  };
  
  return new Proxy(user, {
    set(target, prop, value) {
      if (prop === 'name' && (typeof value !== 'string' || value.length < 2)) {
        throw new Error('Name must be a string with at least 2 characters');
      }
      
      if (prop === 'age' && (typeof value !== 'number' || value < 0 || value > 120)) {
        throw new Error('Age must be a number between 0 and 120');
      }
      
      target[prop] = value;
      return true; // Indicate success
    }
  });
}

const user = createUser('Alice', 30);
user.name = 'Bob'; // Works fine
// user.name = ''; // Error: Name must be a string with at least 2 characters
// user.age = -5; // Error: Age must be a number between 0 and 120

2. Logging and Debugging

// Logging proxy that tracks all property access and modifications
function createLoggingProxy(target, name = 'Object') {
  return new Proxy(target, {
    get(target, prop, receiver) {
      const value = target[prop];
      console.log(`${name}.${prop} accessed, returned: ${value}`);
      return value;
    },
    
    set(target, prop, value, receiver) {
      console.log(`${name}.${prop} changed from ${target[prop]} to ${value}`);
      target[prop] = value;
      return true;
    }
  });
}

const user = createLoggingProxy({ name: 'Alice', role: 'Admin' }, 'user');
console.log(user.name); // Logs: "user.name accessed, returned: Alice" then "Alice"
user.role = 'User'; // Logs: "user.role changed from Admin to User"

3. Default Values

// Object with default values for missing properties
function withDefaults(target, defaults) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      if (prop in target) {
        return target[prop];
      }
      
      if (prop in defaults) {
        return defaults[prop];
      }
      
      return undefined;
    }
  });
}

const settings = withDefaults({
  theme: 'dark'
}, {
  theme: 'light',
  fontSize: 16,
  showSidebar: true
});

console.log(settings.theme); // "dark" (from target)
console.log(settings.fontSize); // 16 (from defaults)
console.log(settings.unknownProp); // undefined

4. Negative Array Indices (Like Python)

// Array with support for negative indices
function createArrayWithNegativeIndices(array) {
  return new Proxy(array, {
    get(target, prop, receiver) {
      // Handle numeric indices
      if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
        // Convert negative index string to number and calculate positive index
        const index = target.length + parseInt(prop, 10);
        return target[index];
      }
      
      return Reflect.get(target, prop, receiver);
    }
  });
}

const arr = createArrayWithNegativeIndices([1, 2, 3, 4, 5]);
console.log(arr[1]); // 2
console.log(arr[-1]); // 5 (last element)
console.log(arr[-2]); // 4 (second-to-last element)

5. Hidden Properties

// Object with hidden properties that don't show up in enumeration
function createObjectWithHiddenProps(visible, hidden) {
  return new Proxy(visible, {
    get(target, prop, receiver) {
      // Check visible properties first
      if (prop in target) {
        return target[prop];
      }
      
      // Then check hidden properties
      if (prop in hidden) {
        return hidden[prop];
      }
      
      return undefined;
    },
    
    has(target, prop) {
      // Only report visible properties with 'in' operator
      return prop in target;
    },
    
    ownKeys(target) {
      // Only enumerate visible properties
      return Reflect.ownKeys(target);
    },
    
    getOwnPropertyDescriptor(target, prop) {
      // Only return descriptors for visible properties
      return Reflect.getOwnPropertyDescriptor(target, prop);
    }
  });
}

const obj = createObjectWithHiddenProps(
  { visible: 'I can be seen' },
  { secret: 'I am hidden' }
);

console.log(obj.visible); // "I can be seen"
console.log(obj.secret); // "I am hidden"
console.log('secret' in obj); // false
console.log(Object.keys(obj)); // ["visible"]

Note: Proxies are not transparent for identity operations. The original target and the proxy are different objects:

const target = {};
const proxy = new Proxy(target, {});
console.log(target === proxy); // false

Reflect API

The Reflect API provides methods that are the same as the proxy handler methods. It's a built-in object that provides methods for interceptable JavaScript operations, making it easier to implement proxies.

Basic Usage

const obj = { x: 1, y: 2 };

// Traditional way to access properties
console.log(obj.x); // 1

// Using Reflect
console.log(Reflect.get(obj, 'x')); // 1

// Traditional way to set properties
obj.y = 3;

// Using Reflect
Reflect.set(obj, 'y', 4);
console.log(obj.y); // 4

Why Use Reflect?

  1. Consistent Function Interface: All operations are methods, not operators.
  2. Status Returns: Many methods return boolean success status instead of throwing errors.
  3. Receiver Forwarding: Makes it easier to forward operations to the original target in Proxy handlers.
  4. More Reliable: Some operations that can fail silently with traditional syntax will return a status with Reflect.

Common Reflect Methods

Reflect Method Equivalent Traditional Syntax Description
Reflect.get(target, prop, receiver?) target[prop] Gets a property value
Reflect.set(target, prop, value, receiver?) target[prop] = value Sets a property value
Reflect.has(target, prop) prop in target Checks if property exists
Reflect.deleteProperty(target, prop) delete target[prop] Deletes a property
Reflect.apply(target, thisArg, args) target.apply(thisArg, args) Calls a function
Reflect.construct(target, args, newTarget?) new target(...args) Creates a new instance
Reflect.defineProperty(target, prop, descriptor) Object.defineProperty(target, prop, descriptor) Defines a property
Reflect.getOwnPropertyDescriptor(target, prop) Object.getOwnPropertyDescriptor(target, prop) Gets property descriptor
Reflect.getPrototypeOf(target) Object.getPrototypeOf(target) Gets prototype
Reflect.setPrototypeOf(target, prototype) Object.setPrototypeOf(target, prototype) Sets prototype
Reflect.isExtensible(target) Object.isExtensible(target) Checks if object is extensible
Reflect.preventExtensions(target) Object.preventExtensions(target) Prevents extensions
Reflect.ownKeys(target) Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)) Gets all own keys

Using Reflect with Proxies

Reflect methods are particularly useful in Proxy handlers to forward operations to the target object:

const target = {
  name: 'Target',
  greet() {
    return `Hello, my name is ${this.name}`;
  }
};

const handler = {
  get(target, prop, receiver) {
    console.log(`Getting ${prop}`);
    // Forward the operation to the target, preserving 'this' binding
    return Reflect.get(target, prop, receiver);
  }
};

const proxy = new Proxy(target, handler);

// The 'this' in target.greet() will correctly refer to the proxy
console.log(proxy.greet()); // "Getting greet" then "Hello, my name is Target"

Important: Without using Reflect.get with the receiver parameter, the this value inside methods would refer to the target, not the proxy, which can cause unexpected behavior.

Advanced Patterns with Proxies and Reflect

1. Method Chaining with Proxies

// Create a chainable API with proxies
function createChainableAPI(baseObject) {
  return new Proxy(baseObject, {
    get(target, prop, receiver) {
      // Get the actual property
      const value = Reflect.get(target, prop, receiver);
      
      // If it's a method, wrap it to return the proxy
      if (typeof value === 'function') {
        return function(...args) {
          const result = value.apply(this, args);
          // If the method returns 'this', return the proxy instead
          return result === target ? receiver : result;
        };
      }
      
      return value;
    }
  });
}

// Example: Chainable calculator
const calculator = createChainableAPI({
  value: 0,
  add(n) {
    this.value += n;
    return this;
  },
  subtract(n) {
    this.value -= n;
    return this;
  },
  multiply(n) {
    this.value *= n;
    return this;
  },
  divide(n) {
    this.value /= n;
    return this;
  },
  result() {
    return this.value;
  }
});

// Chain methods
const result = calculator
  .add(5)
  .multiply(2)
  .subtract(3)
  .divide(2)
  .result();
  
console.log(result); // ((5 * 2) - 3) / 2 = 3.5

2. Revocable Proxies

JavaScript provides a way to create proxies that can be disabled:

// Create a revocable proxy
const target = { message: 'Hello' };
const { proxy, revoke } = Proxy.revocable(target, {
  get(target, prop, receiver) {
    console.log(`Accessing ${prop}`);
    return Reflect.get(target, prop, receiver);
  }
});

// Use the proxy
console.log(proxy.message); // "Accessing message" then "Hello"

// Revoke access - the proxy becomes unusable
revoke();

// Any further attempt to use the proxy will throw a TypeError
// console.log(proxy.message); // TypeError: Cannot perform 'get' on a proxy that has been revoked

Use Case: Revocable proxies are useful for controlling access to objects, especially when you need to grant temporary access that can be revoked later, such as in API tokens or temporary permissions.

3. Virtual Properties

// Object with computed properties that don't actually exist
function createObjectWithVirtualProps(realData) {
  return new Proxy(realData, {
    get(target, prop, receiver) {
      // Handle special virtual properties
      if (prop === 'fullName') {
        return `${target.firstName} ${target.lastName}`;
      }
      
      if (prop === 'age') {
        const birthYear = new Date(target.birthDate).getFullYear();
        const currentYear = new Date().getFullYear();
        return currentYear - birthYear;
      }
      
      // Default behavior for real properties
      return Reflect.get(target, prop, receiver);
    }
  });
}

const person = createObjectWithVirtualProps({
  firstName: 'John',
  lastName: 'Doe',
  birthDate: '1990-05-15'
});

console.log(person.firstName); // "John" (real property)
console.log(person.fullName); // "John Doe" (virtual property)
console.log(person.age); // Current year - 1990 (virtual property)

Performance Considerations

While Proxies and Reflect provide powerful capabilities, they do come with some performance overhead:

  • Proxies add a layer of indirection to every operation, which can impact performance in performance-critical code.
  • Property access through Proxies is slower than direct property access on regular objects.
  • Creating many Proxy objects can increase memory usage.

Best Practice: Use Proxies for specific use cases where their benefits outweigh the performance cost. For high-performance code paths that are called frequently, consider alternative approaches.

Browser Support

Proxies and Reflect are supported in all modern browsers, but they are not available in Internet Explorer. They were introduced in ES6 (ES2015) and have good support in:

  • Chrome 49+
  • Firefox 18+
  • Safari 10+
  • Edge 12+
  • Node.js 6.0.0+

Note: Proxies cannot be polyfilled or transpiled for older browsers due to their fundamental nature. If you need to support older browsers, you'll need to use alternative approaches.

Summary

JavaScript Proxies and the Reflect API provide powerful metaprogramming capabilities that allow you to:

  • Intercept and customize fundamental object operations
  • Implement validation, logging, default values, and other cross-cutting concerns
  • Create virtual objects and properties
  • Build more maintainable and flexible APIs

While they come with some performance overhead, they offer elegant solutions to many complex programming challenges that would otherwise require more verbose and less maintainable code.