JavaScript 设计模式

设计模式大致可以分成三类:

创建型模式 ( Creational Patterns )

这类设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式, 而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活, 典型的有工厂模式(Factory Pattern)、单例模式。

结构型模式(Structural Patterns)

这类设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。 比如 装饰器模式(Decorator Pattern) 和 代理模式(Proxy Pattern)。

行为型模式(Behavioral Patterns)

这类设计模式特别关注对象之间的通信。 比如 观察者模式(Observer Pattern)。

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等。在JavaScript开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

单例模式的实现

var Singleton = function( name ){
    this.name = name;
    this.instance = null;
};

Singleton.prototype.getName = function(){
    alert ( this.name );
};

Singleton.getInstance = function( name ){
    if ( ! this.instance ){
        this.instance = new Singleton( name );
    }
    return this.instance;
};

var a = Singleton.getInstance( 'sven1' );
var b = Singleton.getInstance( 'sven2' );

alert ( a === b );    // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Singleton 类的使用者必须知道这是一个单例类,跟以往通过new XXX的方式来获取对象不同,这里偏要使用 Singleton.getInstance 来获取对象。

var Singleton = function( name ){
    this.name = name;
};

Singleton.prototype.getName = function(){
    alert ( this.name );
};

Singleton.getInstance = (function(){
    var instance = null;
    return function( name ){
        if ( ! instance ){
            instance = new Singleton( name );
        }
        return instance;
    }
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const CreateDiv = (function () {
    let instance = null;
    const CreateDiv = function (html) {
        if (instance) {
            return instance;
        }
        this.html = html;
        this.init();
        return (instance = this);
    };

    CreateDiv.prototype.init = function () {
        const div = document.createElement("div");
        div.innerHTML = this.html;
        document.body.appendChild(div);
    };

    return CreateDiv;
})();

const a = new CreateDiv("hello");
const b = new CreateDiv("world");

console.log(a === b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

用户从这个类中创建对象的时候,可以像使用其他任何普通类一样

CreateDiv 函数内既要保证实例的唯一,还要负责对象创建和执行 init ,这不符合“单一职责原则”的概念。

const CreateDiv = function (html) {
    this.html = html;
    this.init();
};

CreateDiv.prototype.init = function () {
    const div = document.createElement("div");
    div.innerHTML = this.html;
    document.body.appendChild(div);
};

const ProxySingletonCreateDiv = (function () {
    let instance = null;
    return function (html) {
        if (!instance) {
            instance = new CreateDiv(html);
        }
        return instance;
    };
})();

const a = new ProxySingletonCreateDiv("hello");
const b = new ProxySingletonCreateDiv("world");

console.log(a === b);











 
 
 
 
 
 
 
 
 





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

CreateDiv 变成了一个普通的类,它跟 proxySingletonCreateDiv 组合起来可以达到单例模式的效果。

本例是缓存代理的应用之一

JS 中的单例模式

提示

  • JavaScript 其实是一门无类(class-free)语言,也正因为如此,生搬单例模式的概念并无意义。在 JavaScript 中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个“类”呢?这无异于穿棉衣洗澡,传统的单例模式实现在 JavaScript中 并不适用。
  • 全局变量只提供一个实例,并且提供全局访问。但是它并不是单例模式,全局变量存在很多问题,它很容易造成命名空间污染。在大中型项目中,如果不加以限制和管理,程序中可能存在很多这样的变量。JavaScript 中的变量也很容易被不小心覆盖。
var namespace1 = {
    a: function(){
        alert (1);
    },
    b: function(){
        alert (2);
    }
};
1
2
3
4
5
6
7
8

也可以动态地创建命名空间

var MyApp = {};

MyApp.namespace = function( name ){
    var parts = name.split( '.' );
    var current = MyApp;
    for ( var i in parts ){
        if ( ! current[ parts[ i ] ] ){
            current[ parts[ i ] ] = {};
        }
        current = current[ parts[ i ] ];
    }
};

MyApp.namespace( 'event' );
MyApp.namespace( 'dom.style' );

console.dir( MyApp );

// 上述代码等价于:

var MyApp = {
    event: {},
    dom: {
        style: {}
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var user = (function(){
    var __name = 'sven',
        __age = 29;

    return {
        getUserInfo: function(){
            return __name + '-' + __age;
        }
    }

})();
1
2
3
4
5
6
7
8
9
10
11

惰性单例

惰性单例指的是在需要的时候才创建对象实例。

Singleton.getInstance = (function(){
    var instance = null;
    return function( name ){
        if ( ! instance ){
            instance = new Singleton( name );
        }
        return instance;
    }
})();
1
2
3
4
5
6
7
8
9
var createLoginLayer = (function(){
    var div;
    return function(){
        if ( ! div ){
            div = document.createElement( 'div' );
            div.innerHTML = ’我是登录浮窗’;
            div.style.display = 'none';
            document.body.appendChild( div );
        }

        return div;
    }
})();

document.getElementById( 'loginBtn' ).onclick = function(){
    var loginLayer = createLoginLayer();
    loginLayer.style.display = 'block';
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var getSingle = function( fn ){
    var result;
    return function(){
        return result || ( result = fn .apply(this, arguments ) );
    }
};

var createLoginLayer = function(){
    var div = document.createElement( 'div' );
    div.innerHTML = ’我是登录浮窗’;
    div.style.display = 'none';
    document.body.appendChild( div );
    return div;
};

var createSingleLoginLayer = getSingle( createLoginLayer );

document.getElementById( 'loginBtn' ).onclick = function(){
    var loginLayer = createSingleLoginLayer();
    loginLayer.style.display = 'block';
};
 
 
 
 
 
 















1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

发布订阅模式

发布订阅

  • 发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。比如,我们可以订阅ajax请求的error、succ等事件。或者如果想在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧完成之后发布这个事件。在异步编程中使用发布—订阅模式,我们就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点。
  • 发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。
发布订阅与观察者模式的区别
  1. 发布订阅模式里有三个角色,事件订阅和事件发布都需要消息中心来处理,这种消息范式适合于消息发布者与订阅者允许事件暴露给第三方,两者关系是松耦合的;
  2. 而观察者模式里,事件的观察者自身维护着 update 事件处理程序,当被观察者触发更新时,会触发所有关联的观察者的事件处理程序。

DOM 事件

在这里需要监控用户点击 document.body 的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅 document.body上的 click 事件,当 body 节点被点击时,body 节点便会向订阅者发布这个消息。

document.body.addEventListener( 'click', function(){
    alert(2);
}, false );

document.body.addEventListener( 'click', function(){
    alert(3);
}, false );

document.body.addEventListener( 'click', function(){
    alert(4);
}, false );

document.body.click();    // 模拟用户点击
1
2
3
4
5
6
7
8
9
10
11
12
13

注意,手动触发事件更好的做法是IE下用 fireEvent,标准浏览器下用 dispatchEvent 实现

通用实现

实现

  1. 事件订阅
  2. 事件发布
  3. 取消订阅
var event = {
    clientList: [],
    listen: function( key, fn ){
        if ( ! this.clientList[ key ] ){
            this.clientList[ key ] = [];
        }
        this.clientList[ key ].push( fn );  // 订阅的消息添加进缓存列表
    },
    trigger: function(){
        var key = Array.prototype.shift.call( arguments ),
            fns = this.clientList[ key ];

        if ( ! fns || fns.length === 0 ){  // 如果没有绑定对应的消息
            return false;
        }

        for( var i = 0, fn; fn = fns[ i++ ]; ){
            fn.apply( this, arguments );  // arguments是trigger时带上的参数
        }
    },
    remove: function( key, fn ){
        var fns = this.clientList[ key ];

        if ( ! fns ){    // 如果key对应的消息没有被人订阅,则直接返回
            return false;
        }
        if ( ! fn ){    // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
            fns && ( fns.length = 0 );
        } else {
            for ( var l = fns.length -1; l >=0; l-- ){    // 反向遍历订阅的回调函数列表
                var _fn = fns[ l ];
                if ( _fn === fn ){
                    fns.splice( l, 1 );    // 删除订阅者的回调函数
                }
            }
        }
    };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var installEvent = function( obj ){
    for ( var i in event ){
        obj[ i ] = event[ i ];
    }
};
1
2
3
4
5
var salesOffices = {};
installEvent( salesOffices );

salesOffices.listen( 'squareMeter88', fn1 = function( price ){
    console.log( ’价格= ' + price );
});

salesOffices.listen( 'squareMeter88', fn2 = function( price ){
    console.log( ’价格= ' + price );
});

salesOffices.listen( 'squareMeter100', fn3 = function( price ){
    console.log( ’价格= ' + price );
});

salesOffices.trigger( 'squareMeter100', 3000000 );  // 输出:3000000
salesOffices.remove( 'squareMeter88', fn1 );  // 删除 fn1 的订阅
salesOffices.trigger( 'squareMeter88', 2000000 );   // 输出一次:2000000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

全局的发布-订阅对象

注意

❏ 我们给每个发布者对象都添加了 listen 和 trigger 方法,以及一个缓存列表 clientList,这其实是一种资源浪费。

❏ 小明跟售楼处对象还是存在一定的耦合性,小明至少要知道售楼处对象的名字是 salesOffices,才能顺利的订阅到事件。

var Event = (function(){

    var clientList = {},
        listen,
        trigger,
        remove;

    listen = function( key, fn ){
        if ( ! clientList[ key ] ){
            clientList[ key ] = [];
        }
        clientList[ key ].push( fn );
    };

    trigger = function(){
        var key = Array.prototype.shift.call( arguments ),
            fns = clientList[ key ];
        if ( ! fns || fns.length === 0 ){
            return false;
        }
        for( var i = 0, fn; fn = fns[ i++ ]; ){
            fn.apply( this, arguments );
        }

    };

    remove = function( key, fn ){
        var fns = clientList[ key ];
        if ( ! fns ){
            return false;
        }
        if ( ! fn ){
            fns && ( fns.length = 0 );
        }else{
            for ( var l = fns.length -1; l >=0; l-- ){
                var _fn = fns[ l ];
                if ( _fn === fn ){
                    fns.splice( l, 1 );
                }
            }
        }
    };

    return {
        listen: listen,
        trigger: trigger,
        remove: remove
    }

})();

Event.listen( 'squareMeter88', function( price ){     // 小红订阅消息
    console.log( ’价格= ' + price );       // 输出:’价格=2000000'
});

Event.trigger( 'squareMeter88', 2000000 );    // 售楼处发布消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

先发布后订阅

关于发布-订阅模式的思考

我们所了解到的发布—订阅模式,都是订阅者必须先订阅一个消息,随后才能接收到发布者发布的消息。如果把顺序反过来,发布者先发布一条消息,而在此之前并没有对象来订阅它,这条消息无疑将消失在宇宙中。在某些情况下,我们需要先将这条消息保存下来,等到有对象来订阅它的时候,再重新把消息发布给订阅者。就如同QQ中的离线消息一样,离线消息被保存在服务器中,接收人下次登录上线之后,可以重新收到这条消息。

这种需求在实际项目中是存在的,比如在之前的商城网站中,获取到用户信息之后才能渲染用户导航模块,而获取用户信息的操作是一个ajax异步请求。当ajax请求成功返回之后会发布一个事件,在此之前订阅了此事件的用户导航模块可以接收到这些用户信息。但是这只是理想的状况,因为异步的原因,我们不能保证ajax请求返回的时间,有时候它返回得比较快,而此时用户导航模块的代码还没有加载好(还没有订阅相应事件),特别是在用了一些模块化惰性加载的技术后,这是很可能发生的事情。

也许我们还需要一个方案,使得我们的发布—订阅对象拥有先发布后订阅的能力。为了满足这个需求,我们要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次,就像QQ的未读消息只会被重新阅读一次,所以刚才的操作我们只能进行一次。

事件命名冲突处理

可以增加命名空间。

策略模式

策略模式(Strategy Pattern)指的是定义一系列的算法,把它们一个个封装起来,目的就是将算法的使用与算法的实现分离开来

一个基于策略模式的程序至少由两部分组成:

  • 策略类,策略类封装了具体的算法,并负责具体的计算过程
  • 环境类Context,Context 接受客户的请求,随后 把请求委托给某一个策略类

策略模式的应用

举个例子,公司的年终奖是根据员工的工资和绩效来考核的,绩效为A的人,年终奖为工资的4倍,绩效为B的人,年终奖为工资的3倍,绩效为C的人,年终奖为工资的2倍

若使用if来实现,代码则如下:

var calculateBouns = function(salary,level) {
    if(level === 'A') {
        return salary * 4;
    }
    if(level === 'B') {
        return salary * 3;
    }
    if(level === 'C') {
        return salary * 2;
    }
};
// 调用如下:
console.log(calculateBouns(4000,'A')); // 16000
console.log(calculateBouns(2500,'B')); // 7500
1
2
3
4
5
6
7
8
9
10
11
12
13
14

从上述可有看到,函数内部包含过多if...else,并且后续改正的时候,需要在函数内部添加逻辑,违反了开放封闭原则

而如果使用策略模式,就是先定义一系列算法,把它们一个个封装起来,将不变的部分和变化的部分隔开,如下:

var obj = {
        "A": function(salary) {
            return salary * 4;
        },
        "B" : function(salary) {
            return salary * 3;
        },
        "C" : function(salary) {
            return salary * 2;
        }
};
var calculateBouns =function(level,salary) {
    return obj[level](salary);
};
console.log(calculateBouns('A',10000)); // 40000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上述代码中,obj对应的是策略类,而calculateBouns对应上下通信类

又比如实现一个表单校验的代码,常常会像如下写法:

var registerForm = document.getElementById("registerForm");
registerForm.onsubmit = function(){
    if(registerForm.userName.value === '') {
        alert('用户名不能为空');
        return;
    }
    if(registerForm.password.value.length < 6) {
        alert("密码的长度不能小于6位");
        return;
    }
    if(!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)) {
        alert("手机号码格式不正确");
        return;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上述代码包含多处if语句,并且违反了开放封闭原则,如果应用中还有其他的表单,需要重复编写代码。

此处也可以使用策略模式进行重构校验,第一步确定不变的内容,即策略规则对象,如下:

var strategy = {
    isNotEmpty: function(value,errorMsg) {
        if(value === '') {
            return errorMsg;
        }
    },
    // 限制最小长度
    minLength: function(value,length,errorMsg) {
        if(value.length < length) {
            return errorMsg;
        }
    },
    // 手机号码格式
    mobileFormat: function(value,errorMsg) {
        if(!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
            return errorMsg;
        }
    } 
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

然后找出变的地方,作为环境类context,负责接收用户的要求并委托给策略规则对象,如下Validator类:

var Validator = function(){
        this.cache = [];  // 保存效验规则
};
Validator.prototype.add = function(dom,rule,errorMsg) {
    var str = rule.split(":");
    this.cache.push(function(){
        // str 返回的是 minLength:6 
        var strategy = str.shift();
        str.unshift(dom.value); // 把input的value添加进参数列表
        str.push(errorMsg);  // 把errorMsg添加进参数列表
        return strategys[strategy].apply(dom,str);
    });
};
Validator.prototype.start = function(){
    for(var i = 0, validatorFunc; validatorFunc = this.cache[i++]; ) {
        var msg = validatorFunc(); // 开始效验 并取得效验后的返回信息
        if(msg) {
            return msg;
        }
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

通过validator.add方法添加校验规则和错误信息提示,使用如下:

var validateFunc = function(){
    var validator = new Validator(); // 创建一个Validator对象
    /* 添加一些效验规则 */
    validator.add(registerForm.userName,'isNotEmpty','用户名不能为空');
    validator.add(registerForm.password,'minLength:6','密码长度不能小于6位');
    validator.add(registerForm.userName,'mobileFormat','手机号码格式不正确');

    var errorMsg = validator.start(); // 获得效验结果
    return errorMsg; // 返回效验结果
};
var registerForm = document.getElementById("registerForm");
registerForm.onsubmit = function(){
    var errorMsg = validateFunc();
    if(errorMsg){
        alert(errorMsg);
        return false;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上述通过策略模式完成表单的验证,并且可以随时调用,在修改表单验证规则的时候,也非常方便,通过传递参数即可调用。

应用场景

从上面可以看到

使用策略模式的优点有如下:

  • 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句
  • 策略模式提供了开放-封闭原则,使代码更容易理解和扩展
  • 策略模式中的代码可以复用

策略模式不仅仅用来封装算法,在实际开发中,通常会把算法的含义扩散开来,使策略模式也可以用来封装 一系列的“业务规则”

只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它们。

迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。

内部迭代器

内部迭代器在内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。

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 ] );
});
1
2
3
4
5
6
7
8
9
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不相等’ );
1
2
3
4
5
6
7
8
9
10
11
12
13

我们目前能够顺利完成需求,还要感谢在JavaScript里可以把函数当作参数传递的特性,但在其他语言中未必就能如此幸运。

在一些没有闭包的语言中,内部迭代器本身的实现也相当复杂。比如C语言中的内部迭代器是用函数指针来实现的,循环处理所需要的数据都要以参数的形式明确地从外面传递进去。

外部迭代器

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

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
        length: obj.length
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var compare = function( iterator1, iterator2 ){
    if(iterator1.length ! == iterator2.length){
        alert('iterator1和iterator2不相等’);
    }
    while( ! iterator1.isDone() && ! iterator2.isDone() ){
    	if ( iterator1.getCurrItem() ! == iterator2.getCurrItem() ){
        	throw new Error ( 'iterator1和iterator2不相等’ );
        }
        iterator1.next();
        iterator2.next();
	}

    alert ( 'iterator1和iterator2相等’ );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

迭代类数组对象和字面量对象

可迭代

迭代器模式不仅可以迭代数组,还可以迭代一些类数组的对象。

比如 arguments{"0":'a', "1":'b'} 等。

通过上面的代码可以观察到,无论是内部迭代器还是外部迭代器,只要被迭代的聚合对象拥有 length 属性而且可以用下标访问,那它就可以被迭代。

JavaScript 中,for in 语句可以用来迭代普通字面量对象的属性。jQuery中提供了$.each`函数来封装各种迭代行为:

$.each = function( obj, callback ) {
    var value,
        i = 0,
        length = obj.length,
        isArray = isArraylike( obj );

    if ( isArray ) {    // 迭代类数组
        for ( ; i < length; i++ ) {
            value = callback.call( obj[ i ], i, obj[ i ] );
            if ( value === false ) {
                break;
            }
        }
    } else {
        for ( i in obj ) {    // 迭代object对象
            value = callback.call( obj[ i ], i, obj[ i ] );
            if ( value === false ) {
                break;
            }
        }
    }
    return obj;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

倒序迭代器

var reverseEach = function( ary, callback ){
    for ( var l = ary.length -1; l >= 0; l-- ){
        callback( l, ary[ l ] );
    }
};

reverseEach( [ 0, 1, 2 ], function( i, n ){
    console.log( n );  // 分别输出:2, 1 ,0
});
1
2
3
4
5
6
7
8
9

中止迭代器

var each = function( ary, callback ){
    for ( var i = 0, l = ary.length; i < l; i++ ){
        // callback的执行结果返回false,提前终止迭代
        if ( callback( i, ary[ i ] ) === false ){
            break;
        }
    }
};

each( [ 1, 2, 3, 4, 5 ], function( i, n ){
    if ( n > 3 ){         // n大于3的时候终止循环
        return false;
    }
    console.log( n );    // 分别输出:1, 2, 3
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

代理模式

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

代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签。

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。

保护代理与虚拟代理

代理模式包括许多小分类,在 JavaScript 开发中最常用的是虚拟代理缓存代理

虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。

  • 虚拟代理:虚拟代理是把一些开销很大的对象,延迟到真正需要它的时候才去创建执行
  • 安全代理:控制真实对象的访问权限
  • 远程代理(一个对象将不同空间的对象进行局部代理)
  • 智能代理(调用对象代理处理另外一些事情如垃圾回收机制增加额外的服务)

虚拟代理实现图片预加载

在Web开发中,图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src 属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。

常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种场景就很适合使用虚拟代理。

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' );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

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

代理的意义

不用代理的预加载图片函数实现如下:

var MyImage = (function(){
    var imgNode = document.createElement( 'img' );
    document.body.appendChild( imgNode );
    var img = new Image;

    img.onload = function(){
        imgNode.src = img.src;
    };

    return {
        setSrc: function( src ){
            imgNode.src = 'file:// /C:/Users/svenzeng/Desktop/loading.gif';
            img.src = src;
        }
    }
})();

MyImage.setSrc( 'http://imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

注意

  • 职责被定义为“引起变化的原因”。上段代码中的 MyImage 对象除了负责给 img 节点设置 src 外,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
  • 另外,在面向对象的程序设计中,大多数情况下,若违反其他任何原则,同时将违反开放—封闭原则。如果我们只是从网络上获取一些体积很小的图片,或者5年后的网速快到根本不再需要预加载,我们可能希望把预加载图片的这段代码从 MyImage 对象里删掉。这时候就不得不改动 MyImage 对象了。

实际上,我们需要的只是给 img 节点设置 src,预加载图片只是一个锦上添花的功能。如果能把这个操作放在另一个对象里面,自然是一个非常好的方法。于是代理的作用在这里就体现出来了,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体MyImage。

纵观整个程序,我们并没有改变或者增加 MyImage 的接口,但是通过代理对象,实际上给系统添加了新的行为。这是符合开放—封闭原则的。给 img 节点设置 src 和图片预加载这两个功能,被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天我们不再需要预加载,那么只需要改成请求本体而不是请求代理对象即可。

代理和本体接口的一致性

代理接口一致的好处

❏ 用户可以放心地请求代理,他只关心是否能得到想要的结果。

❏ 在任何使用本体的地方都可以替换成使用代理。

如果代理对象和本体对象都为一个函数(函数也是对象),函数必然都能被执行,则可以认为它们也具有一致的“接口”。

var myImage = (function(){
    var imgNode = document.createElement( 'img' );
    document.body.appendChild( imgNode );

    return function( src ){
        imgNode.src = src;
    }
})();

var proxyImage = (function(){
    var img = new Image;

    img.onload = function(){
        myImage( this.src );
    }

    return function( src ){
        myImage( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
        img.src = src;
    }
})();

proxyImage( 'http://imgcache.qq.com/music// N/k/000GGDys0yA0Nk.jpg' );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

虚拟代理合并 HTTP 请求

代理函数 proxySynchronousFile 收集一段时间之内的请求,最后一次性发送给服务器。

比如我们等待2秒之后才把这2秒之内需要同步的文件ID打包发给服务器,如果不是对实时性要求非常高的系统,2秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。代码如下:

var synchronousFile = function( id ){
    console.log( ’开始同步文件,id为: ' + id );
                };

var proxySynchronousFile = (function(){
    var cache = [],    // 保存一段时间内需要同步的ID
        timer;    // 定时器

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

        timer = setTimeout(function(){
            synchronousFile( cache.join( ', ' ) );    // 2秒后向本体发送需要同步的ID集合
            clearTimeout( timer );    // 清空定时器
            timer = null;
            cache.length = 0; // 清空ID集合
        }, 2000 );
    }
})();

var checkbox = document.getElementsByTagName( 'input' );

for ( var i = 0, c; c = checkbox[ i++ ]; ){
    c.onclick = function(){
        if ( this.checked === true ){
            proxySynchronousFile( this.id );
        }
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

缓存代理

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

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
1
2
3
4
5
6
7
8
9
10
11
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
1
2
3
4
5
6
7
8
9
10
11
12
13

高阶函数动态创建代理

代理函数作为函数返回值返回

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

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

/**************** 创建缓存代理的工厂 *****************/
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 proxyMult = createProxyFactory( mult ),
    proxyPlus = createProxyFactory( plus );

alert ( proxyMult( 1, 2, 3, 4 ) );    // 输出:24
alert ( proxyMult( 1, 2, 3, 4 ) );    // 输出:24
alert ( proxyPlus( 1, 2, 3, 4 ) );    // 输出:10
alert ( proxyPlus( 1, 2, 3, 4 ) );    // 输出:10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

工厂模式

参考:

简单工厂模式

工厂方法模式

抽象工厂模式

上次更新: 2022/7/21 10:02:24
贡献者: Jinrui Chen, Jerry Chen