跳到主要内容

代理模式

问题

什么是代理模式?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 的区别?

答案

对比项ProxyObject.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]

相关链接