Node.js && JavaScript 面试常用的设计模式

1,501 阅读7分钟

设计模式是任何软件开发人员日常工作的一部分,不管他们是否意识到这一点。

在本文中,我们将研究如何识别这些设计模式,以及如何在自己的项目中开始使用它们。

什么是设计模式

简单地说,设计模式就是一种可以让你以某种方式更好的组织代码的方法,从而获得一些好处。比如更快的开发速度、代码的可重用性等等。

所有模式都很容易采用OOP范式。尽管考虑到JavaScript的灵活性,你也可以在非OOP项目中实现这些概念。事实上,每年都会有新的设计模式被创建,有太多的设计模式无法在一篇文章中涵盖,本文将重点介绍和实现几种常见的设计模式。

立即执行函数表达式(IIFE)

我要展示的第一个是允许同时定义和调用函数的模式。由于JavaScript作用域的工作方式,使用IIFE可以很好地模拟类中的私有属性之类的东西。事实上,这个特定的模式有时被用作其他更复杂需求的一部分。我们待会再看。

IIFE常见例子

在我们深入研究用例和它背后的机制之前,让我快速地看一下它到底是什么样子的:


(function() {
   const x = 20
   const y = 20
   const answer = x + y
   console.log(answer)
})()

通过将上述代码粘贴到浏览器的控制台,你将立即得到结果,因为正如其名所示,一旦定义了函数,就会立即执行它。

IIFE由一个匿名函数声明组成,在一组括号内(括号将定义转换为函数表达式),然后在它的尾部有一组调用括号。像这样:

(function(/*received parameters*/) {
//your code here
})(/*parameters*/)

用例

模拟静态变量

还记得静态变量吗? 例如在C或c#中。使用静态变量的好处是,如果你在一个函数中定义一个静态变量,不管你调用它多少次,这个变量对函数的所有实例都是通用的。一个简单的例子是像下面这样的:

function autoIncrement() {
  static let number = 0
  number++
  return number
}

上面的函数每次调用都会返回一个新的数字(当然,假设静态关键字在JS中可用)。我们可以在JS中来实现它,你可以像这样模拟一个静态变量:

let autoIncrement = (function() {
  let number = 0

  return function () {
    number++
    return number
  }
})()

立即函数执行后返回一个新函数(闭包),该函数内部引用了number变量。由于JS闭包机制,立即函数执行完毕后,number变量不会立即被垃圾回收掉,所以autoIncrement每次执行总是可以访问number变量(就像它是一个全局变量一样)。

模拟私有变量

ES6类将每个成员都视为公共的,这意味着没有私有属性或方法。但是多亏了IIFE,如果你想的话,你可以模拟它。

const autoIncrementer = (function() {
  let value = 0

  return {
    incr() {
      value++
    },

    get value() {
      return value
    }
  }
})()
> autoIncrementer.incr()
undefined
> autoIncrementer.incr()
undefined
> autoIncrementer.value
2
> autoIncrementer.value = 3
3
> autoIncrementer.value
2

上面的代码展示了一种创建私有变量的方法,立即函数执行后,返回一个object赋值给变量autoIncrementer,在外部只能通过value方法和incr方法获取和修改value的值。

工厂方法模式

这个模式是我最喜欢的模式之一,因为它可以让代码变动清楚简洁。

工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。

这可能听起来并不是那么有用,但请看下面这个简单例子就会明白。

( _ => {

    let factory = new MyEmployeeFactory()

    let types = ["fulltime", "parttime", "contractor"]
    let employees = [];
    for(let i = 0; i < 100; i++) {
     employees.push(factory.createEmployee({type: types[Math.floor( (Math.random(2) * 2) )]})    )}

    //....
    employees.forEach( e => {
     console.log(e.speak())
    })

})()

上面的代码的关键点是通过factory.createEmployee添加了多个对象到employees数组中,所有这些对象都共享相同的接口(他们有相同的一组方法),也有自己特有的属性,但是你真的不需要关心对象创建的细节和什么时候创建。

下面我们具体看一下构造函数MyEmployeeFactory的实现

class Employee {

