javascript 设计模式(修言小册干货,长文建议收藏)

前言

最近拜读了一下修言大神的JavaScript 设计模式核⼼原理与应⽤实践, 对于现阶段的我,能够说受益不浅,本身也学着总结下,分享下干货,力求共同进步!javascript

在软件工程中,设计模式(design pattern)是对软件设计中广泛存在(反复出现)的各类问题,所提出的解决方案。 ——维基百科html

先提炼下,文章缺乏小册前两章,归纳来讲:前端

  • 技术寒冬,前端真的很缺人,经得起推敲的人太少;
  • 前端技术面很大,想把每一面都涵盖到很难,不建议你们死磕
  • 追求极致设计,讲究道术结合;
  • 掌握核心科技,以‘不变’应万变;
  • 前端工程师,首先是软件工程师;

这里强调一下以不变应万变的中不变的是什么,由于这关系到你的核心竞争力是什么在哪里。所谓‘不变的东西’说的驾驭技术的能力,具体来讲分如下三个层次:vue

  • 能用健壮的代码去解决具体问题;
  • 能用抽象的思惟去应对复杂的系统;
  • 能用工程化的思想去规划更大规模的业务;

这三种能力在你的成长过程当中是层层递进的关系,然后两种能力能够说是对架构师的要求。能作到第一点,而且把它作到扎实、作到娴熟的人,已经堪称同辈楷模java

不少人缺少的并非这种高瞻远瞩的激情,而是咱们前面提到的“不变能力”中最基本的那一点——用健壮的代码去解决具体的问题的能力。这个能力在软件工程领域所对标的经典知识体系,偏偏就是设计模式。因此说,想作靠谱开发,先掌握设计模式react

小册的知识体系与格局,用思惟导图展现以下: jquery

下面涉及到的是小册中细讲的设计模式;git

目录:

  • 工厂模式
  • 单例模式
  • 原型模式
  • 修饰器模式
  • 适配器模式
  • 代理模式
  • 策略模式
  • 状态模式
  • 观察者模式
  • 迭代器模式

工厂模式

定义: 工厂模式其实就是将建立的对象的过程单独封装github

简单工厂模式

结合定义咱们来看一段需求,公司须要编写一个员工信息录入系统,当系统里面只建立本身的时候咱们能够:面试

const lilei = {
    name = 'lilei',
    age: 18,
    career: 'coder'
}
复制代码

固然员工确定不会是一个,而且会不断加入,因此使用构造函数写成:

function User(name, age, career) {
    this.name = name;
    this.age = age;
    this.career = career;
}
const lilei = new User('lilei', 18, 'coder')
const lilei = new User('hanmeimei', 20, 'product manager')
// ...

复制代码

上面的代码其实就是构造器,关于构造器模式后面会有具体介绍,咱们采用ES5的构造函数来实现,ES6的class其本质仍是函数,class只不过是语法糖,构造函数,才是它的这面目。

需求继续增长,career字段能携带的信息有限,没法完整诠释人员职能,要给每一个工种的用户添加上一个个性字段,来描述相应的职能。

function Coder(name, age){
    this.name = name;
    this.age = age;
    this.career = 'coder';
    this.work = ['敲代码', '摸鱼', '写bug'];
}
function ProductManager(name, age) {
    this.name = name;
    this.age = age;
    this.career = 'product manager';
    this.work = ['订会议室', '写PRD', '催更']
}

function Factory(name, age, career) {
    switch(career) {
        case 'coder':
            return new Coder(name, age);
            break;
        case 'product manager':
            return new ProductManager(name, age);
            break;
        ...
    }
}
复制代码

如今看至少咱们不用操心构造函数的分配问题了,那么问题来了,你们都看到了省略号了吧,这就意味着每多一个工种就要手动添加一个类上去,假若有几十个工种,那么就会有几十个类?相对来讲,咱们仍是须要不停的声明新的构造函数。

so:

function User(name, age, career, work) {
    this.name = name;
    this.age = age;
    this.career = career;
    this.work = work;
}

function Factory(name, age, career) {
    let work;
    switch() {
        case'coder':
            work =  ['写代码','摸鱼', '写bug'];
            break;
         case 'product manager':
            work = ['订会议室', '写PRD', '催更']
            break
        case 'boss':
            work = ['喝茶', '看报', '见客户']
        case 'xxx':
            // 其它工种的职责分配
            ...
    }
    return new User(name, age, career)
}
复制代码

这样一来咱们须要作事情就简单多了,只须要无脑传参就能够了,不须要手写无数个构造函数,剩下的Factory都帮咱们处理了。

