JS Proxy 核心原理、实战痛点与常用解决方案

7568 字
38 分钟
JS Proxy 核心原理、实战痛点与常用解决方案

目录#


1. 引言:你用的是原对象,还是代理对象?#

在日常前端开发中,Proxy 代理对象早已无处不在。

你在使用 Vue3 时,reactive() 返回的是一个代理对象;你在使用 Immer 时,produce() 回调中的 draft 也是一个代理对象;甚至很多状态管理库(Zustand、Redux Toolkit)底层都依赖 Proxy 来实现响应式或不可变更新。

但很多开发者会遇到这样的困惑:

const obj = { name: 'Alice', age: 25 };
const proxyObj = new Proxy(obj, {});
console.log(proxyObj.name); // 'Alice' — 看起来和原对象一模一样?
console.log(typeof proxyObj); // 'object' — 类型也是 object?
console.log(proxyObj === obj); // false — 但它们不是同一个引用!

问题来了:

  • 为什么 proxyObj 用起来和普通对象几乎一样?
  • 如何判断一个变量到底是普通对象还是 Proxy 代理对象
  • 如果拿到的是一个 Proxy,能否从中提取出原始的 target 对象
  • 主流框架是如何利用 Proxy 的?又有什么坑需要规避?

本文将由浅入深,带你彻底搞懂 Proxy 的核心原理与实战痛点。


2. 检测 Proxy 的多种方案#

2.1 原生 JS 没有内置 isProxy() 方法#

这是很多开发者踩过的第一个坑。与 Vue3 提供了 isProxy() 不同,原生 JavaScript 并没有提供任何内置方法来判断一个对象是否为 Proxy

// ❌ 原生 JS 中不存在这个方法
typeof isProxy; // 'undefined'
// Vue3 中有,但那是 Vue 自己封装的
// import { isProxy } from 'vue';

为什么 JS 不提供?从规范层面来说,Proxy 的设计初衷就是”透明代理”——它应该对使用者完全无感。如果暴露了检测手段,就破坏了这种透明性。

但现实业务中,我们确实有检测需求。下面介绍四种可行的方案。


2.2 方案一:利用 Proxy trap 副作用检测(设置标记属性)#

原理: 在创建 Proxy 时,通过 get trap 拦截访问,给目标对象打上一个”我是被代理的”标记。检测时只需判断该标记是否存在。

// ========== 方案一:trap 副作用标记 ==========
const PROXY_MARKER = '__isProxy__';
function createMarkedProxy(target, handler = {}) {
const originalGet = handler.get || (() => undefined);
const proxyHandler = {
...handler,
get(t, prop, receiver) {
// 拦截到对标记属性的访问时,返回 true
if (prop === PROXY_MARKER) {
return true;
}
return originalGet(t, prop, receiver);
}
};
return new Proxy(target, proxyHandler);
}
function isProxyViaMarker(obj) {
if (obj === null || typeof obj !== 'object') return false;
try {
return obj[PROXY_MARKER] === true;
} catch {
return false;
}
}
// ---- 验证 ----
const target1 = { name: 'Alice' };
const proxy1 = createMarkedProxy(target1);
console.log('=== 方案一验证 ===');
console.log('isProxyViaMarker(target1):', isProxyViaMarker(target1)); // false
console.log('isProxyViaMarker(proxy1):', isProxyViaMarker(proxy1)); // true
console.log('isProxyViaMarker({}):', isProxyViaMarker({})); // false
// ⚠️ 注意:如果原对象本身就有 __isProxy__ 属性,会产生误判
const trickyObj = { name: 'Bob', __isProxy__: true };
console.log('isProxyViaMarker(trickyObj):', isProxyViaMarker(trickyObj)); // true(误判!)
维度说明
优点实现简单,无需额外数据结构
缺点可能和原对象属性名冲突产生误判;污染了访问路径
适用场景团队内部约定,且能控制所有对象创建的场景

2.3 方案二:通过 WeakMap 注册表追踪#

原理: 创建一个全局 WeakMap,每次创建 Proxy 时将 proxy → true 注册进去。检测时查表即可。使用 WeakMap 的好处是不会阻止 Proxy 被垃圾回收。

// ========== 方案二:WeakMap 注册表 ==========
const proxyRegistry = new WeakMap();
function createRegisteredProxy(target, handler = {}) {
const proxy = new Proxy(target, handler);
proxyRegistry.set(proxy, true);
return proxy;
}
function isProxyViaRegistry(obj) {
return obj !== null && typeof obj === 'object' && proxyRegistry.has(obj);
}
// ---- 验证 ----
const target2 = { name: 'Charlie' };
const proxy2 = createRegisteredProxy(target2);
const normalObj = { name: 'David' };
console.log('\n=== 方案二验证 ===');
console.log('isProxyViaRegistry(target2):', isProxyViaRegistry(target2)); // false
console.log('isProxyViaRegistry(proxy2):', isProxyViaRegistry(proxy2)); // true
console.log('isProxyViaRegistry(normalObj):', isProxyViaRegistry(normalObj)); // false
console.log('isProxyViaRegistry(42):', isProxyViaRegistry(42)); // false
console.log('isProxyViaRegistry(null):', isProxyViaRegistry(null)); // false
console.log('isProxyViaRegistry([]):', isProxyViaRegistry([])); // false
// WeakMap 的优势:proxy 被 GC 后,注册表自动清理
// 不会造成内存泄漏
维度说明
优点100% 准确,不会误判;不污染目标对象;GC 友好
缺点只能追踪你自己创建的 Proxy;无法检测第三方库创建的 Proxy
适用场景框架/库内部对自己创建的代理对象进行管理

