最近总想总结梳理下下这几年工作、学习所得,一直也不知道该如何下手才能更加有条理、逻辑分明的写出来,最近看了一段话很有感悟:
从 JavaScript 语言的基础知识到翻过“三座大山”——设计模式、数据结构、基础算法,再到开发框架的设计思想、核心原理和最佳实践,最后再在工程化或者更加综合的场景中应用自己所学。
这一段话很好的诠释了,一个前端开发从初级-中级-高级的成长过程,所以我也打算按这个思路依次展开论述。基础的直接跨过,先从三座大山之设计模式搞起。
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
使用设计模式的目的
为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
设计模式原则
- S – Single Responsibility Principle 单一职责原则
一个程序只做好一件事
如果功能过于复杂就拆分开,每个部分保持独立 - O – OpenClosed Principle 开放/封闭原则
对扩展开放,对修改封闭
增加需求时,扩展新代码,而非修改已有代码 - L – Liskov Substitution Principle 里氏替换原则
子类能覆盖父类
父类能出现的地方子类就能出现 - I – Interface Segregation Principle 接口隔离原则
保持接口的单一独立
类似单一职责原则,这里更关注接口 - D – Dependency Inversion Principle 依赖倒转原则
面向接口编程,依赖于抽象而不依赖于具体
使用方只关注接口而不关注具体类的实现
因为JS本身的特点,主要围绕前两个原则展开
设计模式分类(23种设计模式)
- 创建型
单例模式
原型模式
工厂模式
抽象工厂模式
建造者模式 - 结构型
适配器模式
装饰器模式
代理模式
外观模式
桥接模式
组合模式
享元模式 - 行为型
观察者模式
迭代器模式
策略模式
模板方法模式
职责链模式
命令模式
备忘录模式
状态模式
访问者模式
中介者模式
解释器模式
看着就蒙,也太多了,当然不是所有都需要掌握,了解些经常用到的就行,下面就经常使用的一些解释下:
1、工厂模式
工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。
class Product {
constructor(name) {
this.name = name
}
init() {
console.log('init')
}
fun() {
console.log('fun')
}
}
class Factory {
create(name) {
return new Product(name)
}
}
// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()
- 适用场景
如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性 - 优点
创建对象的过程可能很复杂,但我们只需要关心创建结果。
构造函数和创建者分离, 符合“开闭原则”
一个调用者想创建一个对象,只要知道其名称就可以了。
扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 - 缺点
添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
我们看这个方法的时候有没有发现这个很相似于 依赖注入模式
什么是依赖注入?
依赖注入是一种软件设计模式,一个对象或者函数需要使用其他的对象或者函数(依赖)的时候并不需要关心其内部实现细节,向需要的对象提供依赖关系的任务留给了注入者,有时候我们也称他们为汇编器、供应者或者容器。
例如,考虑视频游戏机如何只需要兼容的光盘或盒式磁带即可运行。不同的光盘或盒式磁带通常包含有关不同游戏的信息。玩家不需要了解游戏机的内部结构,通常只需要插入或更换游戏光盘即可玩游戏。
通过这种方式设计软件或编写代码,可以很容易地删除或替换组件(如上面的示例),编写单元测试,并减少代码库中的样板代码。
依赖注入是其他语言(特别是Java、C#和Python)中常用的一种模式,但在JavaScript中却不太常见。在下面的部分中,您将考虑这种模式的利弊、它在流行的JavaScript框架中的实现,以及如何在考虑依赖注入的情况下设置JavaScript项目。
你为什么需要依赖注入?
为了充分理解依赖注入为什么有用,了解IoC技术解决的问题很重要。例如,考虑前面的视频游戏机类比;尝试用代码表示控制台的功能。在本例中,您将把控制台称为NoSleepStation,并使用以下假设:
NoSleepStation控制台只能玩为NoSleepStation设计的游戏
控制台的唯一有效输入源是光盘
有了这些信息,可以通过以下方式实现NoSleepStation控制台:
// The GameReader class
class GameReader {
constructor(input) {
this.input = input;
}
readDisc() {
console.log("Now playing: ", this.input);
}
}
// The NoSleepStation Console
class class NSSConsole {
gameReader = new GameReader("TurboCars Racer");
play() {
this.gameReader.readDisc();
}
}
// use the classes above to play
const nssConsole = new NSSConsole();
nssConsole.play();
核心控制台逻辑位于“GameReader”类中,它有一个从属的“NSSConsole”。控制台的“play”方法使用“GameReader”实例启动游戏。然而,这里有一些很明显的问题,包括灵活性和测试。
灵活性
前面提到的代码是不灵活的,如果用户想玩不同的游戏,他们必须修改“NSSConsole”类,这类似于在现实生活中拆开控制台。这是因为核心依赖项“GameReader”类被硬编码到“NSSConsole”实现中。
依赖注入通过将类与其依赖项分离来解决这个问题,只在需要时提供这些依赖项。在前面的代码示例中,“NSSConsole”类真正需要的只是来自“GameReader”实例的“readDisc()”方法。
通过依赖注入,可以像这样重写前面的代码:
class GameReader {
constructor(input) {
this.input = input;
}
readDisc() {
console.log("Now playing: ", this.input);
}
changeDisc(input) {
this.input = input;
this.readDisc();
}
}
class NSSConsole {
constructor(gameReader) {
this.gameReader = gameReader;
}
play() {
this.gameReader.readDisc();
}
playAnotherTitle(input) {
this.gameReader.changeDisc(input);
}
}
const gameReader = new GameReader("TurboCars Racer");
const nssConsole = new NSSConsole(gameReader);
nssConsole.play();
nssConsole.playAnotherTitle("TurboCars Racer 2");
此代码中最重要的更改是“NSSConsole”和“GameReader”类已分离。虽然“NSSConsole”仍然需要“GameReader”实例才能运行,但它不必显式创建一个。创建“GameReader”实例并将其传递给“NSSConsole”的任务留给依赖注入提供程序。
测试
依赖注入最重要的优点之一是在单元测试中。通过将提供类依赖项的工作委托给外部提供程序,可以使用模拟或存根来代替未测试的对象:
// nssconsole.test.js
const gameReaderStub = {
readDisc: () => {
console.log("Read disc");
},
changeDisc: (input) => {
console.log("Changed disc to: " + input);
},
};
const nssConsole = new NSSConsole(gameReaderStub);
只要依赖项的接口(公开的方法和属性)不变,就可以使用具有相同接口的简单对象单元测试来代替实际实例。
通过以上例子可以看出:
工厂模式:这些实例都有相同的行为,使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性
依赖注入:更加解藕类已分离,虽然“NSSConsole”仍然需要“GameReader”实例才能运行,但它不必显式创建一个,创建“GameReader”实例并将其传递给“NSSConsole”的任务留给依赖注入提供程序
只是我自己的想法有什么不对,还请大家多指教
2、单例模式
1、单例模式限制实例的数量必须是 一个,这个唯一的实例被称作单例(singleton)。单例模式适合应用在一个广泛的系统范围内、在一个集中的地方来处理某些逻辑。
2、减少了全局变量的使用,从而清除了命名空间污染和命名冲突的风险。
什么是单例模式呢?
3、单例模式的核心:只创建一个实例化对象,通过调用函数时输入不同的数据实现程序效果,即使多次调用构造函数,也要确保只生成一个实例化对象,不是每次调用都会生成一个新的实例化对象。
其实就是判断一下,他曾经有没有 new 出来过对象
如果有,就还继续使用之前的那个对象,
如果没有,那么就给你 new 一个
class createObj{
constructor(){}
fun1(name){
console.log(name)
}
fun2(age){
console.log(age)
}
fun3(name,age){
console.log(name,age)
}
}
let obj='';
function fun(){
if(obj===' '){
obj=new createObj();
return obj;
}else{
return obj;
}
}
const newObj1=fun();
- 优点
划分命名空间,减少全局变量
增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
且只会实例化一次。简化了代码的调试和维护 - 缺点
由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一个单元一起测试。 - 场景例子
定义命名空间和实现分支型方法
登录框
vuex 和 redux中的store
3、组合模式
就是把几个构造函数的启动方式组合再一起然后用一个 ”遥控器“ 进行统一调用(准备一个 组合模式 的构造函数 其实就是准备一个构造函数一次调用那几个构造函数
class TrainOrder {
create () {
console.log('创建火车票订单')
}
}
class HotelOrder {
create () {
console.log('创建酒店订单')
}
}
class TotalOrder {
constructor () {
this.orderList = []
}
addOrder (order) {
this.orderList.push(order)
return this
}
create () {
this.orderList.forEach(item => {
item.create()
})
return this
}
}
// 可以在购票网站买车票同时也订房间
let train = new TrainOrder()
let hotel = new HotelOrder()
let total = new TotalOrder()
total.addOrder(train).addOrder(hotel).create()
- 场景
希望用户忽略组合对象和单个对象的不同,用户将统一地使用组合结构中的所有对象(方法) - 缺点
如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。
4、原型模式
原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象。
class Person {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Student extends Person {
constructor(name) {
super(name)
}
sayHello() {
console.log(`Hello, My name is ${this.name}`)
}
}
let student = new Student("xiaoming")
student.sayHello()
原型模式,就是创建一个共享的原型,通过拷贝这个原型来创建新的类,用于创建重复的对象,带来性能上的提升。
5、观察者模式
观察者模式包含观察目标和观察者两类对象,
一个目标可以有任意数目的与之相依赖的观察者
一旦观察目标的状态发生改变,所有的观察者都将得到通知。
观察者模式(Observer),通常也被叫做 发布-订阅模式 或者 消息模式
官方解释:当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,
解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题
class Observer {
constructor() {
this.message = {}
}
on(type, fn) {
// 判断消息盒子里面有没有设置事件类型
if (!this.message[type]) {
// 证明消息盒子里面没有这个事件类型
// 那么我们直接添加进去
// 并且让他的值是一个数组,再数组里面放上事件处理函数
this.message[type] = [fn]
} else {
// 证明消息盒子里面有这个事件类型
// 那么我们直接向数组里面追加事件处理函数就行了
this.message[type].push(fn);
}
}
emit(type, ...arg) {
// 判断你之前有没有订阅过这个事件
if (!this.message[type]) return;
// 如果有,那么我们就处理一下参数
const event = {
type: type,
arg: arg || {}
};
// 循环执行为当前事件类型订阅的所有事件处理函数
this.message[type].forEach(item => {
item.call(this, event);
});
}
off() {
}
}
const o = new Observer();
// 准备两个事件处理函数
function a(e) {
console.log(e);
console.log('hello');
}
function b(e) {
console.log(e);
console.log('world');
}
// 订阅事件
o.on('abc', a);
o.on('abc', b);
// 发布事件(触发)
o.emit('abc', '100', '200', '300'); // 两个函数都回执行
// 移除事件
// o.off('abc', 'b');
// 再次发布事件(触发)
// o.emit('abc', '100', '200', '300'); // 只执行一个 a 函数了
6、构造器模式
这个是我们认知中的一个不过我发现没在这些专有名词中,我先写出来大家看看
构造器是一个当新建对象的内存被分配后,用来初始化该对象的一个特殊函数,在 JavaScript 中几乎所有的东西都是对象。
同时构造器可以使用的参数,以在第一次创建对象时,设置成员属性的方法的值。
对象定义:状态、属性、行为
function methods(age, name) {
this.age = age;
this.name = name;
}
let a1 = new methods(12, 'li');
let b2 = new methods(24, 'w');
7、装饰器模式
- 动态地给某个对象添加一些额外的职责,,是一种实现继承的替代方案
- 在不改变原对象的基础上,通过对其进行包装扩展,使原有对象可以满足用户的更复杂需求,而不会影响从这个类中派生的其他对象
class Cellphone {
create() {
console.log('生成一个手机')
}
}
class Decorator {
constructor(cellphone) {
this.cellphone = cellphone
}
create() {
this.cellphone.create()
this.createShell()
}
createShell() {
console.log('生成手机壳')
}
}
// 测试代码
let cellphone = new Cellphone()
cellphone.create()
console.log('------------')
let dec = new Decorator(cellphone)
dec.create()