代理模式
问题
什么是代理模式?ES6 Proxy 和传统代理模式有什么关系?前端有哪些典型应用场景?
答案
代理模式(Proxy Pattern)为其他对象提供一种代理以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介作用,可以在不改变原对象的情况下添加额外功能。
核心概念
代理类型
| 类型 | 说明 | 应用场景 |
|---|---|---|
| 保护代理 | 控制访问权限 | 权限验证 |
| 虚拟代理 | 延迟初始化 | 图片懒加载 |
| 缓存代理 | 缓存请求结果 | API 缓存 |
| 远程代理 | 本地代表远程对象 | RPC 调用 |
| 日志代理 | 记录访问日志 | 操作审计 |
传统代理模式
基础实现
// 主题接口
interface Image {
display(): void;
getInfo(): string;
}
// 真实对象 - 耗资源
class RealImage implements Image {
private filename: string;
constructor(filename: string) {
this.filename = filename;
this.loadFromDisk(); // 模拟耗时操作
}
private loadFromDisk() {
console.log(`Loading image: ${this.filename}`);
// 模拟加载延迟
}
display() {
console.log(`Displaying: ${this.filename}`);
}
getInfo() {
return `Image: ${this.filename}`;
}
}
// 代理对象 - 延迟加载
class ProxyImage implements Image {
private realImage: RealImage | null = null;
private filename: string;
constructor(filename: string) {
this.filename = filename;
// 不立即创建 RealImage
}
display() {
if (!this.realImage) {
this.realImage = new RealImage(this.filename);
}
this.realImage.display();
}
getInfo() {
// 无需加载图片即可返回信息
return `Image: ${this.filename} (not loaded)`;
}
}
// 使用
const image = new ProxyImage('large-photo.jpg');
console.log(image.getInfo()); // 不加载图片
image.display(); // 此时才加载
image.display(); // 已加载,直接显示
保护代理
interface Document {
read(): string;
write(content: string): void;
delete(): void;
}
class RealDocument implements Document {
private content: string;
constructor(content: string) {
this.content = content;
}
read() {
return this.content;
}
write(content: string) {
this.content = content;
}
delete() {
this.content = '';
}
}
// 保护代理 - 权限控制
class ProtectedDocument implements Document {
private doc: RealDocument;
private userRole: 'admin' | 'editor' | 'viewer';
constructor(doc: RealDocument, role: 'admin' | 'editor' | 'viewer') {
this.doc = doc;
this.userRole = role;
}
read() {
// 所有角色都可读
return this.doc.read();
}
write(content: string) {
if (this.userRole === 'viewer') {
throw new Error('没有写入权限');
}
this.doc.write(content);
}
delete() {
if (this.userRole !== 'admin') {
throw new Error('只有管理员可以删除');
}
this.doc.delete();
}
}
// 使用
const doc = new RealDocument('Hello World');
const viewerDoc = new ProtectedDocument(doc, 'viewer');
const adminDoc = new ProtectedDocument(doc, 'admin');
console.log(viewerDoc.read()); // OK
// viewerDoc.write('New'); // 抛出错误
adminDoc.delete(); // OK
ES6 Proxy
ES6 的 Proxy 是 JavaScript 内置的代理机制,可以拦截对象的基本操作。
基础用法
const target = {
name: 'John',
age: 30,
};
const handler: ProxyHandler<typeof target> = {
get(target, prop, receiver) {
console.log(`Getting ${String(prop)}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`Setting ${String(prop)} = ${value}`);
return Reflect.set(target, prop, value, receiver);
},
};
const proxy = new Proxy(target, handler);
proxy.name; // Getting name -> 'John'
proxy.age = 31; // Setting age = 31
常用拦截器
const handlers: ProxyHandler<object> = {
// 读取属性
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
// 设置属性
set(target, prop, value, receiver) {
return Reflect.set(target, prop, value, receiver);
},
// 删除属性
deleteProperty(target, prop) {
return Reflect.deleteProperty(target, prop);
},
// in 操作符
has(target, prop) {
return Reflect.has(target, prop);
},
// Object.keys、for...in 等
ownKeys(target) {
return Reflect.ownKeys(target);
},
// 函数调用
apply(target, thisArg, args) {
return Reflect.apply(target as Function, thisArg, args);
},
// new 操作符
construct(target, args, newTarget) {
return Reflect.construct(target as new (...args: unknown[]) => object, args, newTarget);
},
// Object.getOwnPropertyDescriptor
getOwnPropertyDescriptor(target, prop) {
return Reflect.getOwnPropertyDescriptor(target, prop);
},
// Object.defineProperty
defineProperty(target, prop, descriptor) {
return Reflect.defineProperty(target, prop, descriptor);
},
// Object.getPrototypeOf
getPrototypeOf(target) {
return Reflect.getPrototypeOf(target);
},
// Object.setPrototypeOf
setPrototypeOf(target, proto) {
return Reflect.setPrototypeOf(target, proto);
},
// Object.isExtensible
isExtensible(target) {
return Reflect.isExtensible(target);
},
// Object.preventExtensions
preventExtensions(target) {
return Reflect.preventExtensions(target);
},
};
前端实际应用
1. 数据响应式(Vue 3 核心原理)
type EffectFn = () => void;
// 当前正在执行的 effect
let activeEffect: EffectFn | null = null;
// 依赖收集容器
const targetMap = new WeakMap<object, Map<PropertyKey, Set<EffectFn>>>();
// 依赖收集
function track(target: object, key: PropertyKey) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
}
// 派发更新
function trigger(target: object, key: PropertyKey) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) {
deps.forEach((effect) => effect());
}
}
// 创建响应式对象
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key);
const result = Reflect.get(target, key, receiver);
// 深层响应式
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
trigger(target, key);
return result;
},
});
}
// effect 函数
function effect(fn: EffectFn) {
activeEffect = fn;
fn();
activeEffect = null;
}
// 使用
const state = reactive({ count: 0, name: 'Vue' });
effect(() => {
console.log('Count:', state.count);
});
state.count++; // 自动打印: Count: 1
state.count++; // 自动打印: Count: 2
2. 数据验证代理
interface ValidationRule {
validate: (value: unknown) => boolean;
message: string;
}
function createValidatedObject<T extends object>(
target: T,
rules: Partial<Record<keyof T, ValidationRule[]>>
): T {
return new Proxy(target, {
set(target, prop, value, receiver) {
const propRules = rules[prop as keyof T];
if (propRules) {
for (const rule of propRules) {
if (!rule.validate(value)) {
console.error(`验证失败: ${rule.message}`);
return false;
}
}
}
return Reflect.set(target, prop, value, receiver);
},
});
}
// 使用
interface User {
name: string;
age: number;
email: string;
}
const user = createValidatedObject<User>(
{ name: '', age: 0, email: '' },
{
name: [
{ validate: (v) => typeof v === 'string', message: '名字必须是字符串' },
{ validate: (v) => (v as string).length >= 2, message: '名字至少2个字符' },
],
age: [
{ validate: (v) => typeof v === 'number', message: '年龄必须是数字' },
{ validate: (v) => (v as number) >= 0 && (v as number) <= 150, message: '年龄范围0-150' },
],
email: [
{
validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v as string),
message: '邮箱格式不正确',
},
],
}
);
user.name = 'J'; // 验证失败: 名字至少2个字符
user.name = 'John'; // OK
user.age = -1; // 验证失败: 年龄范围0-150
user.age = 25; // OK
3. 函数缓存代理
function createCachedFunction<T extends (...args: unknown[]) => unknown>(
fn: T,
options: {
maxSize?: number;
ttl?: number; // 毫秒
} = {}
): T {
const { maxSize = 100, ttl } = options;
const cache = new Map<string, { value: unknown; timestamp: number }>();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
// 检查缓存是否有效
if (cached) {
if (!ttl || Date.now() - cached.timestamp < ttl) {
console.log('Cache hit');
return cached.value;
}
cache.delete(key);
}
console.log('Cache miss');
const result = Reflect.apply(target, thisArg, args);
// 维护缓存大小
if (cache.size >= maxSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, { value: result, timestamp: Date.now() });
return result;
},
}) as T;
}
// 使用
const expensiveCalculation = (n: number): number => {
console.log('Computing...');
// 模拟耗时计算
return n * n;
};
const cachedCalc = createCachedFunction(expensiveCalculation, { ttl: 5000 });
cachedCalc(10); // Computing... Cache miss -> 100
cachedCalc(10); // Cache hit -> 100
cachedCalc(20); // Computing... Cache miss -> 400
4. API 请求缓存代理
interface CacheEntry {
data: unknown;
expiry: number;
}
function createCachedApi<T extends object>(api: T, ttl = 60000): T {
const cache = new Map<string, CacheEntry>();
return new Proxy(api, {
get(target, prop, receiver) {
const originalMethod = Reflect.get(target, prop, receiver);
if (typeof originalMethod !== 'function') {
return originalMethod;
}
return async (...args: unknown[]) => {
const cacheKey = `${String(prop)}_${JSON.stringify(args)}`;
const cached = cache.get(cacheKey);
if (cached && cached.expiry > Date.now()) {
console.log(`[Cache] Hit: ${cacheKey}`);
return cached.data;
}
console.log(`[API] Fetching: ${cacheKey}`);
const result = await originalMethod.apply(target, args);
cache.set(cacheKey, {
data: result,
expiry: Date.now() + ttl,
});
return result;
};
},
});
}
// 使用
const api = {
async getUser(id: number) {
const response = await fetch(`/api/users/${id}`);
return response.json();
},
async getProducts(category: string) {
const response = await fetch(`/api/products?category=${category}`);
return response.json();
},
};
const cachedApi = createCachedApi(api, 30000);
await cachedApi.getUser(1); // [API] Fetching
await cachedApi.getUser(1); // [Cache] Hit
await cachedApi.getUser(2); // [API] Fetching
5. 私有属性代理
function createPrivateObject<T extends object>(
target: T,
privateProps: (keyof T)[]
): T {
const privateSet = new Set(privateProps);
return new Proxy(target, {
get(target, prop, receiver) {
if (privateSet.has(prop as keyof T)) {
throw new Error(`Property '${String(prop)}' is private`);
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
if (privateSet.has(prop as keyof T)) {
throw new Error(`Property '${String(prop)}' is private`);
}
return Reflect.set(target, prop, value, receiver);
},
has(target, prop) {
if (privateSet.has(prop as keyof T)) {
return false;
}
return Reflect.has(target, prop);
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(
(key) => !privateSet.has(key as keyof T)
);
},
});
}
// 使用
const user = createPrivateObject(
{ name: 'John', password: 'secret123', email: 'john@example.com' },
['password']
);
console.log(user.name); // John
// console.log(user.password); // Error: Property 'password' is private
console.log(Object.keys(user)); // ['name', 'email']
console.log('password' in user); // false
6. 图片懒加载代理
function createLazyImage(placeholder: string) {
return function lazyLoadImage(src: string): HTMLImageElement {
const img = new Image();
img.src = placeholder;
// 代理真实图片加载
const realImg = new Image();
realImg.onload = () => {
img.src = src;
console.log(`Loaded: ${src}`);
};
realImg.onerror = () => {
console.error(`Failed to load: ${src}`);
};
// IntersectionObserver 优化
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
realImg.src = src;
observer.disconnect();
}
});
});
observer.observe(img);
return img;
};
}
// 使用
const lazyImage = createLazyImage('/placeholder.png');
const img = lazyImage('/large-photo.jpg');
document.body.appendChild(img);
7. 属性访问日志代理
function createLoggedObject<T extends object>(
target: T,
options: { name?: string; logGet?: boolean; logSet?: boolean } = {}
): T {
const { name = 'Object', logGet = true, logSet = true } = options;
return new Proxy(target, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (logGet && typeof prop === 'string') {
console.log(
`[${name}] GET ${prop} = ${JSON.stringify(value)}`
);
}
return value;
},
set(target, prop, value, receiver) {
const oldValue = Reflect.get(target, prop, receiver);
const result = Reflect.set(target, prop, value, receiver);
if (logSet && typeof prop === 'string') {
console.log(
`[${name}] SET ${prop}: ${JSON.stringify(oldValue)} -> ${JSON.stringify(value)}`
);
}
return result;
},
});
}
// 使用
const state = createLoggedObject({ count: 0 }, { name: 'AppState' });
state.count; // [AppState] GET count = 0
state.count = 1; // [AppState] SET count: 0 -> 1
常见面试问题
Q1: Proxy 和 Object.defineProperty 的区别?
答案:
| 对比项 | Proxy | Object.defineProperty |
|---|---|---|
| 拦截范围 | 13 种操作 | get/set |
| 数组支持 | 原生支持索引、length | 需要特殊处理 |
| 新属性 | 自动拦截 | 需要手动添加 |
| 性能 | 略低(创建代理) | 略高 |
| 兼容性 | IE 不支持 | IE9+ |
| 深层代理 | 需要递归 | 需要递归 |
// Object.defineProperty
const obj: Record<string, unknown> = {};
Object.defineProperty(obj, 'name', {
get() {
return this._name;
},
set(value) {
this._name = value;
},
});
// Proxy
const proxy = new Proxy({}, {
get(target, key) {
return Reflect.get(target, key);
},
set(target, key, value) {
return Reflect.set(target, key, value);
},
});
Q2: 为什么 Vue 3 选择 Proxy?
答案:
// 1. 数组变异方法天然支持
const arr = reactive([1, 2, 3]);
arr.push(4); // 自动触发更新
arr[10] = 5; // 自动触发更新
// 2. 新增属性自动响应
const obj = reactive({});
obj.newProp = 'value'; // 自动触发更新
// 3. 可以监听 delete 操作
delete obj.prop; // 自动触发更新
// 4. 更好的性能(懒代理)
// 只有访问到的属性才会被代理
Q3: 代理模式和装饰器模式的区别?
答案:
| 对比项 | 代理模式 | 装饰器模式 |
|---|---|---|
| 目的 | 控制访问 | 增强功能 |
| 接口 | 相同接口 | 相同接口 |
| 创建时机 | 代理创建对象 | 包装已有对象 |
| 典型应用 | 延迟加载、权限 | 日志、缓存 |
Q4: 如何用 Proxy 实现负索引数组?
答案:
function createNegativeIndexArray<T>(arr: T[]): T[] {
return new Proxy(arr, {
get(target, prop, receiver) {
const index = Number(prop);
if (!isNaN(index) && index < 0) {
return target[target.length + index];
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
const index = Number(prop);
if (!isNaN(index) && index < 0) {
target[target.length + index] = value;
return true;
}
return Reflect.set(target, prop, value, receiver);
},
});
}
const arr = createNegativeIndexArray([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4
arr[-1] = 10;
console.log(arr); // [1, 2, 3, 4, 10]