2.4 方案三:利用 Proxy revocable 的 revoked 状态#

原理: 使用 Proxy.revocable() 创建可撤销的代理。虽然这主要用于”撤销代理”场景,但我们也可以利用其 { proxy, revoke } 返回结构来判断。

// ========== 方案三:Proxy.revocable 状态检测 ==========
const revocableRegistry = new WeakMap();
function createRevocableTrackedProxy(target, handler = {}) {
const { proxy, revoke } = Proxy.revocable(target, handler);
// 用一个不可枚举的标记来追踪
revocableRegistry.set(proxy, {
revoked: false,
originalRevoke: revoke
});
// 包装 revoke,更新状态
const wrappedRevoke = () => {
const entry = revocableRegistry.get(proxy);
if (entry && !entry.revoked) {
entry.revoked = true;
entry.originalRevoke();
}
};
return { proxy, revoke: wrappedRevoke };
}
function isProxyViaRevocable(obj) {
return revocableRegistry.has(obj);
}
function isRevocableProxyRevoked(obj) {
const entry = revocableRegistry.get(obj);
return entry ? entry.revoked : null;
}
// ---- 验证 ----
const target3 = { secret: 'password123' };
const { proxy: proxy3, revoke: revoke3 } = createRevocableTrackedProxy(target3);
console.log('\n=== 方案三验证 ===');
console.log('isProxyViaRevocable(proxy3):', isProxyViaRevocable(proxy3)); // true
console.log('revoked before:', isRevocableProxyRevoked(proxy3)); // false
revoke3(); // 撤销代理
console.log('revoked after:', isRevocableProxyRevoked(proxy3)); // true
// 撤销后访问会抛出 TypeError
try {
console.log(proxy3.secret);
} catch (e) {
console.log('访问 revoked proxy 抛出:', e.message);
}
维度说明
优点可以追踪代理的撤销状态;适合需要动态回收代理权限的场景
缺点实现复杂;只能用于 revocable proxy;同样只能追踪自己创建的
适用场景临时代理、安全沙箱、需要回收访问权限的场景

2.5 方案四:通过 prototype 链或特殊标记检测#

原理: 利用 Object.getPrototypeOf()Object.prototype.toString 对 Proxy 的行为差异来间接判断。或者使用 Symbol 作为私有标记,避免属性名冲突。

// ========== 方案四:Symbol 私有标记 + prototype 检测 ==========
// 方式 A:Symbol 标记(推荐)
const IS_PROXY_SYMBOL = Symbol.for('__isProxy_internal__');
function createSymbolMarkedProxy(target, handler = {}) {
const originalGet = handler.get || (() => undefined);
const proxyHandler = {
...handler,
get(t, prop, receiver) {
if (prop === IS_PROXY_SYMBOL) {
return true;
}
return originalGet(t, prop, receiver);
}
};
return new Proxy(target, proxyHandler);
}
function isProxyViaSymbol(obj) {
if (obj === null || typeof obj !== 'object') return false;
try {
return obj[IS_PROXY_SYMBOL] === true;
} catch {
return false;
}
}
// 方式 B:通过 Object.prototype.toString 间接判断
// 注意:这不是可靠的检测方法,仅作知识补充
function detectViaToString(obj) {
try {
// Proxy 会拦截 toString 调用,行为可能与普通对象不同
// 但大多数情况下 toString.call(proxy) === '[object Object]'
return Object.prototype.toString.call(obj);
} catch {
return '[object Proxy]'; // 某些 proxy 会拦截失败
}
}
// ---- 验证 ----
const target4 = { name: 'Eve' };
const proxy4 = createSymbolMarkedProxy(target4);
const tricky4 = { name: 'Fake', [IS_PROXY_SYMBOL]: true };
console.log('\n=== 方案四验证 ===');
console.log('isProxyViaSymbol(target4):', isProxyViaSymbol(target4)); // false
console.log('isProxyViaSymbol(proxy4):', isProxyViaSymbol(proxy4)); // true
// Symbol 的隐藏性:普通遍历看不到
console.log('Object.keys(proxy4):', Object.keys(proxy4)); // ['name']
console.log('JSON.stringify(proxy4):', JSON.stringify(proxy4)); // {"name":"Eve"}
// ⚠️ 但如果有人故意设置了同名 Symbol,仍会误判
console.log('isProxyViaSymbol(tricky4):', isProxyViaSymbol(tricky4)); // true(误判!)
// toString 方式并不可靠
console.log('toString(normal):', detectViaToString({})); // [object Object]
console.log('toString(proxy):', detectViaToString(proxy4)); // [object Object](相同!)
维度说明
优点Symbol 具有全局唯一性和隐藏性,不易冲突
缺点Symbol.for 创建的全局 Symbol 仍可能被他人设置;toString 方式完全不可靠
适用场景Symbol 方案适合大多数内部标记场景

2.6 四种检测方案对比总结#

方案准确率能否检测第三方 Proxy性能推荐度
trap 副作用标记⚠️ 可能误判❌ 不能⭐⭐
WeakMap 注册表✅ 100% 准确❌ 不能⭐⭐⭐⭐⭐
revocable 状态✅ 100% 准确❌ 不能⭐⭐⭐
Symbol 标记⚠️ 可能误判❌ 不能⭐⭐⭐

核心结论: 原生 JS 中,你只能检测自己创建的 Proxy。对于第三方库(如 Vue3、Immer)创建的代理对象,必须使用它们提供的 API(如 isProxy()original())。


3. 从 Proxy 中提取原始 target 对象#

3.1 原生 JS 无法直接获取 Proxy 内部的 target#

