欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

掌握JavaScript设计模式:理论与实践(14种常见模式详解)

最编程 2024-08-06 13:40:33
...

写在前面

设计模式是不分语言的,本文介绍的是14种设计模式,几乎涵盖了js中涉及到所有设计模式,部分设计模式代码实践部分也会分别,用面向对象思维 和 js"鸭子类型"思维 两种代码对比同一种设计模式。内容较长,读完定有收获!

js中的this

跟别的语言大相径庭的是,JavaScript的this总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。

  • this 的指向,具体到实际应用中,this的指向大致可以分为以下4种。
    ❏ 作为对象的方法调用。
    ❏ 作为普通函数调用。
    ❏ 构造器调用。
    ❏ Function.prototype.call或Function.prototype.apply调用。下面我们分别进行介绍。
  • 1.作为对象方法调用,this 指向该对象
var obj = {
    a:1,
    getA:function(){
        console.log(this==obj);//输入:true
        console.log(this.a);//输出:1
    }
}
obj.getA();
  • 2.作为普通函数调用,指向全局对象。在浏览器中,这个全局对象就是window对象
window.name = 'windowNmae';
var getName = function(){
    return this.name;
}
console.log(getName());//输出: windowNmae
  • 3.构造器调用,构造器里的this就指向返回的这个对象
var MyClass = function(){
    this.name = 'MyClass'
};
var obj = new MyClass();
console.log(obj.name);//输出: MyClass
  1. Function.prototype.call或Function.prototype.apply调用,可以动态地改变传入函数的this
var obj1 = {
    name:'sven',
    getName:function(){
        return this.name;
    }
};
var obj2 = {
    name:'anne'
};
console.log(obj1.getName());//输出: sven
console.log(obj1.getName.call(obj2));//输出: anne

设计模式介绍

1.单利模式(惰性单利)

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

Singleton.getSingle = (function(){
    var instance = null;
    return function(name){
        if(!instance){
            instance = new Singleton(name);
        }
        return instance;
    }
})();
var singleton1 = Singleton.getSingle('app1');
var singleton2 = Singleton.getSingle('app2');
console.log(singleton1==singleton2);//输出:true
console.log(singleton1.name,singleton2.name);//输出:app1 app1

2.策略模式

很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为S的人年终奖有4倍工资,绩效为A的人年终奖有3倍工资,而绩效为B的人年终奖是2倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖

  • 面向对象语言思想实现
// 策略类
var performanceS = function(){}
performanceS.prototype.calculate = function(salary){
    return salary * 4;
}
var performanceA = function(){}
performanceA.prototype.calculate = function(salary){
    return salary * 3;
}
var performanceB = function(){}
performanceB.prototype.calculate = function(salary){
    return salary * 2;
}

// 奖金类
var Bonus = function(){
    this.salary = null;//原始工资
    this.strategy = null;//绩效对应的策略对象
}
//设置原始工资
Bonus.prototype.setSalary = function(salary){
    this.salary = salary;
}
//设置绩效等级对象的策略对象
Bonus.prototype.setStrategy = function(strategy){
    this.strategy = strategy;
}
//取得奖励
Bonus.prototype.getBonus = function(){
    //把计算奖励的操作委托给策略对象
    return this.strategy.calculate(this.salary)
}

//test 
var bonus = new Bonus();
bonus.setSalary(10000);
bonus.setStrategy(new performanceS())
console.log(bonus.getBonus());//输出:40000

bonus.setStrategy(new performanceA())
console.log(bonus.getBonus());//输出:30000
  • js 版本策略模式
var strategies = {
    'S':function(salary){
        return salary * 4;
    },
    'A':function(salary){
        return salary * 3;
    },
    'B':function(salary){
        return salary * 2;
    }
}
var calculateBonus = function(level,salary){
    return strategies[level](salary);
} 
console.log(calculateBonus('S',10000));//输出:40000
console.log(calculateBonus('A',10000));//输出:30000

3.代理模式

代理就是委托别人do,这里我们讨论常用的两种代理

  • 3.1虚拟代理

在Web开发中,图片预加载是一种常用的技术,如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img节点里,这种场景就很适合使用虚拟代理。

下面我们来实现这个虚拟代理,首先创建一个普通的本体对象,这个对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口,外界调用这个接口,便可以给该img标签设置src属性:

var myImage = (function(){
    var imgNode = document.createElement('img');
    document.body.append(imgNode);
    return {
        setSrc:function(src){
            imgNode.src = src;
        }
    }
})();
myImage.setSrc('https://himg.bdimg.com/sys/portrait/item/ca253731393330373830351216');

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

var myImage = (function(){
    var imgNode = document.createElement('img');
    document.body.append(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://loading.gif');
            img.src = src
        }
    }
})()
proxyImage.setSrc('https://himg.bdimg.com/sys/portrait/item/ca253731393330373830351216')
  • 3.2 缓存代理-计算乘积
    缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果
