乐趣区

关于javascript:理解JavaScript中的设计模式

一、什么是设计模式

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

二、为什么要应用设计模式

许多程序员要么认为设计模式是浪费时间,要么他们不晓得如何失当地利用它们。然而应用适当的设计模式能够帮忙你编写更好,更易了解的代码,并且代码能够轻松保护,因为它更容易了解。

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

例如,如果你在我的项目中应用装璜者模式,那么新程序员将立刻晓得该代码正在做什么,并且他们能够更专一于解决业务问题,而无需破费精力去了解你的代码正在做什么。

当初咱们晓得了什么是设计模式,以及它们为什么重要,让咱们深入研究 JavaScript 中应用的各种设计模式。

三、各种设计模式

1. 模块模式

  • 模块是一段独立的代码,因而咱们能够在不影响其余代码的状况下独自更新模块 。模块还容许咱们 为变量创立独自的作用域来防止命名空间的净化。当它们与其余代码段拆散时,咱们也能够在其余我的项目中重用模块。
  • 模块是任何古代 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();

2. 裸露模块模式(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 中定义,使代码清晰易读。

3.ES6 模块(ES6 Modules)

在 ES6 之前,JavaScript 没有内置的模块零碎,所以开发人员必须依赖第三方库或模块模式来实现模块化。然而在 ES6 中,JavaScript 领有了原生的模块零碎。

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

(1)导出模块

导出函数和变量申明有两种办法:

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

(2)导入模块

与导出模块相似,有两种办法能够应用 import 关键字导入模块。例如:

  • 1)一次导入多个我的项目
// main.js
 
// importing multiple items
import {sum, multiply} from './utils.js';
 
console.log(sum(3, 7));
console.log(multiply(3, 7));
  • 2)导入所有的模块
// main.js
 
// importing all of module
import * as utils from './utils.js';
 
console.log(utils.sum(3, 7));
console.log(utils.multiply(3, 7));

(3)导入 / 导出模块能够应用别名

如果要防止命名抵触,能够在导出和导入时应用别名。例如:

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

4. 单例模式(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) {        // 先判断 instance 是否存在,如果存在则返回实例自身(不会创立新的实例);如果该对象不存在,则将该变量调配给 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();            // 返回的是 instanceA
 
// prints true
console.log(instanceA === instanceB);

在下面的代码中,咱们通过调用 singleton.getInstance 办法创立一个新实例。如果实例已存在,则此办法仅返回该实例,如果实例不存在,则通过调用 init() 函数创立新实例

5. 工厂模式(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 对象。

6. 装璜者模式

装璜者模式用于 扩大对象的性能,而无需批改现有的类或构造函数 。此模式 可用于向对象增加性能,而无需它们批改底层代码。

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 对象作为参数传递。而后咱们笼罩该对象的老本函数,该函数返回汽车的更新老本,并向该对象增加新属性以批示增加了哪个特色。

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());
退出移动版