这是另一个常见痛点。一旦创建了 Proxy,没有任何原生 API 可以从 proxy 对象反向获取到原始的 target

const target = { name: 'Alice' };
const proxy = new Proxy(target, {});
// ❌ 没有这样的 API
// const original = getTarget(proxy); // 不存在!
// const original = proxy.__target__; // 不存在!

Proxy 规范故意隐藏了 target,以维持代理的透明性和安全性。如果允许反向提取,攻击者就可以绕过代理的拦截逻辑,直接修改原始数据。

但在某些业务场景中,我们确实需要原始引用。下面介绍四种解决方案。


3.2 方案一:创建 Proxy 时用 WeakMap 保存映射关系#

原理: 最经典的方案。在创建 Proxy 时,将 proxy → target 的映射存入 WeakMap。后续通过查表获取原始对象。

// ========== 方案一:WeakMap 映射表 ==========
const proxyToTarget = new WeakMap();
function createProxyWithMapping(target, handler = {}) {
const proxy = new Proxy(target, handler);
proxyToTarget.set(proxy, target);
return proxy;
}
function getOriginalTarget(proxy) {
if (proxy === null || typeof proxy !== 'object') return undefined;
return proxyToTarget.get(proxy);
}
// ---- 验证 ----
const user = { name: 'Alice', age: 25 };
const userProxy = createProxyWithMapping(user, {
get(t, prop) {
console.log(`[Proxy] 访问属性: ${String(prop)}`);
return t[prop];
}
});
console.log('\n=== 方案一验证 ===');
console.log('userProxy.name:', userProxy.name); // Alice (触发 get trap)
const original = getOriginalTarget(userProxy);
console.log('original:', original); // { name: 'Alice', age: 25 }
console.log('original === user:', original === user); // true ✅
// 修改原始对象不会影响映射关系
original.name = 'Bob';
console.log('userProxy.name after change:', userProxy.name); // Bob
维度说明
优点实现简单,100% 可靠,GC 友好
缺点只能提取自己创建的 Proxy;需要维护映射表
适用场景最通用的方案,推荐作为默认选择

3.3 方案二:在 handler 的 get trap 中暴露获取原始对象的方法#

原理: 在 Proxy 的 get trap 中拦截某个特殊属性名,当访问该属性时返回原始 target。

// ========== 方案二:get trap 暴露原始对象 ==========
const RAW_SYMBOL = Symbol('__raw__');
function createProxyWithGetRaw(target, handler = {}) {
const originalGet = handler.get || (() => undefined);
const proxyHandler = {
...handler,
get(t, prop, receiver) {
// 拦截特殊符号,直接返回 target
if (prop === RAW_SYMBOL) {
return t;
}
return originalGet(t, prop, receiver);
}
};
return new Proxy(target, proxyHandler);
}
function getTargetViaGetTrap(proxy) {
try {
return proxy[RAW_SYMBOL];
} catch {
return undefined;
}
}
// ---- 验证 ----
const config = { debug: true, version: '1.0' };
const configProxy = createProxyWithGetRaw(config, {
get(t, prop) {
console.log(`[ConfigProxy] 读取: ${String(prop)}`);
return t[prop];
}
});
console.log('\n=== 方案二验证 ===');
console.log('configProxy.debug:', configProxy.debug);
const raw = getTargetViaGetTrap(configProxy);
console.log('raw:', raw); // { debug: true, version: '1.0' }
console.log('raw === config:', raw === config); // true ✅
// 不会被普通遍历发现
console.log('keys:', Object.keys(configProxy)); // ['debug', 'version']
console.log('stringify:', JSON.stringify(configProxy)); // {"debug":true,"version":"1.0"}
维度说明
优点无需额外数据结构;利用 Symbol 保证属性名唯一且不可枚举
缺点如果外部代码恰好遍历 Symbol 属性(Object.getOwnPropertySymbols),可能意外获取
适用场景轻量级方案,适合简单场景

3.4 方案三:使用 Symbol 作为私有标记存储原始引用#

原理: 将原始引用存储在一个全局 Symbol 键下,挂载到目标对象本身。由于是 Symbol 键,普通遍历不可见。

// ========== 方案三:Symbol 私有标记存储 ==========
const TARGET_REF = Symbol.for('__proxy_target_ref__');
function createProxyWithSymbolRef(target, handler = {}) {
// 在 target 上挂载 Symbol 自引用
// 注意:这会修改原对象
if (!target[TARGET_REF]) {
try {
Object.defineProperty(target, TARGET_REF, {
value: target,
writable: false,
enumerable: false,
configurable: false
});
} catch (e) {
// 冻结对象无法添加属性
}
}
return new Proxy(target, handler);
}
function getTargetViaSymbolRef(proxy) {
try {
return proxy[TARGET_REF];
} catch {
return undefined;
}
}
// ---- 验证 ----
const settings = { theme: 'dark', lang: 'zh' };
const settingsProxy = createProxyWithSymbolRef(settings);
console.log('\n=== 方案三验证 ===');
console.log('settingsProxy.theme:', settingsProxy.theme); // 'dark'
const rawSettings = getTargetViaSymbolRef(settingsProxy);
console.log('rawSettings:', rawSettings);
console.log('rawSettings === settings:', rawSettings === settings); // true ✅
// 不可枚举
console.log('keys:', Object.keys(settingsProxy)); // ['theme', 'lang']
// ⚠️ 注意:原对象被修改了,多了一个 Symbol 属性
console.log('ownSymbols:', Object.getOwnPropertySymbols(settings)); // [Symbol(__proxy_target_ref__)]
维度说明
优点不依赖外部数据结构;映射关系随对象生命周期管理
缺点修改了原始对象(添加 Symbol 属性);冻结对象不可用
适用场景可以接受修改原对象的场景

