理解 JavaScript 中的设计模式

本文帮助你了解 JavaScript 中的经常使用的几种设计模式。html

当你开始一个新项目时,你不会当即开始编码。 首先必须定义项目的目的和范围,而后列出项目功能或项目说明书。 在你能够开始编码或者你正在处理更复杂的项目以后,你应该选择最适合你项目的设计模式。程序员

什么是设计模式?

在软件工程中,设计模式是软件设计中常见问题可重用的解决方案。设计模式表明着经验丰富的软件开发人员使用的最佳实践。设计模式能够被认为是编程模板。编程

为何使用设计模式 ?

许多程序员要么认为设计模式是浪费时间,要么他们不知道如何恰当地应用它们。 可是使用适当的设计模式能够帮助你编写更好,更易理解的代码,而且代码能够轻松维护,由于它更容易理解。设计模式

最重要的是,设计模式为软件开发人员提供了一些沟通上的便利。 它们会当即向学习你代码的人显示你的代码的意图。闭包

例如,若是你在项目中使用装饰者模式,那么新程序员将当即知道该代码正在作什么,而且他们能够更专一于解决业务问题,而无需花费精力去理解你的代码正在作什么。编程语言

如今咱们知道了什么是设计模式,以及它们为何重要,让咱们深刻研究 JavaScript 中使用的各类设计模式。ide

模块模式(Module Pattern)

模块是一段独立的代码,所以咱们能够在不影响其余代码的状况下单独更新模块。 模块还容许咱们为变量建立单独的做用域来避免命名空间的污染。 当它们与其余代码段分离时,咱们也能够在其余项目中重用模块。模块化

模块是任何现代 JavaScript 应用程序不可或缺的一部分,有助于保持代码清洁,分离和组织。 有许多方法能够在JavaScript 中建立模块,其中一种是模块模式。函数

Bit之类的平台能够帮助将模块和组件转换为共享的构建块,能够与任何项目共享,发现和开发。 经过零重构,它是一种快速且可扩展的方式来共享和重用代码。学习

与其余编程语言不一样,JavaScript 没有访问修饰符的特性,也就是说,你不能将变量声明为私有(private)或公开(public)。 所以模块模式也经常被用于模拟封装的概念。

此模式使用IIFE(当即调用的函数表达式),闭包和函数做用域来模拟此概念。 例如:

const myModule = (function() {
  
  const privateVariable = 'Hello World';
  
  function privateMethod() {
    console.log(privateVariable);
  }

  return {
    publicMethod: function() {
      privateMethod();
    }
  }

})();

myModule.publicMethod();

因为上面的代码是IIFE,代码会当即执行,返回的对象被分配给 myModule 变量。 因为闭包,即便在IIFE完成以后,返回的对象仍然能够访问 IIFE 内定义的函数和变量。

所以,在 IIFE 中定义的变量和函数对外部做用域来讲基本上是隐藏的,所以它们对 myModule 变量是私有的。

执行代码后,myModule 变量以下所示:

const myModule = {
  publicMethod: function() {
    privateMethod();
  }
};

所以,咱们能够调用 publicMethod(),转而调用 privateMethod()。例如:

// Prints 'Hello World'
module.publicMethod();

暴露模块模式(Revealing Module Pattern)

暴露模块模式是 Christian Heilmann 对模块模式略微的改进版本。 模块模式的问题是咱们必须建立新的公开函数来调用私有函数和变量。

在暴露模块模式中,咱们将返回的对象的属性映射到咱们想要公开的私有函数。 这就是为何它被称为暴露模块模式的缘由。 例如:

const myRevealingModule = (function() {
  
  let privateVar = 'Peter';
  const publicVar  = 'Hello World';

  function privateFunction() {
    console.log('Name: '+ privateVar);
  }
  
  function publicSetName(name) {
    privateVar = name;
  }

  function publicGetName() {
    privateFunction();
  }

  /** reveal methods and variables by assigning them to object     properties */

return {
    setName: publicSetName,
    greeting: publicVar,
    getName: publicGetName
  };
})();

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();

这种模式使咱们更容易理解咱们能够公开访问哪些函数和变量,这有助于代码的可读性。

代码执行后,myRevealingModule 以下所示:

const myRevealingModule = {
  setName: publicSetName,
  greeting: publicVar,
  getName: publicGetName
};

咱们能够调用 myRevealingModule.setName('Mark'),来引用内部的 publicSetName ,以及调用myRevealingModule.getName() ,来引用内部的 publicGetName 。例如:

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();

