当前位置: 首页 > Web前端 > JavaScript

设计模式-单例

时间:2023-03-27 12:21:45 JavaScript

在整个应用程序中共享一个“单一”“全局”实例。单例是一个可以全局访问并且只能实例化一次的类。这个单例可以在整个应用程序中共享,这使得单例成为管理应用程序中全局状态的理想选择。首先,让我们看看使用ES2015Class语法的单例会是什么样子。比如我们建一个类CountergetInstance方法:returninstancegetCount方法:获取当前counter的值increment方法:对counter的值加1decrement方法:counter的值减1letcounter=0;classCounter{getInstance(){返回这个;}getCount(){返回计数器;}increment(){返回++counter;}递减(){返回--计数器;但是,这个类不是标准的单例!单例只允许被实例化一次。现在,我们可以创建Counter的多个实例classletcounter=0;classCounter{getInstance(){returnthis;}getCount(){返回计数器;}increment(){返回++counter;}递减(){返回--计数器;}}constcounter1=newCounter();constcounter2=newCounter();console.log(counter1.getInstance()===counter2.getInstance());//false运行两次new方法,可以发现实例化的counter1和counter2是不同的实例。getInstance得到的值是对两个不同实例的引用,所以counter1和counter2并不严格相等让我们确保只能创建一个Counter类的实例确保只能创建一个实例的一种方法是创建一个类称为实例变量。在构造函数中,我们将实例变量设置为等于使用new方法创建的实例对象。我们可以通过判断实例变量的值是否为空来保证不会出现多次实例化行为。如果实例变量不为空,证明已经实例化过,不需要再次实例化;如果再次实例化,会抛出错误让用户感知letinstance;letcounter=0;classCounter{constructor(){if(instance){thrownewError("只能创建一个实例!");}实例=这个;}getInstance(){返回这个;}getCount(){返回计数器;}increment(){return++counter;}递减(){返回--计数器;}}constcounter1=newCounter();constcounter2=newCounter();//错误:只能创建一个实例!好的,我们现在已经完成了防止创建多个实例。让我们从counter.js导出Counter实例。在导出之前,我们需要“冻结”实例。Object.freeze方法防止使用实例的代码修改实例。我们不能在冻结的实例上添加或修改属性,这降低了值在单例上被覆盖的风险。letinstance;letcounter=0;classCounter{constructor(){if(instance){thrownewError("你只能创建一个实例!");}实例=这个;}getInstance(){返回这个;}getCount(){返回计数器;}increment(){返回++counter;}递减(){返回--计数器;}}constsingletonCounter=Object.freeze(newCounter());exportdefaultsingletonCounter;让我们看一个实现计数器应用程序示例。文件如下:counter.js:包含Counter类,默认导出Counter实例index.js:加载redButton.js和blueButton.js模块redButton.js:导入Counter,添加Counter的increment方法为对红色按钮button进行事件监听,并通过调用getCount方法打印counter的当前值调用getCount方法blueButton.js和redButton.js都从counter.js导入同一个实例。当我们从blueButton.js或redButton.js触发increment方法时,Counter实例上的counter属性的值将被更新。单例的值将在所有文件之间共享,即使我们在不同的文件中触发更新。优点或缺点通过限制实例化来确保只有一个实例对象可以节省大量内存。我们不需要每次实例化的时候都为实例对象申请内存。单例的实例对象只需要申请一次内存,就可以在整个应用中使用。然而,单例模式通常被认为是一种“反模式”,在javascript中应尽可能避免使用。在许多编程语言中,例如Java或C++,不可能像在JavaScript中那样直接创建对象。在那些面向对象的编程语言中,我们需要创建一个类,类创建一个对象。创建的对象具有类实例的值,就像JavaScript示例中的实例值一样。但是,上面示例中显示的类实现实际上有点矫枉过正。我们可以直接在JavaScript中创建对象,例如我们可以简单地使用对象字面量来获得完全相同的结果。让我们介绍一下使用单例的一些缺点!使用对象文字让我们使用我们之前看到的相同示例。然而,这一次,Counter只是一个对象文字,包含:一个计数属性increment方法:将计数器的值加1方法:从计数器的值中减去1letcount=0;constcounter={increment(){返回++count;},减量(){返回--count;}};Object.freeze(counter);export{counter};由于对象是通过引用传递的,redButton.js和blueButton.js都导入了相同的计数器对象引用。在这两个文件中修改触发计数器的增量方法,并且计数值的变化在两个文件中都是可感知的。测试单例模式的测试代码可能很麻烦。由于我们不能每次都创建一个新实例,所有测试都依赖于对上一个测试的全局实例的修改。在这种情况下,测试的顺序很重要,一个共同的修改可能会导致整个测试用例失败。测试完成后,我们需要重置整个实例来重置测试所做的修改。importcounterInstancefrom'../src/counter'constCounter=counterInstance.countertest("递增1次应该是1",()=>{Counter.increment();expect(Counter.getCount()).toBe(1);});test("增加3额外的时间应该是4",()=>{Counter.increment();Counter.increment();Counter.increment();expect(Counter.getCount()).toBe(4);});test("递减1次应该是3",()=>{Counter.decrement();expect(Counter.getCount()).toBe(3);})单例对象的全局行为可以在整个应用程序的任何地方访问和使用。全局变量具有相同的行为:全局变量可以在全局范围内访问和使用,因此也可以在整个应用程序的任何地方访问和使用。设置全局变量通常被认为是一个糟糕的设计,因为修改全局变量的值可能会污染全局范围,这可能会导致一些意想不到的副作用。在ES2015中,创建全局变量并不常见。let和const关键字确保变量在块级范围内,有效防止全局范围的意外污染。JavaScript中的新模块系统(import|export语法)可以更轻松地创建全局可访问的值,而不会污染全局范围,因为它能够从模块中导出值并将这些值导入到其他文件中。然而,单例的一个常见用途是在整个应用程序中拥有某种全局状态。依赖可变对象的开发人员代码的多个模块可能会导致不可预测的行为(副作用)。通常,项目代码中的某些模块会修改全局状态,而某些模块会消费这些全局状态的数据。所以在这种情况下执行顺序很重要:我们不想不小心提前消费数据,因为数据通常在开始时是空的。项目代码越来越多,逻辑越来越复杂,很多组件相互依赖,这时候数据的流向就越来越难理解;React状态管理在React项目中,我们通常使用Redux或ReactContext等工具来管理全局状态,而不是使用单例。尽管这些工具提供类似于单例的全局状态行为,但它们通常提供“只读状态”而不是单例中的“可变状态”。使用Redux时,只有纯函数reducers才能在组件中通过调度程序发送动作后更新状态。虽然使用这些工具不会神奇地消除拥有全局状态的缺点,但我们至少可以确保全局状态按照我们期望的方式改变,因为组件不能直接更新状态。本文中的示例代码地址引用DoReactHooksreplaceRedux-EricElliottWorkingwithSingletonsinJavaScript-VijayPrasannaJavaScriptDesignPatterns:TheSingleton-SamierSaeedSingleton-RefactoringGurupatterns-dev