  speak() {
    return "Hi, I'm a " + this.type + " employee"
  }

}

class FullTimeEmployee extends Employee{
  constructor(data) {
    super()
    this.type = "full time"
    //....
  }
}


class PartTimeEmployee extends Employee{
  constructor(data) {
    super()
    this.type = "part time"
    //....
  }
}

class ContractorEmployee extends Employee{
  constructor(data) {
    super()
    this.type = "contractor"
    //....
  }
}

class MyEmployeeFactory {
  createEmployee(data) {
    if(data.type == 'fulltime') return new FullTimeEmployee(data)
    if(data.type == 'parttime') return new PartTimeEmployee(data)
    if(data.type == 'contractor') return new ContractorEmployee(data)
  }
}

用例

前面的代码已经显示了一个通用用例,但是如果我们想更具体一些,接下来使用这个模式的来处理错误对象的创建。

假设有一个大约有10个端点的Express应用程序,其中每个端点都需要根据用户输入返回2到3个错误,假设我们有30个语句大概像这样:

if(err) {
  res.json({error: true, message: “Error message here”})
}

如果突然需要给error对象新加一个属性,那么需要修改30个位置,这是非常麻烦的。可以过将错误对象定义到一个简单的类中解决这个问题。如果有多种类型的错误对象,这个时候就可以创建一个工厂函数来决定实例化那个错误对象。

如果你要集中创建错误对象的逻辑,那么你在整个代码中要做的就是:

if(err) {
  res.json(ErrorFactory.getError(err))
}

单例模式

这是另一个老的但很好用的设计模式。注意,这是一个非常简单的模式,但是它可以帮助你跟踪正在实例化的类的实例数量。实际上,它可以帮助你一直保持这个数字为1。单例模式允许实例化一个对象一次,然后当每次需要时都使用它,而不是创建一个新的对象。这样可以更好的追踪对这个对象的引用。

通常,其他语言使用一个静态属性实现此模式,一旦实例存在,它们将存储该实例。这里的问题是,正如我之前提到的,我们无法访问JS中的静态变量。我们可以用两种方式实现,一种是使用IIFE而不是类。

另一种方法是使用ES6模块,并让我们的单例类使用本地全局变量来存储实例。通过这样做,类本身被导出到模块之外,但是全局变量仍然是模块的本地变量。

这听起来比看起来要复杂得多:

let instance = null

class SingletonClass {

  constructor() {
    this.value = Math.random(100)
  }

  printValue() {
    console.log(this.value)
  }

  static getInstance() {
    if(!instance) {
        instance = new SingletonClass()
    }

    return instance
  }
}

module.exports = SingletonClass

你可以像下面这样使用:

const Singleton = require(“./singleton”)
const obj = Singleton.getInstance()
const obj2 = Singleton.getInstance()

obj.printValue()
obj2.printValue()

console.log("Equals:", obj === obj2)

上面的代码输出:

0.5035326348000628
0.5035326348000628
Equals::  true

确实,我们只实例化对象一次,并返回现有实例。

用例

在决定是否需要类似于单例的实现时,需要考虑以下问题:你真正需要多少类实例?如果答案是2或更多,那么这不是你的模式。

但是当处理数据库连接时,你可能需要考虑它。

考虑一下,一旦连接到数据库,在整个代码中保持连接活动和可访问性是一个好主意。注意,这可以用很多不同的方法来解决,但是单例模式确实是其中之一。

利用上面的例子,我们可以把它写成这样:

const driver = require("...")

let instance = null


class DBClass {

  constructor(props) {
    this.properties = props
    this._conn = null
  }

  connect() {
    this._conn = driver.connect(this.props)
  }

  get conn() {
    return this._conn
  }

  static getInstance() {
    if(!instance) {
        instance = new DBClass()
    }

    return instance
  }
}

module.exports = DBClass

现在,可以确定,无论在哪里使用getInstance方法,都将返回惟一的活动连接(如果有的话)。

结论

上面几种是我们写代码时常用的设计模式,下一篇我们将重点深入理解观察者模式职责链模式

关注作者github查看更多内容。第一时间获得更新。