《JavaScript设计模式与开发实践》学习笔记part3-代理模式和迭代器模式

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012863664/article/details/82430055

本篇内容主要讲述JavaScript中的两个设计模式:代理模式和迭代器模式

第六章 代理模式

6.1 定义

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签。
例子:小明喜欢A,决定给A 送一束花来表白。刚好小明打听到A 和他有一个共同的朋友B,于是内向的小明决定让B 来代替自己完成送花这件事情。实现代码如下:

var Flower = function () {};
var xiaoming = {
    sendFlower: function (target) {
        var flower = new Flower();
        target.receiveFlower(flower);
    }
};
var B = {
    receiveFlower: function (flower) {
        A.receiveFlower(flower);
    }
};
var A = {
    receiveFlower: function (flower) {
        console.log('收到花 ' + flower);
    }
};
xiaoming.sendFlower(B);

6.2 虚拟代理

在这个虚拟的例子中我们找到两种代理模式的身影。代理B 可以帮助A过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理B处被拒绝掉。这种代理叫作保护代理。A 和B 一个充当白脸,一个充当黑脸。白脸A 继续保持良好的女神形象,不希望直接拒绝任何人,于是找了黑脸B 来控制对A 的访问。
另外,假设现实中的花价格不菲,导致在程序世界里,new Flower 也是一个代价昂贵的操作,那么我们可以把new Flower 的操作交给代理B 去执行,代理B 会选择在A 心情好时再执行new Flower,这是代理模式的另一种形式,叫作虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。

var B = {
    receiveFlower: function (flower) {
        A.listenGoodMood(function () { // 监听A 的好心情
            var flower = new Flower(); // 延迟创建flower 对象
            A.receiveFlower(flower);
        });
    }
};

6.3 虚拟代理实现图片预加载

在Web 开发中,图片预加载是一种常用的技术,如果直接给某个img 标签节点设置src 属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img 节点里,这种场景就很适合使用虚拟代理。
下面我们来实现这个虚拟代理,首先创建一个普通的本体对象,这个对象负责往页面中创建一个img 标签,并且提供一个对外的setSrc 接口,外界调用这个接口,便可以给该img 标签设置src属性:

var myImage = (function () {
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return {
        setSrc: function (src) {
            imgNode.src = src;
        }
    }
})();
myImage.setSrc('http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg');

我们把网速调至5KB/s,然后通过MyImage.setSrc 给该img 节点设置src,可以看到,在图片被加载好之前,页面中有一段长长的空白时间。现在开始引入代理对象proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位的菊花图loading.gif, 来提示用户图片正在加载。代码如下:

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);
    }
    return {
        setSrc: function (src) {
            myImage.setSrc('file:// /C:/Users/svenzeng/Desktop/loading.gif');
            img.src = src;
        }
    }
})();
proxyImage.setSrc('http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg');

现在我们通过proxyImage 间接地访问MyImage。proxyImage 控制了客户对MyImage 的访问,并且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把img 节点的src 设置为一张本地的loading 图片。

6.4 缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
为了节省示例代码,以及让读者把注意力集中在代理模式上面,这里编写一个简单的求乘积的程序,请读者自行把它脑补为复杂的计算。
先创建一个用于求乘积的函数:

var mult = function () {
    console.log('开始计算乘积');
    var a = 1;
    for (var i = 0, l = arguments.length; i < l; i++) {
        a = a * arguments[i];
    }
    return a;
};
mult(2, 3); // 输出:6
mult(2, 3, 4); // 输出:24

现在加入缓存代理函数:

var proxyMult = (function () {
    var cache = {};
    return function () {
        var args = Array.prototype.join.call(arguments, ',');
        if (args in cache) {
            return cache[args];
        }
        return cache[args] = mult.apply(this, arguments);
    }
})();
proxyMult(1, 2, 3, 4); // 输出:24
proxyMult(1, 2, 3, 4); // 输出:24

当我们第二次调用proxyMult( 1, 2, 3, 4 )的时候,本体mult 函数并没有被计算,proxyMult直接返回了之前缓存好的计算结果。通过增加缓存代理的方式,mult 函数可以继续专注于自身的职责——计算乘积,缓存的功能是由代理对象实现的。

第七章 迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
目前,恐怕只有在一些“古董级”的语言中才会为实现一个迭代器模式而烦恼,现在流行的大部分语言如Java、Ruby 等都已经有了内置的迭代器实现,许多浏览器也支持JavaScript 的Array.prototype.forEach。

7.1jQuery中的迭代器的简易实现

现在我们来自己实现一个each 函数,each 函数接受2 个参数,第一个为被循环的数组,第
二个为循环中的每一步后将被触发的回调函数:

var each = function (ary, callback) {
    for (var i = 0, l = ary.length; i < l; i++) {
        callback.call(ary[i], i, ary[i]); // 把下标和元素当作参数传给callback 函数
    }
};
each([1, 2, 3], function (i, n) {
    alert([i, n]);
});

7.2内部迭代器和外部迭代器

7.2.1 内部迭代器

