你应该知道的 4 种 JavaScript 设计模式

共 7184字,需浏览 15分钟

 ·

2022-02-27 02:53

英文 | https://www.digitalocean.com/community/tutorial_series/javascript-design-patterns

翻译 | 杨小爱


前言
每个开发人员都努力编写可维护、可读和可重用的代码。随着应用程序变得越来越大,代码结构变得越来越重要。设计模式被证明是解决这一挑战的关键——为特定情况下的常见问题提供组织结构。
JavaScript Web 开发人员在创建应用程序时经常与设计模式交互,甚至是在不知不觉中。
本文涵盖最重要和最常用的4种 JavaScript 设计模式的解释和示例。
正文
JavaScript 模块是最常用的设计模式,用于保持特定代码段独立于其他组件。这提供了松散耦合以支持结构良好的代码。
对于那些熟悉面向对象语言的人来说,模块就是 JavaScript “类”。类的众多优点之一是封装——保护状态和行为不被其他类访问。模块模式允许公共和私有(加上鲜为人知的受保护和特权)访问级别。
模块应该是立即调用函数表达式 (IIFE) 以允许私有范围 - 即保护变量和方法的闭包(但是,它将返回一个对象而不是函数)。
这是它的样子:
(function() {
// declare private variables and/or functions
return { // declare public variables and/or functions }
})();

在这里,我们在返回我们想要返回的对象之前实例化私有变量和/或函数。我们闭包之外的代码无法访问这些私有变量,因为它不在同一个范围内。让我们看一个更具体的实现:

var HTMLChanger = (function() {    var contents = 'contents'
var changeHTML = function() { var element = document.getElementById('attribute-to-change'); element.innerHTML = contents; }
return { callChangeHTML: function() { changeHTML(); console.log(contents); } };
})();
HTMLChanger.callChangeHTML(); // Outputs: 'contents'console.log(HTMLChanger.contents); // undefined

请注意, callChangeHTML 绑定到返回的对象,并且可以在 HTMLChanger 命名空间中引用。但是,在模块之外时,无法引用内容。

揭示模块模式

模块模式的一种变体称为显示模块模式。目的是维护封装并揭示对象字面量中返回的某些变量和方法。直接实现如下所示:

var Exposer = (function() {    var privateVariable = 10;
var privateMethod = function() { console.log('Inside a private method!'); privateVariable++; }
var methodToExpose = function() { console.log('This is a method I want to expose!'); }
var otherMethodIWantToExpose = function() { privateMethod(); }
return { first: methodToExpose, second: otherMethodIWantToExpose };})();
Exposer.first(); // Output: This is a method I want to expose!Exposer.second(); // Output: Inside a private method!Exposer.methodToExpose; // undefined

尽管这看起来更简洁,但一个明显的缺点是无法引用私有方法。这可能会带来单元测试挑战。同样,公共行为是不可覆盖的。

JavaScript 中的观察者设计模式

很多时候,当应用程序的一部分发生变化时,其他部分也需要更新。在 AngularJS 中,如果 $scope 对象更新,可以触发一个事件来通知另一个组件。观察者模式就是这样结合的——如果一个对象被修改,它会向依赖对象广播发生了变化。

另一个主要的例子是模型-视图-控制器(MVC)架构。模型更改时视图会更新。一个好处是将视图与模型分离以减少依赖关系。

维基百科上的观察者设计模式

如 UML 图中所示,必要的对象是主体、观察者和具体对象。主题包含对具体观察者的引用,以通知任何更改。Observer 对象是一个抽象类,它允许具体的观察者实现 notify 方法。

让我们看一个通过事件管理包含观察者模式的 AngularJS 示例。

// Controller 1$scope.$on('nameChanged', function(event, args) {    $scope.name = args.name;});
...
// Controller 2$scope.userNameChanged = function(name) { $scope.$emit('nameChanged', {name: name});};

对于观察者模式,区分独立对象或主体很重要。

值得注意的是,尽管观察者模式确实提供了许多优点,但缺点之一是随着观察者数量的增加,性能会显着下降。最臭名昭著的观察者之一是观察者模式。在 AngularJS 中,我们可以观察变量、函数和对象。$$digest 循环运行并在修改范围对象时向每个观察者通知新值。

我们可以在 JavaScript 中创建自己的主题和观察者。让我们看看这是如何实现的:

var Subject = function() {    this.observers = [];
return { subscribeObserver: function(observer) { this.observers.push(observer); }, unsubscribeObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers.splice(index, 1); } }, notifyObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers[index].notify(index); } }, notifyAllObservers: function() { for(var i = 0; i < this.observers.length; i++){ this.observers[i].notify(i); }; } };};
var Observer = function() { return { notify: function(index) { console.log("Observer " + index + " is notified!"); } }}
var subject = new Subject();
var observer1 = new Observer();var observer2 = new Observer();var observer3 = new Observer();var observer4 = new Observer();
subject.subscribeObserver(observer1);subject.subscribeObserver(observer2);subject.subscribeObserver(observer3);subject.subscribeObserver(observer4);
subject.notifyObserver(observer2); // Observer 2 is notified!
subject.notifyAllObservers();// Observer 1 is notified!// Observer 2 is notified!// Observer 3 is notified!// Observer 4 is notified!

发布/订阅