3.5 方案四:利用 Proxy.revocable 在 revoke 前保存引用#

原理: Proxy.revocable() 返回 { proxy, revoke }。在撤销之前,我们已经持有 target 的引用。可以设计一个模式,在特定条件下提取。

// ========== 方案四:revocable 模式 ==========
const revocableMap = new WeakMap();
function createRevocableProxy(target, handler = {}) {
const { proxy, revoke } = Proxy.revocable(target, handler);
// 保存 target 和 revoke 函数的引用
revocableMap.set(proxy, { target, revoke, isRevoked: false });
// 包装 revoke
const safeRevoke = () => {
const info = revocableMap.get(proxy);
if (info && !info.isRevoked) {
info.isRevoked = true;
revoke();
}
};
return { proxy, revoke: safeRevoke };
}
function extractTargetBeforeRevoke(proxy) {
const info = revocableMap.get(proxy);
if (!info) return undefined;
if (info.isRevoked) {
console.warn('⚠️ Proxy 已被撤销,无法获取 target');
return undefined;
}
return info.target;
}
// ---- 验证 ----
const secret = { apiKey: 'sk-123456', endpoint: 'https://api.example.com' };
const { proxy: secretProxy, revoke } = createRevocableProxy(secret);
console.log('\n=== 方案四验证 ===');
console.log('secretProxy.apiKey:', secretProxy.apiKey); // 'sk-123456'
// revoke 前可以提取
const beforeRevoke = extractTargetBeforeRevoke(secretProxy);
console.log('beforeRevoke:', beforeRevoke);
console.log('beforeRevoke === secret:', beforeRevoke === secret); // true ✅
revoke(); // 撤销代理
// revoke 后无法提取
const afterRevoke = extractTargetBeforeRevoke(secretProxy);
console.log('afterRevoke:', afterRevoke); // undefined
// 尝试访问 revoked proxy 会抛错
try {
console.log(secretProxy.apiKey);
} catch (e) {
console.log('访问 revoked proxy 错误:', e.message);
}
维度说明
优点自带生命周期管理;撤销后自动保护原始数据
缺点实现最复杂;只能用于 revocable proxy;撤销后无法再获取
适用场景安全沙箱、临时访问令牌、需要严格控制生命周期的场景

3.6 四种提取方案对比总结#

方案可靠性是否修改原对象能否处理第三方 Proxy推荐度
WeakMap 映射表✅ 100%❌ 不修改❌ 不能⭐⭐⭐⭐⭐
get trap 暴露✅ 可靠❌ 不修改❌ 不能⭐⭐⭐⭐
Symbol 私有标记✅ 可靠⚠️ 修改原对象❌ 不能⭐⭐⭐
revocable 模式✅ 可靠❌ 不修改❌ 不能⭐⭐⭐

4. Immer 库实战#

4.1 为什么需要 Immer?#

在 React/Redux 开发中,不可变数据更新是核心原则。但随着状态嵌套越来越深,手写不可变更新变得极其痛苦。

手写方式(痛苦面具 😫):

// 传统的不可变更新 —— 层层展开,噩梦般体验
function oldSchoolReducer(state, action) {
switch (action.type) {
case 'UPDATE_USER_NAME':
return {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
name: action.payload
}
}
};
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.payload, completed: false }
]
};
default:
return state;
}
}
const initialState = {
user: {
profile: { name: 'Alice', age: 25 },
settings: { theme: 'dark' }
},
todos: []
};
const newState = oldSchoolReducer(initialState, {
type: 'UPDATE_USER_NAME',
payload: 'Bob'
});
console.log('\n=== Immer 前置 - 传统方式 ===');
console.log('原始状态:', JSON.stringify(initialState));
console.log('新状态:', JSON.stringify(newState));
console.log('state === newState:', initialState === newState); // false
console.log('state.user === newState.user:', initialState.user === newState.user); // false
console.log('state.todos === newState.todos:', initialState.todos === newState.todos); // true (未变)

4.2 Immer 的 produce 函数#

Immer 基于 Proxy + 结构共享(structural sharing),让你可以用可变的方式写出不可变的代码

// ========== Immer 实战 ==========
// 注意:实际项目中需要 npm install immer
// 这里用 CDN 加载
// import { produce } from 'immer';
// 以下代码可在浏览器控制台直接运行(需先加载 immer)
// <script src="https://unpkg.com/immer/dist/immer.umd.production.min.js"></script>
// 模拟 produce 的简化理解版本(仅供演示原理,实际请用 immer 库)
function simulateProduce(baseState, recipe) {
const proxyToOriginal = new Map();
function createDraft(target) {
if (typeof target !== 'object' || target === null) return target;
return new Proxy(target, {
get(t, prop) {
if (prop === Symbol.for('__is_draft__')) return true;
const value = t[prop];
if (typeof value === 'object' && value !== null) {
return createDraft(value); // 懒代理
}
return value;
},
set(t, prop, value) {
// 任何修改都会触发"拷贝"行为
t[prop] = value;
return true;
}
});
}
const draft = createDraft(baseState);
recipe(draft);
// 实际 Immer 会通过 patch 机制生成新对象
// 这里简化为直接返回(不保证结构共享)
return JSON.parse(JSON.stringify(baseState));
}
console.log('\n=== Immer 原理模拟 ===');
const baseState = { user: { name: 'Alice', age: 25 }, todos: [] };
const simulated = simulateProduce(baseState, draft => {
draft.user.name = 'Bob';
draft.todos.push({ id: 1, text: 'Learn Proxy', done: false });
});
console.log('原始:', JSON.stringify(baseState));
console.log('模拟结果:', JSON.stringify(simulated));

