返回首页

你是怎么理解ES6中Proxy的?使用场景?

问题解析

这个问题考察对ES6 Proxy的理解。Proxy用于定义基本操作的自定义行为,是元编程的重要工具。需要理解Proxy的本质、拦截器(trap)机制、与Reflect的关系以及实际应用场景。

核心概念

1. Proxy的本质

  • 代理模式:拦截并自定义对象的基本操作
  • 元编程:在语言层面修改程序默认行为
  • 拦截器(Trap):拦截特定操作的钩子函数

2. 核心结构

  • new Proxy(target, handler)
  • target:被代理的原始对象
  • handler:包含拦截器的对象

3. 与Reflect的关系

  • Reflect:将对象操作变为函数行为
  • Proxy与Reflect一一对应
  • 配合实现默认行为的转发

详细解答

Proxy基本语法

// 1. 创建Proxy
const target = {
  name: 'Tom',
  age: 25
};

const handler = {
  get(target, prop, receiver) {
    console.log(`Getting ${prop}`);
    return target[prop];
  },
  set(target, prop, value, receiver) {
    console.log(`Setting ${prop} = ${value}`);
    target[prop] = value;
    return true; // 严格模式下必须返回true
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.name); // Getting name, "Tom"
proxy.age = 26;          // Setting age = 26

// 2. Proxy与原始对象的关系
console.log(proxy === target); // false
console.log(proxy.name === target.name); // true

handler拦截属性详解

const handler = {
  // 1. get - 读取属性
  get(target, prop, receiver) {
    if (prop in target) {
      return target[prop];
    }
    return `Property ${prop} not found`;
  },

  // 2. set - 设置属性
  set(target, prop, value, receiver) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a number');
    }
    target[prop] = value;
    return true;
  },

  // 3. has - in操作符
  has(target, prop) {
    return prop in target && !prop.startsWith('_');
  },

  // 4. deleteProperty - delete操作符
  deleteProperty(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error('Cannot delete private property');
    }
    delete target[prop];
    return true;
  },

  // 5. ownKeys - Object.keys等
  ownKeys(target) {
    return Object.keys(target).filter(key => !key.startsWith('_'));
  },

  // 6. getOwnPropertyDescriptor
  getOwnPropertyDescriptor(target, prop) {
    if (prop.startsWith('_')) {
      return undefined;
    }
    return Object.getOwnPropertyDescriptor(target, prop);
  },

  // 7. defineProperty
  defineProperty(target, prop, descriptor) {
    console.log(`Defining ${prop}`);
    return Object.defineProperty(target, prop, descriptor);
  },

  // 8. getPrototypeOf
  getPrototypeOf(target) {
    return Object.getPrototypeOf(target);
  },

  // 9. setPrototypeOf
  setPrototypeOf(target, proto) {
    throw new Error('Cannot change prototype');
  },

  // 10. isExtensible
  isExtensible(target) {
    return Object.isExtensible(target);
  },

  // 11. preventExtensions
  preventExtensions(target) {
    Object.preventExtensions(target);
    return true;
  },

  // 12. apply - 函数调用(target必须是函数)
  apply(target, thisArg, args) {
    console.log(`Called with args: ${args}`);
    return target.apply(thisArg, args);
  },

  // 13. construct - new操作符(target必须是函数)
  construct(target, args, newTarget) {
    console.log(`Constructed with args: ${args}`);
    return new target(...args);
  }
};

Reflect对象

// Reflect提供与Proxy拦截器一一对应的方法
// 将命令式操作变为函数式操作

const obj = { name: 'Tom' };

// 传统方式 vs Reflect方式
// 读取属性
obj.name;
Reflect.get(obj, 'name');

// 设置属性
obj.name = 'Jerry';
Reflect.set(obj, 'name', 'Jerry');

// 判断属性
'name' in obj;
Reflect.has(obj, 'name');

// 删除属性
delete obj.name;
Reflect.deleteProperty(obj, 'name');

// 获取原型
Object.getPrototypeOf(obj);
Reflect.getPrototypeOf(obj);

// 设置原型
Object.setPrototypeOf(obj, proto);
Reflect.setPrototypeOf(obj, proto);

