DEV Community

Cover image for 2. Trap It Like It's Hot: Mastering JavaScript Proxies for Objects & Arrays
Sandheep Kumar Patro
Sandheep Kumar Patro

Posted on

2. Trap It Like It's Hot: Mastering JavaScript Proxies for Objects & Arrays

Understanding JavaScript Proxies: Objects and Arrays

In our previous article, we introduced JavaScript Proxies as a powerful way to intercept and customize object behaviors. Now, let's dive deeper into practical applications, focusing on how Proxies transform the way we work with objects and arrays.

Basic Proxy Handlers

The true power of Proxies lies in their handlers—objects containing "traps" that intercept different operations. The most common traps are get and set, which intercept property access and assignment respectively.

The get Trap: Intercepting Property Access

The get trap fires whenever a property is accessed. This enables powerful patterns like logging, computed properties, and default values:

const person = {
  firstName: "Emma",
  lastName: "Chen"
};

const personProxy = new Proxy(person, {
  get(target, property, receiver) {
    // Log all property access
    console.log(`Accessing: ${property}`);

    // Handle special properties
    if (property === 'fullName') {
      return `${target.firstName} ${target.lastName}`;
    }

    // Provide defaults for missing properties
    if (!(property in target)) {
      return `Property '${property}' doesn't exist`;
    }

    // Return the actual value
    return target[property];
  }
});

console.log(personProxy.firstName); // Logs "Accessing: firstName", returns "Emma"
console.log(personProxy.fullName);  // Logs "Accessing: fullName", returns "Emma Chen"
console.log(personProxy.age);       // Logs "Accessing: age", returns "Property 'age' doesn't exist"
Enter fullscreen mode Exit fullscreen mode

This simple example demonstrates three powerful patterns:

  1. Logging property access — useful for debugging and tracking dependencies
  2. Computed properties — creating virtual properties that don't exist on the original object
  3. Default values — gracefully handling missing properties

The set Trap: Validating and Transforming Values

The set trap allows us to intercept property assignments, enabling validation, transformation, and side effects:

const user = {
  name: "Sam",
  email: "sam@example.com"
};

const userProxy = new Proxy(user, {
  set(target, property, value, receiver) {
    // Validate email format
    if (property === 'email' && !/^\S+@\S+\.\S+$/.test(value)) {
      throw new Error('Invalid email format');
    }

    // Transform data to standardized format
    if (property === 'name') {
      value = value.trim();
    }

    // Log changes
    console.log(`Changed ${property} from ${target[property]} to ${value}`);

    // Update the target
    target[property] = value;
    return true; // Indicate success
  }
});

userProxy.name = "  Taylor Kim  "; // Trims to "Taylor Kim"
try {
  userProxy.email = "invalid-email";
} catch (e) {
  console.error(e.message); // "Invalid email format"
}
Enter fullscreen mode Exit fullscreen mode

This pattern is invaluable for:

  1. Data validation — preventing invalid values from being stored
  2. Data normalization — ensuring consistent formats
  3. Change tracking — logging or reacting to state changes

Using Proxies with Arrays

JavaScript arrays are just objects with special behaviors, so Proxies work beautifully with them too. Let's explore some powerful array-specific patterns:

Tracking Array Mutations

Arrays have methods like push, pop, and splice that modify the array. We can track these operations with Proxies:

function createTrackedArray(initialArray = []) {
  return new Proxy(initialArray, {
    get(target, property, receiver) {
      // Intercept array methods
      const value = target[property];

      if (typeof value === 'function') {
        return function(...args) {
          console.log(`Array method ${property} called with args: ${args}`);
          return value.apply(target, args);
        };
      }

      return value;
    },

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

const trackedArray = createTrackedArray([1, 2, 3]);

trackedArray.push(4);       // "Array method push called with args: 4"
trackedArray[1] = 10;       // "Setting index 1 to 10"
trackedArray.pop();         // "Array method pop called with args: "
console.log(trackedArray);  // [1, 10, 3]
Enter fullscreen mode Exit fullscreen mode

This example intercepts both direct property assignments and method calls like push and pop.

Preventing Negative Indices

JavaScript allows negative array indices but treats them as regular properties rather than accessing elements from the end. We can change this behavior:

const smartArray = new Proxy([1, 2, 3, 4, 5], {
  get(target, property, receiver) {
    // Convert negative indices to positive ones
    const index = Number(property);
    if (Number.isInteger(index) && index < 0) {
      const positiveIndex = target.length + index;
      if (positiveIndex >= 0) {
        return target[positiveIndex];
      }
    }

    return target[property];
  }
});

console.log(smartArray[0]);   // 1
console.log(smartArray[-1]);  // 5 (last element)
console.log(smartArray[-3]);  // 3 (third from end)
Enter fullscreen mode Exit fullscreen mode

This pattern is reminiscent of Python's array indexing, allowing negative indices to access elements from the end of the array.

Creating Reactive Arrays

For frontend frameworks, reactive arrays that trigger updates on changes are essential. Proxies enable this pattern elegantly:

function createReactiveArray(initialArray = [], onChange) {
  return new Proxy(initialArray, {
    set(target, property, value, receiver) {
      const oldValue = target[property];
      target[property] = value;

      // Only trigger for array indices, not for properties like 'length'
      const index = Number(property);
      if (Number.isInteger(index) && index >= 0) {
        onChange({
          type: 'update',
          index,
          oldValue,
          newValue: value
        });
      }

      return true;
    },

    get(target, property, receiver) {
      const value = target[property];

      // Wrap array methods to detect changes
      if (typeof value === 'function') {
        return function(...args) {
          const oldLength = target.length;
          const result = value.apply(target, args);

          // Detect what method was called
          if (property === 'push') {
            onChange({
              type: 'push',
              added: args,
              newLength: target.length
            });
          } else if (property === 'pop') {
            onChange({
              type: 'pop',
              removed: result,
              newLength: target.length
            });
          } // Additional methods could be handled here

          return result;
        };
      }

      return value;
    }
  });
}

// Usage example
const todos = createReactiveArray(
  ['Learn JavaScript', 'Master Proxies'],
  (change) => console.log('Array changed:', change)
);

todos.push('Build an app');
// Logs: "Array changed: {type: 'push', added: ['Build an app'], newLength: 3}"

todos[1] = 'Master JavaScript Proxies';
// Logs: "Array changed: {type: 'update', index: 1, oldValue: 'Master Proxies', newValue: 'Master JavaScript Proxies'}"
Enter fullscreen mode Exit fullscreen mode

This pattern is fundamental to reactive UI frameworks like Vue.js, which use similar techniques to detect and respond to data changes.

Why This Matters in Real Development

These patterns go beyond academic exercises—they solve real problems:

  1. Validation — Ensuring data integrity without verbose checks throughout your code
  2. Debugging — Tracking property access and changes for troubleshooting
  3. Reactivity — Building UI components that update automatically on data changes
  4. API Design — Creating intuitive interfaces with custom behaviors

By leveraging Proxies, you can create more robust, maintainable code with fewer bugs and better developer experience.

Top comments (0)