我们刚刚编写的each 函数属于内部迭代器,each 函数的内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。
内部迭代器在调用的时候非常方便,外界不用关心迭代器内部的实现,跟迭代器的交互也仅仅是一次初始调用,但这也刚好是内部迭代器的缺点。由于内部迭代器的迭代规则已经被提前规定,上面的each 函数就无法同时迭代2 个数组了。
比如现在有个需求,要判断2 个数组里元素的值是否完全相等, 如果不改写each 函数本身的代码,我们能够入手的地方似乎只剩下each 的回调函数了,代码如下:

var compare = function (ary1, ary2) {
    if (ary1.length !== ary2.length) {
        throw new Error('ary1 和ary2 不相等');
    }
    each(ary1, function (i, n) {
        if (n !== ary2[i]) {
            throw new Error('ary1 和ary2 不相等');
        }
    });
    alert('ary1 和ary2 相等');
};
compare([1, 2, 3], [1, 2, 4]); // throw new Error ( 'ary1 和ary2 不相等' );

7.2.2 外部迭代器

外部迭代器必须显式地请求迭代下一个元素。外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工控制迭代的过程或者顺序。代码如下:

var Iterator = function (obj) {
    var current = 0;
    var next = function () {
        current += 1;
    };
    var isDone = function () {
        return current >= obj.length;
    };
    var getCurrItem = function () {
        return obj[current];
    };
    return {
        next: next,
        isDone: isDone,
        getCurrItem: getCurrItem
    }
};
// 改写compare函数
var compare = function (iterator1, iterator2) {
    while (!iterator1.isDone() && !iterator2.isDone()) {
        if (iterator1.getCurrItem() !== iterator2.getCurrItem()) {
            throw new Error('iterator1 和iterator2 不相等');
        }
        iterator1.next();
        iterator2.next();
    }
    alert('iterator1 和iterator2 相等');
}
var iterator1 = Iterator([1, 2, 3]);
var iterator2 = Iterator([1, 2, 3]);
compare(iterator1, iterator2); // 输出:iterator1 和iterator2 相等

外部迭代器虽然调用方式相对复杂,但它的适用面更广,也能满足更多变的需求。内部迭代器和外部迭代器在实际生产中没有优劣之分,究竟使用哪个要根据需求场景而定。

7.3 应用举例

有这么一段代码,目的是根据不同的浏览器获取相应的上传组件对象:

var getUploadObj = function () {
    try {
        return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE 上传控件
    } catch (e) {
        if (supportFlash()) { // supportFlash 函数未提供
            var str = '<object type="application/x-shockwave-flash"></object>';
            return $(str).appendTo($('body'));
        } else {
            var str = '<input name="file" type="file"/>'; // 表单上传
            return $(str).appendTo($('body'));
        }
    }
};

在不同的浏览器环境下,选择的上传方式是不一样的。因为使用浏览器的上传控件进行上传速度快,可以暂停和续传,所以我们首先会优先使用控件上传。如果浏览器没有安装上传控件,则使用Flash 上传, 如果连Flash 也没安装,那就只好使用浏览器原生的表单上传了。
看看上面的代码,为了得到一个upload 对象,这个getUploadObj 函数里面充斥了try,catch以及if 条件分支。缺点是显而易见的。第一是很难阅读,第二是严重违反开闭原则。 在开发和调试过程中,我们需要来回切换不同的上传方式,每次改动都相当痛苦。后来我们还增加支持了一些另外的上传方式,比如,HTML5 上传,这时候唯一的办法是继续往getUploadObj 函数里增加条件分支。
我们来看一下使用迭代去模式去改写这段代码:

var getActiveUploadObj = function () {
    try {
        return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE 上传控件
    } catch (e) {
        return false;
    }
};
var getFlashUploadObj = function () {
    if (supportFlash()) { // supportFlash 函数未提供
        var str = '<object type="application/x-shockwave-flash"></object>';
        return $(str).appendTo($('body'));
    }
    return false;
};
var getFormUpladObj = function () {
    var str = '<input name="file" type="file" class="ui-file"/>'; // 表单上传
    return $(str).appendTo($('body'));
};
var iteratorUploadObj = function () {
    for (var i = 0, fn; fn = arguments[i++];) {
        var uploadObj = fn();
        if (uploadObj !== false) {
            return uploadObj;
        }
    }
};
var uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUpladObj);

重构代码之后,我们可以看到,获取不同上传对象的方法被隔离在各自的函数里互不干扰,try、catch 和if 分支不再纠缠在一起,使得我们可以很方便地的维护和扩展代码。比如,后来我们又给上传项目增加了Webkit 控件上传和HTML5 上传,我们要做的仅仅是下面一些工作。

var getWebkitUploadObj = function () {
    // 具体代码略
};
var getHtml5UploadObj = function () {
    // 具体代码略
};
var uploadObj = iteratorUploadObj(getActiveUploadObj, getWebkitUploadObj,
            getFlashUploadObj, getHtml5UploadObj, getFormUpladObj);

这里我个人的理解是,将获取图片上传控件的逻辑分别封装成各自的方法,这些方法提供了统一的判断依据(return obj, return false)来让迭代器知道执行获取空间的方法是否执行成功,因此迭代器的其中一个实现原理就是,把多个不同的逻辑封装完后,提供统一的规则来判断本次迭代的结果

猜你喜欢

转载自blog.csdn.net/u012863664/article/details/82430055