var mult = function(){
    var a = 1;
    for(var i=0;i<arguments.length;i++){
        a *= arguments[i];
    }
    return a;
}
console.log(mult(2,3));//输出:6
console.log(mult(2,3,4));//输出:24

现在加入缓存代理函数:

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

通过增加缓存代理的方式,mult函数可以继续专注于自身的职责——计算乘积,缓存的功能是由代理对象实现的。

4.迭代器模式

目前的绝大部分语言都内置了迭代器,这里简单介绍下倒叙迭代

var reverseEach = function(ary,callback){
    for(var i=ary.length-1;i>=0;i--){
        callback(i,ary[i])
    }
}
reverseEach([0,1,2],function(i,n){
    console.log(`i = ${i}  n = ${n}`);
    /* 输出
    i = 2  n = 2
    i = 1  n = 1
    i = 0  n = 0
    */
})

5.发布-订阅模式

实际开发经常用到的,可以直接拿来用到项目中

// 全局的发布订阅对象
var EventBus = (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.apply(arguments);
        var fns = clientList[key];
        if(!fns || fns.length==0){
            return false;
        }
        for(var i=0;i<fns.length;i++){
            fns[i].apply(this,arguments)
        }
    };

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

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

})()

var callback = function(price){
    console.log(`价格 = ${price}`)
}

EventBus.listen('sq88',callback);
EventBus.trigger('sq88',20000);
// EventBus.remove('sq88',callback);
EventBus.trigger('sq88',20000);

6. 命令模式

命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

var closeDoorCommmand = {
    execute: function(){
        console.log('关门');
    }
}

var openPcCommand = {
    execute: function(){
        console.log('开电脑');
    }
}

var openQQCommand = {
    execute: function(){
        console.log('登录QQ');
    }
}

var MacroCommand = function(){
    return {
        commandsList:[],
        add:function(command){
            this.commandsList.add(command);
        },

        execute:function(){
            for(var i=0;i<this.commandsList.length;i++){
                var command = this.commandsList[i];
                command.execute();
            }
        }
    }
}

var macroCommand = MacroCommand();
macroCommand.add(closeDoorCommmand)
macroCommand.add(openPcCommand)
macroCommand.add(openQQCommand)
macroCommand.execute();

7.组合模式

在程序设计中,也有一些和“事物是由相似的子事物构成”类似的思想。组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的。

image.png

8.模板方法模式

在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。这也很好地体现了泛化的思想。

var Beverage = function(){}
Beverage.prototype.boilWater = function(){
    console.log('把是煮沸');
}
Beverage.prototype.brew = function(){
    throw new Error('子类必须重写brew方法');
}
Beverage.prototype.pourInCup = function(){
    throw new Error('子类必须重写pourInCup方法');
}
Beverage.prototype.customeerWantsCondiments = function(){
    return true;//默认需要调料
}
Beverage.prototype.init = function(){
    this.boilWater();
    this.brew();
    this.pourInCup();
    if(this.customeerWantsCondiments()){
        this.addComponent();
    }
}

var CoffeeWithHook = function(){};
CoffeeWithHook.prototype = new Beverage();
CoffeeWithHook.prototype.brew = function(){
    console.log('用沸水冲泡咖啡');
}
CoffeeWithHook.prototype.pourInCup = function(){
    console.log('把咖啡倒进杯子');
}
CoffeeWithHook.prototype.customeerWantsCondiments = function(){
    return window.confirm('请问需要调料吗?');
}
var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();

9.享元模式

元模式的核心是运用共享技术来有效支持大量细粒度的对象。
享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引。❏ 内部状态存储于对象内部。❏ 内部状态可以被一些对象共享。❏ 内部状态独立于具体的场景,通常不会改变。❏ 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。

假设有个内衣工厂,目前的产品有50种男式内衣和50种女士内衣,为了推销产品,工厂决定生产一些塑料模特来穿上他们的内衣拍成广告照片。正常情况下需要50个男模特和50个女模特,然后让他们每人分别穿上一件内衣来拍照。不使用享元模式的情况下,在程序里也许会这样写:

var Model = function(sex,underwear){
    this.sex = sex;
    this.underwear = underwear;
}
Model.prototype.takePhoto = function(){
    console.log(`sex = ${this.sex} underwear = ${this.underwear}`);
}
for(var i=0;i<=50;i++){
    var maleModel = new Model('male','underwear' + i);
    maleModel.takePhoto();    
}
for(var i=0;i<=50;i++){
    var femaleModel = new Model('female','underwear' + i);
    femaleModel.takePhoto();    
}

要得到一张照片,每次都需要传入sex和underwear参数,如上所述,现在一共有50种男内衣和50种女内衣,所以一共会产生100个对象。如果将来生产了10000种内衣,那这个程序可能会因为存在如此多的对象已经提前崩溃。下面我们来考虑一下如何优化这个场景。虽然有100种内衣,但很显然并不需要50个男模特和50个女模特。其实男模特和女模特各自有一个就足够了,他们可以分别穿上不同的内衣来拍照。

