JavaScript常见的设计模式

2021年08月10日

JavaScript常见的设计模式

通常在我们解决问题的时候,很多时候不是只有一种方式,我们通常有多种方式来解决,但是肯定会有一种通用且高效的解决方案,这种解决方案在软件开发中我们称它为设计模式。

设计模式并不是一种固定的公式,而是一种思想,是一种解决问题的思路,恰当的使用设计模式,可以实现代码的复用和提高可维护性。

假设有一个空房间,我们要日复一日地往里 面放一些东西。最简单的办法当然是把这些东西 直接扔进去,但是时间久了,就会发现很难从这 个房子里找到自己想要的东西,要调整某几样东 西的位置也不容易。所以在房间里做一些柜子也 许是个更好的选择,虽然柜子会增加我们的成 本,但它可以在维护阶段为我们带来好处。使用 这些柜子存放东西的规则,或许就是一种模式

根据设计模式的参考书 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 中所提到的,总共有 23 种设计模式。这些模式可以分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns)、行为型模式(Behavioral Patterns)。

序号模式 & 描述包括
1创建型模式(Creational patterns)
这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
  • 工厂模式(Factory Pattern)
  • 抽象工厂模式(Abstract Factory Pattern)
  • 单例模式(Singleton Pattern)
  • 建造者模式(Builder Pattern)
  • 原型模式(Prototype Pattern)
2结构型模式(Structural patterns)
这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
  • 适配器模式(Adapter Pattern)
  • 桥接模式(Bridge Pattern)
  • 过滤器模式(Filter、Criteria Pattern)
  • 组合模式(Composite Pattern)
  • 装饰器模式(Decorator Pattern)
  • 外观模式(Facade Pattern)
  • 享元模式(Flyweight Pattern)
  • 代理模式(Proxy Pattern)
3行为型模式(Behavioral patterns)
这些设计模式特别关注对象之间的通信。
  • 责任链模式(Chain of Responsibility Pattern)
  • 命令模式(Command Pattern)
  • 解释器模式(Interpreter Pattern)
  • 迭代器模式(Iterator Pattern)
  • 中介者模式(Mediator Pattern)
  • 备忘录模式(Memento Pattern)
  • 观察者模式(Observer Pattern)
  • 状态模式(State Pattern)
  • 空对象模式(Null Object Pattern)
  • 策略模式(Strategy Pattern)
  • 模板模式(Template Pattern)
  • 访问者模式(Visitor Pattern)

工厂模式

所谓工厂模式就是像工厂一样重复的产生类似的产品,工厂模式只需要我们传入正确的参数,就能生产类似的产品

简单工厂模式

简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

// 简单工厂模式
function Car(color, size) {
  this.color = color
  this.size = size
  console.log(`this is ${this.color} ${this.size} car`)
}

function Bus(color, size) {
  this.color = color
  this.size = size
  console.log(`this is color:${this.color}  size:${this.size} car`)
}

function factory1(type, color, size) {
  switch (type) {
    case 'car':
      return new Car(color, size)
    case 'bus':
      return new Bus(color, size)
    default:
      throw new Error('无此类型工厂')
  }
}

工厂方法模式

工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

function factory2(type, color, size) {
  if (this instanceof factory2) {
    return new this[type](color, size)
  } else {
    return new factory2(type, color, size)
  }
}

factory2.prototype = {
  Car: function(color, size) {
    this.color = color
    this.size = size
    console.log(`this is ${this.color} ${this.size} car`)
  },
  Bus: function(color, size) {
    this.color = color
    this.size = size
    console.log(`this is color:${this.color}  size:${this.size} car`)
  }
}

factory2('Car', 'green', 'small')

抽象工厂模式

定义:抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。

function ColorFactory(subType) {
  if (this instanceof ColorFactory) {
    return new this[subType]()
  } else {
    return new ColorFactory(subType)
  }
}

ColorFactory.prototype = {
  red: function() {
    console.log('red')
  },
  green: function() {
    console.log('green')
  },
  blue: function() {
    console.log('blue')
  }
}

function ShapeFactory(subType) {
  if (this instanceof ShapeFactory) {
    return new this[subType]()
  } else {
    return new ShapeFactory(subType)
  }
}