工厂模式的目的就是为了实现无脑传参,就是为了爽。 -修言

乍一看没什么问题,可是经不起推敲呀。首先映入眼帘的 Bug,是咱们把 Boss 这个角色和普通员工塞进了一个工厂。职能和权限会有很大区别,所以咱们须要对这个群体的对象进行单独的逻辑处理。

怎么办?去修改 Factory的函数体、增长管理层相关的判断和处理逻辑吗?单从功能上来说是可行的,可是这样操做到后期会致使Factory异常庞大,稍有不慎就有可能摧毁整个系统,这一切悲剧的根源只有一个——没有遵照开放封闭原则

开放封闭原则对拓展开放,对修改封闭。说得更准确点,软件实体(类、模块、函数)能够扩展,可是不可修改

由此咱们引出抽象工厂模式

抽象工厂模式

抽象工厂这块知识,对入行以来一直写纯 JavaScript 的同窗可能不太友好——由于抽象工厂在很长一段时间里,都被认为是 Java/C++ 这类语言的专利。

定义:抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式能够向客户端提供一个接口,使客户端在没必要指定产品的具体的状况下,建立多个产品族中的产品对象。

说白了抽象工厂模式,我认为就是工厂模式的扩充版,简单工厂生产实例,抽象工厂生产的是工厂,实际上是实现子类继承父类的方法。

这里比较绕,因此我可耻的把原文的例子搬过来了括弧笑,让咱们来看一下:

假如要作一个山寨手机,基本组成是操做系统(Operating System,咱们下面缩写做 OS)和硬件(HardWare)组成,咱们须要开一个手机工厂才能量产,可是咱们又不知道具体生产的是什么手机,只知道有这两部分组成,因此我先来一个抽象类来约定住这台手机的基本组成

class MobilePhoneFactory {
    // 提供操做系统的接口
    createOS (){
        throw new Error('抽象工厂方法不容许直接调用,你须要将我重写!');
    }
    // 提供硬件的接口
    createHardWare(){
        throw new Error('抽象工厂方法不容许直接调用,你须要将我重写!');
    }
}
复制代码

楼上这个类除了约定手机流水线的通用能力以外,啥也不干,若是你尝试new一个MobilePhoneFactory实力并调用里面的方法,它都会给你报错。在抽象工厂模式里,楼上这个类就是咱们食物链顶端最大的Boss——AbstractFactory(抽象工厂);

抽象工厂不干活,具体工厂(ConcreteFactory)干活!当咱们明确了生产方案之后就能够化抽象为具体,好比如今须要生产Android系统 + 高通硬件手机的生产线,咱们给手机型号起名叫FakeStar,那我就能够定制一个具体工厂

//具体工厂继承自抽象工厂
class FakeStarFactory entends MobilePhptoFactory {
    cresteOS() {
        // 提供安卓系统视力
        return new AndroidOS();
    }
    createHardWare() {
        // 提供高通硬件实例
        return new QualcommHardeWare()
    }
}
复制代码

这里咱们在提供按安卓系统的时候,调用了两个构造函数:AndroidOS和QualcommHardWare,它们分别用于生成具体的操做系统和硬件实例。像这种被咱们拿来用于 new 出具体对象的类,叫作具体产品类(ConcreteProduct)。具体产品类每每不会孤立存在,不一样的具体产品类每每有着共同的功能,好比安卓系统类和苹果系统类,它们都是操做系统,都有着能够操控手机硬件系统这样一个最基本的功能。所以咱们能够用一个抽象产品(AbstractProduct)类来声明这一类产品应该具备的基本功能。

// 定义操做系统这类产品的抽象产品类
class OS {
    controlHardWare() {
        throw new Error('抽象产品方法不容许直接调用,你须要将我重写!');
    }
}

// 定义具体操做系统的具体产品类
class AndroidOS extends OS {
    controlHardWare() {
        console.log('我会用安卓的方式去操做硬件')
    }
}

class AppleOS extends OS {
    controlHardWare() {
        console.log('我会用🍎的方式去操做硬件')
    }
}
...
复制代码

硬件产品同理这里就不重复了。如此一来,当咱们须要生产一台FakeStar手机时,咱们只须要:

// 这是个人手机
const myPhone = new FakeStarFactory()
// 让它拥有操做系统
const myOS = myPhone.createOS()
// 让它拥有硬件
const myHardWare = myPhone.createHardWare()
// 启动操做系统(输出‘我会用安卓的方式去操做硬件’)
myOS.controlHardWare()
// 唤醒硬件(输出‘我会用高通的方式去运转’)
myHardWare.operateByOrder()
复制代码

