前端常用设计模式 - 详解

192 阅读10分钟

一 创建型模式

1 原型模式

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。

function Person () {
  Person.prototype.name = "marry";
  Person.prototype.sayName = function(){
      console.log(this.name);
  }
}

const person1 = new Person();
const person2 = new Person();
person1.sayName();                                        // marry
person2.sayName();                                        // marry
console.log(person1.sayName === person2.sayName);         // true

用同一个原型new出来的实例,拥有相同的原型上的属性和方法。 

【拓展】用构造函数创建函数时不可以使用箭头函数。

2 单例模式

单例模式(Singleton Pattern)涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

该模式的特点:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。
// 单例模式
let box;
const createBox = (_a, _b) => {
    if(!box){
        box = {};
    }
    box.a = _a;
    box.b = _b;
    return box;
};
createBox(3, 6)
console.log(box); //{ a: 3, b: 6 }
createBox(13, 16)
console.log(box); //{ a: 13, b: 16 }
console.log(createBox(3, 6) === createBox(10, 20));           // true,只有一个box

3 工厂模式

工厂模式(Factory Pattern)定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类。该模式在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

function createPerson(name, age) {
  var obj = {};
  obj.name = name;
  obj.age = age;
  obj.writeJs = function () {
      console.log(this.name + 'write js');
  }
  return obj;
}

var p1 = createPerson('mengzhe' , 26);
p1.writeJs(); //mengzhewrite js

var p2 = createPerson('iceman' , 25);
p2.writeJs(); //icemanwrite js

4、抽象工厂模式

抽象工厂模式(Abstract Factory Pattern)提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。

let agency = function(subType, superType) {
  //判断抽象工厂中是否有该抽象类
  if(typeof agency[superType] === 'function') {
    function F() {};
    //继承父类属性和方法
    F.prototype = new agency[superType] ();
    //将子类的constructor指向子类
    subType.constructor = subType;
    //子类原型继承父类
    subType.prototype = new F();
  } else {
    throw new Error('抽象类不存在!')
  }
}

//鼠标抽象类
agency.mouseShop = function() {
  this.type = '鼠标';
}
agency.mouseShop.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用');
  }
}

//键盘抽象类
agency.KeyboardShop = function() {
  this.type = '键盘';
}
agency.KeyboardShop.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用');
  }
}


//普通鼠标子类
function mouse(name) {
  this.name = name;
  this.item = ['买我,我线长',"玩游戏贼溜"]
}
//抽象工厂实现鼠标类的继承
agency(mouse, 'mouseShop');
//子类中重写抽象方法
mouse.prototype.getName = function() {
  return this.name;
}

//普通键盘子类
function Keyboard(name) {
  this.name = name;
  this.item = ['行,你买它吧',"没键盘看你咋玩"]
}
//抽象工厂实现键盘类的继承
agency(Keyboard, 'KeyboardShop');
//子类中重写抽象方法
Keyboard.prototype.getName = function() {
  return this.name;
}


//实例化鼠标
let mouseA = new mouse('联想');
console.log(mouseA.getName(), mouseA.type); //联想 鼠标
let mouseB = new mouse('戴尔');
console.log(mouseB.getName(), mouseB.type); //戴尔 鼠标

//实例化键盘
let KeyboardA = new Keyboard('联想');
console.log(KeyboardA.getName(), KeyboardA.type); //联想 键盘
let KeyboardB = new Keyboard('戴尔');
console.log(KeyboardB.getName(), KeyboardB.type); //戴尔 键盘

二、结构型模式

1、桥接模式

桥接(Bridge)是用于把抽象化与实例化解耦,使得二者可以独立变化。这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类。这两种类型的类可被结构化改变而互不影响。

Array.prototype.myForEach = function (fn) {
  const arr = this;
  for(let i=0; i<arr.length; i++){
      if(!fn.call(arr, i, arr[i])){
          return false;
      }
  }
}

const arr = [1,2,3,4,5,6];
arr.myForEach((index, item) => {
  console.log([index, item]);
  return true;
});

