JavaScript 设计模式(三):代理模式

代理模式

代理模式:为一个对象提供一个代用品或占位符,以便控制它的访问。

当我们不方便直接访问某个对象时,或不满足需求时,可考虑使用一个替身对象来控制该对象的访问。替身对象可对请求预先进行处理,再决定是否转交给本体对象。

生活小栗子:

  1. 代购;
  2. 明星经纪人;
  3. 和谐上网

经常 shopping 的同学,对代购应该不陌生。自己不方便直接购买或买不到某件商品时,会选择委托给第三方,让代购或黄牛去做购买动作。程序世界的代理者也是如此,我们不直接操作原有对象,而是委托代理者去进行。代理者的作用,就是对我们的请求预先进行处理或转接给实际对象。

模式特点

  1. 代理对象可预先处理请求,再决定是否转交给本体;
  2. 代理和本体对外显示接口保持一致性
  3. 代理对象仅对本体做一次包装

模式细分

  1. 虚拟代理(将开销大的运算延迟到需要时执行)
  2. 缓存代理(为开销大的运算结果提供缓存)
  3. 保护代理(黑白双簧,代理充当黑脸,拦截非分要求)
  4. 防火墙代理(控制网络资源的访问)
  5. 远程代理(为一个对象在不同的地址控件提供局部代表)
  6. 智能引用代理(访问对象执行一些附加操作)
  7. 写时复制代理(延迟对象复制过程,对象需要真正修改时才进行)

JavaScript 中常用的代理模式为 “虚拟代理” 和 “缓存代理”。

模式实现

实现方式:创建一个代理对象,代理对象可预先对请求进行处理,再决定是否转交给本体,代理和本体对外接口保持一致性(接口名相同)。

// 例子:代理接听电话,实现拦截黑名单
var backPhoneList = ['189XXXXX140'];       // 黑名单列表
// 代理
var ProxyAcceptPhone = function(phone) {
    // 预处理
    console.log('电话正在接入...');
    if (backPhoneList.includes(phone)) {
        // 屏蔽
        console.log('屏蔽黑名单电话');
    } else {
        // 转接
        AcceptPhone.call(this, phone);
    }
}
// 本体
var AcceptPhone = function(phone) {
    console.log('接听电话:', phone);
};

// 外部调用代理
ProxyAcceptPhone('189XXXXX140'); 
ProxyAcceptPhone('189XXXXX141'); 

代理并不会改变本体对象,遵循 “单一职责原则”,即 “自扫门前雪,各找各家”。不同对象承担独立职责,不过于紧密耦合,具体执行功能还是本体对象,只是引入代理可以选择性地预先处理请求。例如上述代码中,我们向 “接听电话功能” 本体添加了一个屏蔽黑名单的功能(保护代理),预先处理电话接入请求。

虚拟代理(延迟执行)

虚拟代理的目的,是将开销大的运算延迟到需要时再执行。

虚拟代理在图片预加载的应用,代码例子来至 《JavaScript 设计模式与开发实践》

// 本体
var myImage = (function(){
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})();

// 代理
var proxyImage = (function(){
    var img = new Image;
    img.onload = function() {
        myImage.setSrc(this.src);             // 图片加载完设置真实图片src
    }
    return {
        setSrc: function(src) {
            myImage.setSrc('./loading.gif');  // 预先设置图片src为loading图
            img.src = src;
        }
    }
})();

// 外部调用
proxyImage.setSrc('./product.png');           // 有loading图的图片预加载效果

缓存代理(暂时存储)

缓存代理的目的,是为一些开销大的运算结果提供暂时存储,以便下次调用时,参数与结果不变情况下,从缓存返回结果,而不是重新进行本体运算,减少本体调用次数。

应用缓存代理的本体,要求运算函数应是一个纯函数,简单理解比如一个求和函数 sum, 输入参数 (1, 1), 得到的结果应该永远是 2

纯函数:固定的输入,有固定的输出,不影响外部数据。

模拟场景:60道判断题测试,每三道题计分一次,根据计分筛选下一步的三道题目?

三道判断题得分结果:

  1. (0, 0 ,0)
  2. (0, 0, 1)
  3. (0, 1, 0)
  4. (0, 1, 1)
  5. (1, 0, 0)
  6. (1, 0, 1)
  7. (1, 1, 0)
  8. (1, 1, 1)

总共七种计分结果。60/3 = 20,共进行 20 次计分,每次计分执行 3 个循环累计,共 60 个循环。接下来,借用 “缓存代理” 方式,来实现最少本体运算次数。

// 本体:对三道题答案进行计分
var countScore = function(ansList) {
    let [a, b, c] = ansList;
    return a + b + c;
}

// 代理:对计分请求预先处理
var proxyCountScore = (function() {
    var existScore = {};    // 设定存储对象
    return function(ansList) {
        var attr = ansList.join(',');  // eg. ['0,0,0']
        if (existScore[attr] != null) {
            // 从内存返回
            return existScore[attr];
        } else {
            // 内存不存在,转交本体计算并存入内存
            return existScore[attr] = countScore(ansList);
        }
    }
})();

// 调用计分
proxyCountScore([0,1,0]);

60 道题目,每 3 道题一次计分,共 20 次计分运算,但总的计分结果只有 7 种,那么实际上本体 countScore() 最多只需运算 7 次,即可囊括所有计算结果。

通过缓存代理的方式,对计分结果进行临时存储。用答案字符串组成属性名 ['0,1,0'] 作为 key 值检索内存,若存在直接从内存返回,减少包含复杂运算的本体被调用的次数。之后如果我们的题目增加至 90 道, 120 道,150 道题时,本体 countScore() 运算次数仍旧保持 7 次,中间节省了复杂运算的开销。