ShapeFactory.prototype = {
  rectangle: function() {
    console.log('rectangle')
  },
  square: function() {
    console.log('square')
  },
  circle: function() {
    console.log('circle')
  }
}

function FactoryProducer(type) {
  switch (type) {
    case 'color':
      return ColorFactory
    case 'shape':
      return ShapeFactory
    default:
      throw new Error('无此类型工厂')
  }
}

const colorFactory = FactoryProducer('color')

let green = colorFactory('green')

单列模式

该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。保证一个类仅有一个实例,并提供一个访问它的全局访问点。

在 JavaScript 中,单例作为一个共享的资源命名空间,它将实现代码与全局命名空间隔离开来,从而为函数提供一个单一的访问点。

示例

const Singleton = (function() {
  let instance = null

  function init() {
    let count = 0

    return {
      setCount: function(num) {
        count = num
      },
      getCount: function() {
        return count
      }
    }
  }

  return {
    getInstance: function() {
      // 获取实例
      if (!instance) {
        instance = init()
      }

      return instance
    }
  }
})()

let singleton1 = Singleton.getInstance()
let singleton2 = Singleton.getInstance()

singleton1.setCount(8)

let count = singleton2.getCount()
console.log('count:', count)

原型模式

定义

1、每当创建一个函数,都会有一个 prototype (原型属性)。 2、原型(prototype) 这个属性的指针 指向一个对象,而这个对象的用途 可以由特定类型的所有实例 共享 属性和方法 ! 3、原型(prototype)是共享所有的属性和方法, 也就是说:如果 new 了 两个实例化 ,他们的方法 做对比 返回 true , 共同使用一个地址 。

为什么要使用原型模式

构造函数模式 一般我们会这样构造一个函数

function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = function() {
    console.log('name is:', this.name)
  }
}

var person1 = new Person('zhangsan', 21, 'teacher')
var person2 = new Person('lisi', 22, 'boss')

console.log(person1.sayName === person2.sayName)

从上面的例子我们看出person1.sayName === person2.sayName的值为false。 构造函数模式解决了创建多个相似对象的问题和对象识别的问题,但是不足的地方是,采用这种模式会创建多个完成同样任务的Function实例。

使用原型模式

function Person() {}

Person.prototype = {
  constructor: Person,
  name: 'zhangsan',
  friends: ['a', 'b'],
  sayName: function() {
    console.log(this.name)
  }
}

var person1 = new Person()
var person2 = new Person()

person1.friends.push('c')

console.log(person2.friends) // [ 'a', 'b', 'c' ]

原型对象的缺点 原型对象的好处是原型中的所有属性和方法可以被很多实例共享,缺点是当原型中包含引用类型的值的属性时,一个实例对象对这个引用类型的属性做了修改,在其他实例对象中也可以体现出来。

function Person() {}

Person.prototype = {
  constructor: Person,
  name: 'zhangsan',
  friends: ['a', 'b'],
  sayName: function() {
    console.log(this.name)
  }
}

var person1 = new Person()
var person2 = new Person()

person1.friends.push('c')

console.log(person2.friends)

从上面的示例我们看出了原型对象的弊端,所以实际使用中组合使用构造函数模式和原型模式

function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.friends = ['a', 'b']
}

Person.prototype = {
  constructor: Person,
  sayName: function() {
    console.log(this.name)
  }
}

var person1 = new Person('zhangsan', 21, 'teacher')
var person2 = new Person('lisi', 22, 'boss')

person1.friends.push('c')
console.log(person2.friends) //[ 'a', 'b' ]

策略模式

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

场景

公司根据个人绩效给员工发放奖金

不使用策略模式实现

var calculateBonus = function( performanceLevel, salary ){
    if ( performanceLevel === 'S' ){
        return salary * 4;
    }
    if ( performanceLevel === 'A' ){
        return salary * 3;
    }
    if ( performanceLevel === 'B' ){
        return salary * 2;
    }
};
calculateBonus( 'B', 20000 ); // 输出:40000
calculateBonus( 'S', 6000 ); // 输出:24000