暴露模块模式相较于模块模式的优势:

  • 咱们能够修改 return 语句中的一行代码,来将成员从 public(公开) 更改成 private(私有) ,反之亦然。
  • 返回的对象不包含函数定义,全部右侧表达式都在 IIFE 中定义,使代码清晰易读。

ES6 模块(ES6 Modules)

在ES6以前,JavaScript 没有内置的模块系统,因此开发人员必须依赖第三方库或模块模式来实现模块化。可是在 ES6 中,JavaScript 拥有了原生的模块系统。

ES6 模块存储在单独的文件中。每一个文件只能有一个模块。默认状况下,模块中的全部内容都是私有的。函数、变量和类使用 export 关键字来向外公开。模块内的代码老是在 严格模式(strict mode) 下运行。

导出模块

导出函数和变量声明有两种方法:

  • 1) 经过在函数和变量声明前添加 export 关键字。例如:
// utils.js
export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}

export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}

// This is a private function

function privateLog() {
  console.log('Private Function');
}
  • 2) 经过在代码末尾添加 export 关键字,并包含咱们要导出的函数和变量的名称。例如:
// utils.js
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
function divide(num1, num2) {
  console.log('Divide:', num1, num2);
  return num1 / num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
export {multiply, divide};

导入模块

与导出模块相似,有两种方法可使用 import 关键字导入模块。 例如:

  • 1) 一次导入多个项目
// main.js

// importing multiple items
import { sum, multiply } from './utils.js';

console.log(sum(3, 7));
console.log(multiply(3, 7));

导入全部模块

// main.js

// importing all of module
import * as utils from './utils.js';

console.log(utils.sum(3, 7));
console.log(utils.multiply(3, 7));

导入/导出模块可使用别名

若是要避免命名冲突,能够在导出和导入时使用别名。例如:

  • 1)重命名导出
// utils.js

function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}

function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}

export {sum as add, multiply};
  • 2) 重命名导入
// main.js

import { add, multiply as mult } from './utils.js';

console.log(add(3, 7));
console.log(mult(3, 7));

你能够查看 JavaScript 模块简史 和 ECMAScript 6 Modules(模块)系统及语法详解 来完整了解 JavaScript 模块化进程和 ES6 Modules(模块)的更多信息。

单例模式(Singleton Pattern)

Singleton(单例) 是一个只能实例化一次的对象。 若是不存在,则单例模式会建立类的新实例。 若是存在实例,则它只返回对该对象的引用。 对构造函数的任何重复调用老是会获取相同的对象。

JavaScript 一直支持单例模式。 咱们只是不称他们为单例,咱们称之为 对象字面量。 例如:

const user = {
  name: 'Peter',
  age: 25,
  job: 'Teacher',
  greet: function() {
    console.log('Hello!');
  }
};

由于 JavaScript 中的每一个对象占用一个惟一的内存位置,当咱们调用 user 对象时,咱们其实是返回对该对象的引用。

若是咱们尝试将用户变量复制到另外一个变量并修改该变量。 例如:

const user1 = user;
user1.name = 'Mark';

咱们会看到的结果是两个对象都被修改,由于 JavaScript 中的对象是经过引用而不是经过值传递的。因此内存中只有一个对象。例如:

// prints 'Mark'
console.log(user.name);

// prints 'Mark'
console.log(user1.name);

// prints true
console.log(user === user1);

可使用构造函数实现单例模式。例如:

let instance = null;

function User() {
  if(instance) {
    return instance;
  }

  instance = this;
  this.name = 'Peter';
  this.age = 25;
  
  return instance;
}

const user1 = new User();
const user2 = new User();

// prints true
console.log(user1 === user2);

调用此构造函数时,它会检查 instance 对象是否存在。 若是该对象不存在,则将该变量分配给 instance变量。若是对象存在,它只返回该对象。

单例模式也可使用模块模式实现。 例如:

const singleton = (function() {
  let instance;
  
  function init() {
    return {
      name: 'Peter',
      age: 24,
    };
  }

  return {
    getInstance: function() {
      if(!instance) {
        instance = init();
      }
      
      return instance;
    }
  }
})();

const instanceA = singleton.getInstance();
const instanceB = singleton.getInstance();

// prints true
console.log(instanceA === instanceB);

在上面的代码中,咱们经过调用 singleton.getInstance 方法建立一个新实例。 若是实例已存在,则此方法仅返回该实例,若是实例不存在,则经过调用 init() 函数建立新实例。

