JavaScript Proxy与Reflect

在 JavaScript 中如何拦截对象的读取、修改、删除操作?Vue3的响应式系统如何实现?本文将深入讲解 JavaScript 的元编程世界,探索 Proxy 和 Reflect 的奥秘。

前言:从Vue3的响应式说起

javascript 复制代码
// Vue3的响应式数据实现原理
const reactive = (target) => {
    return new Proxy(target, {
        get(target, key, receiver) {
            track(target, key); // 依赖收集
            return Reflect.get(target, key, receiver);
        },
        set(target, key, value, receiver) {
            const result = Reflect.set(target, key, value, receiver);
            trigger(target, key); // 触发更新
            return result;
        }
    });
};

Vue3的响应式数据实现原理的背后,就是 Proxy 和 Reflect 。理解它们,我们就能真正掌握 JavaScript 元编程的能力。

理解Proxy与Reflect

什么是Proxy(代理)?

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

Proxy工作流程示意图

text 复制代码
┌─────────┐   操作请求   ┌─────────┐   转发操作    ┌─────────┐
│ 客户端   │───────────→ │  Proxy  │───────────→ │ 目标对象 │
│         │             │  代理    │             │         │
│         │←─────────── │         │←─────────── │         │
└─────────┘   响应结果   └─────────┘   实际结果    └─────────┘
                ↓                           ↓
         ┌─────────────┐            ┌─────────────┐
         │ 拦截和自定义  │            │ 原始行为     │
         │   (陷阱)   │            │             │
         └─────────────┘            └─────────────┘

什么是Reflect(反射)?

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

为什么需要Reflect?

  1. 统一的操作API
  2. 更好的错误处理(返回布尔值而非抛出异常)
  3. 与Proxy陷阱方法一一对应

代理基础:创建第一个代理

javascript 复制代码
// 目标对象
const target = {
  name: '张三',
  age: 30
};

// 处理器对象(包含陷阱方法)
const handler = {
  // 拦截属性读取
  get(target, property, receiver) {

    // 添加自定义逻辑
    if (property === 'age') {
      return `${target[property]}岁`;
    }

    // 默认行为:使用Reflect
    return Reflect.get(target, property, receiver);
  },

  // 拦截属性设置
  set(target, property, value, receiver) {
    // 添加验证逻辑
    if (property === 'age' && (value < 0 || value > 150)) {
      console.warn('年龄必须在0-150之间');
      return false; // 返回false表示设置失败
    }

    // 默认行为:使用Reflect
    return Reflect.set(target, property, value, receiver);
  }
};

// 创建代理
const proxy = new Proxy(target, handler);

console.log(proxy.name);  // 张三
console.log(proxy.age);   // 30岁
console.log(target.age); // 30

13种可拦截的陷阱方法

完整陷阱方法概览

