整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Javascript设计模式-单例模式

Javascript设计模式-单例模式

整个应用之内全局共享一个实例的模式,但它在JS中竟然是一种反模式

所谓单例模式是指遵循这个模式设计的类,仅会被实例化一次,并且其实例允许全局获取。单例模式下派生的示例允许我们在全局共享唯一实例,因此非常适合用于保存整个应用的全局状态。

首先让我们先看看在ES2015的语法下单例模式长什么样子。比如我们想要创建一个计数器类,用于保存全局的某个行为发生的次数,那么对于这个类的设计,我们应该考虑实现如下4个方法:

  • getInstance方法,用于返回全局唯一的实例
  • getCount方法用于返回当前实例的counter实例变量的值
  • increment方法用于为counter属性的值加一
  • decrement方法用于为counter属性的值减一
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方法却返回了两个不同对象的引用:他们不是严格相等的。

接下来我们需要想办法保证通过Counter类仅允许一个实例被创建。

一种解决方案就是创建一个instance变量。在Counter类的构造函数中,当新的实例被创建时,将实例对象的引用赋值给instance。在第一个实例被创建之后我们就可以通过instance变量是否有值来判断是否需要阻断新进入的实例化过程。如果需要阻断,那就意味着已经有一个被创建的对象存在。这显然不符合单例模式的标准,于是我们抛出一个异常以便用户明白哪里出了问题。

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 counter1=new Counter();
const counter2=new Counter();
// Error: You can only create one instance!

完美!现在我们已经不允许重复创建Counter类的实例了。

接下来让我们从counter.js文件中导出Counter实例。但在这之前,我们应该先对此实例执行freeze操作。Object.freeze方法能够保证实例的消费者无法修改单例。在被冻结的实例中的属性不可被添加或者修改,因此就降低了不小心覆盖单例属性值的风险。

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方法作为事件监听的回调函数,并通过调用counter实例的getCounter方法获取当前的计数器值
  • blueButton.js:导入Counter类,并且向蓝色按钮添加Counter实例的increment方法作为事件监听的回调函数,并通过调用counter实例的getCounter方法获取当前的计数器值

blueButton.js和redButton.js都引入了同一个counter.js的实例。也就是说这个实例作为Counter变量被导入到了两个文件中。

无论在redButton.js或是blueButton.js中调用increment方法时,Counter实例的counter属性值的更新会同步反应在两个文件中。不管我们点击的是红色按钮还是蓝色按钮:所有引用Counter实例的对象会共享同一个值。这就是为什么即便我们在不同的文件中调用递增方法,计数器的值都会增加的原因。


优 / 劣势

对一次性实例化的严格限制显然有节省内存空间的潜力。相对于每次都为新对象分配内存,通过单例模式我们仅需要为一个对象分配内存即可。然而,单例模式实际上被认为是一种反模式,并且应该避免在JavaScript中使用。

在很多编程语言中,比如Java或者C++,不可能像我们在JavaScript中一样直接创建对象。在这些面向对象的编程语言中,需要创建类,类创建对象。这些创建了的对象都拥有类实例的值,正如上面JavaScript示例中的instance实例一样。

但是像在上面示例代码中那样去声明一个单例模式的类显然有点大炮打蚊子。既然我们可以在JavaScript中直接创建对象,为什么不能直接创建一个对象来达到同样的目的呢?接下来我们说说使用单例模式的缺陷!

也,可以直接使用一个普通对象

仍然使用上面示例中的场景。只不过这一次我们将counter直接定义为一个拥有如下属性的简单对象:

  • count属性
  • increment方法为count属性递增一
  • decrement方法为count属性递减一
let count=0;

const counter={
  increment() {
    return ++count;
  },
  decrement() {
    return --count;
  }
};

Object.freeze(counter);
export { counter };

由于对象传递的是引用,所以redButton.js和blueButton.js都导入了counter对象的同一个引用。无论在哪个文件中修改count属性的值都会修改counter对象的值,这个结果可以在两个引用它的文件中观察到。

