JavaScript中的代理模式的详解

一. 代理模式的定义

代理模式的定义:为其他对象提供一种代理,以控制对着这个对象的访问。

在代理模式中,一个对象充当另一个对象的接口。

这种模式看起来像是额外的开销,但是出于性能因素的考虑却是非常有用的。代理充当了本体对象的守护对象,并且试图使本体对象做尽可能少的工作。

二. 代理模式的适用场景

代理模式的适用场景有:

延迟一个大对象的实例化;
访问远程对象;
访问控制;
… …
三. 代理模式的实现

在代理模式中,一个对象充当另一个对象的接口,使得本体对象做尽可能少的工作。

/* =============== 本体类 =============== */
var Client = function() {};
Client.prototype = {
    add: function() {
        // 添加功能... ...
    },
    delete: function() {
        // 删除功能... ...
    },
    update: function() {
        // 修改功能... ...
    }
};

/* =============== 代理类 =============== */
var Proxy = function() {
    this.client = new Client();
};
Proxy.prototype = {
    add: function() {
        return this.client.add();
    },
    delete: function() {
        return this.client.delete();
    },
    update: function() {
        return this.client.update();
    }
};
3.1 虚拟代理

假如Client类有很多方法,并且大多数都庞大且复杂,为了实例它会占用很多很多CPU。那当我们需要使用这个对象时才去实例化它不是更好吗?虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。

我们把上面的代码用虚拟代理重构一下:

/* =============== 本体类 =============== */
var Client = function() {};
Client.prototype = {
    add: function() {
        // 添加功能... ...
    },
    delete: function() {
        // 删除功能... ...
    },
    update: function() {
        // 修改功能... ...
    }
};

/* =============== 代理类 =============== */
var Proxy = function() {
    this.client = null;
};
Proxy.prototype = {
    // 在必要的时候才创建实例对象
    _init: function() {
        if (!this.client) {
            this.client = new Client();
        }
    },
    add: function() {
        this._init();
        return this.client.add();
    },
    delete: function() {
        this._init();
        return this.client.delete();
    },
    update: function() {
        this._init();
        return this.client.update();
    }
};
3.2 缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储。在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。

例如:

/* =============== 开销大的本体类 =============== */
var calculate = function() {
    var result;

    // 复杂且庞大的计算 .... ...

    return result;
};

/* =============== 缓存代理类 =============== */
var calculateProxy = (function() {
    var cache = {}; // 缓存计算结果

    return function() {
        var args = Array.prototype.join.call(arguments, ",");
        if(args in cache) {
            return cache[args];
        }
        return cache[args] = calculate.apply(this, arguments);
    }
})();

/* =============== 客户端实现 =============== */
calculateProxy(1, 2, 3, 4, 5); // 本体calculate函数被计算,并写入缓存结果
calculateProxy(1, 2, 3, 4, 5); // 本体calculate函数并没有被计算,而是直接返回之前缓存好的计算结果
calculateProxy(1, 2, 3); // 本体calculate函数被计算,并写入缓存结果
通过增加缓存代理的方式,本体calculate函数可以专注于自身的计算职能,而缓存的额功能则由代理对象来实现。

3.3 用高阶函数动态创建代理

通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。这些方法被当作参数传入一个专门用于创建缓存代理的工厂中。这样,我们就可以为加减乘除等创建缓存代理,代码如下:

/********** 计算乘积 **********/
var mult = function() {
    var result = 1;
    for(var i = 0, l = arguments.length; i < l; i++) {
        result = result * arguments[i];
    }
    return result;
};
/********** 计算加和 **********/
var plus = function() {
    var result = 0;
    for(var i = 0, l = arguments.length; i < l; i++) {
        result = result + arguments[i];
    }
    return result;
};

/********** 创建缓存代理的工厂 **********/
var createProxyFactory = function(fn) {
    var cache = {};

    return function() {
       var args = Array.prototype.join.call(arguments, ",");
        if(args in cache) {
            return cache[args];
        }
        return cache[args] = fn.apply(this, arguments);
    };
};

var multProxy = createProxyFactory(mult);
var plusProxy = createProxyFactory(plus);

/********** 客户端实现 **********/
multProxy(1, 2, 3, 4, 5); // 120
plusProxy(1, 2, 3, 4, 5); // 15
3.4 其他代理模式

代理模式的变种很多,主要有:

远程代理:为一个对象在不同的地址空间提供局部代表。
保护代理:用于控制不同权限的对象对目标对象的访问。
智能引用代理:取代了简单的指针,它在访问对象时执行了一些附加操作,比如计算一个对象被引用的次数。
… …
代理模式包括许多小分类,在JavaScript开发中最常用的是虚拟代理和缓存代理。