javascript 复制代码
const completeHandler = {
  // 1. 属性访问拦截
  get(target, prop, receiver) {
    console.log(`get: ${prop}`);
    return Reflect.get(target, prop, receiver);
  },

  // 2. 属性赋值拦截
  set(target, prop, value, receiver) {
    console.log(`set: ${prop} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  },

  // 3. in操作符拦截
  has(target, prop) {
    console.log(`has: ${prop}`);
    return Reflect.has(target, prop);
  },

  // 4. delete操作符拦截
  deleteProperty(target, prop) {
    console.log(`delete: ${prop}`);
    return Reflect.deleteProperty(target, prop);
  },

  // 5. 构造函数调用拦截
  construct(target, args, newTarget) {
    console.log(`construct with args:`, args);
    return Reflect.construct(target, args, newTarget);
  },

  // 6. 函数调用拦截
  apply(target, thisArg, args) {
    console.log(`apply: thisArg=`, thisArg, 'args=', args);
    return Reflect.apply(target, thisArg, args);
  },

  // 7. Object.getOwnPropertyDescriptor拦截
  getOwnPropertyDescriptor(target, prop) {
    console.log(`getOwnPropertyDescriptor: ${prop}`);
    return Reflect.getOwnPropertyDescriptor(target, prop);
  },

  // 8. Object.defineProperty拦截
  defineProperty(target, prop, descriptor) {
    console.log(`defineProperty: ${prop}`, descriptor);
    return Reflect.defineProperty(target, prop, descriptor);
  },

  // 9. 原型访问拦截
  getPrototypeOf(target) {
    console.log('getPrototypeOf');
    return Reflect.getPrototypeOf(target);
  },

  // 10. 原型设置拦截
  setPrototypeOf(target, prototype) {
    console.log(`setPrototypeOf:`, prototype);
    return Reflect.setPrototypeOf(target, prototype);
  },

  // 11. 可扩展性判断拦截
  isExtensible(target) {
    console.log('isExtensible');
    return Reflect.isExtensible(target);
  },

  // 12. 防止扩展拦截
  preventExtensions(target) {
    console.log('preventExtensions');
    return Reflect.preventExtensions(target);
  },

  // 13. 自身属性枚举拦截
  ownKeys(target) {
    console.log('ownKeys');
    return Reflect.ownKeys(target);
  }
};

陷阱方法详细解析

1. get陷阱:属性访问拦截

get() 会在获取属性值的操作中被调用:

javascript 复制代码
const getHandler = {
  get(target, property, receiver) {
    // 处理不存在的属性
    if (!(property in target)) {
      throw new Error(`属性不存在: ${property}`);
    }

    // 处理私有属性(约定以_开头的属性为私有)
    if (property.startsWith('_')) {
      throw new Error(`不能访问私有属性: ${property}`);
    }
    return Reflect.get(target, property, receiver);
  }
};
  • 参数说明:
    1. target:目标对象
    2. property:引用的目标对象上的属性
    3. receiver:代理对象
  • 返回值说明:返回值无限制

2. set陷阱:属性赋值拦截

set() 会在设置属性值的操作中被调用:

javascript 复制代码
const setHandler = {
  set(target, property, value, receiver) {
    // 只读属性检查(约定以$开头的属性为只读)
    if (property.startsWith('$')) {
      throw new Error(`属性不可修改: ${property}`);
    }
    // 执行设置
    return Reflect.set(target, property, value, receiver);
  }
};
  • 参数说明:
    1. target:目标对象
    2. property:引用的目标对象上的属性
    3. value:要给属性设置的值
    4. receiver:代理对象
  • 返回值说明:返回 true 表示成功;返回false表示失败。

3. has陷阱:in操作符拦截

has() 会在 in 操作符中被调用:

javascript 复制代码
const hasHandler = {
  has(target, property) {
    // 隐藏某些属性(不在in操作中暴露)
    const hiddenProperties = ['password', 'token', '_internal'];
    if (hiddenProperties.includes(property)) {
      console.log(`隐藏属性: ${property}`);
      return false;
    }

    // 虚拟属性(不存在但返回true)
    const virtualProperties = ['isAdmin', 'hasAccess'];
    if (virtualProperties.includes(property)) {
      console.log(`虚拟属性: ${property}`);
      return true;
    }

    return Reflect.has(target, property);
  }
};
  • 参数说明:
    1. target:目标对象
    2. value:要给属性设置的值
  • 返回值说明:返回布尔值表示属性是否存在,返回非布尔型会被转成布尔型。

注:Object.keys() 不受 has() 影响,仍会返回所有属性。

4. apply陷阱:函数调用拦截

apply() 会在调用函数时被调用:

javascript 复制代码
const applyHandler = {
  apply(target, thisArg, argumentsList) {
    // 参数验证
    if (target.name === 'calculate') {
      if (argumentsList.length < 2) {
        throw new Error('calculate函数需要至少2个参数');
      }
      if (!argumentsList.every(arg => typeof arg === 'number')) {
        throw new Error('calculate函数的所有参数必须是数字');
      }
    }
    return Reflect.apply(target, thisArg, argumentsList);
  }
};
  • 参数说明:
    1. target:目标对象
    2. thisArg:调用函数时的this参数
    3. argumentsList:调用函数时的参数列表
  • 返回值说明:返回值无限制

5. construct陷阱:构造函数拦截

construct() 会在调用函数时被调用:

javascript 复制代码
const constructHandler = {
  construct(target, argumentsList, newTarget) {
    // 单例模式:确保只有一个实例
    if (target._instance) {
      console.log('返回现有实例');
      return target._instance;
    }

    // 创建实例
    const instance = Reflect.construct(target, argumentsList, newTarget);

    // 为单例保存实例
    if (target.name === 'Singleton') {
      target._instance = instance;
    }

    // 为实例添加额外属性
    instance.createdAt = new Date();
    instance._id = Math.random().toString(36).substr(2, 9);

    console.log('创建新实例,ID:', instance._id);
    return instance;
  }
};
  • 参数说明:
    1. target:目标构造函数
    2. argumentsList:传给目标构造函数的参数列表
    3. newTarget:最初被调用的构造函数
  • 返回值说明:必须返回一个对象

6. ownKeys陷阱:影响Object.keys()等遍历

ownKeys() 会在Object.keys()及类似方法中被调用:

javascript 复制代码
const ownKeysHandler = {
  ownKeys(target) {
    // 过滤掉以_开头的私有属性
    const keys = Reflect.ownKeys(target);
    return keys.filter(key => {
      if (typeof key === 'string') {
        return !key.startsWith('_');
      }
      return true;
    });
  }
};
  • 参数说明:
    1. target:目标构造函数
  • 返回值说明:必须返回一个包含字符串或符号的可枚举对象

7. getOwnPropertyDescriptor陷阱:影响Object.getOwnPropertyDescriptor()

getOwnPropertyDescriptor() 会在Object.getOwnPropertyDescriptor()方法中被调用:

javascript 复制代码
const getOwnPropertyDescriptorHandler = {
  getOwnPropertyDescriptor(target, prop) {
    // 隐藏私有属性的描述符
    if (prop.startsWith('_')) {
      return undefined;
    }
    return Reflect.getOwnPropertyDescriptor(target, prop);
  }
};
  • 参数说明:
    1. target:目标构造函数
    2. prop:引用的目标对象上的字符串属性
  • 返回值说明:必须返回对象,或在属性不存在时返回 undefined

8. defineProperty陷阱:拦截Object.defineProperty()

defineProperty() 会在Object.defineProperty()方法中被调用:

javascript 复制代码
const definePropertyHandler = {
  defineProperty(target, prop, descriptor) {
    // 防止修改只读属性
    if (prop === 'id' && target.id) {
      console.warn('id属性是只读的');
      return false;
    }
    return Reflect.defineProperty(target, prop, descriptor);
  }
};
  • 参数说明:
    1. target:目标构造函数
    2. prop:引用的目标对象上的字符串属性
    3. descriptor:包含可选的enumrable、configurable、value、get 和 set 等定义的对象
  • 返回值说明:必须返回布尔值,表示属性是否成功定义

9. deleteProperty陷阱:拦截delete操作

deleteProperty() 会在 delete 操作中被调用:

javascript 复制代码
const deletePropertyHandler = {
  deleteProperty(target, prop) {
    // 防止删除重要属性
    const protectedProps = ['id', 'createdAt'];
    if (protectedProps.includes(prop)) {
      console.warn(`不能删除受保护的属性: ${prop}`);
      return false;
    }
    return Reflect.deleteProperty(target, prop);
  }
};
  • 参数说明:
    1. target:目标构造函数
    2. property:引用的目标对象上的属性
  • 返回值说明:必须返回布尔值,表示属性是否被成功删除

10. preventExtensions陷阱:拦截Object.preventExtensions()

preventExtensions() 会在 Object.preventExtensions() 方法中被调用:

javascript 复制代码
const preventExtensionsHandler = {
  preventExtensions(target) {
    // 在阻止扩展前添加标记
    target._frozenAt = new Date();
    return Reflect.preventExtensions(target);
  }
};
  • 参数说明:
    1. target:目标构造函数
  • 返回值说明:必须返回布尔值,表示 target 是否已经不可扩展

11. getPrototypeOf陷阱:拦截Object.getPrototypeOf()

getPrototypeOf() 会在 Object.getPrototypeOf() 方法中被调用:

javascript 复制代码
const getPrototypeOfHandler = {
  getPrototypeOf(target) {
    return Reflect.getPrototypeOf(target);
  }
};
  • 参数说明:
    1. target:目标构造函数
  • 返回值说明:必须返回对象或null

12. setPrototypeOf陷阱:拦截Object.setPrototypeOf()

setPrototypeOf() 会在 Object.setPrototypeOf() 方法中被调用:

javascript 复制代码
const setPrototypeOfHandler = {
  setPrototypeOf(target, prototype) {
    return Reflect.setPrototypeOf(target, prototype);
  }
};
  • 参数说明:
    1. target:目标构造函数
    2. property:引用的目标对象上的属性
  • 返回值说明:必须返回布尔值,表示原型赋值是否成功

13. isExtensible陷阱:拦截Object.isExtensible()

isExtensible() 会在 Object.isExtensible() 方法中被调用:

javascript 复制代码
const isExtensibleHandler = {
  isExtensible(targete) {
    return Reflect.isExtensible(targete);
  }
};
  • 参数说明:
    1. target:目标构造函数
  • 返回值说明:必须返回布尔值,表示 target 是否可扩展

可撤销代理

什么是可撤销代理?

可撤销代理 (Revocable Proxy)是一种特殊的代理,它提供了一个 revoke() 方法,调用该方法后,代理将不再可用,所有对代理的操作都会抛出 TypeError,即:会中断代理对象和目标对象之间的联系。

javascript 复制代码
const target = { name: 'zhangsan' };
const handler = {
  get() {
    return 'foo';
  }
}
// 创建可撤销代理
const { proxy, revoke } = Proxy.revocable(target, handler);

console.log(proxy.name); // 'zhangsan'

// 撤销代理
revoke();

// 代理已失效
try {
  console.log(proxy.name); // TypeError: Cannot perform 'get' on a proxy that has been revoked
} catch (error) {
  console.log('错误:', error.message);
}

实现响应式数据(Vue3原理)

javascript 复制代码
// 简易版Vue3响应式系统
class ReactiveSystem {
  constructor() {
    this.targetMap = new WeakMap(); // 存储依赖关系
    this.effectStack = [];          // 当前正在执行的effect
    this.batchQueue = new Set();    // 批量更新队列
    this.isBatching = false;        // 是否处于批量更新模式
  }

  // 核心:创建响应式对象
  reactive(target) {
    return new Proxy(target, {
      get(target, key, receiver) {
        // 获取原始值
        const result = Reflect.get(target, key, receiver);

        // 依赖收集
        track(target, key);

        // 深度响应式(如果值是对象,继续代理)
        if (result && typeof result === 'object') {
          return reactive(result);
        }

        return result;
      },

      set(target, key, value, receiver) {
        // 获取旧值
        const oldValue = target[key];

        // 设置新值
        const result = Reflect.set(target, key, value, receiver);

        // 触发更新(值确实改变时才触发)
        if (oldValue !== value) {
          trigger(target, key);
        }

        return result;
      },

      // 处理删除操作
      deleteProperty(target, key) {
        const hasKey = key in target;
        const result = Reflect.deleteProperty(target, key);

        if (hasKey && result) {
          trigger(target, key);
        }

        return result;
      }
    });
  }

  // 依赖收集
  track(target, key) {
    if (this.effectStack.length === 0) return;

    let depsMap = this.targetMap.get(target);
    if (!depsMap) {
      depsMap = new Map();
      this.targetMap.set(target, depsMap);
    }

    let dep = depsMap.get(key);
    if (!dep) {
      dep = new Set();
      depsMap.set(key, dep);
    }

    // 收集当前正在执行的effect
    const activeEffect = this.effectStack[this.effectStack.length - 1];
    if (activeEffect) {
      dep.add(activeEffect);
      // effect也需要知道哪些依赖收集了它
      activeEffect.deps.push(dep);
    }
  }

  // 触发更新
  trigger(target, key) {
    const depsMap = this.targetMap.get(target);
    if (!depsMap) return;

    const dep = depsMap.get(key);
    if (dep) {
      // 如果是批量更新模式,加入队列
      if (this.isBatching) {
        dep.forEach(effect => this.batchQueue.add(effect));
      } else {
        // 立即执行所有依赖的effect
        dep.forEach(effect => {
          if (effect !== this.effectStack[this.effectStack.length - 1]) {
            effect.run();
          }
        });
      }
    }
  }

  // 创建effect(副作用)
  effect(fn) {
    const effect = new ReactiveEffect(fn, this);
    effect.run();

    // 返回停止函数
    const runner = effect.run.bind(effect);
    runner.effect = effect;
    runner.stop = () => effect.stop();

    return runner;
  }

  // 开始批量更新
  batchStart() {
    this.isBatching = true;
  }

  // 结束批量更新
  batchEnd() {
    this.isBatching = false;

    // 执行队列中的所有effect
    this.batchQueue.forEach(effect => {
      if (effect !== this.effectStack[this.effectStack.length - 1]) {
        effect.run();
      }
    });

    this.batchQueue.clear();
  }

  // computed计算属性
  computed(getter) {
    let value;
    let dirty = true;

    const runner = this.effect(() => {
      value = getter();
      dirty = false;
    });

    return {
      get value() {
        if (dirty) {
          runner();
        }
        return value;
      }
    };
  }

  // watch侦听器
  watch(source, callback, options = {}) {
    let getter;

    if (typeof source === 'function') {
      getter = source;
    } else {
      getter = () => this.traverse(source);
    }

    let oldValue;
    const job = () => {
      const newValue = runner();
      callback(newValue, oldValue);
      oldValue = newValue;
    };

    const runner = this.effect(getter, {
      lazy: true,
      scheduler: job
    });

    oldValue = runner();

    if (options.immediate) {
      job();
    }
  }

  // 深度遍历对象(用于watch)
  traverse(value, seen = new Set()) {
    if (typeof value !== 'object' || value === null || seen.has(value)) {
      return value;
    }

    seen.add(value);

    for (const key in value) {
      this.traverse(value[key], seen);
    }

    return value;
  }
}

// 响应式effect类
class ReactiveEffect {
  constructor(fn, scheduler) {
    this.fn = fn;
    this.scheduler = scheduler;
    this.deps = [];
    this.active = true;
  }

  run() {
    if (!this.active) return;

    try {
      this.scheduler.effectStack.push(this);
      return this.fn();
    } finally {
      this.scheduler.effectStack.pop();
    }
  }

  stop() {
    if (this.active) {
      // 从所有依赖中移除自己
      this.deps.forEach(dep => dep.delete(this));
      this.deps.length = 0;
      this.active = false;
    }
  }
}

实现不可变数据(immer.js原理)

javascript 复制代码
// 简易版immer实现
class Immer {
  constructor(baseState) {
    this.baseState = baseState;
    this.draft = this.createDraft(baseState);
    this.isModified = false;
  }

  // 创建草稿(代理)
  createDraft(base) {
    // 如果不是对象,直接返回
    if (typeof base !== 'object' || base === null) {
      return base;
    }

    // 处理数组
    if (Array.isArray(base)) {
      return this.createArrayDraft(base);
    }

    // 处理对象
    return this.createObjectDraft(base);
  }

  // 创建对象草稿
  createObjectDraft(base) {
    const draft = Array.isArray(base) ? [] : {};

    // 复制所有属性
    for (const key in base) {
      if (base.hasOwnProperty(key)) {
        draft[key] = this.createDraft(base[key]);
      }
    }

    // 创建代理
    return new Proxy(draft, {
      get: (target, prop, receiver) => {
        // 特殊属性处理
        if (prop === '__immer_draft__') return true;
        if (prop === '__immer_original__') return base;

        return Reflect.get(target, prop, receiver);
      },

      set: (target, prop, value, receiver) => {
        this.isModified = true;

        // 如果新值是普通值,直接设置
        if (typeof value !== 'object' || value === null) {
          return Reflect.set(target, prop, value, receiver);
        }

        // 如果新值是草稿,获取其最终状态
        if (value.__immer_draft__) {
          value = this.finalize(value);
        }

        // 如果新值是对象,创建新的草稿
        const newDraft = this.createDraft(value);
        return Reflect.set(target, prop, newDraft, receiver);
      },

      deleteProperty: (target, prop) => {
        this.isModified = true;
        return Reflect.deleteProperty(target, prop);
      }
    });
  }

  // 创建数组草稿(需要特殊处理)
  createArrayDraft(base) {
    const draft = this.createObjectDraft(base);

    // 代理数组方法
    const arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];

    arrayMethods.forEach(method => {
      const originalMethod = Array.prototype[method];

      draft[method] = function (...args) {
        this.isModified = true;
        return originalMethod.apply(this, args);
      }.bind({ isModified: false }); // 这里简化处理
    });

    return draft;
  }

  // 获取草稿
  getDraft() {
    return this.draft;
  }

  // 完成修改,生成新状态
  produce(producer) {
    // 执行生产者函数
    producer(this.draft);

    // 如果没有修改,返回原状态
    if (!this.isModified) {
      return this.baseState;
    }

    // 最终化草稿,生成新状态
    return this.finalize(this.draft);
  }

  // 最终化草稿(将草稿转换为普通对象)
  finalize(draft) {
    // 如果不是草稿,直接返回
    if (!draft || !draft.__immer_draft__) {
      return draft;
    }

    const original = draft.__immer_original__;
    const result = Array.isArray(original) ? [] : {};
    let hasChanges = false;

    // 收集所有键(包括原对象和新对象)
    const allKeys = new Set([
      ...Object.keys(original || {}),
      ...Object.keys(draft)
    ]);

    for (const key of allKeys) {
      const originalValue = original ? original[key] : undefined;
      const draftValue = draft[key];

      // 如果属性被删除
      if (!(key in draft)) {
        hasChanges = true;
        continue;
      }

      // 如果值是草稿,递归最终化
      if (draftValue && draftValue.__immer_draft__) {
        const finalized = this.finalize(draftValue);
        if (finalized !== originalValue) {
          hasChanges = true;
          result[key] = finalized;
        } else {
          result[key] = originalValue;
        }
      }
      // 如果值被修改
      else if (draftValue !== originalValue) {
        hasChanges = true;
        result[key] = draftValue;
      }
      // 值未修改
      else {
        result[key] = originalValue;
      }
    }

    return hasChanges ? result : original;
  }
}

// 简化API
function produce(baseState, producer) {
  const immer = new Immer(baseState);
  return immer.produce(producer);
}

结语

Proxy和Reflect开启了JavaScript元编程的新篇章,它们不仅是框架开发的利器,也是理解现代JavaScript运行时特性的关键。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

相关推荐
mCell8 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell9 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭9 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清9 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
萧曵 丶9 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
银烛木9 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076609 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声9 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易9 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得010 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化