返回首页

30. 说说你对Proxy和Reflect的理解?

问题解析

Proxy 和 Reflect 是 ES6 引入的元编程特性,它们提供了拦截和操作对象行为的能力。面试中主要考察它们的基本用法、拦截器(trap)机制、使用场景以及与 Object.defineProperty 的区别。

核心概念

1. Proxy(代理)

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

// 基本语法
const proxy = new Proxy(target, handler);

// target:要代理的目标对象
// handler:包含拦截器的对象

// 示例
const target = {
  name: 'Alice',
  age: 25
};

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

const proxy = new Proxy(target, handler);

console.log(proxy.name); // Getting name -> Alice
proxy.age = 26;          // Setting age = 26

2. Reflect(反射)

Reflect 是一个内置对象,提供拦截 JavaScript 操作的方法。它的方法与 Proxy 的 handler 方法一一对应。

// Reflect 提供了一系列静态方法
const obj = { name: 'Bob' };

// 替代 Object 方法
Reflect.get(obj, 'name');           // 'Bob'
Reflect.set(obj, 'age', 30);        // true
Reflect.has(obj, 'name');           // true
Reflect.deleteProperty(obj, 'age'); // true
Reflect.ownKeys(obj);               // ['name']

// 替代 new 操作符
class Person {
  constructor(name) {
    this.name = name;
  }
}
const person = Reflect.construct(Person, ['Alice']);

// 替代 Function.prototype.apply/call
function greet(greeting) {
  return `${greeting}, ${this.name}`;
}
Reflect.apply(greet, person, ['Hello']); // 'Hello, Alice'

详细解答

1. Proxy 的拦截器(Traps)

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