四. 代理模式的实际应用

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

在Web开发中,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张图片作为loading图占位,然后用异步的方式加载图片,等图片加载完成后再将其插入img节点中。这种延迟初始化的场景就很适合使用虚拟代理。

引入代理对象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("loading.gif");
            img.src = src;
        }
    };
})();

proxyImage.setSrc("yun_qi_img/1.jpg");
备注:该代码摘抄自《JavaScript设计模式与开发实践》第6章P92。

4.2 虚拟代理实现合并HTTP请求

假设在做一个标签管理的功能时,当点击标签删除按钮,该对应的标签就会对服务器进行标签删除的网络请求。当在短时间内点击多次标签删除按钮,可以预见,如此频繁的网络请求将会带来相当大的开销。

解决方案是:我们可以收集一段时间内的请求,最后一次性发送给服务器。比如等待2秒钟之后,才把这2秒之内需要删除的标签打包发送给服务器。

/* ============== 删除标签的本体类 ============== */
var deleteTag = function(tagName) {
    // 删除标签的网络请求与功能实现
    // ... ...
}

/* ============== 删除标签的代理类 ============== */
var deleteTagProxy = (function() {
    var cache = [], // 保存一段时间需要删除的标签名
        timer; // 定时器

    return function(tagName) {
        cache.push(tagName);
        if(timer) { // 保证不会覆盖已经启动的定时器
            return;
        }

        // 2s后向本体发送需要同步的标签名集合
        timer = setTimeout(function() {
            deleteTag(cache.join(","));

            // 清空定时器
            clearTimeout(timer);
            timer = null;

            // 清空标签名集合
            cache = [];
        }, 2000);
    };
})();

/* ============== 删除标签的交互实现 ============== */
/*
 * 标签删除按钮的DOM结构为:<div class="btn-delete-tag" data-tagName="my-tag"></div>
 */
var deleteTagBtn = document.getElementByClassName("btn-delete-tag");

deleteTagBtn.forEach(function(element, index) {
    element.addEventListener("click", function() {

        deleteTagProxy(this.dataSet.tagName);

    }, false);
});
4.3 缓存代理用于ajax异步请求数据

在项目中常常会遇到分页的需求。同一页的数据理论上只需要去后台拉去一次。这些已经拉取好的数据在某个地方被缓存之后,下次再请求同一页时,便可以直接从缓存中读取数据。

这里适合使用缓存代理模式。

/* =============== ajax工具函数 =============== */
function ajax(options) {
    options = options || {};
    options.type = (options.type || "GET").toUpperCase();
    options.dataType = options.dataType || "json";
    var params = formatParams(options.data);

    //创建XMLHttpRequest
    if (window.XMLHttpRequest) { // IE6+及现代浏览器
        var xhr = new XMLHttpRequest();
    } else { //IE6及其以下版本浏览器
        var xhr = new ActiveXObject('Microsoft.XMLHTTP');
    }

    // 接收数据
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            var status = xhr.status;
            if (status >= 200 && status < 300) {
                options.success && options.success(xhr.responseText, xhr.responseXML);
            } else {
                options.fail && options.fail(status);
            }
        }
    }

    // 连接和发送数据
    if (options.type == "GET") {
        xhr.open("GET", options.url + "?" + params, true);
        xhr.send(null);
    } else if (options.type == "POST") {
        xhr.open("POST", options.url, true);
        xhr.send(params);
    }
}

//格式化参数
function formatParams(data) {
    var arr = [];
    for (var name in data) {
        arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name]));
    }
    arr.push(("v=" + Math.random()).replace(".",""));
    return arr.join("&");
}

/* =============== ajax异步请求分页数据 =============== */
var getPageContext = function(pageId) {
    var result;
    ajax({
        url: "/test/index.php",
        method: "get",
        dataType: "json",
        data: {
            id: pageId
        },
        success: function(response) {
             result = response;
        }
    });
    return result;
};

/* =============== 缓存代理类 =============== */
var getPageContextProxy = (function() {
    var cache = {}; // 缓存计算结果

    return function() {
        var args = Array.prototype.join.call(arguments, ",");
        if(args in cache) {
            return cache[args];
        }
        return cache[args] = getPageContext.apply(this, arguments);
    }
})();

/* =============== 客户端实现 =============== */
getPageContextProxy(1); // 向服务器请求第1页数据
getPageContextProxy(2); // 向服务器请求第2页数据
getPageContextProxy(1); // 从缓存中读取第1页数据
五. 总结

在JavaScript开发中最常用的是虚拟代理和缓存代理。

虽然代理模式很有用,但是在实际业务开发中,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象时,再编写代理也不迟。

时间: 2024-11-02 00:38:10

JavaScript中的代理模式的详解的相关文章

