Skip to content

Proxy:拦截对象的十三种行为

"Proxy 是 JavaScript 中最强大的元编程特性之一,它允许我们拦截并自定义对象的几乎所有操作。通过 Proxy,我们可以实现从数据绑定到访问控制等各种高级功能。"

Proxy 是 ES6 引入的一个内建对象,它允许我们创建一个代理对象,用于拦截和自定义 JavaScript 对象的基本操作(如属性查找、赋值、枚举、函数调用等)。这为 JavaScript 提供了强大的元编程能力。

一、Proxy 基础语法

Proxy 构造函数接受两个参数:target(目标对象)和 handler(处理器对象):

javascript
const target = {};
const handler = {
  // 拦截器方法
};

const proxy = new Proxy(target, handler);

Proxy 创建一个代理对象,该对象会拦截应用到目标对象上的操作,并根据处理器对象中的陷阱(traps)方法来定义这些操作的自定义行为。

二、Proxy 的十三种陷阱方法

Proxy 支持拦截对象的十三种基本操作:

1. get - 属性读取陷阱

javascript
const target = { message: 'Hello' };
const handler = {
  get(target, prop, receiver) {
    console.log(`读取属性: ${prop}`);
    return target[prop];
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.message); // 读取属性: message → "Hello"

2. set - 属性设置陷阱

javascript
const target = {};
const handler = {
  set(target, prop, value, receiver) {
    console.log(`设置属性 ${prop} 为 ${value}`);
    target[prop] = value;
    return true; // 表示设置成功
  }
};

const proxy = new Proxy(target, handler);
proxy.name = 'JavaScript'; // 设置属性 name 为 JavaScript

3. has - in 操作符陷阱

javascript
const target = { public: true };
const handler = {
  has(target, prop) {
    // 隐藏以 _ 开头的私有属性
    if (prop.startsWith('_')) {
      return false;
    }
    return prop in target;
  }
};

const proxy = new Proxy(target, handler);
console.log('public' in proxy);  // true
console.log('_private' in proxy); // false

4. deleteProperty - delete 操作符陷阱

javascript
const target = { canDelete: true, cannotDelete: true };
const handler = {
  deleteProperty(target, prop) {
    if (prop === 'cannotDelete') {
      console.log(`不能删除属性: ${prop}`);
      return false; // 阻止删除
    }
    return delete target[prop];
  }
};

const proxy = new Proxy(target, handler);
delete proxy.canDelete;     // 成功删除
delete proxy.cannotDelete;  // 不能删除属性: cannotDelete

5. ownKeys - Object.getOwnPropertyNames 和 Object.keys 陷阱

javascript
const target = { visible: 1, _hidden: 2 };
const handler = {
  ownKeys(target) {
    // 只返回非私有属性
    return Reflect.ownKeys(target).filter(key => !key.startsWith('_'));
  }
};

const proxy = new Proxy(target, handler);
console.log(Object.keys(proxy)); // ["visible"]

6. getOwnPropertyDescriptor - Object.getOwnPropertyDescriptor 陷阱

javascript
const target = { normal: 1 };
const handler = {
  getOwnPropertyDescriptor(target, prop) {
    console.log(`获取属性描述符: ${prop}`);
    return Object.getOwnPropertyDescriptor(target, prop);
  }
};

const proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'normal');
// 获取属性描述符: normal

7. defineProperty - Object.defineProperty 陷阱

javascript
const target = {};
const handler = {
  defineProperty(target, prop, descriptor) {
    console.log(`定义属性: ${prop}`);
    return Object.defineProperty(target, prop, descriptor);
  }
};

const proxy = new Proxy(target, handler);
Object.defineProperty(proxy, 'newProp', {
  value: 'newValue',
  writable: true
});
// 定义属性: newProp

8. apply - 函数调用陷阱

javascript
const target = function() { return 'original'; };
const handler = {
  apply(target, thisArg, argumentsList) {
    console.log('函数被调用');
    return 'proxied: ' + target.apply(thisArg, argumentsList);
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy()); // 函数被调用 → "proxied: original"

9. construct - new 操作符陷阱

javascript
class OriginalClass {
  constructor(value) {
    this.value = value;
  }
}

const handler = {
  construct(target, argumentsList, newTarget) {
    console.log('构造函数被调用');
    const instance = Reflect.construct(target, argumentsList, newTarget);
    instance.proxied = true;
    return instance;
  }
};

const ProxyClass = new Proxy(OriginalClass, handler);
const instance = new ProxyClass(42);
console.log(instance.value);   // 42
console.log(instance.proxied); // true

10. getPrototypeOf - Object.getPrototypeOf 陷阱

javascript
const target = {};
const handler = {
  getPrototypeOf(target) {
    console.log('获取原型');
    return Object.getPrototypeOf(target);
  }
};

const proxy = new Proxy(target, handler);
Object.getPrototypeOf(proxy); // 获取原型

11. setPrototypeOf - Object.setPrototypeOf 陷阱

javascript
const target = {};
const proto = { x: 1 };
const handler = {
  setPrototypeOf(target, prototype) {
    console.log('设置原型');
    return Object.setPrototypeOf(target, prototype);
  }
};

const proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto); // 设置原型