测试时的麻烦事

测试单例模式的测试代码需要点技巧。由于我们不能每次都创建一个新的实例,因此所有测试都依赖于上一个测试用例对于全局引入的那个对象的修改。在此例中,测试用例的顺序关系到整个测试套件的成败。在测试之后,我们还需要注意重置对象的状态,以便其他不相关但也引入了单例对象的其他测试用例不受影响。

import Counter from "../src/counterTest";

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);
});

代码的坏味道:隐藏对于单例模块的依赖

当引入某一个其他模块,在此例中是superCounter.js时,引用superCounter的代码可能并不很清楚它的深层依赖是一个单例对象。而假设同时其他文件比如index.js中引入这个superCounter类,并且调用了递增或者递减方法。如此一来我们可能会在无意间改变了单例对象的值。这会导致意外行为的发生,由于多个superCounter的实例运行与整个应用中,而多个superCounter实例实际上对单例对象进行了多次引用,于是所有对于单例对象的引用都会导致counter属性的修改。而这一修改很可能并不是代码作者的本意,但却无意间导致了意外行为的产生。

import Counter from "./counter";

export default class SuperCounter {
  constructor() {
    this.count=0;
  }

  increment() {
    Counter.increment();
    return (this.count +=100);
  }

  decrement() {
    Counter.decrement();
    return (this.count -=100);
  }
}

全局行为

单例实例本应在整个应用中被全局引用。但是全局变量本质上都会具有统一的特质:既然全局变量可以在全局作用域中使用,那我们理所当然的能够在全局任何地方获取到全局变量的引用。

于是问题出现了,在程序工程领域中,全局变量通常被认为是一个糟糕的设计。无意中对全局变量的覆盖最终造成全局变量污染,而这也是自有工程化的编写代码以来最常见的以外行为产生的原因。

在ES2015时代开启之后,创建全局变量已经不太常见。新引入的let和const关键字通过绑定代码块作用域,能够阻止开发人员无意间污染全局作用域。另外新引入的模块系统在更易于创建全局可获取的变量之余,免除了对全局作用域的污染。

但是单例模式最常见使用场景反而正是这种需要在整个应用中保存某种全局状态的用例。多个模块依赖于同一个可变对象的编程范式容易导致不可预知的行为发生。

常见的使用场景是代码库中的一方修改数据,另一方消费数据。因此各自的执行顺序则至关重要:毕竟我们可不希望一不小心在还没有任何数据的时候开始执行消费数据的代码。当整个应用越来越大,组件之间的依赖越来越复杂时,想要搞明白庞杂应用之内的相互调用关系也就变得愈加棘手。

React中的状态管理

在React中,应用通常会依赖一些状态管理工具,诸如Redux或者React Context,但单例模式的数据管理并不是选择之一。虽然这些工具的状态管理行为看上去似乎与单例模式有些相像,但他们通常会提供一种状态只读的能力以区别于单例模式下的可变数据模型。当使用Redux时,只有纯函数类型的reducer在收到组件通过dispatcher向其发送的action之后才允许更新状态。

虽然使用全局状态的缺点并不会因为引入了这些状态管理工具而奇迹般的消失,但由于组件无法直接更新状态,因此至少可以保证全局状态在变更时是来自于代码的真实意图。

原文地址:https://www.patterns.dev/posts/singleton-pattern/

么是策略模式

定义一系列的算法,封装起来,并且他们之间可以相互替换。将算法的使用和算法的实现分离开来。

现在有这么一个功能,过年发年终奖根据绩效来发不同的奖金。
绩效为 S 的有4倍工资,绩效为 A 的有3倍工资,绩效为 B 的有2倍工资。

通过构造函数实现

// 算法
var ps=function () { }
ps.prototype.calculate=function (salary) {
    return salary * 4;
}

var pA=function () { }
pA.prototype.calculate=function (salary) {
    return salary * 3;
}

var pB=function () { }
pB.prototype.calculate=function (salary) {
    return salary * 2;
}