// [ 0, 1 ]
// [ 1, 2 ]
// [ 2, 3 ]
// [ 3, 4 ]
// [ 4, 5 ]
// [ 5, 6 ]

2、外观模式(了解)

外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。

const getName = () => {
    return "Mary";
}
const getAge = () => {
    return 18;
}
const getUserInfo = () => {
    return `我是${getName()},今年我${getAge()}岁了。`;
}
console.log(getUserInfo());                    // 我是Mary,今年我18岁了。

3、享元模式

享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。

享元模式适用于以下场景:

程序中使用大量的相似对象,造成很大的内存开销 对象的大多数状态都可以变为外部状态,剥离外部状态之后,可以用相对较少的共享对象取代大量对象 享元模式要求将对象的属性分为 “内部状态” 和 “外部状态”,使用时,内外分离,单独封装。享元模式中常使用工厂模式来实现对象属性的内部状态。

享元模式的缺点是:对象数量少的情况下,使用该模式会增大系统的开销,而且实现的复杂度较大。

/**
 * 实现文件上传的经典案例
 */
function Upload (uploadType) {
  this.uploadType = uploadType;
}
 
/* 删除文件(内部状态) */
Upload.prototype.delFile = function (id) {
    uploadManger.setExternalState(id, this);// 把当前id对应的外部状态都组装到共享对象中
    // 大于3000k提示
    if(this.fileSize < 3000) {
        return this.dom.parentNode.removeChild(this.dom);
    }
    if(window.confirm("确定要删除文件吗?" + this.fileName)) {
        return this.dom.parentNode.removeChild(this.dom);
    }
}
 
/** 工厂对象实例化
 *  如果某种内部状态的共享对象已经被创建过,那么直接返回这个对象,否则,创建一个新的对象
 */
const UploadFactory = (() => {
    const createdFlyWeightObjs = {};
    return {
        create: uploadType => {
            if(createdFlyWeightObjs[uploadType]) {
                return createdFlyWeightObjs[uploadType];
            }
            return createdFlyWeightObjs[uploadType] = new Upload(uploadType);
        }
    };
})();
 
/* 管理器封装外部状态 */
const uploadManger = (() => {
    const uploadDatabase = {};
 
    return {
        add: (id, uploadType, fileName, fileSize) => {
            const flyWeightObj = UploadFactory.create(uploadType);
            const dom = document.createElement('div');
            dom.innerHTML = "<span>文件名称:" + fileName + ",文件大小:" + fileSize +"</span>"
                          + "<button class='delFile'>删除</button>";
 
            dom.querySelector(".delFile").onclick = () => {
                flyWeightObj.delFile(id);
            };
            document.body.appendChild(dom);
 
            uploadDatabase[id] = {
                fileName: fileName,
                fileSize: fileSize,
                dom: dom
            };
 
            return flyWeightObj;
        },
        setExternalState: (id, flyWeightObj) => {
            const uploadData = uploadDatabase[id];
            for(let i in uploadData) {
                // 直接改变形参(新思路!!)
                flyWeightObj[i] = uploadData[i];
            }
        }
    };
})();
 
/*触发上传动作*/
let id = 0;
window.startUpload = (uploadType, files) => {
    for(let i=0,file; file = files[i++];) {
        const uploadObj = uploadManger.add(++id, uploadType, file.fileName, file.fileSize);
    }
};

4、适配器模式

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。比如:手机不能插在电脑上,必须有一个转换器,这个转换器就是适配器。

适配器模式也经常用于日常的数据处理上,比如把一个有序的数组转化成我们需要的对象格式:

const arr = ['Vue.js', 'book', '前端框架', '6月10日'];
 
const dataConversion = (arr) => {// 转化成我们需要的数据结构
    return {
        name: arr[0],
        type: arr[1],
        title: arr[2],
        time: arr[3]
    }
}
 
dataConversion(arr);
 
// {name: "Vue.js", type: "book", title: "前端框架", time: "6月10日"}

5、代理模式

在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。为其他对象提供一种代理以控制对这个对象的访问。

(1)、虚拟代理

虚拟代理就是把一些开销很大的对象,延迟到真正需要它的时候才去创建执行。