工厂模式(Factory Pattern)

工厂模式是一种使用工厂方法建立对象的设计模式,而不指定建立对象的确切的类或构造函数。

工厂模式用于在不公开实例化逻辑的状况下建立对象。当咱们须要根据特定条件生成不一样的对象时,可使用此模式。例如:

class Car{
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'brand new';
    this.color = options.color || 'white';
  }
}

class Truck {
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'used';
    this.color = options.color || 'black';
  }
}

class VehicleFactory {
  createVehicle(options) {
    if(options.vehicleType === 'car') {
      return new Car(options);
    } else if(options.vehicleType === 'truck') {
      return new Truck(options);
      }
  }
}

在这里,我建立了一个 Car 和 Truck 类(带有一些默认值),用于建立新的 car 和 truck 对象。 我已经定义了一个 VehicleFactory 类,用于根据 options 对象中收到的 vehicleType 属性建立并返回一个新对象。

const factory = new VehicleFactory();

const car = factory.createVehicle({
  vehicleType: 'car',
  doors: 4,
  color: 'silver',
  state: 'Brand New'
});

const truck= factory.createVehicle({
  vehicleType: 'truck',
  doors: 2,
  color: 'white',
  state: 'used'
});

// Prints Car {doors: 4, state: "Brand New", color: "silver"}
console.log(car);

// Prints Truck {doors: 2, state: "used", color: "white"}
console.log(truck);

我建立了一个 VehicleFactory 类的新对象 factory 。以后,咱们能够经过调用 factory.createVehicle并,传递一个带有 carType 属性 options 对象,且值为 car 或 truck 的来建立一个新的 Car 或 Truck对象。

装饰者模式(Decorator Pattern)

装饰者模式用于扩展对象的功能,而无需修改现有的类或构造函数。 此模式可用于向对象添加功能,而无需它们修改底层代码。

这种模式的一个简单例子是:

function Car(name) {
  this.name = name;

  // Default values
  this.color = 'White';
}

// Creating a new Object to decorate
const tesla= new Car('Tesla Model 3');

// Decorating the object with new functionality

tesla.setColor = function(color) {
  this.color = color;
}

tesla.setPrice = function(price) {
  this.price = price;
}

tesla.setColor('black');
tesla.setPrice(49000);

// prints black
console.log(tesla.color);

这种模式的一个更实际的例子是:

比方说,汽车的成本取决于它的功能数量。 若是没有装饰者模式,咱们必须为不一样的功能组合建立不一样的类,每一个类都有一个成本方法来计算成本。 例如:

class Car() {
}

class CarWithAC() {
}

class CarWithAutoTransmission {
}

class CarWithPowerLocks {
}

class CarWithACandPowerLocks {
}

可是使用装饰者模式,咱们能够建立一个基类 ·Car`,并使用装饰者函数将不一样配置的成本计算方法添加到其对象中。例如:

class Car {
  constructor() {
    // Default Cost
    this.cost = function() {
      return 20000;
    }
  }
}

// Decorator function
function carWithAC(car) {
  car.hasAC = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 500;
  }
}

// Decorator function
function carWithAutoTransmission(car) {
  car.hasAutoTransmission = true;
   const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 2000;
  }
}

// Decorator function
function carWithPowerLocks(car) {
  car.hasPowerLocks = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 500;
  }
}

首先,咱们建立一个基类 Car,用于建立 Car 对象。 而后,而后咱们为了避免同的功能建立了装饰者函数,并将 Car 对象做为参数传递。 而后咱们覆盖该对象的成本函数,该函数返回汽车的更新成本,并向该对象添加新属性以指示添加了哪一个特征。

要添加新功能,咱们能够执行如下操做:

const car = new Car();
console.log(car.cost());

carWithAC(car);
carWithAutoTransmission(car);
carWithPowerLocks(car);

最后,咱们能够像这样计算汽车的成本:

// Calculating total cost of the car
console.log(car.cost());

结语

咱们已经了解了JavaScript中使用的各类设计模式,可是这里还一些没有介绍的,能够用 JavaScript 实现的设计模式。

虽然了解各类设计模式很重要,但一样重要的是不要过分使用它们。 在使用设计模式以前,你应该仔细考虑你所处的问题是否符合该设计模式。 要了解模式是否适合你的问题,你应该研究设计模式的思想,以及该设计模式的应用。

相关文章
相关标签/搜索