JavaScript 中常见设计模式和使用(单例、装饰、策略)

343 阅读7分钟

「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

一、单例模式

特点

  • 确保只有一个实例
  • 可以全局访问

为什么要使用单例模式?

  1. 表示全局唯一
  2. 避免资源访问冲突
  3. 省内存

单例模式的缺点

  1. 可测试性不好
  2. 无法使用面向对象的继承,多态等能力
  3. 构造函数没有参数,降低代码可读性

适用场景

适用于弹框的实现, 全局缓存

实现

// 保证一个实例
const singleton = function(name) {
  this.name = name
  this.instance = null
}
// 相关方法
singleton.prototype.getName = function() {
  console.log(this.name)
}

// 通过
singleton.getInstance = function(name) {
  if (!this.instance) { // 关键语句
    this.instance = new singleton(name)
  }
  return this.instance
}

// 调用
const a = singleton.getInstance('a') // 通过 getInstance 来获取实例
const b = singleton.getInstance('b')
console.log(a === b)

单例模式属于创建型模式,它提供了一种创建对象的方式

弹框层的实践

实现弹框的一种做法是先创建好弹框, 然后使之隐藏, 这样子的话会浪费部分不必要的 DOM 开销, 我们可以在需要弹框的时候再进行创建, 同时结合单例模式实现只有一个实例, 从而节省部分 DOM 开销。下列为登入框部分代码:

const createLoginLayer = function() {
  const div = document.createElement('div')
  div.innerHTML = '登入浮框'
  div.style.display = 'none'
  document.body.appendChild(div)
  return div
}
// 单例模式
const getSingle = function(fn) {
  const result
  return function() {
    return result || result = fn.apply(this, arguments)
  }
}

//使用,调用getSingle 创建实例
const createSingleLoginLayer = getSingle(createLoginLayer)

document.getElementById('loginBtn').onclick = function() {
  createSingleLoginLayer()
}

二、装饰者模式

装饰模式是一种结构型设计模式, 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

适合应用场景

动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。

  1. 如果你希望在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为,可以使用装饰模式。装饰能将业务逻辑组织为层次结构,你可为各层创建一个装饰,在运行时将各种不同逻辑组合成对象。由于这些对象都遵循通用接口,客户端代码能以相同的方式使用这些对象。
  2. 如果用继承来扩展对象行为的方案难以实现或者根本不可行,你可以使用该模式。许多编程语言使用 final最终关键字来限制对某个类的进一步扩展。 复用最终类已有行为的唯一方法是使用装饰模式: 用封装器对其进行封装。

特点

  • 你无需创建新子类即可扩展对象的行为。
  • 你可以在运行时添加或删除对象的功能。
  • 你可以用多个装饰封装对象来组合几种行为。
  • 单一职责原则。 你可以将实现了许多不同行为的一个大类拆分为多个较小的类。

实现方式

  1. 确保业务逻辑可用一个基本组件及多个额外可选层次表示。
  2. 找出基本组件和可选层次的通用方法。 创建一个组件接口并在其中声明这些方法。
  3. 创建一个具体组件类, 并定义其基础行为。
  4. 创建装饰基类, 使用一个成员变量存储指向被封装对象的引用。 该成员变量必须被声明为组件接口类型, 从而能在运行时连接具体组件和装饰。 装饰基类必须将所有工作委派给被封装的对象。
  5. 确保所有类实现组件接口。
  6. 将装饰基类扩展为具体装饰。 具体装饰必须在调用父类方法 (总是委派给被封装对象) 之前或之后执行自身的行为。
  7. 客户端代码负责创建装饰并将其组合成客户端所需的形式。

例子🌰

class ConcreteComponent {
    operation() {
        return 'ConcreteComponent';
    }
}
class Decorator  {
    constructor(component) {
        this.component = component;
    }
    operation() {
        return this.component.operation();
    }
}
// 装饰
class ConcreteDecoratorA {
    constructor(component) {
        this.component = component;
    }
   operation() {
        return `ConcreteDecoratorA: ${this.component.operation()}`;
    }
}
class ConcreteDecoratorB {
    constructor(component) {
        this.component = component;
    }
    operation() {
        return `ConcreteDecoratorB: ${this.component.operation()}`;
    }
}
function clientCode(component) {
    console.log(`RESULT: ${component.operation()}`);
}


const simple = new ConcreteComponent();
console.log(simple.operation()); // ConcreteComponent

const decorator1 = new ConcreteDecoratorA(simple);
const decorator2 = new ConcreteDecoratorB(decorator1);
console.log(decorator2.operation()); // ConcreteDecoratorB: ConcreteDecoratorA: ConcreteComponent

我们可以看到,原本的 simple实例通过ConcreteDecoratorAConcreteDecoratorA两个类的装饰后,返回的值从ConcreteComponent变为了ConcreteDecoratorB: ConcreteDecoratorA: ConcreteComponent

策略模式

策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。

痛点

假如现在有个需求,需要开发一个导游程序,该程序的核心功能是提供美观的地图,以帮助用户在任何城市中快速定位。

开始:用户希望有导航功能,输入地址后就能在地图上看到前往目的地的最快路线。我们程序的第一版只能规划公路路线,满足了驾车用户的需求。

然后:显然不是所有人都会开车,所以我们更新了步行路线的功能和公共交通路线的功能。这只是个开始。 不久后, 你又要为骑行者规划路线。 又过了一段时间, 你又要为游览城市中的所有景点规划路线。

最后:虽然我们都满足了所有需求,但是这时候我们的代码已经臃肿不堪,无论是修复简单缺陷还是微调街道权重, 对某个算法进行任何修改都会影响整个类, 从而增加在已有正常运行代码中引入错误的风险。

解决

策略模式建议找出负责用许多不同方式完成特定任务的类, 然后将其中的算法抽取到一组被称为策略的独立类中。名为上下文的策略类必须包含一个成员变量来存储对于每种策略的引用。上下文并不执行任务,而是将工作委派给已连接的策略对象。

我们每次增加新路线只需要独立开发完后,添加到上下文的策略类中,然后用户在通过策略类调用需要的方法,因此,上下文可独立于具体策略。 这样你就可在不修改上下文代码或其他策略的情况下添加新算法或修改已有算法了。

适合应用场景

  1. 当你想使用对象中各种不同的算法变体,并希望能在运行时切换算法时,可使用策略模式。策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象, 从而以间接方式在运行时更改对象行为。
  2. 当你有许多仅在执行某些行为时略有不同的相似类时,可使用策略模式。策略模式让你能将不同行为抽取到一个独立类层次结构中,并将原始类组合成同一个 从而减少重复代码。
  3. 当类中使用了复杂条件运算符以在同一算法的不同变体中切换时,可使用该模式。

优缺点

优点:

  • 可以在运行时切换对象内的算法。
  • 可以将算法的实现和使用算法的代码隔离开来。
  • 可以使用组合来代替继承。
  • 开闭原则。 你无需对上下文进行修改就能够引入新的策略。 缺点:
  • 如果你的算法极少发生改变,那么没有任何理由引入新的类和接口。使用该模式只会让程序过于复杂。
  • 客户端必须知晓策略间的不同——它需要选择合适的策略。

例子🌰

/*策略类*/
var levelOBJ = {
    "A": function(money) {
        return money * 4;
    },
    "B" : function(money) {
        return money * 3;
    },
    "C" : function(money) {
        return money * 2;
    } 
};
/*环境类*/
var calculateBouns =function(level,money) {
    return levelOBJ[level](money);
};
console.log(calculateBouns('A',10000)); // 40000