比如:我们在浏览一些购物商城的时候,会发现,当网络不太好的情况下,有些图片是加载不出来的,会有暂无图片的一张图片去代替它实际的图片,等网路图片加载完成之后,暂无图片就会被实际的图片代替。这就是使用的图片的懒加载。图片的懒加载也可是使用虚拟代理的模式来进行设计:

// 图片懒加载
const myImage = (() {
    const imgNode = document.createElement('img');
    document.body.appendChild( imgNode );
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})();
 
const proxyImage = (() {
    const img = new Image();
    img.onload = () => {
        myImage.setSrc( this.src );
    }
    return {
        setSrc: src => {
            myImage.setSrc('http://seopic.699pic.com/photo/40167/3716.jpg_wh1200.jpg');
            img.src = src;
        }
    }
})();
 
proxyImage.setSrc('http://seopic.699pic.com/photo/40167/7823.jpg_wh1200.jpg');

(2)、缓存代理

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

比如,前后端分离,向后端请求分页的数据的时候,每次页码改变时都需要重新请求后端数据,我们可以将页面和对应的结果进行缓存,当请求同一页的时候,就不再请求后端的接口而是从缓存中去取数据。

const getFib = (number) => {
    if (number <= 2) {
        return 1;
    } else {
        return getFib(number - 1) + getFib(number - 2);
    }
}
 
const getCacheProxy = (fn, cache = new Map()) => {
    return new Proxy(fn, {
        apply(target, context, args) {
        const argsString = args.join(' ');
        if (cache.has(argsString)) {
            // 如果有缓存,直接返回缓存数据 
            console.log(`输出${args}的缓存结果: ${cache.get(argsString)}`);
            return cache.get(argsString);
        }
        const result = fn(...args);
        cache.set(argsString, result);
        return result;
        }
    })
}
const getFibProxy = getCacheProxy(getFib);
getFibProxy(40); 

(3)、用 ES6 的 Proxy 构造函数实现代理

ES6 所提供 Proxy 构造函数能够让我们轻松的使用代理模式:

const proxy = new Proxy(target, handler);

Proxy 构造函数传入两个参数:要代理的对象 和 用来定制代理行为的对象。(如果想知道 Proxy 的具体使用方法,可参考阮一峰的《 ECMAScript入门 - Proxy 》)

三、行为型模式

1、观察者模式(发布/订阅模式)

观察者的具体实现。得到通知后将完成一些具体的业务逻辑处理。

比如:我们平时接触的 DOM 事件就利用了观察者模式:

div.onclick = function(){
    alert ( ”click' );
}

上述代码中,只要 div 订阅了的 click 事件,当点击 div 的时候,就会触发 click 事件。

自己写一个观察者模式:

function Journal(){
    const fnList = [];
    return {
        //订阅
        subscribe: (fn) => {
            const index = fnList.indexOf(fn);
            if(index!=-1) return fnList;
            fnList.push(fn);
            return fnList;
        },
        //退订
        unsubscribe: (fn) => {
            const index = fnList.indexOf(fn);
            if(index==-1) return fnList;
            fnList.splice(index, 1);
            return fnList;
        },
        //发布
        notify: () => {
            fnList.forEach(item => {
                item.update();
            });
        }
    }
}
 
const o = new Journal();
 
// 创建订阅者
function Observer(person, data) {
    return {
        update: () => {
            console.log(`${person}${data}`);
        }
    }
}
 
const f1 = new Observer("张三", "今天天气不错");
const f2 = new Observer("李四", "我吃了三个汉堡");
const f3 = new Observer("王二", "你长得可真好看");
 
// f1,f2,f3订阅了
o.subscribe(f1);
o.subscribe(f2);
o.subscribe(f3);
 
//f1取消了订阅
o.unsubscribe(f1);
 
//发布
o.notify();
 
// 李四:我吃了三个汉堡
// 王二:你长得可真好看

2、迭代器模式

const myForEach = (arr, callback) => {
    for ( let i = 0, l = arr.length; i < l; i++ ){
        callback.call(arr, i, arr[i]); 
        // 把下标和元素当作参数传给callback 函数
    }
};
 
const arr = ["a", "b", "c"];
myForEach(arr, (i, n) => {
    console.log( '自定义下标为: '+ i,'自定义值为:' + n );
});
 
// 自定义下标为: 0 自定义值为:a
// 自定义下标为: 1 自定义值为:b
// 自定义下标为: 2 自定义值为:c

3、策略模式

策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

优点:策略模式的使用,避免过多的 if...else... 判断,也可以替代简单逻辑的 switch。

// 策略模式
const formatDemandItemType2 = (value) => {
  const obj = {
      1: '基础',
      2: '高级',
      3: 'VIP',
  }
  
  return obj[value]
}

console.log(formatDemandItemType2(1));
console.log(formatDemandItemType2(2));
console.log(formatDemandItemType2(3));

// 基础
// 高级
// VIP

4、模板方法模式

一种只需使用继承就可以实现的模式。

模板方法模式由两部分组成:抽象父类 和 实现子类。

// 抽象父类——饮料(Beverage)
const Beverage = function(){};
Beverage.prototype.boilWater = function(){
  console.log('把水煮沸');
};
Beverage.prototype.pourInCup = function(){
  throw new Error( '子类必须重写pourInCup' );
};
Beverage.prototype.addCondiments = function(){
  throw new Error( '子类必须重写addCondiments方法' );
};
Beverage.prototype.customerWantsConditions = function(){
  return true; // 默认需要调料
};
Beverage.prototype.init = function(){
  this.boilWater();
  this.pourInCup();
  if(this.customerWantsCondiments()){
    this.addCondiments();
  }
};
 
// 实现子类——咖啡(Coffee)
const Coffee = function(){};
Coffee.prototype = new Beverage();
Coffee.prototype.pourInCup = function(){
  console.log('把咖啡倒进杯子');
};
Coffee.prototype.addCondiments = function(){
  console.log('加糖和牛奶');
};
Coffee.prototype.customerWantsCondiments = function(){
 return window.confirm( '请问需要调料吗?' );
};
 
// 来一杯咖啡
const aCupOfCoffee = new Coffee();
aCupOfCoffee.init();
 
// 把水煮沸
// 把咖啡倒进杯子
// 加糖和牛奶

也可以这样实现:

const Beverage = function( param ){
  const boilWater = function(){
   console.log( '把水煮沸' );
  };
  const brew = param.brew || function(){
   throw new Error( '必须传递brew方法' );
  };
  const pourInCup = param.pourInCup || function(){
    throw new Error( '必须传递pourInCup方法' );
  };
  const addCondiments = param.addCondiments || function(){
   throw new Error( '必须传递addCondiments方法' );
  };
 
  const F = function(){};
  F.prototype.init = function(){
   boilWater();
   brew();
   pourInCup();
   addCondiments();
  };
  return F;
};
 
const Coffee = Beverage({
  brew: function(){
     console.log( '用沸水冲泡咖啡' );
  },
  pourInCup: function(){
    console.log('把咖啡倒进杯子');
  },
  addCondiments: function(){
    console.log('加糖和牛奶');
  }
});
 
const coffee = new Coffee();
coffee.init();
 
// 把水煮沸
// 用沸水冲泡咖啡
// 把咖啡倒进杯子
// 加糖和牛奶

5、状态模式

状态模式(State)允许一个对象在其内部状态改变的时候改变它的行为。

const Game = (function() {    
  let _currentState = [];
  const states = {
    jump() {console.log('跳跃!')},
    move() {console.log('移动!')},
    shoot() {console.log('射击!')},
    squat() {console.log('蹲下!')}
  }
  
  const action = {
    changeState(arr) {  // 更改当前动作
      _currentState = arr
      return this
    },
    go() {
      _currentState.forEach(item => states[item] && states[item]())
      return this
    }
  }
  
  return {
    change: action.changeState,
    go: action.go
  }
})()
 
Game.change(['jump', 'shoot'])
    .go()                    // 跳跃!  射击!
    .go()                    // 跳跃!  射击!
    .change(['squat'])
    .go()                    // 蹲下!