真实的 Immer 使用方式(需要安装库):

/*
// 真实项目中:
// npm install immer
import { produce } from 'immer';
// ===== Redux Reducer 场景 =====
const initialState = {
user: {
profile: { name: 'Alice', age: 25 },
settings: { theme: 'dark', notifications: true }
},
todos: [
{ id: 1, text: 'Learn Proxy', completed: false },
{ id: 2, text: 'Master Immer', completed: true }
],
meta: { lastUpdated: Date.now() }
};
function todoReducer(state = initialState, action) {
return produce(state, draft => {
switch (action.type) {
case 'UPDATE_USER_NAME':
// ✅ 直接"修改",Immer 保证不可变性
draft.user.profile.name = action.payload;
break;
case 'ADD_TODO':
draft.todos.push({
id: Date.now(),
text: action.payload,
completed: false
});
break;
case 'TOGGLE_TODO':
const todo = draft.todos.find(t => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
break;
case 'UPDATE_THEME':
draft.user.settings.theme = action.payload;
break;
case 'CLEAR_COMPLETED_TODOS':
// 数组方法也能正常工作
draft.todos = draft.todos.filter(t => !t.completed);
break;
}
});
}
// ---- 验证 ----
let state = initialState;
state = todoReducer(state, { type: 'UPDATE_USER_NAME', payload: 'Bob' });
console.log(state.user.profile.name); // 'Bob'
state = todoReducer(state, { type: 'ADD_TODO', payload: 'Write Blog' });
console.log(state.todos.length); // 3
state = todoReducer(state, { type: 'TOGGLE_TODO', payload: 1 });
console.log(state.todos[0].completed); // true
// 验证不可变性
console.log(state.todos === initialState.todos); // false (引用不同)
console.log(state.todos[1] === initialState.todos[1]); // true (结构共享,未修改的部分复用)
*/

4.3 Immer 的核心优势#

特性手写展开符Immer
代码简洁度深层嵌套需多层展开,冗长直接赋值,简洁明了
可读性难以一眼看出修改了哪些字段修改逻辑清晰直观
性能每次都创建完整新对象(除非手动优化)结构共享,未修改部分复用引用
类型安全TypeScript 类型推导容易出错配合 TypeScript 推导优秀
数组操作需要用 concat/slice/filter 组合可直接 push/splice/sort

5. Immer 的 original API 深度解析#

5.1 original() 的作用#

Immer 的 original() 函数是处理 draft 代理对象时的核心 API。它的作用是:

从 Immer 的 draft(草稿代理)中获取到未经修改的原始对象

import { produce, original } from 'immer';
const baseState = {
users: [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 }
],
selectedUserId: 1
};
produce(baseState, draft => {
const user = draft.users[0];
// ❌ 这是 draft 代理对象,不是原始对象
console.log('user 是代理:', Object.prototype.toString.call(user));
// ✅ original() 返回原始对象(未修改的状态)
const originalUser = original(user);
console.log('originalUser:', originalUser);
console.log('originalUser === baseState.users[0]:', originalUser === baseState.users[0]); // true
});

5.2 实际业务使用场景#

场景一:条件判断时需要原始值#

当你的逻辑需要对比”修改前”和”修改后”的状态时:

/*
import { produce, original } from 'immer';
const state = { user: { name: 'Alice', role: 'user' } };
const newState = produce(state, draft => {
const currentUser = draft.user;
const originalUser = original(draft.user);
// 修改
currentUser.role = 'admin';
// 对比修改前后的值
if (originalUser.role !== currentUser.role) {
console.log(`角色从 "${originalUser.role}" 变更为 "${currentUser.role}"`);
// 触发审计日志、发送通知等
}
});
*/

场景二:避免 draft 污染第三方库#

有些第三方库不接受 Proxy 对象,需要原始对象:

/*
import { produce, original } from 'immer';
const state = { config: { apiKey: 'sk-123', retries: 3 } };
produce(state, draft => {
const rawConfig = original(draft.config);
// 假设某个第三方 HTTP 库不接受 Proxy 对象
// axios.post('/api', rawConfig); // ✅ 安全
// 如果用 draft.config 可能会出问题
// axios.post('/api', draft.config); // ⚠️ 某些库会报错
});
*/

场景三:序列化/存储#

/*
import { produce, original } from 'immer';
const state = { data: { items: [1, 2, 3] } };
produce(state, draft => {
draft.data.items.push(4);
// localStorage 存储时需要原始对象
const rawData = original(draft.data);
localStorage.setItem('myData', JSON.stringify(rawData));
// 注意:如果直接 JSON.stringify(draft.data)
// Immer 会自动处理,但 original() 确保拿到确定性的原始值
});
*/

5.3 踩坑点与注意事项#

⚠️ 坑点一:original() 只对被修改过的 draft 有意义#

/*
import { produce, original } from 'immer';
const state = { user: { name: 'Alice' } };
produce(state, draft => {
// 如果 draft.user 没有被修改过
// original(draft.user) === baseState.user // true
// 但如果修改了,original 返回的是修改前的快照
draft.user.name = 'Bob';
// original() 返回的是 produce 调用时的 baseState 中的值
const orig = original(draft.user);
console.log(orig.name); // 'Alice' (修改前的值)
console.log(draft.user.name); // 'Bob' (修改后的值)
});
*/

⚠️ 坑点二:original() 有性能开销#