ES6 的 Proxy

ES6新增的 Proxy 代理对象的操作,具体的实现方式是在 handler 上定义对象自定义方法集合,以便预先管控对象的操作。

ES6 的 Proxy语法:let proxyObj = new Proxy(target, handler);

  • target: 本体,要代理的对象
  • handler: 自定义操作方法集合
  • proxyObj: 返回的代理对象,拥有本体的方法,不过会被 handler 预处理
// ES6的Proxy
let Person = {
    name: '以乐之名'
};

const ProxyPerson = new Proxy(Person, {
    get(target, key, value) {
        if (key != 'age') {
            return target[key];
        } else {
            return '保密'
        }
    },
    set(target, key, value) {
        if (key === 'rate') {
            target[key] = value === 'A' ? '推荐' : '待提高'
        }
    }
})

console.log(ProxyPerson.name);  // '以乐之名'
console.log(ProxyPerson.age);   // '保密'
ProxyPerson.rate = 'A';         
console.log(ProxyPerson.rate);  // '推荐'
ProxyPerson.rate = 'B';         
console.log(ProxyPerson.rate);  // '待提高'

handler 除常用的 set/get,总共支持 13 种方法:

handler.getPrototypeOf()
// 在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时

handler.setPrototypeOf()
// 在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时

handler.isExtensible()
// 在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时

handler.preventExtensions()
// 在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时

handler.getOwnPropertyDescriptor()
// 在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时

handler.defineProperty()
// 在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时

handler.has()
// 在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时

handler.get()
// 在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时

handler.set()
// 在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时

handler.deleteProperty()
// 在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时

handler.ownKeys()
// 在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy) 时

handler.apply()
// 在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。

handler.construct()
// 在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行 new proxy() 时

适用场景

  • 虚拟代理:
    1. 图片预加载(loading 图)
    2. 合并HTTP请求(数据上报汇总)
  • 缓存代理:(前提本体是纯函数)
    1. 缓存异步请求数据
    2. 缓存较复杂的运算结果
  • ES6 的 Proxy:
    1. 实现对象私有属性
    2. 实现表单验证

“策略模式” 可应用于表单验证信息,“代理方式” 也可实现。这里引用 Github - jawil 的一个例子,思路供大家分享。

// 利用 proxy 拦截格式不符数据
function validator(target, validator, errorMsg) {
    return new Proxy(target, {
        _validator: validator,
        set(target, key, value, proxy) {
            let errMsg = errorMsg;
            if (value == null || !value.length) {
                console.log(`${errMsg[key]} 不能为空`);
                return target[key] = false;
            }
            let va = this._validator[key];  // 这里有策略模式的应用
            if (!!va(value)) {
                return Reflect.set(target, key, value, proxy);
            } else {
                console.log(`${errMsg[key]} 格式不正确`);
                return target[key] = false;
            }
        }
    })
}

// 负责校验的逻辑代码
const validators = {
    name(value) {
        return value.length >= 6;
    },
    passwd(value) {
        return value.length >= 6;
    },
    moblie(value) {
        return /^1(3|5|7|8|9)[0-9]{9}$/.test(value);
    },
    email(value) {
        return /^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value)
    }
}

// 调用代码
const errorMsg = {
    name: '用户名',
    passwd: '密码',
    moblie: '手机号码',
    email: '邮箱地址'
}
const vali = validator({}, validators, errorMsg)
let registerForm = document.querySelector('#registerForm')
registerForm.addEventListener('submit', function () {
    let validatorNext = function* () {
        yield vali.name = registerForm.userName.value
        yield vali.passwd = registerForm.passWord.value
        yield vali.moblie = registerForm.phone.value
        yield vali.email = registerForm.email.value
    }
    let validator = validatorNext();
    for (let field of validator) {
        validator.next();
    }
}

实现思路: 利用 ES6 的 proxy 自定义 handlerset() ,进行表单校验并返回结果,并且借用 “策略模式" 独立封装验证逻辑。使得表单对象,验证逻辑,验证器各自独立。代码整洁性,维护性及复用性都得到增强。

关于 “设计模式” 在表单验证的应用,可参考 jawil 原文:《探索两种优雅的表单验证——策略设计模式和ES6的Proxy代理模式》

优缺点

  • 优点:
    1. 可拦截和监听外部对本体对象的访问;
    2. 复杂运算前可以进行校验或资源管理;
    3. 对象职能粒度细分,函数功能复杂度降低,符合 “单一职责原则”;
    4. 依托代理,可额外添加扩展功能,而不修改本体对象,符合 “开发-封闭原则”
  • 缺点:
    1. 额外代理对象的创建,增加部分内存开销;
    2. 处理请求速度可能有差别,非直接访问存在开销,但 “虚拟代理” 及 “缓存代理” 均能提升性能

参考文章

本文首发Github,期待Star!
https://github.com/ZengLingYong/blog

作者:以乐之名
本文原创,有不当的地方欢迎指出。转载请指明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 代理模式,顾名思义,就是A要对C做一件事情,让B帮忙做(怎么听起来怪怪的)。 下面写几个常见的使用代理模式的例子 ...
    Sccong阅读 176评论 0 0
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,621评论 2 17
  • javascript设计模式与开发实践 设计模式 每个设计模式我们需要从三点问题入手: 定义 作用 用法与实现 单...
    穿牛仔裤的蚊子阅读 3,936评论 0 13
  • 1. 设计模式概述 简介在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。即设计模式是在某种场合下对某个...
    nimw阅读 524评论 0 0
  • 一函数定义 1内置函数 Python内置了很多有用的函数,我们可以直接调用。不像C#中调用函数,需要先实例化类,再...
    凌雲木阅读 335评论 0 2