Skip to main content
โšก Calmops

Proxy Objects: Interception and Traps in JavaScript

Proxy Objects: Interception and Traps in JavaScript

Proxy objects intercept and customize operations on objects. This article covers traps, handlers, and practical metaprogramming patterns.

Introduction

Proxy objects enable:

  • Intercepting property access
  • Validating assignments
  • Logging operations
  • Lazy loading
  • Virtual properties
  • Advanced abstractions

Understanding Proxy helps you:

  • Build frameworks
  • Implement validation
  • Create reactive systems
  • Intercept operations

Proxy Basics

Creating a Proxy

// โœ… Good: Create a basic proxy
const target = { x: 1, y: 2 };

const handler = {
  get(target, prop) {
    console.log(`Getting ${prop}`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`Setting ${prop} to ${value}`);
    target[prop] = value;
    return true;
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.x); // Logs: Getting x โ†’ 1
proxy.y = 3; // Logs: Setting y to 3

Proxy Traps

// Common proxy traps:
const handler = {
  get(target, prop) { }, // Property access
  set(target, prop, value) { }, // Property assignment
  has(target, prop) { }, // in operator
  deleteProperty(target, prop) { }, // delete operator
  ownKeys(target) { }, // Object.keys()
  getOwnPropertyDescriptor(target, prop) { }, // Property descriptor
  defineProperty(target, prop, descriptor) { }, // Object.defineProperty()
  getPrototypeOf(target) { }, // Object.getPrototypeOf()
  setPrototypeOf(target, proto) { }, // Object.setPrototypeOf()
  isExtensible(target) { }, // Object.isExtensible()
  preventExtensions(target) { }, // Object.preventExtensions()
  apply(target, thisArg, args) { }, // Function call
  construct(target, args) { } // new operator
};

Common Proxy Patterns

Validation

// โœ… Good: Validate property assignments
const validator = {
  set(target, prop, value) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a number');
    }
    if (prop === 'email' && !value.includes('@')) {
      throw new TypeError('Invalid email');
    }
    target[prop] = value;
    return true;
  }
};

const user = new Proxy({}, validator);

user.age = 30; // OK
user.email = '[email protected]'; // OK

user.age = 'thirty'; // Error: Age must be a number
user.email = 'invalid'; // Error: Invalid email

Logging

// โœ… Good: Log all operations
const logger = {
  get(target, prop) {
    console.log(`[GET] ${prop}`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`[SET] ${prop} = ${value}`);
    target[prop] = value;
    return true;
  },
  deleteProperty(target, prop) {
    console.log(`[DELETE] ${prop}`);
    delete target[prop];
    return true;
  }
};

const obj = new Proxy({}, logger);

obj.x = 1; // [SET] x = 1
console.log(obj.x); // [GET] x
delete obj.x; // [DELETE] x

Lazy Loading

// โœ… Good: Lazy load properties
const lazyLoader = {
  get(target, prop) {
    if (!(prop in target)) {
      console.log(`Loading ${prop}...`);
      // Simulate loading
      target[prop] = `Loaded: ${prop}`;
    }
    return target[prop];
  }
};

const data = new Proxy({}, lazyLoader);

console.log(data.user); // Loading user... โ†’ Loaded: user
console.log(data.user); // Loaded: user (no loading)

Default Values

// โœ… Good: Provide default values
const defaults = {
  get(target, prop) {
    return target[prop] ?? `Default: ${prop}`;
  }
};

const config = new Proxy({}, defaults);

console.log(config.host); // Default: host
console.log(config.port); // Default: port

config.host = 'localhost';
console.log(config.host); // localhost

Advanced Proxy Patterns

Negative Array Indexing

// โœ… Good: Support negative array indexing
const negativeArray = {
  get(target, prop) {
    const index = Number(prop);
    if (index < 0) {
      return target[target.length + index];
    }
    return target[index];
  }
};

const arr = new Proxy([1, 2, 3, 4, 5], negativeArray);

console.log(arr[-1]); // 5 (last element)
console.log(arr[-2]); // 4 (second to last)
console.log(arr[0]); // 1 (first element)

Observable Objects

// โœ… Good: Create observable objects
function observable(target, callback) {
  return new Proxy(target, {
    set(target, prop, value) {
      if (target[prop] !== value) {
        callback({ prop, oldValue: target[prop], newValue: value });
        target[prop] = value;
      }
      return true;
    }
  });
}

// Usage
const user = observable({}, (change) => {
  console.log(`${change.prop} changed from ${change.oldValue} to ${change.newValue}`);
});

user.name = 'John'; // name changed from undefined to John
user.age = 30; // age changed from undefined to 30
user.age = 31; // age changed from 30 to 31

Reactive Objects

