本文基于《JavaScript设计模式与开发实践》一书,用一些例子总结一下JS常见的设计你模式与实现方法。

定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

实现代理模式的单例

首先创建一个CreateDiv的构造函数 只负责创建div

  1. var CreateDiv = function(html) {
  2. this.html = html;
  3. this.init();
  4. }
  5. CreateDiv.prototype.init = function(){
  6. var div = document.createElement('div');
  7. div.innerHTML = this.html;
  8. document.body.appendChild(div)
  9. }

接下来引入代理类proxySingletonCreateDiv

  1. var ProxysingletonCreateDiv = (function(){
  2. var instance;
  3. return function(html) {
  4. if (!instance) {
  5. instance = new CreateDiv(html)
  6. }
  7. return instance
  8. }
  9. })()
  10. var a = new ProxysingletonCreateDiv('test1');
  11. var b = new ProxysingletonCreateDiv('test2');
  12. console.log(a === b) // true

通用的惰性单例

惰性单例是指在需要的时候才创建对象实例, 而不像之前的代码那样,利用自执行行数在代码执行时就把对象实例创建。
当我们点击登录按钮时,页面中可能出现一个弹框,而这个弹框是唯一的,无论点击多少次登录按钮,弹框只会被创建一次,那么这种情况下就适合用单例模式来创建弹框。

  1. <html>
  2. <body>
  3. <button id="loginBtn">登录</button>
  4. </body>
  5. <script>
  6. var loginLayer = (function(){
  7. var div = document.createElement('div');
  8. div.innerHtml = '我是登录浮窗';
  9. div.style.display = 'none';
  10. document.body.appendChild(div)
  11. return div;
  12. })();
  13. document.getElementById('loginBtn').onclick = function() {
  14. loginLayer.style.display = 'block'
  15. }
  16. </script>
  17. </html>


当打开一个网站时,需要登录,但登录的弹窗只会在点击登录按钮的时候出现,甚至有的网站不需要登录就能直接浏览。那么很有可能将白白浪费一些DOM节点。这时我们并不需要再页面加载时就创建一个弹窗。

正确的打开方式可以用下面的方式实现。

  1. <html>
  2. <body>
  3. <button id="loginBtn">登录</button>
  4. </body>
  5. <script>
  6. var createLoginLayer = (function(){
  7. var div;
  8. return function(){
  9. if (!div) {
  10. var div = document.createElement('div');
  11. div.innerHtml = '我是登录浮窗';
  12. div.style.display = 'none';
  13. document.body.appendChild(div)
  14. }
  15. return div;
  16. }
  17. })();
  18. document.getElementById('loginBtn').onclick = function() {
  19. var loginLayer = createLoginLayer();
  20. loginLayer.style.display = 'block';
  21. }
  22. </script>
  23. </html>

通用的惰性单例的实现就是要抽离所有的单例模式都是要实现的——控制只有一个对象。那么我们来看看控制只有一个对象的操作抽象出来是个什么样子:

  1. var obj;
  2. if (!obj) {
  3. obj = xxx
  4. }

现在我们局把如何管理单例的逻辑从原来的代码抽离出来,这些逻辑被封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数:

  1. var getSingle = function(fn) {
  2. var result;
  3. return function(){
  4. return result || (result = fn.apply(this, arguments))
  5. }
  6. }

接下来将用于创建登录浮窗的方法用参数fn的形式传入getSingle,我们可以不仅传入createLoginLayer,还能传入createScript、createIframe等。之后再让getSingle返回一个新的函数,并且用一个变量result来保存fn的计算结果。result变量因为保存在闭包中,它通用的惰性单例永远不会被销毁。在将来的请求中,如果resutlt已经被赋值,那么它将返回这个值。代码如下。

  1. var createLoginLayer = function(){
  2. var div = document.createElement('div');
  3. div.innerHtml = '我是登录浮窗';
  4. div.style.display = 'none';
  5. document.body.appendChild(div)
  6. return div;
  7. };
  8. var createSingleLoginLayer = getSingle(createLoginLayer)
  9. document.getElementById('loginBtn').onclick = function() {
  10. var loginLayer = createSingleLoginLayer();
  11. loginLayer.style.display = 'block'
  12. }

至此我们实现了一个getSingle函数来帮我们实现只有一个实例对象的目的,并且将实例对象要做的职责独立出来,两个方法互不干扰。

完整案例

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Document</title>
  7. <style>
  8. .prompt {
  9. position: fixed;
  10. top: 10px;
  11. left: 50%;
  12. transform: translateX(-50%);
  13. font-size: 18px;
  14. color: #ffffff;
  15. background-color: #df8263;
  16. border-radius: 3px;
  17. padding: 10px 30px;
  18. min-width: 100px;
  19. }
  20. .prompt p {
  21. padding: 0;
  22. margin: 0;
  23. }
  24. .prompt.default {
  25. background-color: #df8263;
  26. }
  27. .prompt.warning {
  28. background-color: #d9b131;
  29. }
  30. .prompt.success {
  31. background-color: #31d952;
  32. }
  33. .prompt.fail {
  34. background-color: #cab9b9;
  35. }
  36. .prompt-close {
  37. position: absolute;
  38. top: 50%;
  39. right: 10px;
  40. transform: translateY(-50%);
  41. width: 20px;
  42. height: 20px;
  43. display: flex;
  44. justify-content: center;
  45. align-items: center;
  46. cursor: pointer;
  47. }
  48. </style>
  49. </head>
  50. <body>
  51. <button id="login">登录</button>
  52. <script>
  53. var PromptComponent = function () {
  54. this.createDom = this.singlePrompt(this.createPrompt);
  55. }
  56. PromptComponent.prototype.init = function (params) {
  57. var style = params.style || 'default',
  58. message = params.message || '提示!',
  59. duration = parseInt(params.duration) || 1500;
  60. var promptDom = this.createDom(style, message);
  61. promptDom.style.display = 'block';
  62. setTimeout(function () {
  63. promptDom.style.display = 'none';
  64. }, duration)
  65. }
  66. PromptComponent.prototype.singlePrompt = function (fn) {
  67. var instance;
  68. return function () {
  69. return instance || (instance = fn.apply(this, arguments));
  70. }
  71. }
  72. PromptComponent.prototype.createPrompt = function (style, message) {
  73. var div = document.createElement('div');
  74. var strHtml = '<p>'+ message +'</p><span class="prompt-close">×</span>'
  75. div.className = 'prompt ' + style;
  76. div.innerHTML = strHtml;
  77. div.style.display = 'none';
  78. div.addEventListener('click', function () {
  79. div.style.display = 'none';
  80. }, false)
  81. document.body.appendChild(div);
  82. return div;
  83. }
  84. var warning = new PromptComponent();
  85. document.getElementById('login').addEventListener('click', function () {
  86. new Promise((resolve, reject) => {
  87. setTimeout(() => {
  88. resolve({code: 200})
  89. }, 1000)
  90. }).then(res => {
  91. if (res.code == 200) {
  92. warning.init({
  93. style: 'success',
  94. message: '登陆成功'
  95. })
  96. } else {
  97. warning.init({
  98. style: 'fail',
  99. message: '登陆失败'
  100. })
  101. }
  102. })
  103. }, false)
  104. </script>
  105. </body>
  106. </html>

经验案例: https://zhuanlan.zhihu.com/p/145271407