/*
import { produce, original } from 'immer';
const largeState = {
items: Array.from({ length: 10000 }, (_, i) => ({ id: i, value: `item-${i}` }))
};
produce(largeState, draft => {
// ❌ 避免在循环中频繁调用 original()
for (const item of draft.items) {
const orig = original(item); // 性能差!
}
// ✅ 正确做法:在循环外获取需要的原始引用
// 或者避免在 draft 中使用 original
});
*/

⚠️ 坑点三:original() 返回的对象是只读的#

/*
import { produce, original } from 'immer';
produce({ user: { name: 'Alice' } }, draft => {
const orig = original(draft.user);
// ❌ 不要修改 original 返回的对象!
// 它指向原始状态树,修改它会破坏不可变性
// orig.name = 'Bob'; // 应该通过 draft 修改
// ✅ 正确做法
draft.user.name = 'Bob';
});
*/

5.4 original() 的完整可运行示例#

// ========== original() 完整示例 ==========
// 使用 immer 的 UMD 构建版本
// 在浏览器中运行:
// <script src="https://unpkg.com/immer/dist/immer.umd.production.min.js"></script>
// 然后在控制台运行:
/*
const { produce, original, isDraft } = immer;
const baseState = {
users: [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
],
settings: { theme: 'light', lang: 'en' }
};
console.log('\n=== Immer original() 完整示例 ===');
const newState = produce(baseState, draft => {
// 检查是否为 draft
console.log('isDraft(draft):', isDraft(draft)); // true
console.log('isDraft(draft.users[0]):', isDraft(draft.users[0])); // true
// 获取原始对象
const originalUser = original(draft.users[0]);
console.log('originalUser:', originalUser);
console.log('originalUser === baseState.users[0]:', originalUser === baseState.users[0]); // true
// 修改 draft
draft.users[0].name = 'Alice Modified';
draft.users[0].email = 'alice_new@example.com';
// original() 仍然返回修改前的值
const origAfterChange = original(draft.users[0]);
console.log('original after change:', origAfterChange);
// { id: 1, name: 'Alice', email: 'alice@example.com' }
console.log('draft user after change:', draft.users[0]);
// { id: 1, name: 'Alice Modified', email: 'alice_new@example.com' }
});
console.log('\n最终状态:', JSON.stringify(newState));
console.log('原状态未变:', JSON.stringify(baseState));
*/

6. Vue3 响应式原理延伸#

6.1 Vue3 核心响应式依赖 Proxy#

Vue3 抛弃了 Vue2 的 Object.defineProperty,全面拥抱 Proxy 来实现响应式系统。

/*
// Vue3 响应式核心原理简化版
// 实际源码远比这复杂,但核心思想一致:
function reactive(target) {
const handler = {
get(obj, key) {
// 收集依赖(追踪哪些组件用了这个属性)
track(obj, key);
const result = obj[key];
// 递归代理嵌套对象
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(obj, key, value) {
const oldValue = obj[key];
obj[key] = value;
// 触发更新(通知依赖了该属性的组件重新渲染)
if (oldValue !== value) {
trigger(obj, key);
}
return true;
}
};
const observed = new Proxy(target, handler);
// Vue 内部用 WeakMap 标记这个对象已被代理
toRawMap.set(observed, target);
return observed;
}
*/

6.2 Vue 内置的 isProxy() 和 toRaw()#

与原生 JS 不同,Vue3 提供了完善的 API 来处理 Proxy 对象

// ========== Vue3 API 示例 ==========
// 以下代码在浏览器中运行需要 Vue3:
// <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
/*
const { reactive, readonly, shallowReactive, isProxy, isReactive, isReadonly, toRaw } = Vue;
// ---- 创建不同类型的响应式对象 ----
const reactiveObj = reactive({ name: 'Alice', age: 25 });
const readonlyObj = readonly({ name: 'Bob', age: 30 });
const shallowObj = shallowReactive({ user: { name: 'Charlie' } });
const normalObj = { name: 'David', age: 35 };
console.log('\n=== Vue3 检测 API 验证 ===');
// isProxy: 只要是 proxy 就返回 true
console.log('isProxy(reactiveObj):', isProxy(reactiveObj)); // true
console.log('isProxy(readonlyObj):', isProxy(readonlyObj)); // true
console.log('isProxy(shallowObj):', isProxy(shallowObj)); // true
console.log('isProxy(normalObj):', isProxy(normalObj)); // false
// isReactive: 只有 reactive 创建的才返回 true
console.log('isReactive(reactiveObj):', isReactive(reactiveObj)); // true
console.log('isReactive(readonlyObj):', isReactive(readonlyObj)); // false
console.log('isReactive(shallowObj):', isReactive(shallowObj)); // true
// isReadonly: 只有 readonly 创建的才返回 true
console.log('isReadonly(reactiveObj):', isReadonly(reactiveObj)); // false
console.log('isReadonly(readonlyObj):', isReadonly(readonlyObj)); // true
// toRaw: 从 proxy 中提取原始对象
const rawReactive = toRaw(reactiveObj);
console.log('toRaw(reactiveObj) === original:', rawReactive === reactiveObj.__v_raw); // Vue 内部有 __v_raw
// 验证 toRaw 返回的是真正的原始对象
const baseObj = { name: 'Eve', age: 28 };
const reactiveBase = reactive(baseObj);
console.log('toRaw(reactiveBase) === baseObj:', toRaw(reactiveBase) === baseObj); // true ✅
*/

6.3 Vue 封装方案 vs 原生手写方案#