// โœ… Good: Create reactive objects
class Reactive {
  constructor(data) {
    this.data = data;
    this.watchers = new Map();

    return new Proxy(data, {
      set: (target, prop, value) => {
        if (target[prop] !== value) {
          target[prop] = value;
          this.notify(prop, value);
        }
        return true;
      }
    });
  }

  watch(prop, callback) {
    if (!this.watchers.has(prop)) {
      this.watchers.set(prop, []);
    }
    this.watchers.get(prop).push(callback);
  }

  notify(prop, value) {
    if (this.watchers.has(prop)) {
      this.watchers.get(prop).forEach(callback => {
        callback(value);
      });
    }
  }
}

// Usage
const user = new Reactive({ name: 'John', age: 30 });

user.watch('name', (newValue) => {
  console.log(`Name changed to ${newValue}`);
});

user.name = 'Jane'; // Name changed to Jane

Function Interception

// โœ… Good: Intercept function calls
const functionProxy = {
  apply(target, thisArg, args) {
    console.log(`Calling ${target.name} with args:`, args);
    const result = target.apply(thisArg, args);
    console.log(`Result:`, result);
    return result;
  }
};

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

const proxiedAdd = new Proxy(add, functionProxy);

proxiedAdd(2, 3);
// Calling add with args: [2, 3]
// Result: 5

Constructor Interception

// โœ… Good: Intercept constructor calls
const constructorProxy = {
  construct(target, args) {
    console.log(`Creating instance of ${target.name} with args:`, args);
    return Reflect.construct(target, args);
  }
};

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

const ProxiedUser = new Proxy(User, constructorProxy);

const user = new ProxiedUser('John');
// Creating instance of User with args: ['John']

Practical Applications

API Client

// โœ… Good: Create API client with proxy
function createAPIClient(baseURL) {
  return new Proxy({}, {
    get(target, prop) {
      return async (...args) => {
        const url = `${baseURL}/${prop}`;
        const response = await fetch(url);
        return response.json();
      };
    }
  });
}

// Usage
const api = createAPIClient('https://api.example.com');

api.users(); // Fetches https://api.example.com/users
api.posts(); // Fetches https://api.example.com/posts

Form Validation

// โœ… Good: Form validation with proxy
function createForm(schema) {
  const data = {};

  return new Proxy(data, {
    set(target, prop, value) {
      const validator = schema[prop];

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

      target[prop] = value;
      return true;
    }
  });
}

// Usage
const form = createForm({
  email: (value) => value.includes('@'),
  age: (value) => value >= 18,
  password: (value) => value.length >= 8
});

form.email = '[email protected]'; // OK
form.age = 25; // OK
form.password = 'secure123'; // OK

form.email = 'invalid'; // Error: Invalid value for email

Caching Proxy

// โœ… Good: Cache function results
function createCachedFunction(fn) {
  const cache = new Map();

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

      if (cache.has(key)) {
        console.log('From cache:', key);
        return cache.get(key);
      }

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

// Usage
const fibonacci = createCachedFunction((n) => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

fibonacci(10); // Computes with caching
fibonacci(10); // From cache

Best Practices

  1. Keep handlers simple:

    // โœ… Good
    const handler = {
      get(target, prop) {
        return target[prop];
      }
    };
    
  2. Return true from set trap:

    // โœ… Good
    set(target, prop, value) {
      target[prop] = value;
      return true;
    }
    
  3. Use Reflect for consistency:

    // โœ… Good
    get(target, prop) {
      return Reflect.get(target, prop);
    }
    
  4. Handle errors properly:

    // โœ… Good
    set(target, prop, value) {
      if (!isValid(value)) {
        throw new Error('Invalid value');
      }
      target[prop] = value;
      return true;
    }
    

Common Mistakes

  1. Forgetting to return true from set:

    // โŒ Bad
    set(target, prop, value) {
      target[prop] = value;
    }
    
    // โœ… Good
    set(target, prop, value) {
      target[prop] = value;
      return true;
    }
    
  2. Not handling all traps:

    // โŒ Bad - incomplete handler
    const handler = { get() { } };
    
    // โœ… Good - handle necessary traps
    const handler = { get() { }, set() { } };
    
  3. Performance issues:

    // โŒ Bad - expensive operations in trap
    get(target, prop) {
      expensiveOperation();
      return target[prop];
    }
    
    // โœ… Good - optimize trap performance
    get(target, prop) {
      return target[prop];
    }
    

Summary

Proxy objects are powerful for metaprogramming. Key takeaways:

  • Intercept operations with traps
  • Validate assignments
  • Log operations
  • Implement lazy loading
  • Create reactive systems
  • Build advanced abstractions
  • Use Reflect for consistency

Next Steps

Comments