// 在Proxy中使用Reflect转发默认行为
const proxy = new Proxy(obj, {
  get(target, prop, receiver) {
    console.log(`Getting ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(`Setting ${prop}`);
    return Reflect.set(target, prop, value, receiver);
  }
});

get拦截实战

// 1. 数组负索引支持
function createArray(...elements) {
  const handler = {
    get(target, prop, receiver) {
      const index = Number(prop);
      if (index < 0) {
        prop = String(target.length + index);
      }
      return Reflect.get(target, prop, receiver);
    }
  };

  return new Proxy(elements, handler);
}

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

// 2. 默认值
const withDefaults = new Proxy({}, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    }
    return `Default value for ${prop}`;
  }
});

console.log(withDefaults.foo); // "Default value for foo"

// 3. 属性访问链式安全(可选链的替代)
const safe = new Proxy({}, {
  get(target, prop) {
    if (prop === 'get') {
      return () => target;
    }
    if (!(prop in target)) {
      target[prop] = safe; // 返回新的Proxy
    }
    return target[prop];
  }
});

// safe.a.b.c.get() 不会报错,返回 {}

set拦截实战

// 1. 数据校验
const validator = {
  set(target, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('Age must be an integer');
      }
      if (value < 0 || value > 150) {
        throw new RangeError('Age must be between 0 and 150');
      }
    }

    if (prop === 'email') {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(value)) {
        throw new Error('Invalid email format');
      }
    }

    console.log(`Setting ${prop} = ${value}`);
    target[prop] = value;
    return true;
  }
};

const person = new Proxy({}, validator);
person.age = 25;     // OK
// person.age = -5;  // RangeError
// person.age = 'a'; // TypeError

// 2. 私有属性保护
const privateFields = new Set(['_password', '_secret']);
const privacyHandler = {
  set(target, prop, value) {
    if (privateFields.has(prop)) {
      throw new Error(`Cannot set private property: ${prop}`);
    }
    target[prop] = value;
    return true;
  },
  get(target, prop) {
    if (privateFields.has(prop)) {
      throw new Error(`Cannot access private property: ${prop}`);
    }
    return target[prop];
  }
};

const user = new Proxy({ name: 'Tom', _password: '123' }, privacyHandler);
console.log(user.name);      // 'Tom'
// console.log(user._password); // Error

// 3. 响应式数据绑定
function createReactive(obj, callback) {
  return new Proxy(obj, {
    set(target, prop, value) {
      const oldValue = target[prop];
      target[prop] = value;
      if (oldValue !== value) {
        callback(prop, value, oldValue);
      }
      return true;
    }
  });
}

const data = createReactive({ count: 0 }, (prop, val, old) => {
  console.log(`${prop} changed from ${old} to ${val}`);
});

data.count = 1; // "count changed from 0 to 1"

函数代理

// 1. 函数参数日志
function logWrapper(fn) {
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      console.log(`Calling ${target.name} with:`, args);
      const result = Reflect.apply(target, thisArg, args);
      console.log(`${target.name} returned:`, result);
      return result;
    }
  });
}

const add = logWrapper((a, b) => a + b);
add(2, 3);
// Calling anonymous with: [2, 3]
// anonymous returned: 5

// 2. 函数缓存(Memoization)
function memoize(fn) {
  const cache = new Map();
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);
      if (cache.has(key)) {
        console.log('Cache hit');
        return cache.get(key);
      }
      const result = Reflect.apply(target, thisArg, args);
      cache.set(key, result);
      return result;
    }
  });
}

const fib = memoize(function(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
});

console.log(fib(40)); // 快速计算,大量缓存命中

// 3. 构造函数代理
class Person {
  constructor(name) {
    this.name = name;
  }
}

const PersonProxy = new Proxy(Person, {
  construct(target, args, newTarget) {
    console.log(`Creating instance with args:`, args);
    return Reflect.construct(target, args, newTarget);
  }
});

const person = new PersonProxy('Tom');
// Creating instance with args: ['Tom']

深入理解

1. Proxy的代理层级

// Proxy只能代理一层,嵌套对象需要递归代理
function deepProxy(obj, handler) {
  Object.keys(obj).forEach(key => {
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      obj[key] = deepProxy(obj[key], handler);
    }
  });
  return new Proxy(obj, handler);
}

// 更好的实现:使用get拦截器递归代理
function createDeepProxy(target, handler) {
  const preProxy = new WeakSet();

  function makeProxy(obj) {
    if (preProxy.has(obj) || typeof obj !== 'object' || obj === null) {
      return obj;
    }
    preProxy.add(obj);

    return new Proxy(obj, {
      ...handler,
      get(target, prop, receiver) {
        const value = Reflect.get(target, prop, receiver);
        return makeProxy(value);
      }
    });
  }

  return makeProxy(target);
}

const nested = createDeepProxy({ a: { b: { c: 1 } } }, {
  set(target, prop, value) {
    console.log(`Setting ${prop}`);
    return Reflect.set(target, prop, value);
  }
});

nested.a.b.c = 2; // 触发所有层级的set

2. Proxy与原始对象的区分

const target = {};
const proxy = new Proxy(target, {});

// Proxy与target是不同的对象
console.log(proxy === target); // false

// 但操作会同步到target
proxy.a = 1;
console.log(target.a); // 1

// 使用Proxy.revocable创建可撤销代理
const { proxy: revocableProxy, revoke } = Proxy.revocable({}, {
  get(target, prop) {
    return target[prop];
  }
});

console.log(revocableProxy.foo); // undefined
revoke(); // 撤销代理
// revocableProxy.foo; // TypeError: Cannot perform 'get' on a proxy that has been revoked

3. Proxy的性能考量

// Proxy有性能开销,不适合高频调用场景

// 性能测试
const obj = {};
const proxy = new Proxy({}, {});

console.time('plain object');
for (let i = 0; i < 1000000; i++) {
  obj[i] = i;
}
console.timeEnd('plain object');

console.time('proxy');
for (let i = 0; i < 1000000; i++) {
  proxy[i] = i;
}
console.timeEnd('proxy');

// Proxy通常比直接操作慢几倍到几十倍
// 适用场景:开发工具、框架底层、需要拦截的场景
// 不适用:高频计算、性能敏感的核心逻辑

4. Proxy的this绑定问题

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

const proxy = new Proxy(target, {
  get(target, prop, receiver) {
    const value = target[prop];
    if (typeof value === 'function') {
      // 确保this指向proxy
      return value.bind(receiver);
    }
    return value;
  }
});

console.log(proxy.greet()); // "Hello, Tom"

// 或者使用Reflect保证正确的this绑定
const proxy2 = new Proxy(target, {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  }
});

5. 不可代理的对象

// 某些内置对象不能被代理或代理后行为异常

// 1. Date对象
const date = new Date();
const proxyDate = new Proxy(date, {});
// proxyDate.getTime(); // TypeError: this is not a Date object

// 解决方法
const proxyDateFixed = new Proxy(date, {
  get(target, prop) {
    const value = target[prop];
    if (typeof value === 'function') {
      return value.bind(target);
    }
    return value;
  }
});

// 2. Map、Set等集合类型
const map = new Map();
const proxyMap = new Proxy(map, {});
// proxyMap.set('key', 'value'); // TypeError

// 3. 已冻结的对象
const frozen = Object.freeze({ a: 1 });
const proxyFrozen = new Proxy(frozen, {
  set() {
    console.log('set called');
    return true;
  }
});
proxyFrozen.a = 2; // 严格模式下TypeError,非严格模式下静默失败

最佳实践

1. 数据验证

// 使用Proxy实现ORM风格的模型验证
class Model {
  constructor(data = {}) {
    return new Proxy(this, {
      set(target, prop, value) {
        const schema = target.constructor.schema || {};
        const field = schema[prop];

        if (field) {
          // 类型检查
          if (field.type && typeof value !== field.type) {
            throw new TypeError(`${prop} must be ${field.type}`);
          }
          // 必填检查
          if (field.required && (value === undefined || value === null)) {
            throw new Error(`${prop} is required`);
          }
          // 自定义验证
          if (field.validate && !field.validate(value)) {
            throw new Error(`${prop} validation failed`);
          }
        }

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

class User extends Model {
  static schema = {
    name: { type: 'string', required: true },
    age: {
      type: 'number',
      validate: v => v >= 0 && v <= 150
    },
    email: {
      type: 'string',
      validate: v => /^[^\s@]+@[^\s@]+$/.test(v)
    }
  };
}

const user = new User();
user.name = 'Tom';  // OK
user.age = 25;      // OK
// user.age = -5;   // Error

2. 私有属性保护

// 使用Symbol或WeakMap配合Proxy实现真正的私有属性
const privateData = new WeakMap();

function withPrivacy(Class) {
  return new Proxy(Class, {
    construct(target, args) {
      const instance = new target(...args);
      privateData.set(instance, {});

      return new Proxy(instance, {
        get(target, prop) {
          if (typeof prop === 'string' && prop.startsWith('_')) {
            throw new Error(`Private property ${prop} is not accessible`);
          }
          return target[prop];
        },
        set(target, prop, value) {
          if (typeof prop === 'string' && prop.startsWith('_')) {
            throw new Error(`Private property ${prop} is not accessible`);
          }
          target[prop] = value;
          return true;
        }
      });
    }
  });
}

@withPrivacy
class BankAccount {
  constructor() {
    this._balance = 0; // 受保护的私有属性
  }

  deposit(amount) {
    this._balance += amount;
  }

  getBalance() {
    return this._balance;
  }
}

const account = new BankAccount();
account.deposit(100);
console.log(account.getBalance()); // 100
// console.log(account._balance);  // Error

3. 观察者模式

// 使用Proxy实现响应式系统
class Observable {
  constructor(obj) {
    this.listeners = new Map();

    return new Proxy(obj, {
      set: (target, prop, value) => {
        const oldValue = target[prop];
        target[prop] = value;

        if (oldValue !== value) {
          this.notify(prop, value, oldValue);
        }
        return true;
      }
    });
  }

  subscribe(prop, callback) {
    if (!this.listeners.has(prop)) {
      this.listeners.set(prop, new Set());
    }
    this.listeners.get(prop).add(callback);

    // 返回取消订阅函数
    return () => {
      this.listeners.get(prop).delete(callback);
    };
  }

  notify(prop, newValue, oldValue) {
    if (this.listeners.has(prop)) {
      this.listeners.get(prop).forEach(cb => cb(newValue, oldValue));
    }
    // 通知通配符监听
    if (this.listeners.has('*')) {
      this.listeners.get('*').forEach(cb => cb(prop, newValue, oldValue));
    }
  }
}

// 使用
const state = new Observable({ count: 0, name: 'Tom' });

const unsubscribe = state.subscribe('count', (newVal, oldVal) => {
  console.log(`Count changed: ${oldVal} -> ${newVal}`);
});

state.count = 1; // "Count changed: 0 -> 1"
state.count = 2; // "Count changed: 1 -> 2"

unsubscribe();
state.count = 3; // 无输出

4. 函数参数日志与性能监控

// 开发环境调试工具
function createInstrumentedFunction(fn, options = {}) {
  const { logArgs = true, logResult = true, logTime = true } = options;

  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const start = performance.now();

      if (logArgs) {
        console.log(`[${target.name}] Called with:`, args);
      }

      try {
        const result = Reflect.apply(target, thisArg, args);

        if (logTime) {
          console.log(`[${target.name}] Execution time: ${performance.now() - start}ms`);
        }

        if (logResult) {
          console.log(`[${target.name}] Returned:`, result);
        }

        return result;
      } catch (error) {
        console.error(`[${target.name}] Threw:`, error);
        throw error;
      }
    }
  });
}

// 使用
const instrumentedAdd = createInstrumentedFunction((a, b) => a + b);
instrumentedAdd(2, 3);
// [anonymous] Called with: [2, 3]
// [anonymous] Execution time: 0.1ms
// [anonymous] Returned: 5

5. 不可变数据

// 使用Proxy实现不可变对象
function immutable(obj) {
  return new Proxy(obj, {
    set() {
      throw new Error('Cannot modify immutable object');
    },
    deleteProperty() {
      throw new Error('Cannot delete property of immutable object');
    }
  });
}

const config = immutable({ apiUrl: 'https://api.example.com' });
console.log(config.apiUrl); // OK
// config.apiUrl = '...';   // Error
// delete config.apiUrl;    // Error

// 深不可变
function deepImmutable(obj) {
  Object.freeze(obj);
  Object.keys(obj).forEach(key => {
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      deepImmutable(obj[key]);
    }
  });
  return obj;
}

面试要点

  1. Proxy的本质

    • 代理模式,拦截并自定义对象操作
    • 元编程工具,修改程序默认行为
    • new Proxy(target, handler)
  2. 常用拦截器(trap)

    • get/set:属性读写
    • has:in操作符
    • deleteProperty:delete操作符
    • ownKeys:Object.keys等
    • apply:函数调用
    • construct:new操作符
  3. Reflect的作用

    • 与Proxy一一对应的函数式API
    • 在Proxy中用于转发默认行为
    • 返回布尔值表示操作是否成功
  4. 使用场景

    • 数据验证
    • 私有属性保护
    • 观察者模式/响应式数据
    • 函数参数日志
    • 缓存(memoization)
    • 不可变数据
  5. 注意事项

    • Proxy与原始对象是不同的对象
    • Proxy只能代理一层,嵌套需要递归处理
    • 有性能开销,不适合高频调用
    • 某些内置对象(Date、Map等)代理后行为异常
    • 使用Proxy.revocable创建可撤销代理
  6. 与Object.defineProperty的区别

    • Proxy是代理整个对象,defineProperty是定义单个属性
    • Proxy可以拦截更多操作(in、delete等)
    • Proxy是ES6新特性,defineProperty是ES5
    • Vue3使用Proxy替代defineProperty实现响应式
  7. this绑定问题

    • 方法中的this默认指向原始对象
    • 需要在get拦截器中bind或使用Reflect.get保证正确this