40 种 javascript 设计模式总结及实例代码演示
前言
- 讲设计模式的书很多,但是专讲 js 设计模式的书不多,比较著名的就是曾琛老师的《javascript 设计模式与开发实践》,里面讲了 14 种 javascript 设计模式以及这些设计模式所遵循的几大编码原则。
- 无意中发现了百度张容铭的《javascript 设计模式》一书,竟然讲了 40 种设计模式。
- 尽管也同意曾琛老师说的,由于 js 中本来一切都是对象,所以有些其他语言需要使用某种设计模式解决的问题,可能在 js 中并不需要使用这种模式就能解决问题。比如用来创建对象的工厂模式。但是如果我们能使用 js 去完成这些设计模式的编写,那也没坏处,它可以使我们建立起一种超越具体编程语言的编程思维。
- 另外,这本书中很多代码中都有值得学习的书写规范、编码技巧及编码思想,还是能够受益匪浅的。
- 张的这本书好的一点是,每一种设计模式都是结合具体的应用场景来编写的,并且但凡有代码实现的地方,几乎每一行代码都事无巨细地加了注释,这是我在其他技术类书籍中很罕见到的,简直佩服。
- 而缺点也很明显,这本书的校验工作不甚认真,有很多代码有错误之处,另外很多代码示例省略了 html 页面相关的实现。
-
所以——我抄书了!不过我不觉得抄书可耻,从学习编程的第一天,我就秉持着一个理念:学习过程中的所有代码,只有自己敲一遍,才算学过(甚至这样都只能达到基本理解而非学懂)
- 本文下面对书中 40 种设计模式的总结
- 我还完成了书中所有代码示例的实现,
- 对书中的错误进行了修正,
- 补全了所有作者省略掉的代码,
- 保证每一个示例都可以在 node 环境或者浏览器页面或者浏览器控制台可以看到演示效果。
- 具体请看这个 github 仓库:javascript_Pattern
希望可以帮助到需要的同学。
lesson01-func-to-obj
从函数到对象
本节课围绕实现一个验证用户输入的用户名、邮箱、密码的功能,从函数逐渐到对象,使用了多种不同方式去实现,并总结了各种方式的特点。
-
01:函数式编程实现
- 缺点:全局函数污染全局变量空间(函数也是变量)
-
02:用对象字面量给对象添加方法
- 对象式定义
- 点语法调用
- 缺点:无法复用和继承(即无法用 new 关键字复制出一个相同的对象)
-
03:使用点语法来给对象添加方法
- 同上,只不过定义时使用函数,添加方法时使用对象语法;
-
04:用函数方法返回包装对象
- 调用方法:先利用函数返回一个对象,再调用该对象的方法
- 缺点:返回的对象与定义的对象没有关联【无法同步】
-
05:使用类的方式:构造函数(使用 this 赋值)
- 使用 this 定义成员方法;
- 使用 new 创建新对象
- 缺点:每次实例化都会对 this 上的属性进行复制,消耗大
-
06:使用类的方式:构造函数(使用原型赋值)
- 使用原型赋值,避免类的共用属性和方法被重复实例化赋值,减少性能开销
- 缺点:每一个方法都要写一次 prototype, 太多重复
-
07:使用类的方式:构造函数(使用原型对象赋值)
- 不用每个方法都写 prototype, 只需要写一次,以原型对象的方式一次性赋值;
- 缺点:会重写类的 prototype 属性,导致 constructor 属性丢失
-
08:使用类的方式:构造函数(使用原型对象赋值)– 实现链式调用
- 每个方法都返回 this(即实例对象自身)来实现链式调用
lesson02-opp
面向对象编程基础
-
01:类的属性与方法封装
- 私有属性:在类内部使用 var 关键字定义,外界访问不到
- 私有方法:在类内部定义的 function,外界访问不到
- 对象公有属性:通过 this 关键字创建的属性,外界可以通过点语法访问到;
- 对象公有方法:通过 this 关键字创建的方法,外界可以通过点语法访问到;
- 特权方法:通过 this 关键字创建的方法,并且以 set 和 get 为前缀的方法,方法可以访问私有属性,外界可通过它获得或修改私有属性;
- 构造器:在初始化实例时利用特权方法为一些私有属性进行初始赋值;
- 类的静态共有属性:在类外部通过点语法定义的属性,新创建的对象中无法获取它,可以通过类的点语法获取
- 类的静态共有方法:在类外部通过点语法定义的方法,新创建的对象中无法获取它,可以获得类的点语法获取
- 公有属性:通过 prototype 对象添加的属性,所有实例都可以通过原型链访问,并且拿到相同的值
- 公有方法:通过 prototype 对象添加的方法,所有实例都可以通过原型链访问,并且只有一份,不会重复创建。
-
02:利用闭包实现类的静态变量
- 利用闭包添加类的静态私有属性和静态私有方法
- 缺点:在闭包外部利用原型添加静态公有属性和方法,从形式上看似脱离了闭包这个类
-
03:在闭包内部实现一个完整的类
- 这样实现,类的定义和与原型添加静态公有属性和方法都在同一个闭包内,更像一个整体
-
04:创建对象的安全模式
- 利用 instanceof 方法类判断是否使用 new 关键字
- 可以保证忘记写 new 的时候也能正常实例化对象
-
05:类式继承
- 将父类的实例赋值给子类的原型
- 如果父类中的共有属性是引用类型,则子类的一个实例更改该继承过来的共有属性,就会直接影响到其他子类实例
- 实例化父类时无法对父类构造函数进行初始化
-
06:构造函数继承
- 利用 call 来更改子类执行环境为父类,利用绑定的 this,子类继承了父类的共有属性。
- 使用此种方法,继承没有涉及子类的原型,所以父类的原型方法自然不会被子类继承。
- 如果想被子类继承就必须要放在构造函数中
- 每个实例都会单独拥有一份,不能共有,违背了代码复用原则。
-
07:组合继承
- 综合两种继承方式的优点,过滤掉缺点
- 缺点:父类构造函数执行了两遍
-
08:原型继承
- 利用过渡对象的原型继承其他对象来生成新的对象
- 缺点:由于是类式继承的封装,父类对象中的值类型属性被复制,引用类型的属性被共用
-
09:寄生式继承
- 利用原型继承方法,new 一个新对象
- 对新对象进行拓展,可以为新对象添加自己私有的方法和属性
- 返回新对象
- 由于是对原型继承的二次封装,所以具有一样的缺点
-
10:寄生组合式继承
- 是寄生继承与构造函数继承的组合方式
- 在构造函数继承中调用父类的构造函数
- 在寄生方法中将父类原型的一个副本赋值给子类的原型,并且修正因为重写子类原型而导致子类的 constractor 属性被修改的问题
- 缺点:子类再想添加原型方法必须通过 prototype 对象的点语法形式添加
-
11:单继承
- 利用 for…in 将源对象属性复制到目标对象上
- 这种方法只能进行浅复制
-
12:多继承
- 利用函数的 arguments 对象 和 for 循环,进行多个对象的继承(属性复制)
- 将该方法绑定到对象原型上, 这样所有对象就可以直接拥有此方法
-
13:多态
- 多态,就是同一个方法多种调用方式
- 也是通过判断 arguments 对象的长度,不同长度执行不同方法来实现
lesson03-create_pattern
创建型设计模式
-
01-01:无工厂模式
- 通过类实例化对象
- 多个相似的对象需要多个相似的类
-
01-02:基类工厂
- 写多个基类
- 但是用一个统一的公厂类进行封装,对参数进行 swich 判断,来返回不同的类实例
- 调用者只需要记住工厂类,而不必记住其它所有类
- 如果再有新的类型需求,还需要增加基类,修改工厂类
-
01-03:简单工厂
- 不用再写多个基类
- 只有一个工厂类
- 多个不同对象的相似属性和行为抽象为工厂类的属性和方法
- 多个不同对象的不同属性和行为使用分支判断类进行分别处理
- 它的使用场合通常限制在创建单一对象
- 02-01:工厂方法模式前传 1———— 类实例化方式
- 02-02:工厂方法模式前传 2———— 基类工厂模式
-
02-03:工厂方法模式
- 将实际创建对象的工作推迟到子类当中
- 核心类是抽象类
- 采用安全模式类
- 将创建对象的基类放在工厂方法类的原型中
-
03-01:抽象类
- 可以声明但不能使用的类
- 用来定义一个产品簇,声明一些必备方法,如果子类中没有重写就会抛出错误
-
03-02 抽象工厂模式
- 通过对类的工厂抽象使其业务用于对产品类簇的创建,而不负责创建某一类产品的实例
- 使用抽象工厂来为抽象的父类创建子类
- 在抽象工厂中让子类继承父类的一个实例(缓存类的实例),以此获得父类的对象属性
- 由于抽象工厂中的父类是抽象类,而抽象工厂方法不需要实例化,所以抽象工厂添加抽象类直接采用点语法来添加
-
04-01 建造者模式
- 将一个复杂对象的构件层与其表示层相互分离,同样的构建过程可采用不同的表示
-
建造者模式与工厂模式的区别:
- 工厂模式关心的是创建的最终结果,即对象实例或者类簇,比如一个人
- 建造者模式关心的是创建的过程,更关注细节,比如这个人穿什么衣服,是男还是女,兴趣爱好是什么等
-
05-01 构造函数继承基类
- 使用构造函数继承基类,然后重写基类方法的方式,实现多种子类继承同一父类却能创建不同种类对象的需求
- 问题:每次子类继承都会执行一遍父类构造函数,如果父类构造函数很耗资源,则无法接受
-
05-02 原型继承
- 通过将耗资源的方法放入基类的原型来避免重复执行;
- 通过让子类的原型继承基类的实例,然后重写子类原型上继承自基类的方法来实现继承和子类的差异化实现
- 无论何时都能对基类活子类的原型进行拓展,所有实例都将自动获得这些新拓展的方法。
-
06 单例模式
- 只允许实例化 1 次的对象类
- 单例模式常被用来定义命名空间
- 单例模式的另一个作用是进行代码库的模块管理
- 单例模式的第三个作用:管理静态变量
- 单例的可以延迟创建
lesson04-structure_pattern
结构型设计模式
-
01 外观模式
- 为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更容易。
- 常见的应用为封装一个统一接口来实现不同浏览器的相同功能的兼容。
- 外观模式的另一个作用:小型代码库中用来封装多个功能,简化底层操作方法
-
02 适配器模式
- 适配器模式是将一个类(对象)的接口(方法或属性)转换成另外一个接口,以满足用户需求,解决接口不兼容问题
- 用法一:组件库接口适配
- 用法二:参数适配
- 用法三:数据适配
-
03 代理模式
- 由于一个对象不能直接引用另一个对象,所以需要通过代理对象在这两个对象之间起到中介的作用。
-
04 装饰者模式
- 在不改变原对象的基础上,通过对其进行包装拓展(添加属性或者方法)使原有对象可以满足用户更复杂的需求、
- 通过缓存对象原有的方法和属性,然后在装饰者方法中调用原有属性和方法和新增的方法和属性来实现功能拓展
- 与适配器模式的不同:适配器需要了解原有方法的细节,而装饰器则不关心原有方法的实现细节。
-
05 桥接模式
- 在系统沿着多个维度变化的同时,又不增加其复杂度并已达到解耦。
- 主要特点是将实现层与抽象层解耦分离,使两部分可以独立变化
- 有时会增加开发成本和性能开销
-
06 组合模式
- 又称部分 - 整体模式,将对象组合成树形结构以表示“部分 - 整体”的层次结构。
- 组合模式使得用户对单个对象和组合对象的使用具有一致性。
- 接口要统一,可以通过继承同一个虚拟类来实现。
-
07 享元模式
- 运用共享技术有效地支持大量的细粒度的对象,避免对象间拥有相同内容造成多余开销
- 享元模式将数据和方法分成内部数据、内部方法和外部数据、外部方法。
- 内部方法与内部数据指相似或共有的数据和方法,将这一部分提取出来可以减少开销,提高性能
lesson05-behaviour_pattern
行为型设计模式
-
01 模板方法模式
- 父类中定义一组操作算法骨架,而降一些实现步骤延迟到子类中,使得子类可以不改变父类的算法机构的同时可重新定义算法中某些实现步骤
- 将多个模型抽象化归一,从中抽象提出出一个最基本的模板,其他模块只需要继承这个模板方法,也可以拓展某些方法
- 继承类也可以作为模板类
-
02 观察者模式
- 又称发布 - 订阅者模式或消息机制
- 它定义了一种依赖关系,解决了主体对象与观察者
- 包含两个基本方法:接收消息、向订阅者发布消息
- 还需要取消注册方法、消息容器。
- 接收消息(即消息注册)作用是将订阅者注册的消息推入消息容器,接收两个参数: 消息类型和处理动作
- 发布消息作用是当观察者发布一个消息时将所有订阅者订阅的消息一次执行,接收两个参数:消息类型及行为参数
-
03 状态模式
- 当一个对象的内部状态发生改变时,会导致其行为的改变。
- 对于分支条件内部独立结果的管理,可以使用状态模式
- 每一种条件作为对象内部的一种状态,面对不同判断结果,它其实就是选择对象内的一种状态
- 对于状态模式,主要目的就是将条件判断的不同结果转化为对象内部的状态,既然是状态对象的内部状态,所以一般作为状态对象内部的私有变量,然后提供一个能够调用状态对象内部状态的接口方法对象。这样方便管理状态。
-
04 策略模式
- 将定义的一组算法封装起来,使其相互之间可以替换
- 封装的算法具有一定的独立性,不会随客户端变化而变化
- 结构上与状态模式很像,在内部封装一个对象,通过返回的接口对象实现对内部对象的调用,但不需要管理状态
- 典型应用场景:表单验证
-
05 职责链模式
- 解决请求的发送者与请求的接受者之间的耦合,通过职责链上的多个对象分解请求流程,实现请求在多个对象之间的传递,直到最后一个对象完成请求的处理。
- 把每件事独立出一个模块对象去处理,完整的需求被分解成相互独立的模块需求
- 方便进行单元测试,保证每个组件对象的处理逻辑的安全性
-
06 命令模式
- 将请求与实现解耦并封装成独立对象,从而使不同的请求对客户端的实现参数化
- 将创建模块的逻辑封装在一个对象里,这个对象提供一个参数化的请求接口,通过调用这个接口并传递一些参数实现调用命令对象内部的一些方法
-
07 访问者模式
- 针对于对象结构中的元素,定义在不改变该对象的前提下访问结构中元素的新方法
- 我么知道 call 和 apply 的作用就是更改函数执行时的作用域,这正是访问者模式的精髓,通过这两种方法我们就可以让某个对象在其他作用域中运行
- 访问者模式解决数据与数据的操作方法之间的耦合,将数据的操作方法独立于数据,使其可以自由化演变。
-
08 中介者模式
- 通过中介者对象封装一系列对象之间的交互,使对象之间不再相互引用,降低他们之间的耦合
- 主要是通过模块间或对象间的复杂通信,来解决模块间或对象间的耦合
- 中介者对象的本质是封装多个对象的交互,并且这些对象的交互一般都是在中介者内部实现的。
- 与观察者模式相比,观察者模式中的订阅者是双向的,而中介者模式中订阅是单向的。
-
09 备忘录模式
- 在不破坏对象封装性的前提下,在对象之外捕获并保存该对象内部的状态以便日后对象使用或者对象恢复到以前的某个状态
- 主要任务是对现有数据或状态做缓存,这些数据或缓存将在可预期的将来再次用到
- 备忘录对象是对数据缓存器的一次保护性封装,防止外界访问
- 当数据量过大时会严重占用系统资源
-
10 迭代器模式
- 在不暴露对象内部结构的同时,可以顺序地访问聚合对象内部的元素
-
11 解释器模式
- 对于一种语言,给出其文法形式,并定义一种解释器,通过使用这种解释器来解释语言中定义的句子
lensson06-skill_pattern
技巧型设计模式
-
01 链模式
- 通过在对象方法中将当前对象返回,实现对同一个对象方法的链式调用
-
02 委托模式
- 多个对象接收并处理同一请求,他们将请求委托给另一个对象同一处理请求
- 利用元素冒泡原理可以将所有子元素上的相同事件绑定到父元素上
-
03 数据访问对象模式
- 抽象和封装对数据源的访问与存储。
- 关键在于定义私有前缀和增删改查接口
-
04 节流模式
- 对重复的业务逻辑进行节流控制,执行最后一次操作并取消其他操作,以提高性能
- 节流器的关键点在于计数器和事件执行间隔时间的控制,在某个间隔内只允许触发一次。
- 节流模式可应用于优化滚动事件性能、优化浮层显隐性能、图片懒加载、统计回报次数优化等
-
05 简单模板模式
- 通过格式化字符串拼凑出视图,以避免创建视图时大量的节点操作。优化内存开销。
- 从定义可看出其主要针对的是有大量创建视图的高消耗 DOM 操作场景。
-
06 惰性模式
- 减少每次代码执行的重复性的分支判断,通过对对象重定义来屏蔽原对象中的分支判断
- 第一种是在文件加载进来时通过闭包执行该方法对其重新定义(会使页面加载时占用一定资源)
- 第二种是在第一种方式基础上做一次延迟执行,在函数第一次调用时对其重定义;
-
07 参与者模式
- 在特定的作用域中执行给定的函数,并将参数原封不动地传递。
- 主要是利用 bind 方法,它应用了函数柯里化思想来进行传参和作用域绑定
-
08 等待者模式
- 通过对多个异步进程监听,来触发未来发生的动作
- 其实就是简单地实现 Promise 规范,通过 when/then/done/fail 来处理请求和回调,通过 resolve 和 reject 状态来监听异步的结果。
lesson07-architecture_patterns
架构型设计模式
-
01 同步模块模式
- 请求发出后,无论模块是否存在,立即执行后续逻辑,实现模块开发中对模块的立即引用
- 可以解决多人协作开发时代码互相冲突导致开发进程阻塞的问题
- 实质是将所有模块按照层级关系绑定到一个单体对象上,使用时调用该单体对象来完成操作;
- 主要有两个方法,定义模块的 define 方法和引用模块的 module 方法
- 缺点:无法引用尚未加载完成的模块。
-
02 异步模块模式
- 请求发出后,继续其他业务逻辑,知道模块加载完成后执行后续的逻辑,实现模块开发中对模块加载完成后的引用。
- 核心思路利用闭包返回模块,对未加载模块进行计数,直到未加载模块为 0,然后执行回调
- 在异步加载脚本文件时,要设置 asycn = true;
-
03 Widget 模式
- 是指借用 Web Widget 思想将页面分解成部件,针对部件开发,最终组合成完整的页面
- 此模式多针对与页面视图的构造,它将页面分解为很多个组件(即模块),一个完整的组件包含该模块完整的视图和一套完整的功能;其实就是组件化技术。
- 模板引擎一般分四步:处理数据 => 获取模板 => 处理模板 => 编译执行;
- 对于模板内的 js 语法解析,主要是 eval() 与 new Function() 技术来实现
-
04 MVC 模式
- 即模型(model)- 视图(view)- 控制器(controller)
- 用一种将业务逻辑、数据、视图分离的方式组织架构代码,主要解决层次混乱问题
-
05 MVP 模式
- 即模型 (Model)- 视图 (View)- 管理器 (Presenter)
- View 层不直接引用 Model 层内的数据,而是通过 Presenter 层实现对 Model 层内的数据访问
- 所有层次的交互都发生在 Presenter 层中
-
06 MVVM 模式
- 即模型(Model)- 视图(View)- 视图模型(ViewModel)
- 为视图层量身定做一套视图模型,并在视图模型中创建属性和方法,为视图层绑定数据并实现交互
- 这样做以后 View 层可以直接使用 HTML 来完成。