当有一天须要产出一款新机投入市场的时候,咱们是否是不须要对抽象工厂MobilePhoneFactory作任何修改,只须要拓展它的种类:

class newStarFactory extends MobilePhoneFactory {
    createOS() {
        // 操做系统实现代码
    }
    createHardWare() {
        // 硬件实现代码
    }
}
复制代码

这么个操做,对原有的系统不会形成任何潜在影响所谓的“对拓展开放,对修改封闭”就这么圆满实现了。

总结

抽象工厂模式的四个角色:

  • 抽象工厂(不能用于生成具体实例):用于成名最终目标产品的共性。
  • 具体工厂:继承抽象工厂、实现抽象工厂里声明的方法,用于建立具体的产品类。
  • 抽象产品(不能用于生成具体实例):用于具体产品中共性的抽离。
  • 具体产品:好比上面咱们提到的具体的硬件等;

单例模式

定义: 保证一个类只有一个实例,并提供一个访问他的全局访问点。

通常状况下咱们建立一个类(本质是构造函数)后,能够经过new关键字调用构造函数进而生成任意多的实例对象:

class SingleDog {
    show() {
        console.log('我是一只单身狗');
    }
}

const s1 = new SingleDog();
const s2 = new SingleDog();

// false 
s1 === s2 
复制代码

很明显s1与s2没有任何瓜葛,由于每次new出来的实例都会给咱们开辟一块新的内存空间。那么咱们怎么才能让对此new出来都是那惟一的一个实例呢?那就须要咱们的构造函数具有判断本身是否被建立过一个实例的能力。

核心代码:

// 定义Storage
class SingleDog {
    show() {
        console.log('我是一只单身狗');
    }
    getInstace() {
        // 判断是否已经new过一个实例
        if(!SingleDog.instance){
            // 若这个惟一实例不存在,则建立它
            SingleDog.instance = new SingleDog();
        }
        // 若是有则直接返回
        return SingleDog.instance;
    }
}
const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()

// true
s1 === s2
复制代码

生产实践:redux、vuex中的Store,或者咱们常用的Storage都是单例模式。

咱们来实现一下Storage:

class Storage{
    static getInstance() {
        if(!Storage.instance) {
            Storage.instance = new Storage();
        }
        return  Storage.instance;
    }
    getItem(key) {
        return localStorage.getItem(key);
    }
    setItem(key, value){
        return localStorage.setItem(key, value);
    }
}
const storage1 = Storage.getInstance()
const storage2 = Storage.getInstance()

storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')

// 返回true
storage1 === storage2
复制代码

思考一下如何实现一个全局惟一的模态框呢?

原型模式

原型模式不只是一种设计模式,它仍是一种编程范式(programming paradigm),是 JavaScript 面向对象系统实现的根基。

原型模式这一章节小册并无讲述什么稀奇的知识点主要是关于Prototype相关的须要强调的是javascript是以原型为中心的语言,ES6中的类实际上是原型继承的语法糖。

ECMAScript 2015 中引入的JavaScript类实质上是JavaScript现有的基于原型的继承的语法糖。类语法不会为 JavaScript 引入新的面向对象的继承模型。 ——MDN

在原型模式下当咱们想要建立一个对象时会先找到一个对象做为原型,而后在经过克隆原型的方式来建立出一个与原型同样(共享一套数据/方法)的对象。

其实谈原型模式就是在谈原型范式,原型编程范式的核心思想就是利用实例来描述对象,用实例做为定义对象和继承的基础。在JavaScript中,原型编程范式的体现就是基于原型链的继承。这其中,对原型、原型链的理解是关键。

这里应当注意,在一些面试中,面试官可能会能够混淆javascript中的原型范式和强类型语言中的原型模式,当他们这么作的时候颇有多是为了考察你对对象深拷贝的理解。

在JavaScript中实现深拷贝,有一种取巧的方式——JSON.stringify:

注意这方法是本身的局限性的,好比没法处理function、没法处理正则等等,咱们在面试中不该该局限于这种方法,应该拓展出更多的可实施方案,好比递归等其余方法,回答递归的时候应该注意递归函数中值的类型的判断以及递归爆栈的问题。

深拷贝是没有完美方案的,每一种方案都有他本身的case

关于深拷贝,有想深刻研究的,小册做者在这里推荐了个比较好的地址能够关注下:

装饰器模式

