JS 设计模式之代理模式

164 阅读6分钟

什么是代理模式

代理者通过 聚合/组合 被代理者,以实现被代理者的行为方法

以海淘为例:刚开始张三能很简单的通过国内平台购买商品,直到他有购买海外商品的需求时,他发现在国内可以使用的购买方式不能购买海外商品,为了免去一系列的额外操作他找到了代理者(海淘平台)帮他完成。

//代理者(海淘平台)
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