共计 4284 个字符,预计需要花费 11 分钟才能阅读完成。
在整个利用中共享一个「繁多的」「全局的」实例
实现
单例是能够全局拜访并且仅实例化一次的类。这个 繁多实例 是能够在整个利用中被共享的,这使得单例非常适合管理应用程序中的全局状态
首先,让咱们看看应用 ES2015 Class 语法的单例会是什么样子。举个例子,咱们构建一个 名为 Counter
的类
- getInstance 办法:返回实例
- getCount 办法:获取以后 counter 的值
- increment 办法:counter 的值 加 1
- decrement 办法:counter 的值 减 1
let counter = 0;
class Counter {getInstance() {return this;}
getCount() {return counter;}
increment() {return ++counter;}
decrement() {return --counter;}
}
然而,这个类不是规范的单例!单例只容许被实例化一次。而当初,咱们能够创立 Counter 类的多个实例
let counter = 0;
class Counter {getInstance() {return this;}
getCount() {return counter;}
increment() {return ++counter;}
decrement() {return --counter;}
}
const counter1 = new Counter();
const counter2 = new Counter();
console.log(counter1.getInstance() === counter2.getInstance()); // false
两次运行 new 办法,能够发现实例化进去的 counter1 与 counter2 是不同的实例。通过 getInstance 失去的值是两个不同的实例的援用,所以 counter1 与 counter2 不是严格相等的
让咱们确保 Counter 类只能创立 的一个实例
确保只能创立一个实例的一种办法是创立一个名为 instance 的变量。在构造函数中,咱们设置了 instance 变量等于用 new 办法创立进去的实例对象。咱们能够通过判断 instance 变量的值是否为空来保障不会有屡次的实例化行为。如果 instance 变量不为空,则证实曾经实例化过了,则不须要再进行实例化;如果再进行实例化,则会抛出谬误让用户感知到
let instance;
let counter = 0;
class Counter {constructor() {if (instance) {throw new Error("只能创立一个实例!");
}
instance = this;
}
getInstance() {return this;}
getCount() {return counter;}
increment() {return ++counter;}
decrement() {return --counter;}
}
const counter1 = new Counter();
const counter2 = new Counter();
// Error: 只能创立一个实例!
好的,咱们当初曾经做到能够避免创立多个实例了。
让咱们从 counter.js
导出 Counter
实例。在导出之前,咱们须要「解冻」这个实例。Object.freeze
办法能够避免应用这个实例的代码批改这个实例。咱们无奈增加或批改被解冻的实例上的属性,这升高了 Singleton 上的值被笼罩的危险。
let instance;
let counter = 0;
class Counter {constructor() {if (instance) {throw new Error("You can only create one instance!");
}
instance = this;
}
getInstance() {return this;}
getCount() {return counter;}
increment() {return ++counter;}
decrement() {return --counter;}
}
const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;
让咱们看一个实现 Counter 的利用例子。文件如下:
- counter.js:蕴含 Counter 类,并将 Counter 实例默认导出
- index.js:加载 redButton.js 和 blueButton.js 模块
- redButton.js:导入 Counter,并将 Counter 的 increment 办法作为事件监听器增加到红色按钮,并通过调用 getCount 办法打印 counter 的以后值
- blueButton.js:导入 Counter,并将 Counter 的 increment 办法作为事件监听器增加到蓝色按钮,并通过调用 getCount 办法打印 counter 的以后值
blueButton.js 和 redButton.js 都从 counter.js 导入雷同的实例。
当咱们不论从 blueButton.js 或者 redButton.js 触发 increment 办法,Counter 实例上的 counter 属性的值都会更新。单例的值会在所有的文件中被共享,只管咱们是在不同的文件中触发了更新。
长处 or 毛病
通过限度实例化来保障只有一个实例对象能够节约很多内存。咱们不用在每次实例化都为实例对象申请内存,单例的实例对象只须要申请一次内存就能够在整个利用中应用。然而单例模式常常被当作「反模式」,并且须要尽量避免在 javascript 中应用到它。
在许多编程语言中,例如 Java 或 C++,不可能像在 JavaScript 中那样间接创建对象。在那些面向对象的编程语言中,咱们须要创立一个类,它会创立一个对象。该创立的对象具备类实例的值,就像 JavaScript 示例中的实例值一样。
然而,下面示例中显示的类实现实际上是矫枉过正。咱们能够间接在 JavaScript 中创建对象,比方咱们能够简略地应用 对象字面量 来实现完全相同的后果。让咱们来介绍一下应用单例的一些毛病!
应用对象字面量
让咱们应用与之前看到的雷同的示例。然而这一次,Counter 只是一个蕴含以下内容的对象字面量:
- 一个 count 属性
- increment 办法:counter 的值 加 1
- decrement 办法:counter 的值 减 1
let count = 0;
const counter = {increment() {return ++count;},
decrement() {return --count;}
};
Object.freeze(counter);
export {counter};
因为对象是通过援用传递的,redButton.js 和 blueButton.js 都在导入对同一个 counter 对象的援用。批改这些文件中的任何一个中触发 counter 的 increment 办法,count 值的扭转这在两个文件中都是可感知到的。
测试
测试单例模式的代码会比拟麻烦。因为咱们不能每次都创立新实例,因而所有测试都依赖于对上一次测试的全局实例的批改。在这种状况下,测试的程序很重要,一个一般的批改可能会导致整个测试 case 失败。测试后,咱们须要重置整个实例以重置测试所做的批改。
import counterInstance from '../src/counter'
const Counter = counterInstance.counter
test("incrementing 1 time should be 1", () => {Counter.increment();
expect(Counter.getCount()).toBe(1);
});
test("incrementing 3 extra times should be 4", () => {Counter.increment();
Counter.increment();
Counter.increment();
expect(Counter.getCount()).toBe(4);
});
test("decrementing 1 times should be 3", () => {Counter.decrement();
expect(Counter.getCount()).toBe(3);
})
全局行为
一个单例对象能够在整个利用的任何中央被获取和应用。全局变量也有雷同的行为:全局变量能够在全局作用域被获取和应用,所以也能够在整个利用的任何中央被获取和应用。
设置全局变量个别被认为是一种比拟不好的设计,因为批改全局变量的值可能会净化全局作用域,这可能会导致一些意想不到的副作用。
在 ES 2015,创立全局变量是不常见的。let
和 const
关键字保障了变量在块级作用域下,从而无效的避免意外净化全局作用域。JavaScript 中的新模块零碎(import | export 语法)通过可能从模块中导出值,并将这些值导入其余文件中,使得创立全局可拜访的值更容易而不会净化全局作用域。
然而,单例的常见用法是在整个利用中领有某种全局状态。开发者的代码有多个模块同时依赖某个可变对象可能会导致 不可预知的行为(副作用)。
通常状况下,我的项目代码中某些模块会批改全局状态,而某些模块又会去生产这些全局状态的数据。所以执行程序在这种状况下就比拟重要了:咱们不想要意外的提前生产数据,因为一开始是数据个别是空的。随着我的项目代码越来越多,逻辑越来越简单,许许多多的组件相互依赖,这个时候了解数据的流向就变得越来月辣手;
React 的状态治理
在 React 的我的项目中,咱们通常应用像 Redux 或者 React Context 这样的工具来治理全局状态而不是应用单例。尽管这些工具提供的全局状态行为和单例很像,但他们个别会提供「只读状态」而不是像单例中应用「可变状态」。应用 Redux 时,只有纯函数 reducer 能够在组件中通过 dispatcher 发送一个 action 后更新状态。
只管应用这些工具不会神奇地打消领有全局状态的毛病,但咱们至多能够确保全局状态依照咱们的预期形式发生变化,因为组件不能间接更新状态。
本文示例代码地址
参考文献
- Do React Hooks replace Redux – Eric Elliott
- Working with Singletons in JavaScript – Vijay Prasanna
- JavaScript Design Patterns: The Singleton – Samier Saeed
- Singleton – Refactoring Guru
- patterns-dev