// 算法的使用
var Bouns=function () {
    this.salary=null;     // 原始工资
    this.strategy=null;   // 绩效等级的策略对象
}

// 设置原始工资
Bouns.prototype.setSalary=function (salary) {
    this.salary=salary;
}

// 设置绩效等级的策略对象
Bouns.prototype.setStrategy=function (strategy) {
    this.strategy=strategy;
}

Bouns.prototype.getBouns=function () {
    return this.strategy.calculate(this.salary)
}

var bouns=new Bouns();
bouns.setSalary(1000)
bouns.setStrategy(new ps())
console.log(bouns.getBouns());  //=> 4000

函数对象的方式

var strategies={
    S: function (salary) {
        return salary * 4
    },
    A: function (salary) {
        return salary * 3
    },
    B: function (salary) {
        return salary * 2
    }
}

// 定义使用方式
var bouns=function (level, salary){
    return strategies[level](salary)
}

console.log(bouns('S',1000));  //=> 4000
console.log(bouns('A',1000))   //=> 3000

表单验证

一般表单验证

<body>
    <form action="#" method="post" id="form">
      请输入用户名:<input type="text" name="username" /><br />
      请输入密码:<input type="password" name="password" /><br />
      请输入手机号:<input type="text" name="phone" /><br />
      <button>提交</button>
    </form>
    <script>
      var form=document.getElementById("form");
      form.onsubmit=function () {
        if (form.username.value==="") {
          alert("用户名不能为空");
          return false;
        }
        if (form.password.value.length < 6) {
          alert("密码长度不能少于 6 位");
          return false;
        }
        if (!/(^1[3|5|8][0-9]{9}$)/.test(form.phone.value)) {
          alert("手机号码格式不正确");
          return false;
        }
      };
    </script>
</body>

缺点:

  • 如果表单继续增加那么代码不断变大,太多的if-else语句来覆盖全部的验证规则
  • 缺乏拓展性,如果新增一种校验规则或者密码的长度变了,那我们就需要对代码进行破坏
  • 复用性太差,其他表单使用那么就要把这一长串校验逻辑复制黏贴



运用策略模式的表单校验

、CSS的元素显示模式

1.作用:网页标签非常多,在不同地方会用到不同类型的标签,了解他们的特点可以更好的布局页面
2.HTML元素一般分为块元素和行内元素

(一)块元素

1.div为最典型的块元素,还有h1-h6,p,ul,ol,li等
2.特点

  • 比较霸道,自己独占一行
  • 高度,宽度,外边距以及内边距都可以控制
  • 宽度默认是容器(父级宽度)的100%
  • 是一个容器及盒子,里面可以放行内或块级元素

3.注意

  • 文字类的元素内不能使用块级元素,如:p
  • h1-h6等都为文字类的块级标签,里面也不能放其他块级元素

(二)行内元素

1.span为最典型的行内元素,还有a,strong,b,em,i,del,s,ins,u等
2.特点

  • 相邻行内元素在一行上,一行可以显示多个
  • 高度、宽度直接设置是无效的
  • 默认宽度就是它本身内容的宽度
  • 行内元素只能容纳文本或其他行内元素

3.注意

  • 链接里面不能再放链接
  • 特殊情况,链接a里面可以放块级元素,但给a链接转换一下块级模式最安全

(三)行内块元素

1.同时有块元素和行内元素的特点,如:img,input,td等
2.特点

  • 和相邻行内元素(行内块)在一行上,但是他们之间会有空白缝隙,一行可以显示多个(行内元素特点)
  • 默认宽度就是它本身内容的宽度(行内元素特点)
  • 高度,行高外边距以及内边距都可以控制(块级元素特点)

(四)元素显示模式转换

1.转化为块元素(display:block;)

2.转化为行内元素(display:inline;)

3.转化为行内块(display:inline-block;)

单行文字垂直居中的小技巧

总结


版权声明:本文为CSDN博主「依旧i248」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。


原文链接:https://blog.csdn.net/weixin_65548623/article/details/124192437