Deep Principles of Property Hijacking and Proxy in JavaScript

Deep Principles of Property Hijacking and Proxy in JavaScript

In JavaScript, Property Hijacking is a mechanism that allows custom logic to be executed by intercepting operations on an object's properties, such as access, assignment, or deletion. This is primarily achieved through ES5's Object.defineProperty and the ES6-introduced Proxy object. Proxy offers more powerful and comprehensive interception capabilities, serving as a crucial tool for metaprogramming (writing programs that manipulate programs). Below, I will delve into their principles, comparisons, and implementation details.


1. Background and Basic Concepts

  • What is Property Hijacking?
    In JavaScript, operations like reading, assigning, or deleting object properties are typically transparent. Property hijacking allows us to "listen" to these operations and trigger custom behaviors when they occur, such as data validation, logging, or data binding.
  • Why is it Needed?
    In Vue 2.x, Object.defineProperty is used to implement the reactive system; in Vue 3, Proxy is adopted instead. It also serves as the foundation for implementing advanced patterns like the Observer pattern or data validation.

2. Interception via Object.defineProperty

Object.defineProperty can only intercept the reading (get) and setting (set) of existing properties, but cannot intercept operations like adding new properties or deleting properties. Its working principle is as follows:

Step 1: Define an object and add interception

let obj = { name: 'Alice' };
let _age = 18; // Internal variable to store the real value

Object.defineProperty(obj, 'age', {
  enumerable: true,    // Enumerable
  configurable: true,  // Configurable
  get() {
    console.log(`Reading age: ${_age}`);
    return _age;
  },
  set(newVal) {
    if (newVal < 0) {
      console.error('Age cannot be negative');
      return;
    }
    console.log(`Setting age: ${newVal}`);
    _age = newVal;
  }
});

Step 2: Test the interception effect

console.log(obj.age); // Output: Reading age: 18
obj.age = 25;         // Output: Setting age: 25
obj.age = -5;         // Output: Age cannot be negative (no modification)
  • Limitations:
    • Cannot intercept obj.newProp = 1 (adding new properties).
    • Cannot intercept delete obj.name (deleting properties).
    • Requires defining interceptors for each property individually, leading to high performance overhead.

3. Comprehensive Interception with Proxy

Proxy is an ES6-introduced "proxy" object that creates a proxy for an object, allowing interception and customization of its fundamental operations (such as property access, assignment, enumeration, function invocation, etc.). It provides 13 interceptable operations (called "traps").

Step 1: Create a Proxy

let target = { name: 'Alice', age: 18 };
let handler = {
  // Intercept property reading
  get(obj, prop) {
    console.log(`Reading property: ${prop}`);
    return prop in obj ? obj[prop] : 'Default value';
  },
  // Intercept property setting
  set(obj, prop, value) {
    if (prop === 'age' && value < 0) {
      throw new Error('Age cannot be negative');
    }
    console.log(`Setting property: ${prop} = ${value}`);
    obj[prop] = value;
    return true; // Indicates success
  },
  // Intercept property deletion
  deleteProperty(obj, prop) {
    console.log(`Deleting property: ${prop}`);
    delete obj[prop];
    return true;
  },
  // Intercept the 'in' operator
  has(obj, prop) {
    console.log(`Checking property existence: ${prop}`);
    return prop in obj;
  }
};

let proxy = new Proxy(target, handler);

Step 2: Test Proxy interception

console.log(proxy.name); // Output: Reading property: name → "Alice"
proxy.age = 25;          // Output: Setting property: age = 25
proxy.newProp = 100;     // Output: Setting property: newProp = 100 (supports addition)
console.log('age' in proxy); // Output: Checking property existence: age → true
delete proxy.name;       // Output: Deleting property: name
  • Advantages:
    • Can intercept up to 13 operations (e.g., get, set, has, deleteProperty, apply (function call), construct (new operation), etc.).
    • Can intercept operations like adding new properties, deleting properties, and array manipulations.
    • No need to define interceptors for each property individually.

4. Deep Principle: Implementing a Reactive System

Take Vue 3's reactivity as an example. Its core utilizes Proxy to recursively proxy every layer of an object, enabling deep property observation:

function reactive(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj; // Return non-objects directly
  }
  const handler = {
    get(target, key) {
      console.log(`Reading: ${key}`);
      const res = Reflect.get(target, key);
      // Recursively proxy if the value is an object
      return typeof res === 'object' ? reactive(res) : res;
    },
    set(target, key, value) {
      console.log(`Setting: ${key} -> ${value}`);
      return Reflect.set(target, key, value);
    }
  };
  return new Proxy(obj, handler);
}

let data = reactive({ 
  user: { name: 'Bob', hobbies: ['coding'] } 
});
data.user.name;           // Output: Reading: user → Reading: name
data.user.hobbies.push('music'); // Note: Array push does not trigger set; requires special handling
  • Note: Array methods (e.g., push, pop) modifying the array do not trigger the set trap because they modify the array's contents rather than assigning a property. Vue 3 handles this by rewriting array methods.

5. Performance and Considerations

  • Performance Comparison: Proxy is more efficient than Object.defineProperty because it uses "lazy interception," only triggering when a property is accessed, without traversing all properties.
  • Limitations:
    • Proxy cannot proxy primitive values (e.g., strings, numbers).
    • The proxied object is not equal to the original object: proxy !== target.
    • Some built-in objects (e.g., Date, Map) may have internal slots that cannot be fully proxied.
  • Reflect Object: Often used with Proxy, it provides static methods corresponding one-to-one with Proxy traps, enabling the invocation of an object's default behavior and avoiding repetitive code.

6. Practical Application Scenarios

  1. Data Validation: Validate data types during property assignment.
  2. Logging: Track access and modifications of object properties.
  3. Caching Mechanism: Implement caching logic in the get trap.
  4. Observer Pattern: Automatically notify dependencies when properties change.
  5. Negative Index Arrays: Implement arr[-1] to access the last element via Proxy.
// Example: Negative Index Array
function createNegativeArray(arr) {
  return new Proxy(arr, {
    get(target, prop) {
      let index = Number(prop);
      if (index < 0) index = target.length + index;
      return target[index];
    }
  });
}
let arr = createNegativeArray([10, 20, 30]);
console.log(arr[-1]); // Output: 30

Summary

  • Object.defineProperty: Suitable for simple property interception but limited in functionality; primarily used in ES5 environments.
  • Proxy: Comprehensive functionality, capable of intercepting various object operations; the modern solution for advanced metaprogramming and reactive systems.
  • Recommendation: Prefer Proxy in new projects, but be mindful of edge cases (e.g., array methods, built-in objects).