维度Vue3 封装方案原生手写方案
检测 APIisProxy()isReactive()isReadonly()无,需要自己实现
提取原对象toRaw()WeakMap 映射或其他方式
类型区分内置类型判断,开箱即用需要自定义标记逻辑
内部标记ReactiveFlags.IS_REACTIVE 等内部 Symbol自定义 Symbol 或属性
适用场景Vue3 生态内通用场景,任何项目

6.4 Vue 内部的实现揭秘#

/*
// Vue3 内部如何区分 reactive、readonly、shallow?
// 核心是通过在 Proxy handler 上挂载标记:
const enum ReactiveFlags {
SKIP = '__v_skip', // 跳过响应式
IS_REACTIVE = '__v_isReactive', // 标记为 reactive
IS_READONLY = '__v_isReadonly', // 标记为 readonly
IS_SHALLOW = '__v_isShallow', // 标记为 shallow
RAW = '__v_raw' // 指向原始对象
}
// 在 mutableHandlers (reactive 的 handler) 上:
const mutableHandlers = {
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) return true;
if (key === ReactiveFlags.RAW) return target;
// ... 正常的 get 逻辑
}
};
// 在 readonlyHandlers 上:
const readonlyHandlers = {
get(target, key, receiver) {
if (key === ReactiveFlags.IS_READONLY) return true;
if (key === ReactiveFlags.RAW) return target;
// ... 正常的 get 逻辑
}
};
// 这就是为什么 Vue 的 isProxy() 能区分类型:
function isProxy(value) {
return isReactive(value) || isReadonly(value);
}
function isReactive(value) {
if (isReadonly(value)) {
return isReactive(value[ReactiveFlags.RAW]);
}
return !!(value && value[ReactiveFlags.IS_REACTIVE]);
}
*/

7. Proxy 在前端生态的应用与常见问题#

7.1 落地场景一览#

库/框架Proxy 用途核心 API
Vue3响应式数据绑定reactive(), ref(), computed()
Immer不可变数据更新produce(), original()
MobX可观察状态(MobX 6+)makeAutoObservable(), observable()
Zustand部分实现中用于中间件可选的 proxy 中间件
Redux ToolkitcreateSlice 内部使用 ImmercreateReducer()
Valtio基于 Proxy 的极简状态管理proxy(), useSnapshot()

7.2 常见问题与解决方案#

问题一:代理对象引用判断失效#

// ========== 问题:引用判断失效 ==========
const obj = { name: 'test' };
const proxy = new Proxy(obj, {});
console.log('\n=== 问题一:引用判断失效 ===');
console.log('proxy === obj:', proxy === obj); // false
console.log('proxy.name === obj.name:', proxy.name === obj.name); // true (值相等)
// ❌ 常见错误:用 Object.is 或 === 比较
function processUser(user) {
if (user === obj) { // 永远为 false!
console.log('是同一个对象');
}
}
processUser(proxy); // 不输出
// ✅ 解决方案 1:使用框架提供的工具
// Vue3: toRaw(user) === toRaw(proxy)
// Immer: original(draftUser) === baseUser
// ✅ 解决方案 2:统一使用 proxy,始终通过同一入口获取
const userStore = {
_user: obj,
get user() {
return this._user; // 始终返回同一个引用
}
};
// ✅ 解决方案 3:用唯一标识代替引用比较
const userWithId = { id: 'user-001', name: 'test' };
const proxyWithId = new Proxy(userWithId, {});
function findUser(users, target) {
return users.find(u => u.id === target.id); // ✅ 用 id 比较
}

问题二:JSON.stringify 对 Proxy 的处理#

// ========== 问题:JSON.stringify 对 Proxy 的处理 ==========
console.log('\n=== 问题二:JSON.stringify 对 Proxy ===');
// 情况 1:空 handler —— JSON.stringify 正常
const obj1 = { a: 1, b: 'hello', c: [1, 2, 3] };
const proxy1 = new Proxy(obj1, {});
console.log('空 handler:', JSON.stringify(proxy1)); // {"a":1,"b":"hello","c":[1,2,3]} ✅
// 情况 2:拦截了 ownKeys —— 可能导致序列化异常
const proxyWithOwnKeys = new Proxy(obj1, {
ownKeys(target) {
// 只返回 'a',过滤掉其他属性
return ['a'];
}
});
console.log('拦截 ownKeys:', JSON.stringify(proxyWithOwnKeys)); // {"a":1} ⚠️ 其他属性丢失!
// 情况 3:拦截了 get —— 可能改变序列化结果
const proxyWithGet = new Proxy(obj1, {
get(target, prop) {
if (prop === 'a') return 999; // 修改了返回值
return target[prop];
}
});
console.log('拦截 get:', JSON.stringify(proxyWithGet)); // {"a":999,"b":"hello","c":[1,2,3]} ⚠️
// ✅ 解决方案:序列化前用原始对象
// JSON.stringify(toRaw(proxy)) 或 JSON.stringify(original(draft))
// ✅ 或者确保 Proxy 的 handler 不会干扰序列化相关的方法

问题三:第三方库兼容性#

// ========== 问题:第三方库兼容性 ==========
console.log('\n=== 问题三:第三方库兼容性 ===');
// 有些库使用 Object.getPrototypeOf 或特殊方法检测对象类型
// Proxy 可能干扰这些检测
const data = { items: [1, 2, 3], count: 3 };
const dataProxy = new Proxy(data, {
get(target, prop) {
console.log(`[Proxy] get: ${String(prop)}`);
return target[prop];
}
});
// ❌ 问题:lodash 的某些方法可能触发大量 trap
// _.cloneDeep(dataProxy) 会触发 n 次 get,性能差
// _.isEqual(dataProxy, data) 可能因为 proxy 干扰而行为异常
// ❌ 问题:某些库会检查 constructor
console.log('dataProxy.constructor === Object:', dataProxy.constructor === Object); // true (通常没问题)
// ❌ 问题:instanceof 检测
class MyClass {}
const myObj = new MyClass();
const myProxy = new Proxy(myObj, {});
console.log('myProxy instanceof MyClass:', myProxy instanceof MyClass); // true ✅ (Proxy 通常保持 instanceof)
// ✅ 解决方案:传给第三方库前用原始对象
// someLibrary(toRaw(proxy))

