大家好,我是老码,今天咱们聊聊 JavaScript Proxy,这玩意儿可不只是个摆设,玩好了能让你的代码飞起来。咱们主要讲讲它在数据响应式和模拟对象行为这两大领域的骚操作,尤其是看看 Vue 3 怎么用它实现响应式,以及怎么用它做 Mocking。
开场白:Proxy 是个啥玩意儿?
简单来说,Proxy 就是一个“代理人”,它可以拦截并控制对另一个对象的各种操作。你可以把它想象成一个门卫,你想进屋(访问对象),得先过他这关。他可以让你进,也可以不让你进,甚至可以篡改你进屋后看到的东西。
第一部分:Proxy 的基本用法
先来点基础知识热热身,别一下子就晕了。
// 目标对象
const target = {
name: '老码',
age: 30
};
// 处理函数 (handler)
const handler = {
get: function(target, property, receiver) {
console.log(`有人想访问 ${property} 属性`);
return Reflect.get(target, property, receiver); // 必须返回,否则访问不到
},
set: function(target, property, value, receiver) {
console.log(`有人想修改 ${property} 属性为 ${value}`);
target[property] = value;
return true; // set 必须返回 true,表示设置成功
}
};
// 创建 Proxy 实例
const proxy = new Proxy(target, handler);
// 访问属性
console.log(proxy.name); // 输出: 有人想访问 name 属性 老码
// 修改属性
proxy.age = 35; // 输出: 有人想修改 age 属性为 35
console.log(target.age); // 输出: 35
这段代码创建了一个 Proxy,拦截了对 target
对象的 get
和 set
操作。每次访问或修改属性,都会先执行 handler 里的对应函数。
Handler 函数都有哪些?
Proxy 的 handler 可以拦截很多操作,常用的包括:
- get(target, property, receiver): 拦截读取属性的操作。
- set(target, property, value, receiver): 拦截设置属性的操作。
- has(target, property): 拦截
in
操作符。 例如:'name' in proxy
。 - deleteProperty(target, property): 拦截
delete
操作。 例如:delete proxy.age
。 - ownKeys(target): 拦截
Object.getOwnPropertyNames(proxy)
和Object.getOwnPropertySymbols(proxy)
。 - getOwnPropertyDescriptor(target, property): 拦截
Object.getOwnPropertyDescriptor(proxy, property)
。 - defineProperty(target, property, descriptor): 拦截
Object.defineProperty(proxy, property, descriptor)
。 - preventExtensions(target): 拦截
Object.preventExtensions(proxy)
。 - getPrototypeOf(target): 拦截
Object.getPrototypeOf(proxy)
。 - setPrototypeOf(target, prototype): 拦截
Object.setPrototypeOf(proxy, prototype)
。 - apply(target, thisArg, argumentsList): 拦截函数调用。只有当目标对象是函数时才有效。
- construct(target, argumentsList, newTarget): 拦截
new
操作符。只有当目标对象是构造函数时才有效。
每个 handler 函数都有自己的参数,但第一个参数永远是 target
,表示目标对象。receiver
通常是 Proxy 实例本身,用于处理继承的情况。
Reflect 是个啥?
在 handler 函数里,我们经常看到 Reflect.get(target, property, receiver)
这样的代码。 Reflect
是一个内置对象,提供了一组与对象操作相关的静态方法,这些方法与 Proxy handler 的方法一一对应。
使用 Reflect
的好处是:
- 默认行为:
Reflect
提供了默认的对象操作行为,比如Reflect.get
就是获取属性的默认行为。 - 错误处理: 如果对象操作失败,
Reflect
会抛出错误,而不是默默地失败。 - this 指向:
Reflect
会正确处理this
指向,避免一些意想不到的问题。
第二部分:Proxy 在数据响应式中的应用 (Vue 3 为例)
Vue 3 抛弃了 Vue 2 的 Object.defineProperty
,拥抱了 Proxy。这是为什么呢?
特性 | Object.defineProperty | Proxy |
---|---|---|
监听对象 | 只能监听对象的属性 | 可以直接监听整个对象 |
监听数组 | 需要特殊处理 | 可以直接监听数组 |
监听新增/删除属性 | 无法直接监听 | 可以直接监听 |
性能 | 性能较差 | 性能更好,尤其是在大型对象上 |
简单来说,Proxy 完胜。
Vue 3 的响应式原理
Vue 3 的响应式系统主要依赖两个函数: reactive()
和 ref()
。 reactive()
用于创建响应式对象, ref()
用于创建响应式基本类型数据。 它们都使用了 Proxy。
我们来模拟一下 reactive()
的实现:
function reactive(target) {
if (typeof target !== 'object' || target === null) {
return target; // 不是对象,直接返回
}
const handler = {
get(target, key, receiver) {
track(target, key); // 收集依赖
return Reflect.get(target, key, receiver);
},
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;
}
};
return new Proxy(target, handler);
}
// 简化的依赖收集和触发更新函数 (实际 Vue 3 中更复杂)
const targetMap = new WeakMap();
let activeEffect = null;
function effect(fn) {
activeEffect = fn;
fn(); // 立即执行一次,收集依赖
activeEffect = null;
}
function track(target, key) {
if (activeEffect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect);
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
const deps = depsMap.get(key);
if (deps) {
deps.forEach(effect => {
effect();
});
}
}
// 示例
const data = reactive({ count: 0 });
effect(() => {
console.log('count is:', data.count);
});
data.count++; // 输出: count is: 1
data.count = 10; // 输出: count is: 10
这段代码模拟了 Vue 3 的响应式核心。 reactive()
函数将普通对象转换为响应式对象,每次访问属性时, track()
函数会收集依赖;每次修改属性时, trigger()
函数会触发更新。 effect()
函数用于注册副作用函数,当依赖的数据发生变化时,副作用函数会自动执行。
响应式数组
Proxy 也能轻松处理数组的响应式。Vue 3 对数组的一些特殊方法(如 push
, pop
, shift
, unshift
, splice
, sort
, reverse
)进行了特殊处理,以便在修改数组时触发更新。
const originalArrayProto = Array.prototype;
const arrayMethods = Object.create(originalArrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
arrayMethods[method] = function(...args) {
const result = originalArrayProto[method].apply(this, args);
// 触发更新
trigger(this, 'length'); // 数组长度可能发生变化
return result;
};
});
function reactiveArray(arr) {
Object.setPrototypeOf(arr, arrayMethods); //修改数组的原型链
return arr
}
const myArray = reactiveArray([1, 2, 3]);
effect(() => {
console.log('array is:', myArray.join(','));
});
myArray.push(4); // 输出: array is: 1,2,3,4
myArray.splice(1, 1); // 输出: array is: 1,3,4
这段代码通过修改数组的原型链,拦截了数组的修改方法,并在修改后触发更新。
第三部分:Proxy 在 Mocking 中的应用
在单元测试中,我们经常需要 Mock 外部依赖,比如 API 请求、数据库操作等。Proxy 可以很方便地创建 Mock 对象,模拟这些外部依赖的行为。
Mocking API 请求
假设我们有一个函数,用于获取用户数据:
async function getUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return data;
}
在单元测试中,我们不想真正发送 API 请求,而是希望 Mock fetch
函数,返回预定义的数据。
// Mock fetch 函数
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ id: 1, name: 'Mock User' })
})
);
// 测试 getUserData 函数
test('getUserData should return mock user data', async () => {
const userData = await getUserData(1);
expect(userData).toEqual({ id: 1, name: 'Mock User' });
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
这段代码使用 jest.fn()
创建了一个 Mock fetch
函数,并用 toHaveBeenCalledWith()
验证了 fetch
函数是否被正确调用。
使用 Proxy 创建更灵活的 Mock 对象
有时候,我们需要根据不同的参数,返回不同的 Mock 数据。Proxy 可以更灵活地实现这一点。
const mockData = {
1: { id: 1, name: 'User 1' },
2: { id: 2, name: 'User 2' }
};
const mockFetch = new Proxy({}, {
get(target, property) {
return () => Promise.resolve({
json: () => Promise.resolve(mockData[property])
});
}
});
global.fetch = mockFetch;
test('getUserData should return different data for different user IDs', async () => {
const userData1 = await getUserData(1);
const userData2 = await getUserData(2);
expect(userData1).toEqual({ id: 1, name: 'User 1' });
expect(userData2).toEqual({ id: 2, name: 'User 2' });
});
这段代码创建了一个 Proxy,拦截了对 fetch
对象的属性访问。当访问 fetch[userId]
时,Proxy 会返回一个函数,该函数返回对应 userId
的 Mock 数据。
Mocking 复杂的对象
有时候,我们需要 Mock 的对象非常复杂,包含很多属性和方法。手动创建 Mock 对象会非常繁琐。Proxy 可以帮助我们动态地创建 Mock 对象,只 Mock 我们需要的部分。
const realObject = {
method1: () => 'real method1',
method2: () => 'real method2',
method3: () => 'real method3',
property1: 'real property1',
property2: 'real property2'
};
const mockedMethods = ['method1', 'method2'];
const mockObject = new Proxy(realObject, {
get(target, property) {
if (mockedMethods.includes(property)) {
return jest.fn(() => `mocked ${property}`);
}
return Reflect.get(target, property);
}
});
test('mockObject should mock specified methods', () => {
expect(mockObject.method1()).toBe('mocked method1');
expect(mockObject.method2()).toBe('mocked method2');
expect(mockObject.method3()).toBe('real method3'); // 未被 mock,返回原始值
expect(mockObject.property1).toBe('real property1'); // 未被 mock,返回原始值
});
这段代码创建了一个 Proxy,只 Mock 了 method1
和 method2
方法,其他属性和方法都保持不变。
第四部分:Proxy 的高级应用
Proxy 还有一些更高级的应用,比如:
- 数据校验: 在
set
handler 中对数据进行校验,防止非法数据写入。 - 权限控制: 根据用户权限,控制对对象的访问。
- 日志记录: 在
get
和set
handler 中记录日志,方便调试。 - 性能优化: 在
get
handler 中进行缓存,避免重复计算。
数据校验示例
const validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('Age is not an integer');
}
if (value < 0 || value > 150) {
throw new RangeError('Age is invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
// Indicate success
return true;
}
};
const person = new Proxy({}, validator);
person.age = 100;
console.log(person.age); // 100
person.age = 'young'; // 抛出 TypeError: Age is not an integer
person.age = 200; // 抛出 RangeError: Age is invalid
权限控制示例 (简化版)
const hasPermission = (user, action, target) => {
// 假设这里根据用户角色和权限列表进行判断
if (user === 'admin' && action === 'delete') {
return true;
}
return false;
}
const protectedData = {
sensitiveInfo: 'Top Secret',
publicInfo: 'Open to all'
}
const user = 'guest';
const protectedProxy = new Proxy(protectedData, {
get(target, property) {
return target[property];
},
set(target, property, value) {
throw new Error('No permission to modify');
},
deleteProperty(target, property) {
if (hasPermission(user, 'delete', target)) {
delete target[property];
return true;
} else {
throw new Error('No permission to delete');
}
}
});
console.log(protectedProxy.publicInfo); // Open to all
// protectedProxy.sensitiveInfo = 'New Value'; // 抛出 Error: No permission to modify
// delete protectedProxy.sensitiveInfo; // 抛出 Error: No permission to delete
const admin = 'admin';
user = admin; //切换到管理员身份
delete protectedProxy.sensitiveInfo; // 成功删除
总结
Proxy 是一个强大的工具,可以拦截和控制对对象的各种操作。它在数据响应式、Mocking 等领域都有广泛的应用。掌握 Proxy 的用法,可以让你写出更灵活、更可维护的代码。希望今天的讲座能帮助你更好地理解和应用 Proxy。
课后作业
- 实现一个
deepReactive()
函数,可以递归地将对象及其嵌套对象转换为响应式对象。 - 使用 Proxy 实现一个简单的缓存系统,当访问对象的属性时,如果属性已经存在于缓存中,则直接返回缓存中的值,否则计算属性的值并将其缓存起来。
好了,今天的课就到这里。下课!