什么是代理模式
代理者通过 聚合/组合 被代理者,以实现被代理者的行为方法
以海淘为例:刚开始张三能很简单的通过国内平台购买商品,直到他有购买海外商品的需求时,他发现在国内可以使用的购买方式不能购买海外商品,为了免去一系列的额外操作他找到了代理者(海淘平台)帮他完成。
//代理者(海淘平台)
class TaoBao {
constructor(buyer) {
this.buyer = buyer;
}
buy(product){
this.buyer.buy(product)
}
}
//被代理者(我)
class Buyer {
constructor(name) {
this.name = name;
}
buy(product){
console.log(this.name+'买到了'+product.name);
}
}
//商品
class Product {
constructor(name) {
this.name = name;
}
}
/*
* 开始海淘
*/
new TaoBao(new Buyer('张三')).buy(new Product('ps5'))
为什么要用代理模式
同样是上面的案例,如果不使用代理模式,那么张三需要自己完成一系列的海淘操作,如开国外设账户,报关,国外快递转国内快递等等。为了实现海淘需要为这个类添加许多方法,这不符合设计模式中的开闭原则
。为此我们新构建一个代理类(海淘平台)由它来完成 ‘海淘’ 这一单职责
,实现对功能的解藕
。
基于上述思想,其它如 加强控制,拓展功能,提高性能
等的优化场景我们将在下文结合实际案例说明。
几种应用场景
缓存代理
缓存获取开销大的数据,如异步请求数据等
以分页数据的缓存为例:
function getlist(page){
return new Promise((resolve) => {
setTimeout(() => {
//模拟后台接口
resolve('第'+ page +'页数据');
}, 1000);
});
}
// getData 代理者
const pageProxy = (function() {
let cache = {};
return {
get:function(page) {
if (cache[page]) {
return Promise.resolve(cache[page]);
}
// getlist 被代理者
return getlist(page).then((res) => {
cache[page] = res;
})
},
cacheClear:function (){
cache = {}
}
}
})();
pageProxy.get(1);//获取第1页数据 耗时1s
pageProxy.get(2);//获取第2页数据 耗时1s
pageProxy.get(1);//当你需要回到第一页时 立即从缓存获取第一页数据
pageProxy.cacheClear();//清除缓存
pageProxy.get(1);//获取第1页数据 耗时1s
事件代理
以 ui > li 添加点击事件绑定
为例:
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
一般做法:
function ul_init(){
const ul = document.getElementById("list");
const li_list = ul.getElementsByTagName('li');
for (let i=0;i<li_list.length;i++)
{
li_list[i].onclick = function (e) {
console.log(e);
}
}
};
缺点:新增的li节点没有绑定上onclick事件
解决思路:利用事件冒泡机制,将li的事件委托给父节点代理。
代理模式:
function ul_init(){
// $ul 为 代理者
const $ul = document.getElementById("list");
// 当点击li时,由于冒泡机制最后会触发代理者onclick事件
$ul.onclick = function (e) {
/*
* 此处为最简单的ul li示范
* 实际中涉及多层冒泡需对 e.target 进行判断
* 这里简单认为 e.target 为 被代理者既 li 实例
*/
const $li = e.target
//...后续的业务逻辑
}
};
虚拟代理
将大开销的对象放到合适的时机执行创建
以图片懒加载为例
<img data-src="./images/1.jpg" alt="">
<img data-src="./images/2.jpg" alt="">
const imgs = document.querySelectorAll('img');
// 代理者
function lazyLoad() {
//...判断条件 如滚动到预设位置
if(flag){
//载入预想的图片 imgs[i]为被代理的对象
imgs[i].src = imgs[i].getAttribute('data-src');
}
}
// 将代理者放到合适的执行环境中 如滚动实时执行判断
window.onscroll = function () {
lazyLoad()
}
保护代理
为某一方法执行设置前提条件,在代理层就过滤一些非法行为
以用户鉴权为例
class User {
constructor(role) {
this.role = role;
}
//访问指定页面
visitPage(url){
window.location.href= url
}
}
class PageDefend {
constructor(user) {
this.user = user;
}
visitPage(url){
//判断权限
if(this.user.role === 'visiter'){
console.log('游客无权访问');
} else {
this.user.visitPage(url)
}
}
}
new PageDefend(new User('visiter')).visitPage('xx.html');
//console.log('游客无权访问');
ES6中的Proxy
什么是Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
语法
const p = new Proxy(target, handler)
target 为被代理对象,handler 一个通常以函数作为属性的对象,用于捕获 target 对象的各种操作。
handler 对象的方法
案例由 MDN 官网改造而来
handler.apply()
作用:拦截函数的调用
function sum(a, b) {
return a + b;
}
const handler = {
apply: function(target, thisArg, argumentsList) {
//target:sum方法
//thisArg:被调用时的上下文对象
//argumentsList:参数数组 [a,b]
return target(argumentsList[0]*10,argumentsList[1]*10)
//proxy1(1, 2) -> sum(10,20);
}
};
const proxy1 = new Proxy(sum, handler);
sum(1, 2) // 3
proxy1(1, 2)// 30
handler.construct()
作用:用于拦截new 操作符
function monster(disposition) {
this.disposition = disposition;
}
const handler = {
construct(target, args) {
//target 为 monster1
//args 为参数列表
return new target('代理后');
}
};
const proxy1 = new Proxy(monster, handler);
new monster('代理前') // monster {disposition: '代理前'}
new proxy1('代理前') // monster {disposition: '代理后'}
handler.defineProperty()
作用:拦截对对象属性的 Object.defineProperty() 操作
var p = new Proxy({}, {
defineProperty: function(target, prop, descriptor) {
// prop 待检索其描述的属性名 'a'
// descriptor 待定义或修改的属性的描述符
console.log('prop:'+prop);
return true;
}
});
/*
* 注意 desc 只有以下属性生效
* enumerable
* configurable
* writable
* value
* get
* set
*/
var desc = { configurable: true, enumerable: true, value: 10 };
Object.defineProperty(p, 'a', desc); // "prop: a"
handler.defineProperty()
作用:拦截对对象属性的 delete 操作
var p = new Proxy({}, {
deleteProperty: function(target, prop) {
// prop 删除的属性
console.log('delete ' + prop);
return true;
}
});
delete p.a; // "delete a"
handler.get()
作用:拦截对象的读取属性操作
var p = new Proxy({}, {
get: function(target, property, receiver) {
// property :被获取的属性名。
// receiver:Proxy或者继承Proxy的对象
console.log("读取到属性" + property);
return 10;
}
});
console.log(p.a); // "读取到属性a"
// 10
handler.getOwnPropertyDescriptor()
作用:拦截对对象属性的 Object.getOwnPropertyDescriptor() 操作
var p = new Proxy({ a: 20}, {
getOwnPropertyDescriptor: function(target, prop) {
// prop :属性名。
console.log('属性名: ' + prop);
//注意这里返回需是合法的 descriptor
return { configurable: true, enumerable: true, value: 10 };
}
});
Object.getOwnPropertyDescriptor(p, 'a').value
// "属性名: a"
// 10
handler.getPrototypeOf()
作用:捕获代理对象的原型读取操作,具体有以下5种
var obj = {};
var p = new Proxy(obj, {
getPrototypeOf(target) {
//返回值必须是一个对象或者 null
return Array.prototype;
}
});
console.log(
Object.getPrototypeOf(p) === Array.prototype, // true
Reflect.getPrototypeOf(p) === Array.prototype, // true
p.__proto__ === Array.prototype, // true
Array.prototype.isPrototypeOf(p), // true
p instanceof Array // true
);
handler.has()
作用:捕获对象的in操作
var p = new Proxy({}, {
has: function(target, prop) {
console.log('called: ' + prop);
return true;
}
});
console.log('a' in p); // "called: a"
// true
handler.isExtensible()
作用:拦截对对象的Object.isExtensible()
var p = new Proxy({}, {
isExtensible: function(target) {
console.log('called');
return true;//也可以return 1;等表示为true的值
}
});
console.log(Object.isExtensible(p));
// "called"
// true
handler.ownKeys()
作用:捕获对象的 Object.getOwnPropertyNames() 操作
var p = new Proxy({}, {
ownKeys: function(target) {
console.log('called');
return ['a', 'b', 'c'];
}
});
console.log(Object.getOwnPropertyNames(p));
// "called"
// [ 'a', 'b', 'c' ]
handler.preventExtensions()
作用:捕获对象的 Object.preventExtensions() 操作
var p = new Proxy({}, {
preventExtensions: function(target) {
console.log('called');
Object.preventExtensions(target);
return true;
}
});
console.log(Object.preventExtensions(p));
// "called"
// false
handler.set()
作用:捕获对象赋值操作
var p = new Proxy({}, {
set: function(target, prop, value, receiver) {
target[prop] = value;
console.log('属性'+prop+'被设置为'+value);
return true;
}
})
p.a = 10; // "属性a被设置为10"
console.log(p.a);// 10
receiver 最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)。
备注:假设有一段代码执行 obj.name = "jen", obj 不是一个 proxy,且自身不含 name 属性,但是它的原型链上有一个 proxy,那么,那个 proxy 的 set() 处理器会被调用,而此时,obj 会作为 receiver 参数传进来。
handler.setPrototypeOf()
作用:捕获对象的 Object.setPrototypeOf() 操作
var handlerReturnsFalse = {
setPrototypeOf(target, newProto) {
//newProto 对象新原型或为null.
//返回 false则表示 不设置newProto
return false;
}
};
var newProto = {}, target = {};
var p1 = new Proxy(target, handlerReturnsFalse);
Object.setPrototypeOf(p1, newProto); // throws a TypeError
Reflect.setPrototypeOf(p1, newProto); // returns false
应用
实现缓存代理
回过头去我们试着使用 Proxy 来重构 我们之前的缓存代理案例
//获取数据的方法不变
function getlist(page){
return new Promise((resolve) => {
setTimeout(() => {
//模拟后台接口
resolve('第'+ page +'页数据');
}, 1000);
});
}
// 利用es6 Proxy 方法重构代理者
const pageProxy = new Proxy(getlist,{
cache:new Map(),
apply: function(target, thisArg, argumentsList) {
//页数
const page = argumentsList[0]
//第二个参数控制是否缓存读取
const noCache = argumentsList[1]
//缓存 Map
const cache = this.cache
if(noCache){
//清除缓存
cache.has(page) && cache.delete(page);
}
const pageRes = cache.get(page)
if (pageRes) {
return Promise.resolve(pageRes);
} else {
return target(page).then((res) => {
cache.set(page,res);
})
}
}
})
pageProxy(1);//获取第1页数据 耗时1s
pageProxy(2);//获取第2页数据 耗时1s
pageProxy(1);//立即获取第1页数据缓存
pageProxy(1,true);//重新获取第1页数据 耗时1s