原文地址:Understanding Design Patterns in JavaScript编程
原文做者:Sukhjinder Arora设计模式
译者:HelloGitHub-Robert闭包
当启动一个新的项目时候,咱们不该该立刻开始编程。而是首先应该定义项目的目的和范围,而后列出其功能或规格。若是你已经开始编程或者正在从事一个复杂的项目,则应该选择一个最适合你项目的设计模式。编程语言
在软件工程中,设计模式是针对软件设计中常见问题的可重用解决方案。设计模式也是经验丰富的开发人员针对特定问题的最佳实践。它能够被看成编程的模板。ide
许多工程师要么认为设计模式浪费时间,要么不知道如何恰当的使用设计模式。但若是能正确使用设计模式,则能够帮助你写出更好的可读性更高的代码,而且代码更容易被维护和理解。函数
最重要的是,设计模式为软件开发人员提供了通用的词汇表。它们能让学习你代码的人很快了解代码的意图。例如,若是你的项目中使用了装饰器模式,那么新的开发能够很快就知道这段代码的做用,从而他们能够将更多精力放在解决业务问题上,而不是试图理解代码在作什么。学习
咱们已经知道了什么是设计模式和它的重要性,下面咱们深刻研究一下 JavaScript 中的 6 种设计模式。this
模块是一段独立的代码,所以咱们能够更新模块而不会影响代码的其它部分。模块还容许咱们经过为变量建立单独的做用域来避免命名空间污染。当它们与其它代码解耦时,咱们还能够在其它项目中重用模块。设计
模块是任何现代 JavaScript 应用程序不可或缺的一部分,有助于保持代码干净,独立和有条理。在 JavaScript 中有许多方法能够建立模块,其中一种是模块模式。对象
与其它编程语言不一样,JavaScript 没有访问修饰符,也就是说,你不能将变量声明为私有的或公共的。所以,模块模式也可用来模拟封装的概念。
模块模式使用 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();
揭示模块模式是 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();
与模块模式相比,揭示模块模式的优点有:
在 ES6 以前,JavaScript 没有内置模块,所以开发人员必须依靠第三方库或模块模式来实现模块。可是自从 ES6,JavaScript 内置了模块。
ES6 的模块是以文件形式存储的。每一个文件只能有一个模块。默认状况下,模块内的全部内容都是私有的。经过使用 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'); }
// 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 关键字导入模块的方法。例如:
// 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));
// 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};
// main.js import { add, multiply as mult } from './utils.js'; console.log(add(3, 7)); console.log(mult(3, 7));
一个单例对象是只能实例化一次的对象。若是不存在,则单例模式将建立类的新实例。若是存在实例,则仅返回对该对象的引用。重复调用构造函数将始终获取同一对象。
JavaScript 是一直内置单例的语言。咱们只是不称它们为单例,咱们称它们为对象字面量。例如:
const user = { name: 'Peter', age: 25, job: 'Teacher', greet: function() { console.log('Hello!'); } };
由于 JavaScript 中的每一个对象都占用一个惟一的内存位置,而且当咱们调用该 user 对象时,其实是在返回该对象的引用。
若是咱们尝试将 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 对象是否存在。若是对象不存在,则将 this 变量分配给 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() 函数建立一个新实例。
工厂模式使用工厂方法建立对象而不须要指定具体的类或构造函数的模式。
工厂模式用于建立对象而不须要暴露实例化的逻辑。当咱们须要根据特定条件生成不一样的对象时,可使用此模式。例如:
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 方法而且传递 options 对象,其 vehicleType 属性可能为 car 或者 truck 来建立新 Car 或 Truck 对象。
装饰器模式用于扩展对象的功能,而无需修改现有的类或构造函数。此模式可用于将特征添加到对象中,而无需修改底层的代码。
此模式的一个简单示例为:
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);
这种模式的一个更实际的例子是:
假设汽车的成本取决于其功能的数量。若是没有装饰器模式,咱们将不得不为不一样的功能组合建立不一样的类,每一个类都有一个 cost 方法来计算成本。例如:
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 对象为参数。而后经过返回更新后的小汽车成原本覆盖对象的成本函数,且添加了一个用来标识某个特性是否已经被添加的属性。
要添加新的功能,咱们只须要像下面同样就能够:
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 实现的设计模式。
尽管了解各类设计模式很重要,但不要过分使用它们也一样重要。在使用设计模式以前,你应该仔细考虑你的问题是否适合该设计模式。要知道某个模式是否适合你的问题,应该好好研究该设计模式以及它的应用。
关注 HelloGitHub 公众号 收到第一时间的更新。
还有更多开源项目的介绍和宝藏项目等待你的发掘。