然而,发布/订阅模式使用位于希望接收通知的对象(订阅者)和触发事件的对象(发布者)之间的主题/事件通道。此事件系统允许代码定义特定于应用程序的事件,这些事件可以传递包含订阅者所需值的自定义参数。这里的想法是避免订阅者和发布者之间的依赖关系。

这与观察者模式不同,因为任何订阅者都实现了适当的事件处理程序来注册和接收发布者广播的主题通知。

尽管存在区别,但许多开发人员选择将发布/订阅设计模式与观察者聚合。发布/订阅模式中的订阅者通过某种消息传递媒介得到通知,但观察者通过实现类似于主题的处理程序得到通知。

在 AngularJS 中,订阅者使用 $on('event', callback) 来“订阅”事件,而发布者使用 $emit('event', args) 或 $broadcast('event', args) 来“发布”事件.

JavaScript 中的原型设计模式

任何 JavaScript 开发人员要么看到过关键字原型,被原型继承弄糊涂了,要么在他们的代码中实现了原型。Prototype 设计模式依赖于 JavaScript 原型继承。原型模型主要用于在性能密集型情况下创建对象。

创建的对象是传递的原始对象的克隆(浅克隆)。原型模式的一个用例是执行广泛的数据库操作以创建用于应用程序其他部分的对象。如果另一个进程需要使用这个对象,而不是必须执行这个大量的数据库操作,克隆先前创建的对象将是有利的。

维基百科上的原型设计模式

这个 UML 描述了如何使用原型接口来克隆具体的实现。

要克隆一个对象,必须存在一个构造函数来实例化第一个对象。接下来,通过使用关键字原型变量和方法绑定到对象的结构。

让我们看一个基本的例子:

var TeslaModelS = function() {    this.numWheels    = 4;    this.manufacturer = 'Tesla';    this.make         = 'Model S';}
TeslaModelS.prototype.go = function() { // Rotate wheels}
TeslaModelS.prototype.stop = function() { // Apply brake pads}

构造函数允许创建单个 TeslaModelS 对象。当创建新的 TeslaModelS 对象时,它会保留在构造函数中初始化的状态。此外,维护函数 go 和 stop 很容易,因为我们用原型声明了它们。一种在原型上扩展函数的同义方式,如下所述:

var TeslaModelS = function() {    this.numWheels    = 4;    this.manufacturer = 'Tesla';    this.make         = 'Model S';}
TeslaModelS.prototype = { go: function() { // Rotate wheels }, stop: function() { // Apply brake pads }}

揭示原型模式

与模块模式类似,原型模式也有一个显着的变化。显示原型模式提供了对公共和私有成员的封装,因为它返回一个对象字面量。

由于我们要返回一个对象,我们将在原型对象前面加上一个函数。通过扩展上面的示例,我们可以选择要在当前原型中公开的内容以保留其访问级别:

var TeslaModelS = function() {    this.numWheels    = 4;    this.manufacturer = 'Tesla';    this.make         = 'Model S';}
TeslaModelS.prototype = function() {
var go = function() { // Rotate wheels };
var stop = function() { // Apply brake pads };
return { pressBrakePedal: stop, pressGasPedal: go }
}();

请注意,由于超出了返回对象的范围,函数 stop 和 go 将如何与返回对象屏蔽。由于 JavaScript 原生支持原型继承,因此无需重写底层特性。

JavaScript 中的单例设计模式

Singleton 只允许单个实例化,但同一对象的多个实例。Singleton 限制客户端创建多个对象,在创建第一个对象后,它将返回自身的实例。

对于大多数以前没有使用过单例的人来说,很难找到单例的用例。一个例子是使用办公室打印机。如果一个办公室有十个人,他们都使用一台打印机,十台计算机共享一台打印机(实例)。通过共享一台打印机,他们共享相同的资源。

var printer = (function () {
var printerInstance;
function create () {
function print() { // underlying printer mechanics }
function turnOn() { // warm up // check for paper }
return { // public + private states and behaviors print: print, turnOn: turnOn }; }
return { getInstance: function() { if(!printerInstance) { printerInstance = create(); } return printerInstance; } };
function Singleton () { if(!printerInstance) { printerInstance = intialize(); } };
})();

create 方法是私有的,因为我们不希望客户端访问它,但是请注意 getInstance 方法是公共的。每个职员可以通过与 getInstance 方法交互来生成一个打印机实例,如下所示:

var officePrinter = printer.getInstance();

在 AngularJS 中,Singleton 很普遍,最值得注意的是服务、工厂和提供者。由于它们维护状态并提供资源访问,因此创建两个实例会破坏共享服务/工厂/提供者的观点。

当多个线程尝试访问同一资源时,多线程应用程序中会出现竞争条件。单例容易受到竞争条件的影响,例如,如果没有先初始化实例,则两个线程可以创建两个对象,而不是返回和实例。这违背了单例的目的。因此,开发人员在多线程应用程序中实现单例时必须了解同步。

结论

设计模式经常用于大型应用程序中,但要了解,在某些时候某些地方,其中一种模式可能优于另一种的模式,需要实践。

在构建任何应用程序之前,您应该彻底考虑每个参与者以及他们如何相互交互。在回顾了模块、原型、观察者和单例设计模式之后,您应该能够识别这些模式并在项目中使用它们。



学习更多技能

请点击下方公众号

浏览 13
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报