问题四:Map/Set 的 Proxy 陷阱#

// ========== 问题:Map/Set 的 Proxy 陷阱 ==========
console.log('\n=== 问题四:Map/Set 的 Proxy 陷阱 ===');
// ❌ 错误示范:直接代理 Map
const map = new Map([['key1', 'value1']]);
const mapProxy = new Proxy(map, {});
try {
// 这会抛出 TypeError!
// 因为 map.get 内部依赖 this 绑定到原始 Map
// 而 Proxy 的 this 指向 proxy 对象
console.log(mapProxy.get('key1'));
} catch (e) {
console.log('mapProxy.get 错误:', e.message);
// TypeError: Method Map.prototype.get called on incompatible receiver #<Map>
}
// ✅ 解决方案 1:在 handler 中正确绑定 this
const safeMapProxy = new Proxy(map, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
// 如果获取到的是方法,绑定到原始 target
if (typeof value === 'function') {
return value.bind(target);
}
return value;
}
});
console.log('safeMapProxy.get("key1"):', safeMapProxy.get('key1')); // 'value1' ✅
// ✅ 解决方案 2:使用 Vue3 等库的封装
// Vue3 的 reactive() 内部处理了 Map/Set 的代理问题
// reactive(new Map()) 可以正常工作
// Set 同理
const set = new Set([1, 2, 3]);
const safeSetProxy = new Proxy(set, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === 'function') {
return value.bind(target);
}
return value;
}
});
console.log('safeSetProxy.has(2):', safeSetProxy.has(2)); // true ✅
console.log('safeSetProxy.size:', safeSetProxy.size); // 3 ✅

7.3 常见问题速查表#

问题原因解决方案
proxy === obj 为 falseProxy 是新的对象实例toRaw() / original() 提取原对象后再比较
JSON.stringify 异常handler 拦截了 ownKeys/get用原始对象序列化,或调整 handler 逻辑
lodash.cloneDeep 性能差触发大量 trap传入原始对象,或使用 _.cloneDeep(toRaw(proxy))
Map/Set 方法报错this 绑定丢失handler 中 bind(target) 方法,或使用库的封装
instanceof 检测失效某些特殊 proxy 实现通常 Proxy 保持 instanceof,特殊场景用 Symbol.hasInstance

8. 总结:知识闭环#

本文从日常开发中的困惑出发,完整地梳理了 Proxy 的核心知识链:

┌─────────────────────────────────────────────────────────┐
│ Proxy 知识闭环 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Proxy │───▶│ 检测 Proxy │───▶│ 提取原始对象 │ │
│ │ 创建 │ │ isProxy │ │ toRaw / │ │
│ │ │ │ (原生无内置) │ │ original() │ │
│ └──────────┘ └──────────────┘ └───────┬───────┘ │
│ │ │
│ ┌─────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 主流库实践 │ │
│ │ │ │
│ │ • Vue3: isProxy() / isReactive() / toRaw() │ │
│ │ • Immer: produce() / original() / isDraft() │ │
│ │ • MobX: observable() / toJS() │ │
│ │ • 自研: WeakMap 映射 / Symbol 标记 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 常见问题与规避 │ │
│ │ │ │
│ │ • 引用比较失效 → 用唯一标识 / toRaw │ │
│ │ • JSON 序列化异常 → 用原始对象 │ │
│ │ • 第三方库兼容 → 传原始对象 / 调整 handler │ │
│ │ • Map/Set 陷阱 → bind(target) / 库封装 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘

核心要点回顾#

  1. 原生 JS 没有 isProxy(),检测只能靠自己实现(WeakMap 注册表是最佳方案)
  2. 无法从 Proxy 反向获取 target,需要在创建时保存映射关系
  3. Immer 的 original() 是从 draft 中获取原始对象的标准方式
  4. Vue3 的 isProxy()toRaw() 是对原生 Proxy 的优雅封装
  5. Proxy 与第三方库交互时需要格外小心,必要时传入原始对象
  6. Map/Set 的 Proxy 代理需要注意 this 绑定问题

Proxy 是现代前端生态的基石技术之一。理解它的原理、掌握它的痛点与解决方案,将帮助你在 Vue3、Immer、Redux Toolkit 等工具的实践中游刃有余。


💡 最佳实践建议:

  • 如果你在项目中使用 Vue3,直接使用 isProxy()toRaw(),不要重复造轮子
  • 如果你在使用 Immer,用 original() 获取原始引用,用 isDraft() 判断是否为草稿
  • 如果你在自研框架,用 WeakMap 做注册表管理,这是最可靠的原生方案
  • 传递数据给第三方库时,注意是否需要先解包为原始对象

本文所有代码示例均可直接在浏览器控制台或 Node.js 环境中运行验证。如使用需要外部库(immer、vue)的代码,请先通过 CDN 或 npm 安装对应依赖。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

JS Proxy 核心原理、实战痛点与常用解决方案
https://boke.hackerdream.xyz/posts/js-proxy-deep-dive/
作者
晴天
发布于
2026-04-28
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录