装饰器模式(DecoratorPattern)容许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是做为现有的类的一个包装。

优势:装饰类和被装饰类能够独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式能够动态扩展一个实现类的功能。

缺点:多层装饰比较复杂。

类装饰器的参数

当咱们给一个类添加装饰器时:

function classDecorator(target) {
    target.hasDecorator = true
  	return target
}

// 将装饰器“安装”到Button类上
@classDecorator
class Button {
    // Button类的相关逻辑
}
复制代码

此处的 target 就是被装饰的类自己。看着眼熟不?react中的高级组件(HOC)就是使用这个实现的。

方法装饰器的参数

而当咱们给一个方法添加装饰器时:

function funcDecorator(target, name, descriptor) {
    let originalMethod = descriptor.value
    descriptor.value = function() {
        console.log('我是Func的装饰器逻辑');
        ... // 你须要拓展的操做
        return originalMethod.apply(this, arguments);
    }
    return descriptor
}

class Button {
    @funcDecorator
    onClick () { 
        console.log('我是Func的原有逻辑')
    }
}   
复制代码
  • 第一个参数target 变成了Button.prototype,即类的原型对象。这是由于 onClick 方法老是要依附其实例存在的,修饰onClik实际上是修饰它的实例。但咱们的装饰器函数执行的时候,Button 实例还并不存在。为了确保实例生成后能够顺利调用被装饰好的方法,装饰器只能去修饰 Button 类的原型对象

  • 第二个参 数name,是咱们修饰的目标属性属性名。

  • 第三个参数descriptor,它的真面目就是“属性描述对象”(attributes object),它由各类各样的属性描述符组成,这些描述符又分为数据描述符和存取描述符,很明显,拿到了 descriptor,就至关于拿到了目标方法的控制权。:

这里须要注意

  • 当咱们在react中给方法添加装饰器的时候,方法样使用上边写法,不能使用()=>{}箭头函数的写法,缘由是箭头函数写法若是class类没有实例出来是获取不到的

  • 接着上一条说,使用上述写法的时候应该在组件的constructor中使用bind修改onClick方法的this指向。

高阶组件(HOC)的应用

高阶组件(HOC)的主要有两个类型:

  • 属性代理

新组件类继承子React.component类,对传入的组件进行一系列操做,从而产生一个新的组件,达到加强组件的做用。

一、 操做props

二、 访问ref

三、 抽取state

四、 封装组件

class WrappedComponent extends Component {
    render() {
        return <input name="name" {...this.props.name} />;
    }
}

const HOC = (WrappedComponent) =>
    class extends Component {
        constructor(props) {
            super(props);
            this.state = {
                name: '',
            };

            this.onNameChange = this.onNameChange.bind(this);
        }

        onNameChange(event) {
            this.setState({
                name: event.target.value,
            })
        }

        render() {
            const newProps = {
                name: {
                    value: this.state.name,
                    onChange: this.onNameChange,
                },
            }
            return <WrappedComponent {...this.props} {...newProps} />;
        }
    }
复制代码
  • 反向继承

新组件类继承子原组件类,拦截生命周期、渲染劫持和控制state。

export default function ConsoleLog(WrappedComponent, params = []) {
  return class extends WrappedComponent {

    consoleLog() {
      if (params && params.length > 0) {
        params.forEach((info) => {
          console.log(`${info}==` + JSON.stringify(this.props[info]));
        })
      } else {
        console.log("this.props", JSON.stringify(this.props))
      }
    }

    render() {
      this.consoleLog()
      return super.render();
    }
  }
}
复制代码

反向继承不能保证完整的子组件树被解析React Components, Elements, and Instances这篇文章主要明确了一下几个点:

  • 元素(element)是一个是用DOM节点或者组件来描述屏幕显示的纯对象,元素能够在属性(props.children)中包含其余的元素,一旦建立就不会改变。咱们经过JSX和React.createClass建立的都是元素。

  • 组件(component)能够接受属性(props)做为输入,而后返回一个元素树(element tree)做为输出。有多种实现方式:Class或者函数(Function)。

因此, 反向继承不能保证完整的子组件树被解析的意思的解析的元素树中包含了组件(函数类型或者Class类型),就不能再操做组件的子组件了,这就是所谓的不能彻底解析。

小结(未完待续)

关于react的高阶组件事后我会在整理出一份详细的博客,由于可操做性很强,一段两段也说不清。

因为后半部分做者还在更新中,因此没有加进去,有兴趣的能够关注下,以后就能够愉快的阅读了。

关注我而后带走它!

相关文章
相关标签/搜索