JavaScript中的replace()方法使用详解

  这篇文章主要介绍了JavaScript中的replace()方法使用详解,是JS入门学习中的基础知识,需要的朋友可以参考下 该方法找到一个正则表达式的字符串之间的匹配,并取代了匹配的子带的新的子串. 替换字符串可以包含以下特殊替换模式: 语法 ? 1 string.replace(regexp/substr, newSubStr/function[, flags]); 下面是参数的详细信息: regexp : 一个RegExp对象.匹配被替换参数的返回#2. substr : 一个字符串,由

JavaScript中的splice()方法使用详解

  这篇文章主要介绍了JavaScript中的splice()方法使用详解,是JS入门学习中的基础知识,需要的朋友可以参考下 JavaScript数组的splice()方法改变数组的内容,增加了新的元素,同时消除旧元素. 语法 ? 1 array.splice(index, howMany, [element1][, ..., elementN]); 下面是参数的详细信息: index : 在该索引开始改变的数组. howMany : 整数,表示旧数组元素数去除.如果的howmany为0,没有元

JavaScript中的getDay()方法使用详解

  这篇文章主要介绍了JavaScript中的getDay()方法使用详解,是JS入门学习中的基础知识,需要的朋友可以参考下 javascript Date.getDay()方法按照本地时间返回一周中的一天为所述指定的日期.通过getDay返回的值是对应于星期几的整数:0代表星期日,1代表星期一,2表示星期二,依此类推. 语法 ? 1 Date.getDay() 下面是参数的详细信息: NA 返回值: 按照本地时间返回星期几为指定日期. 例子: ? 1 2 3 4 5 6 7 8 9 10 11

JavaScript中reduce()方法的使用详解

  这篇文章主要介绍了JavaScript中reduce()方法的使用详解,是JS入门学习中的基础知识,需要的朋友可以参考下 JavaScript 数组reduce()方法同时应用一个函数针对数组的两个值(从左到右),以减至一个值. 语法 ? 1 array.reduce(callback[, initialValue]); 下面是参数的详细信息: callback : 函数执行在数组中每个值 initialValue : 对象作为第一个参数回调的第一次调用使用 返回值: 返回数组的减少单一个值

JavaScript中的bold()方法使用详解

  这篇文章主要介绍了JavaScript中的bold()方法使用详解,是JS入门学习中的基础知识,需要的朋友可以参考下 此方法将导致就好像它是在一个标签的字符串显示为粗体. 语法 ? 1 string.bold( ) 下面是参数的详细信息: NA: 返回值: 返回字符串含有标签 例子: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <html> <head> <title>JavaScript String bold() Method</

JavaScript中的small()方法使用详解

  这篇文章主要介绍了JavaScript中的small()方法使用详解,是JS入门学习中的基础知识,需要的朋友可以参考下 此方法导致要显示在一个小的字体,就好像它是在一个标记的字符串. 语法 ? 1 string.small( ) 下面是参数的详细信息: NA 返回值: 返回字符串标签 例子: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 <html> <head> <title>JavaScript String small() Method<

JavaScript中的lastIndexOf()方法使用详解

  这篇文章主要介绍了JavaScript中的lastIndexOf()方法使用详解,是JS入门学习中的基础知识,需要的朋友可以参考下 此方法调用String对象之内返回索引指定的值最后一次出现,开始搜索在的fromIndex或如果没有找到该值则返回-1. 语法 ? 1 string.lastIndexOf(searchValue[, fromIndex]) 下面是参数的详细信息: searchValue : 一个字符串,表示要搜索的值 fromIndex : 在调用字符串内的位置,从开始搜索.

JavaScript中的slice()方法使用详解

  这篇文章主要介绍了JavaScript中的slice()方法使用详解,是JS入门学习中的基础知识,需要的朋友可以参考下 此方法提取字符串的一部分,并返回一个新的字符串. 语法 ? 1 string.slice( beginslice [, endSlice] ); 下面是参数的详细信息: beginSlice : 从零开始的索引位置开始提取 endSlice : 从零开始的索引位置结束提取.如果省略,切片中提取的字符串的末尾 注意:作为一个负指数,endSlice表示从字符串末尾的偏移. s

JavaScript中的substr()方法使用详解

  这篇文章主要介绍了JavaScript中的substr()方法使用详解,是JS入门学习中的基础知识,需要的朋友可以参考下 这个方法在一个字符串返回字符开始于通过指定的字符数的指定位置. 语法 ? 1 string.substr(start[, length]); 下面是参数的详细信息: start : 在位置开始提取字符(一个介于0和整数小于字符串的长度) length : 要用来提取的字符数 注意:如果start 是负数,substr 使用它作为从字符串的末尾字符索引 返回值: subst