const handler = {
  // 读取属性时触发
  get(target, prop, receiver) {
    if (prop.startsWith('_')) {
      throw new Error('Private property access denied');
    }
    console.log(`GET: ${String(prop)}`);
    return Reflect.get(target, prop, receiver);
  },

  // 设置属性时触发
  set(target, prop, value, receiver) {
    if (prop.startsWith('_')) {
      throw new Error('Cannot modify private property');
    }
    if (prop === 'age' && (value < 0 || value > 150)) {
      throw new Error('Invalid age');
    }
    console.log(`SET: ${String(prop)} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  },

  // 判断属性是否存在时触发
  has(target, prop) {
    if (prop.startsWith('_')) return false;
    return Reflect.has(target, prop);
  },

  // 删除属性时触发
  deleteProperty(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error('Cannot delete private property');
    }
    console.log(`DELETE: ${String(prop)}`);
    return Reflect.deleteProperty(target, prop);
  },

  // 获取属性描述符时触发
  getOwnPropertyDescriptor(target, prop) {
    if (prop.startsWith('_')) return undefined;
    return Reflect.getOwnPropertyDescriptor(target, prop);
  },

  // 定义属性时触发
  defineProperty(target, prop, descriptor) {
    console.log(`DEFINE: ${String(prop)}`);
    return Reflect.defineProperty(target, prop, descriptor);
  },

  // 获取所有属性键时触发
  ownKeys(target) {
    return Reflect.ownKeys(target).filter(key => !String(key).startsWith('_'));
  }
};

const proxy = new Proxy(target, handler);

// 使用示例
console.log(proxy.name);     // GET: name -> Alice
proxy.age = 25;              // SET: age = 25
console.log('name' in proxy); // true
console.log('_private' in proxy); // false

2. 函数代理

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

const proxySum = new Proxy(sum, {
  apply(target, thisArg, args) {
    console.log(`Called with args: ${args}`);
    if (args.some(arg => typeof arg !== 'number')) {
      throw new TypeError('All arguments must be numbers');
    }
    return Reflect.apply(target, thisArg, args);
  },

  construct(target, args) {
    throw new Error('Cannot instantiate a function');
  }
});

console.log(proxySum(1, 2)); // Called with args: 1,2 -> 3
// proxySum('a', 'b'); // TypeError

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

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

const person = new ProxyPerson('Alice');
console.log(person.createdAt); // Date object

3. 数组代理

const array = [1, 2, 3];

const proxyArray = new Proxy(array, {
  get(target, prop) {
    if (prop === 'last') {
      return target[target.length - 1];
    }
    if (prop === 'first') {
      return target[0];
    }
    return Reflect.get(target, prop);
  },

  set(target, prop, value) {
    if (prop === 'length') {
      if (value < 0) throw new Error('Length cannot be negative');
    }
    console.log(`Setting index ${prop} to ${value}`);
    return Reflect.set(target, prop, value);
  }
});

console.log(proxyArray.first);  // 1
console.log(proxyArray.last);   // 3
proxyArray.push(4);             // Setting index 3 to 4

深入理解

1. Proxy 与 Object.defineProperty 的区别

// Object.defineProperty - Vue 2.x 使用的方式
const obj1 = {};
Object.defineProperty(obj1, 'name', {
  get() {
    return this._name;
  },
  set(value) {
    this._name = value;
  },
  enumerable: true,
  configurable: true
});

// 缺点:
// 1. 只能拦截已存在的属性
// 2. 无法监听数组索引和长度的变化(需要特殊处理)
// 3. 无法监听新增属性
// 4. 需要递归遍历所有属性

// Proxy - Vue 3.x 使用的方式
const obj2 = {};
const proxy2 = new Proxy(obj2, {
  get(target, prop) {
    return target[prop];
  },
  set(target, prop, value) {
    target[prop] = value;
    return true;
  }
});

// 优点:
// 1. 可以拦截新增属性
// 2. 可以监听数组变化
// 3. 支持更多拦截操作
// 4. 不需要递归遍历

// 对比示例
const arr = [];

// Object.defineProperty 方式
const arrProxy1 = [];
Object.defineProperty(arrProxy1, 'push', {
  value: function(...args) {
    console.log('Array modified');
    return Array.prototype.push.apply(this, args);
  }
});

// Proxy 方式
const arrProxy2 = new Proxy([], {
  set(target, prop, value) {
    console.log(`Setting ${prop} = ${value}`);
    return Reflect.set(target, prop, value);
  }
});

arrProxy2.push(1); // Setting 0 = 1, Setting length = 1

2. 可撤销代理

const { proxy, revoke } = Proxy.revocable({ data: 'secret' }, {
  get(target, prop) {
    return target[prop];
  }
});

console.log(proxy.data); // 'secret'

revoke(); // 撤销代理

// console.log(proxy.data); // TypeError: Cannot perform 'get' on a proxy that has been revoked

3. 嵌套代理

function createDeepProxy(target, handler) {
  const cache = new WeakMap();

  function createProxy(obj) {
    if (typeof obj !== 'object' || obj === null) {
      return obj;
    }

    if (cache.has(obj)) {
      return cache.get(obj);
    }

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

    cache.set(obj, proxy);
    return proxy;
  }

  return createProxy(target);
}

const nested = {
  level1: {
    level2: {
      level3: 'deep value'
    }
  }
};

const deepProxy = createDeepProxy(nested, {
  get(target, prop) {
    console.log(`Accessing: ${prop}`);
    return Reflect.get(target, prop);
  }
});

console.log(deepProxy.level1.level2.level3);
// Accessing: level1
// Accessing: level2
// Accessing: level3
// 'deep value'

最佳实践

1. 数据验证

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

      if (validator) {
        const isValid = validator(value);
        if (!isValid) {
          throw new Error(`Invalid value for ${String(prop)}: ${value}`);
        }
      }

      return Reflect.set(target, prop, value);
    }
  });
}

const userSchema = {
  name: (v) => typeof v === 'string' && v.length >= 2,
  age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
  email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
};

const user = createValidator(userSchema);
user.name = 'Alice';  // OK
user.age = 25;        // OK
user.email = 'alice@example.com'; // OK
// user.age = -1;     // Error: Invalid value for age: -1

2. 私有属性保护

function withPrivateProperties(obj) {
  const privatePrefix = '_';

  return new Proxy(obj, {
    get(target, prop) {
      if (typeof prop === 'string' && prop.startsWith(privatePrefix)) {
        throw new Error(`Access to private property ${prop} is denied`);
      }
      return Reflect.get(target, prop);
    },

    set(target, prop, value) {
      if (typeof prop === 'string' && prop.startsWith(privatePrefix)) {
        throw new Error(`Modification of private property ${prop} is denied`);
      }
      return Reflect.set(target, prop, value);
    },

    has(target, prop) {
      if (typeof prop === 'string' && prop.startsWith(privatePrefix)) {
        return false;
      }
      return Reflect.has(target, prop);
    },

    ownKeys(target) {
      return Reflect.ownKeys(target).filter(
        key => typeof key !== 'string' || !key.startsWith(privatePrefix)
      );
    },

    getOwnPropertyDescriptor(target, prop) {
      if (typeof prop === 'string' && prop.startsWith(privatePrefix)) {
        return undefined;
      }
      return Reflect.getOwnPropertyDescriptor(target, prop);
    }
  });
}

const myObj = withPrivateProperties({
  public: 'visible',
  _private: 'hidden'
});

console.log(myObj.public);    // 'visible'
// console.log(myObj._private); // Error
console.log(Object.keys(myObj)); // ['public']

3. 响应式系统(简化版 Vue3)

function reactive(target) {
  const deps = new Map();

  function getDep(key) {
    if (!deps.has(key)) {
      deps.set(key, new Set());
    }
    return deps.get(key);
  }

  return new Proxy(target, {
    get(target, key) {
      const dep = getDep(key);
      if (activeEffect) {
        dep.add(activeEffect);
      }
      return Reflect.get(target, key);
    },

    set(target, key, value) {
      const result = Reflect.set(target, key, value);
      const dep = getDep(key);
      dep.forEach(effect => effect());
      return result;
    }
  });
}

let activeEffect = null;

function watchEffect(effect) {
  activeEffect = effect;
  effect();
  activeEffect = null;
}

// 使用
const state = reactive({ count: 0 });

watchEffect(() => {
  console.log('Count:', state.count);
});

state.count++; // 触发 effect,输出: Count: 1
state.count++; // 触发 effect,输出: Count: 2

4. 日志和调试

function createLogger(target, name = 'Object') {
  return new Proxy(target, {
    get(target, prop) {
      const value = Reflect.get(target, prop);
      console.log(`[${name}] GET ${String(prop)} =`, value);
      return value;
    },

    set(target, prop, value) {
      const oldValue = target[prop];
      console.log(`[${name}] SET ${String(prop)}:`, oldValue, '->', value);
      return Reflect.set(target, prop, value);
    },

    apply(target, thisArg, args) {
      console.log(`[${name}] CALL with args:`, args);
      return Reflect.apply(target, thisArg, args);
    }
  });
}

const loggedArray = createLogger([], 'MyArray');
loggedArray.push(1); // [MyArray] GET push = function
                     // [MyArray] SET 0: undefined -> 1
                     // [MyArray] SET length: 0 -> 1

面试要点

  1. Proxy 的作用:创建对象的代理,拦截和自定义基本操作
  2. Reflect 的作用:提供一组静态方法,对应 Proxy 的拦截器
  3. 与 Object.defineProperty 的区别:Proxy 可以拦截更多操作,支持新增属性,性能更好
  4. 常用拦截器:get、set、has、deleteProperty、apply、construct 等
  5. 使用场景:数据验证、私有属性、响应式系统、日志记录

常见面试题

// 面试题 1:实现一个只读代理
function readonly(target) {
  return new Proxy(target, {
    set() {
      console.warn('Cannot modify readonly object');
      return true;
    },
    deleteProperty() {
      console.warn('Cannot delete property from readonly object');
      return true;
    }
  });
}

const readonlyObj = readonly({ name: 'Alice' });
// readonlyObj.name = 'Bob'; // Warning

// 面试题 2:实现一个缓存代理
function createCacheProxy(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 expensiveFn = createCacheProxy((n) => {
  console.log('Computing...');
  return n * n;
});

expensiveFn(5); // Computing... 25
expensiveFn(5); // Cache hit 25

// 面试题 3:Proxy 能否代理 null 或 undefined?
// 不能,会抛出 TypeError
// new Proxy(null, {}); // TypeError

// 面试题 4:如何实现链式调用的代理?
const chainable = new Proxy({}, {
  get(target, prop) {
    if (prop === 'getValue') {
      return () => target._value;
    }
    return (value) => {
      target._value = value;
      return chainable;
    };
  }
});

const result = chainable.setA(1).setB(2).setC(3).getValue();
console.log(result); // 3 (最后设置的值)

// 面试题 5:Proxy 的性能考虑
// 1. Proxy 比 Object.defineProperty 性能好
// 2. 但 Proxy 仍然有开销,不适合高频调用场景
// 3. 可以使用缓存优化嵌套 Proxy
// 4. 生产环境注意内存泄漏(WeakMap 缓存)