var Model = function(sex){
    this.sex = sex;
}
Model.prototype.takePhoto = function(){
    console.log(`sex = ${this.sex} underwear = ${this.underwear}`);

}
var maleModel = new Model('male');
var femaleModel = new Model('female');
for(var i=0;i<50;i++){
    maleModel.underwear = 'underwear' + i;
    maleModel.takePhoto();
}
for(var i=0;i<50;i++){
    femaleModel.underwear = 'underwear' + i;
    femaleModel.takePhoto();
}

10.职责链模式

职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。


image.png
  • 异步的职责链

而在现实开发中,我们经常会遇到一些异步的问题,比如我们要在节点函数中发起一个ajax异步请求,异步请求返回的结果才能决定是否继续在职责链中passRequest。

var Chain = function(fn){
    this.fn = fn;
    this.successor = null;
};
Chain.prototype.setNextSuccessor = function(successor){
    return this.successor = successor;
}
Chain.prototype.passRequest = function(){
    var ret = this.fn.apply(this,arguments);
    if(ret == 'nextSuccessor'){
        return this.successor && this.successor.passRequest.apply(this.successor,arguments);
    }
    return ret;
}


Chain.prototype.next = function(){
    return this.successor && this.successor.passRequest.apply(this.successor,arguments);
}

var fn1 = new Chain(function(){
    console.log(1);
    return 'nextSuccessor'
})
var fn2 = new Chain(function(){
    console.log(2);
    var self = this;
    setTimeout(function(){
        self.next();
    },1000);
})
var fn3 = new Chain(function(){
    console.log(3);
})
fn1.setNextSuccessor(fn2).setNextSuccessor(fn3);
fn1.passRequest();

11.中介者模式

中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系

image.png

12.装饰器模式

这种给对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式

Function.prototype.before = function(beforeFn){
    var that = this;//保存原函数的引用
    return function(){//返回包含了原函数和新函数的“代理”函数
        beforeFn.apply(this,arguments);//执行新函数,且保证this不被劫持,函数接收的参数
        return that.apply(this,arguments);//执行原函数并返回原函数的执行结果,且保证this不被劫持
    }
}

Function.prototype.after = function(afterFn){
    var that = this;
    return function(){
        var ret = that.apply(this,arguments);
        afterFn.apply(this,arguments);
        return ret;
    }
}
function myClick(){
    console.log('myclick');
}

myClick();
myClick.before(function(){
    console.log('前')
}).after(()=>console.log('后'))();

13. 状态模式

状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。

点灯程序 (弱光 --> 强光 --> 关灯)循环

// 关灯
var OffLightState = function(light) {
  this.light = light;
};
// 弱光
var WeakLightState = function(light) {
  this.light = light;
};
// 强光
var StrongLightState = function(light) {
  this.light = light;
};

var Light = function(){
  /* 开关状态 */
  this.offLight = new OffLightState(this);
  this.weakLight = new WeakLightState(this);
  this.strongLight = new StrongLightState(this);
  /* 快关按钮 */
  this.button = null;
};
Light.prototype.init = function() {
  var button = document.createElement("button"),
    self = this;
  this.button = document.body.appendChild(button);
  this.button.innerHTML = '开关';
  this.currentState = this.offLight;
  this.button.click = function() {
    self.currentState.buttonWasPressed();
  }
};
// 让抽象父类的抽象方法直接抛出一个异常(避免状态子类未实现buttonWasPressed方法)
Light.prototype.buttonWasPressed = function() {
  throw new Error("父类的buttonWasPressed方法必须被重写");
};
Light.prototype.setState = function(newState) {
  this.currentState = newState;
};

/* 关灯 */
OffLightState.prototype = new Light();  // 继承抽象类
OffLightState.prototype.buttonWasPressed = function() {
  console.log("关灯!");
  this.light.setState(this.light.weakLight);
}
/* 弱光 */
WeakLightState.prototype = new Light();
WeakLightState.prototype.buttonWasPressed = function() {
  console.log("弱光!");
  this.light.setState(this.light.strongLight);
};
/* 强光 */
StrongLightState.prototype = new Light();
StrongLightState.prototype.buttonWasPressed = function() {
  console.log("强光!");
  this.light.setState(this.light.offLight);
};

14. 适配器模式

var renderMap = function(map){
    if(map.show instanceof Function){
        map.show();
    }
}
var googleMap = {
    show:function(){
        console.log('开始渲染谷歌地图');
    }
}
var baiduMap = {
    display:function(){
        console.log('开始渲染百度地图')
    }
}

var baiduMapAdapter = {
    show:function(){
        return baiduMap.display();
    }
}
renderMap(googleMap);
renderMap(baiduMapAdapter);

推荐阅读