这个代码很简单,但是也有显儿易见的缺点,比如,有很多的条件分支语句,如果我们要加一个等级必须得去修改函数内部,复用性差

使用策略模式重构代码

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 );
};

这就是策略模式,函数calculateBonus只是一个计算的工具,而每种策略都是在内部完成算法,这样只需要把策略和每种策略对应的工资传入就可以得出年终奖是多少。实际应用中我们也可以使用策略模式实现表单验证器。

代理模式

代理模式:为一个对象提供一个代用品或占位符,以便控制它的访问。 当我们不方便直接访问某个对象时,或不满足需求时,可考虑使用一个替身对象来控制该对象的访问。替身对象可对请求预先进行处理,再决定是否转交给本体对象。
生活中的代购,科学上网等都是一种代理行为。

代理接听电话,实现拦截黑名单

var backPhoneList = ['189XXXXX140'];       // 黑名单列表
// 代理
var ProxyAcceptPhone = function(phone) {
    // 预处理
    console.log('电话正在接入...');
    if (backPhoneList.includes(phone)) {
        // 屏蔽
        console.log('屏蔽黑名单电话');
    } else {
        // 转接
        AcceptPhone.call(this, phone);
    }
}
// 本体
var AcceptPhone = function(phone) {
    console.log('接听电话:', phone);
};

// 外部调用代理
ProxyAcceptPhone('189XXXXX140'); 
ProxyAcceptPhone('189XXXXX141'); 

代理并不会改变本体对象,遵循 “单一职责原则”,不同对象承担独立职责,不过于紧密耦合,具体执行功能还是本体对象,只是引入代理可以选择性地预先处理请求。例如上述代码中,我们向 “接听电话功能” 本体添加了一个屏蔽黑名单的功能(保护代理),预先处理电话接入请求。

图片预加载

// 本体
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);             // 图片加载完设置真实图片src
    }
    return {
        setSrc: function(src) {
            myImage.setSrc('./loading.gif');  // 预先设置图片src为loading图
            img.src = src;
        }
    }
})();

// 外部调用
proxyImage.setSrc('./product.png');           // 有loading图的图片预加载效果

在我们需要在一个对象后多次进行访问控制访问和上下文,代理模式是非常有用处的。 jQuery代理方法的实现如下:

// Bind a function to a context, optionally partially applying any
 // arguments.
 proxy: function( fn, context ) {
   if ( typeof context === "string" ) {
     var tmp = fn[ context ];
     context = fn;
     fn = tmp;
   }

   // Quick check to determine if target is callable, in the spec
   // this throws a TypeError, but we will just return undefined.
   if ( !jQuery.isFunction( fn ) ) {
     return undefined;
   }

   // Simulated bind
   var args = slice.call( arguments, 2 ),
     proxy = function() {
       return fn.apply( context, args.concat( slice.call( arguments ) ) );
     };

   // Set the guid of unique handler to the same of original handler, so it can be removed
   proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;

   return proxy;
 }

设计原则

S(Single responsibility principle)——单一职责原则

一个程序或一个类或一个方法只做好一件事,如果功能过于复杂,我们就拆分开,每个方法保持独立,减少耦合度。

O(Open Closed Principle)——开放封闭原则

对扩展开放,对修改封闭;增加新需求的时候,我们需要做的是增加新代码,而非去修改源码。

L(Liskov Substitution Principle, LSP)——李氏置换原则

子类能覆盖父类,父类能出现的地方子类就能出现。(在JS中没有类概念,使用较少)

I (Interface Segregation Principle)——接口独立原则

保持接口的单一独立,类似于单一原则,不过接口独立原则更注重接口。

D(Dependence Inversion Principle ,DIP)——依赖倒置原则

面向接口编程,依赖于抽象而不依赖于具体,使用方只关注接口而不需要关注具体的实现。

资料

写下你的想法
文章目录
  • JavaScript常见的设计模式
    • 工厂模式
      • 简单工厂模式
        • 工厂方法模式
          • 抽象工厂模式
          • 单列模式
            • 示例
            • 原型模式
              • 定义
                • 为什么要使用原型模式
                • 策略模式
                  • 场景
                  • 代理模式
                    • 设计原则
                      • 资料