12. isExtensible - Object.isExtensible 陷阱

javascript
const target = {};
const handler = {
  isExtensible(target) {
    console.log('检查是否可扩展');
    return Object.isExtensible(target);
  }
};

const proxy = new Proxy(target, handler);
Object.isExtensible(proxy); // 检查是否可扩展

13. preventExtensions - Object.preventExtensions 陷阱

javascript
const target = {};
const handler = {
  preventExtensions(target) {
    console.log('阻止扩展');
    return Object.preventExtensions(target);
  }
};

const proxy = new Proxy(target, handler);
Object.preventExtensions(proxy); // 阻止扩展

三、实际应用示例

1. 实现负索引数组

javascript
function createNegativeArray(...elements) {
  const target = [...elements];
  
  const handler = {
    get(target, prop) {
      // 处理负索引
      if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
        const index = +prop;
        const actualIndex = target.length + index;
        if (actualIndex >= 0 && actualIndex < target.length) {
          return target[actualIndex];
        }
      }
      return target[prop];
    },
    
    set(target, prop, value) {
      // 处理负索引设置
      if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
        const index = +prop;
        const actualIndex = target.length + index;
        if (actualIndex >= 0 && actualIndex < target.length) {
          target[actualIndex] = value;
          return true;
        }
      }
      target[prop] = value;
      return true;
    }
  };
  
  return new Proxy(target, handler);
}

const arr = createNegativeArray('a', 'b', 'c', 'd');
console.log(arr[-1]); // 'd'
console.log(arr[-2]); // 'c'
arr[-1] = 'z';
console.log(arr[3]);  // 'z'

2. 实现链式调用

javascript
function createChainable(obj) {
  const handler = {
    get(target, prop) {
      if (prop in target) {
        return target[prop];
      }
      
      // 创建链式调用方法
      return function(...args) {
        console.log(`调用方法: ${prop}`, args);
        // 这里可以添加实际的逻辑
        return new Proxy({}, handler); // 返回新的代理以支持链式调用
      };
    }
  };
  
  return new Proxy(obj, handler);
}

const api = createChainable({});
api.users().list().filter({ active: true }).sort('name');
// 调用方法: users []
// 调用方法: list []
// 调用方法: filter [ { active: true } ]
// 调用方法: sort [ 'name' ]

3. 实现自动深响应

javascript
function createReactive(obj) {
  // 递归创建响应式对象
  function makeReactive(target) {
    if (typeof target !== 'object' || target === null) {
      return target;
    }
    
    const handler = {
      get(target, prop) {
        console.log(`获取属性: ${prop}`);
        const value = target[prop];
        // 递归处理嵌套对象
        return makeReactive(value);
      },
      
      set(target, prop, value) {
        console.log(`设置属性: ${prop} = ${value}`);
        target[prop] = value;
        // 触发更新
        triggerUpdate(prop, value);
        return true;
      }
    };
    
    return new Proxy(target, handler);
  }
  
  function triggerUpdate(prop, value) {
    console.log(`触发更新: ${prop} 已更新为 ${value}`);
    // 这里可以实现实际的更新逻辑
  }
  
  return makeReactive(obj);
}

const reactiveObj = createReactive({ user: { name: 'Alice', age: 30 } });
console.log(reactiveObj.user.name); // 获取属性: user → 获取属性: name → "Alice"
reactiveObj.user.age = 31; // 设置属性: age = 31 → 触发更新: age 已更新为 31

四、注意事项和最佳实践

1. 性能考虑

Proxy 会带来一定的性能开销,特别是在频繁访问的场景中:

javascript
// 避免在高频操作中使用复杂的 Proxy
const expensiveProxy = new Proxy(target, {
  get(target, prop) {
    // 复杂的处理逻辑会显著影响性能
    return target[prop];
  }
});

2. 正确返回布尔值

某些陷阱方法需要返回布尔值来表示操作是否成功:

javascript
const handler = {
  set(target, prop, value) {
    target[prop] = value;
    return true; // 必须返回 true 表示设置成功
  },
  
  deleteProperty(target, prop) {
    const result = delete target[prop];
    return result; // 返回删除操作的结果
  }
};

3. 使用 Reflect 进行转发

在自定义陷阱方法中,通常使用 Reflect 来转发操作:

javascript
const handler = {
  get(target, prop, receiver) {
    // 添加自定义逻辑
    console.log(`访问属性: ${prop}`);
    // 转发给原始对象
    return Reflect.get(target, prop, receiver);
  }
};

一句话总结

Proxy 提供了拦截和自定义 JavaScript 对象操作的强大能力,通过十三种陷阱方法,我们可以完全控制对象的行为,实现从数据绑定到访问控制等各种高级功能。

Proxy 是现代 JavaScript 中实现元编程的重要工具,它使我们能够创建具有自定义行为的对象,但同时也需要注意性能影响和正确使用各种陷阱方法。