第一章 面向对象的JavaScript
一. 语言类型
1. 动态类型语言和鸭子类型
1.1 动态类型语言
编程语言按照数据类型大体可以分为两类,一类是静态类型语言,一类是动态类型语言。
- 静态类型语言在编译时确定变量类型,动态类型语言需要在程序运行时确定变量类型
- 动态类型语言对变量类型的宽容给实际编程带来了很大的灵活性。由于无需进行类型检测,我们可以尝试调用任何对象的任意方法,而无需去考虑它原本是否被设计为拥有该方法。
1.2 鸭子类型
鸭子类型(duck typing)是设计模式中的一个概念,通俗说法是“如果它走路像鸭子,叫起来也像鸭子,那么它就是鸭子”。鸭子类型指导我们只关注对象的行为,而不关心对象本身,即只关注HAS-A,而不是IS-A。
2. 面向接口编程
动态类型语言的面向对象设计中,鸭子类型的概念至关重要,利用鸭子类型的思想,就可以在动态类型语言中轻松实现“面向接口编程,而不是面向实现编程”。面向接口编程是设计模式中重要的思想。
- 面向接口编程:抽离内部实现进行外部沟通,内部变动不会影响外部与其他实现的交互
- 面向对象编程:将事件中的实体抽象成具体的对象,再将属性和方法封装到对象中,通过不同的对象完成整个事件
2.1 封装
封装的目的是将信息隐藏,一般包括封装数据、封装实现、封装类型和封装变化。例如创建型模式的目的就是封装创建对象的变化,结构型模式封装的是对象之间的组合关系,行为型模式封装的是对象的行为变化。
2.2 继承
ECMAScript5 提供了一种创建对象的方式Object.create(),使创建的新对象指定继承另外一个对象。Object.create()在JavaScript中又被称为原型继承,底层实现是通过clone来创建对象。
Object.myCreate = (obj)=>{
function Fun(){};
Fun.prototype = obj;
return new Fun();
}
var clonePlane = Object.myCreate(plane);
原型继承具有以下规则
- 所有数据都是对象
JavaScript的数据类型分为基本类型和引用类型,其设计者本意是除了undefined以外,一切都应该是对象。为了实现这个目标,JavaScript内置了Number、String、Boolean将基本类型转换为引用类型数据。
JavaScript中的根对象是Object.prototype对象。
要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
function Person(name){
this.name = name;
}
Person.prototype.getName = function(){
return this.name;
}
var p1 = new Person('小明');
var p2 = new Person('小红');
上面代码中的Person不是一个类,而是一个函数构造器。通过new运算符创建对象时,实际是先克隆Object.prototype对象,再进行一些其他额外的操作(为新对象初始化属性、改变this指向等等)
我们可以模拟实现一下new运算符的实现过程
function objectNew(){
// 从 Object.prototype克隆一个空对象
var obj = new Object();
// 取得外部传入的构造器,Person
var Constructor = [].shift.call(arguments);
// 指向正确的原型
obj.__proto__ = Constructor.prototype;
// 借用外部构造器给obj设置属性
var ret = Constructor.apply(obj,arguments);
// 确保构造器返回一个对象
return typeof ret === 'object' ? ret : obj;
}
- 对象会记住它的原型
通过new运算符创建的对象,其__proto__属性永远指向其构造函数的原型对象。
obj.__proto__ = Constructor.prototype;
如果对象无法响应某个请求,它会把这个请求委托给它的构造函数的原型
对象获取属性时,会先在对象上查找,没有的话会沿着__proto__指针,在原型链上查找,直到根对象Object.prototype的原型null为止,找不到会返回undefined。
2.3 多态
多态的实际含义是同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。
var makeSound=function(animal){
if(animal instanceof Duck){
console.log('嘎嘎嘎');
}else if(animal instanceof Chicken){
console.log('咯咯咯');
}
}
makeSound(new Duck()); // 嘎嘎嘎
makeSound(new Chicken()); // 咯咯咯
上面这个例子就体现了一个“多态性”,但是这样简单的多态无法令人满意,如果添加一个新的动物,就需要改写makeSound方法。而且动物越多,makeSound方法就会越大,导致代码维护成本提高。
多态背后的思想是将“做什么”和“谁去做以及怎么做”分离开来,也就是将“不变的事情”与“可能改变的事情”分离开来。
多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。
把不变的部分隔离开来,把可变的部分封装起来,就赋予了扩展程序的能力!!!
二. this指向及更改
1. this
JavaScript内的this在函数运行时在执行环境中动态绑定,而非函数被声明时的环境。
this指向可以归结为以下几类:
- 作为对象方法调用:对象
- 普通函数调用:全局对象或undefined
- 构造器调用:返回的新对象,如果构造器显示返回一个object类型数据,this指向这个数据
- call和apply调用:第一个参数对象,如果参数类型为基本类型,则会调用构造器转为引用类型
2. call和apply用途
- 改变this指向
- 模拟实现Function.prototype.bind方法
- 借用其他对象的方法
三. 必包和高阶函数
1. 闭包
闭包作用概括如下:
- 封装变量,使其私有化
- 延续局部变量生命周期
2. 高阶函数
高阶函数至少满足以下一个条件:
- 函数可以作为参数被传递
- 函数可以作为返回值输出
1)作为参数传递场景
回调函数
function(useId,callback){
if(useId === 'xxxxx'){
callback();
}
}
Array.prototype.sort
[3,2,1,4,6,5].sort((a,b)=> a - b);
2)作为返回值输出
判断数据类型
var isType = function(type){
return function(obj){
return Object.prototype.toString.call(obj) === `[object ${type}]`;
}
}
var isArray = isType('Array');
console.log(isArray([1,2,3,4,5])); // true
3)currying 柯里化
currying又称为部分求值,一个currying函数首先接收一部分参数但不会立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包中被保存起来。等到函数被真正需要求值的时候,之前传入的参数会被统一求值。
我们通过currying实现一个每月花销记事本的功能
var currying = function(fn){
var args = [];
return function(){
if(arguments.length === 0){
return fn.apply(this,args);
}else{
[].push.apply(args,arguments);
return arguments.callee;
}
}
}
var cost = (function(){
var money = 0;
return function(){
for(var i=0;i<arguments.length;i++){
money += arguments[i];
}
return money;
}
})();
var cost = currying(cost);
cost(100);
cost(200);
console.log(cost()); // 300 由currying实现了一个输出总花费的功能
第二章 JavaScript设计模式
一. 设计模式简介
软件设计模式又称为设计模式,是一套被反复使用、多数人知晓、经过分类编目、代码设计经验的总结。它描述了在软件设计开发过程中一些不断重复发生的问题,以及该问题的解决方案。其目的是为了提高代码的可重用性、可读性和可靠性。
简单理解设计模式是人们在面对同类型软件工程设计问题时,总结出的一些经验。设计模式并不是代码,而是某类问题的通用解决方案。
学习设计模式可以提高程序员的思维能力、编程能力和设计能力,可以使程序设计更加标准化、提高软件开发效率,使代码可重用性高、可读性强、灵活且可维护性高。
二. 设计模式分类
设计模式根据特点可以划分为三类,分别是创建型,结构型,行为型。
-
创建型: 封装复杂的创建过程 blog.csdn.net/qq_52793248…
- 常用:
⼯⼚模式(简单工厂、工厂方法、抽象工厂)、单例模式、建造者模式 - 不常用:
原型模式
- 常用:
-
结构型: 类或对象组合在一起的经典结构 blog.csdn.net/qq_52793248…
- 常用:
代理模式、桥接模式、装饰者模式、适配器模式 - 不常用:
外观模式、组合模式、享元模式、过滤器模式
- 常用:
-
⾏为型: 多个类或者对象间协同工作,涉及算法和对象间职责分配 blog.csdn.net/qq_52793248…
- 常用:
观察者模式、模板方法模式、策略模式、职责链模式、迭代器模式、状态模式 - 不常用:
中介者模式、访问者模式、解释器模式、备忘录模式、命令模式
- 常用:
1. 创建型
单例模式
定义:保证在运行期间一个类仅有一个实例,并提供一个访问它的全局方法
单例模式分为两种:
- 饿汉式:实例在声明时就完成初始化,但是会造成内存浪费
- 懒汉式:实例在使用时才初始化,实现按需加载,但是在多并发情况下线程不安全,需要添加外部状态锁
通过一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。
var Singleton = function(name,age){
this.name = name;
this.age = age;
this.instance = null;
}
Singleton.getInstance = function(name){
if(!this.instance){
this.instance = new Singleton(name);
}
return this.instance;
}
var a = Singleton.getInstance('a');
var b = Singleton.getInstance('b');
单例模式适用场景:
- 基于Message的全局提示框(登陆、异常提示框)
- 全局的配置信息、用户信息
单例模式的优缺点:
- 优点:节约内存,资源共享
- 缺点:违背了单一职责原则、开放封闭原则
工厂模式
目的:封装对象的创建过程,提升创建对象的可复用性
工厂模式又分为:简单工厂模式、工厂方法模式、抽象工厂模式
- 简单工厂模式
- 原理:一个工厂类可以构建所有对象
- 缺点:违背了单一职责原则、开放封闭原则
function Fruit(type){
switch (type){
case "苹果": return new Apple();
case "梨子": return new Pear();
default: throw new IllegalArgumentException("暂时没有这种水果");
}
}
- 工厂方法模式
- 原理:一个具体工厂只生产一个具体产品
- 优点:符合单一职责、开放封闭原则
function Fruit(type){
switch (type){
case "苹果":
const appleFactory = new AppleFactory();
const apple = appleFactory.create();
return apple;
case "梨子":
const pearFactory = new PearFactory();
const pear = pearFactory.create();
return pear;
default: throw new IllegalArgumentException("暂时没有这种水果");
}
}
- 抽象工厂模式
- 原理:一个工厂负责生产一类相关的产品
- 优点:符合开闭原则、依赖倒置原则
- 缺点:
- 抽象工厂类有可能因为构建的对象过多,导致其成为超级类
- 添加新对象时,需要编写新的工厂类,增加了代码的复杂度
- 引入抽象层会增加系统的抽象性和理解难度
抽象工厂模式通常包含三个角色:
- 工厂函数(Factory):创建其他对象的函数,负责创建对象并返回
- 抽象接口(Interface):对象的基本结构和属性
- 具体实现(Concrete):实现基本结构和属性,扩展其他自定义属性和方法
// 定义抽象接口
class Button {
constructor(text) {
this.text = text;
}
render() {
console.log(`Rendering ${this.text} button...`);
}
}
// 定义具体实现
class SubmitButton extends Button {
constructor() {
super('Submit');
this.disabled = false;
}
render() {
console.log(`Rendering ${this.text} button...`);
console.log(`Disabled: ${this.disabled}`);
}
}
// 定义工厂函数
function createButton(type) {
switch(type) {
case 'submit':
return new SubmitButton();
default:
return new Button('Default');
}
}
// 创建按钮实例
const myButton = createButton('submit');
建造者模式
特点:创建一种类型的复杂对象,通过设置不同参数,定制化地创建不同对象
建造者模式与工厂模式区别:
- 工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象
- 建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建复杂度不同的对象
优点:符合开放封闭原则
2. 结构型
代理模式
通过代理对象访问目标对象,在不改变目标对象的情况下,控制访问并拓展功能
常用代理模式:
- 保护代理:用于控制不同权限的对象对目标对象的访问
- 虚拟代理:把一些开销很大的对象,延迟到真正需要它的时候才去创建
- 缓存代理:为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递的参数和之前一样,则可以直接返回前面存储的运算结果
虚拟代理实现图片预加载:
var myImage = (function(){
var imgNode = document.createElement('img');
document.appendChild(imgNode);
return {
setSrc:function(src){
imgNode.src = src;
}
}
})();
var proxyImage = (function(){
var img = new Image;
img.onload = function(){
myImage.setSrc(this.src);
}
return {
setSrc:function(src){
myImage.setSrc('file://loading.gif');
img.src = src;
}
}
})();
proxyImage.setSrc('file://1.jpg');
上面代码中代理负责预加载图片,预加载的操作完成后把请求重新交给本体。
虚拟代理要求接口一致性:
代理模式要求代理和本体的接口一致性,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做的好处有两个:
- 用户可以放心地请求代理,他只关心是否能得到想要的结果
- 在任何使用本体的地方都可以放心的替换为代理
如果本体和代理都为一个函数,函数必然都能被执行,则可以认为它们具有接口一致性。
缓存代理实现数学运算:
// 计算乘积
var mult = function(){
var a = 1;
for(let i=0;i<arguments.length;i++){
a = a * arguments[i];
}
return a;
}
// 计算和
var sum = function(){
var a = 0;
for(let i=0;i<arguments.length;i++){
a = a + arguments[i];
}
return a;
}
// 创建缓存代理的工厂函数 --- 高阶函数
var creatProxyFun = function(fn){
var cache= {};
return function(){
var args = Array.prototype.join.call(arguments,',');
if(args in cache){
return cache[args];
}else{
return cache[args] = fn.apply(this,arguments);
}
}
}
// 创建运算代理
var proxyMult = creatProxyFun(mult);
console.log(proxyMult(1,2,3,4,5)); // 120
var proxySum = creatProxyFun(sum);
console.log(proxySum(1,2,3,4,5)); // 15
其他代理模式:
- 防火墙代理:控制网络资源的访问,保护主题不让坏人接近
- 远程代理:为一个对象在不同的地址空间提供局部代理,在Java中,远程代理可以是另外一个虚拟机中的对象
- 保护代理:用于对象应该有不同访问权限的情况
- 智能引用代理:取代了简单的指针,它在访问对象时执行了一些附加的操作,比如计算访问次数等
- 写时复制代理:通常用于复制一个庞大的对象的情况,延迟复制过程,当对象真正被修改时,才对它进行复制操作
代理模式的优缺点:
- 优点:增强目标对象功能,扩展性好
- 缺点:增加系统复杂性
桥接模式
定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化
特点:用抽象关联来取代传统的多层继承,将类之间的静态继承关系转变为动态的组合关系
优点:符合开放封闭、单一职责原则
装饰者模式
在不改变对象自身的基础上,动态地给对象增加职责。
前提是装饰者和被装饰者拥有一致的接口。
- JavaScript中的装饰者
// 对象
var plane = {
fire: function () {
console.log("老代码");
},
};
var fire1 = plane.fire;
// 更新对象函数,通过记录的原始函数,实现功能叠加
plane.fire = function(){
fire1();
console.log('新代码');
}
- AOP实现装饰函数
场景一:数据统计上报
描述:页面点击一个按钮,会弹出登陆弹窗并进行数据上报
// 常用方式
var showLogin = function(){
console.log('打开登陆弹窗');
svelog(data,'上报数据');
}
// 使用AOP装饰函数
Function.prototype.after = function(fn){
var self = this;
return function(){
var ret = self.apply(this,arguments);
if(ret){
fn.apply(this,arguments);
}
return ret;
}
}
var showLogin = function(){
console.log('打开登陆弹窗');
}
showLogin = showLogin.after(svelog);
场景二:表单验证
描述:表单提交前需要验证用户名、密码是否符合要求,验证通过才可以提交
Function.prototype.before = function(fn){
var self = this;
return function(){
var ret = fn.apply(this,arguments);
if(ret){
return self.apply(this,arguments);
}else{
return false;
}
}
}
formSumbit = formSumbit.before(validata);
sumbit.onclick = function(){
formSumbit();
}
- 装饰者模式和代理模式
| 区别 | 代理模式 | 装饰者模式 |
|---|---|---|
| 适用场景 | 直接访问本体不方便或者权限不够时 | 为对象动态添加行为 |
| 表现 | 强调代理与本体的关系 | 形成一条装饰链 |
| 目的 | 对本体的访问 | 对本体的扩展 |
适配器模式
将类的接口转换为客户期望的另一个接口,适配器可以让不兼容的两个类一起协同工作。
var googleMap = {
show: function(){
console.log('开始渲染谷歌地图');
}
}
var baiduMap = {
display:function(){
console.log('开始渲染百度地图');
}
}
// 适配器
var baiduMapAdpter = {
show: function(){
return baiduMap.display();
}
}
var renderMap = function(map){
if(map && map.show instanceof Function){
map.show();
}
}
renderMap(googleMap);
renderMap(baiduMapAdpter);
优点:符合开放封闭原则
组合模式
- 组合模式用途
组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。除了用来表示树形结构以外,组合模式的另外一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
- 请求在树形结构中传递
如果子节点是叶对象,叶对象自身会处理这个请求;如果子节点是组合对象,请求会继续往下传递。叶对象下面不会再有叶对象,一个叶对象就是树的这条枝叶的尽头,组合对象下面可能还有其他的子节点。
- 组合模式使用-扫描文件夹
// 创建文件夹构造器
var Folder = function(name){
this.name = name;
this.files = [];
}
Folder.prototype.add = function(file){
this.files.push(file);
}
Folder.prototype.scan = function(){
console.log('开始扫描文件夹' + this.name);
for(let i=0;i<this.files.length;i++){
this.files[i].scan();
}
}
// 创建文件构造器
var File = function(name){
this.name = name;
}
File.prototype.add = function(){
throw new Error('文件下面不能添加文件')
}
File.prototype.scan = function(){
console.log('开始扫描文件' + this.name);
}
// 生成文件夹和文件
var folder1 = new Folder('学子资料');
var f1 = new File('react');
var f2 = new File('vue');
folder1.add(f1);
folder1.add(f2);
// 扫描文件
folder1.scan();
// 开始扫描文件夹学子资料
// 开始扫描文件react
// 开始扫描文件vue
- 组合模式使用陷阱
- 组合模式不是父子关系:叶对象和组合对象都具有相同的接口
- 对叶对象操作的一致性:调用接口的行为始终保持不变
- 双向映射关系
- 用职责链模式提高组合模式性能
- 引用父对象
在扫描文件夹的代码中,如果要增加一个删除文件的需求,或者需要从子节点开始向上扫描文件等,就需要保证当前节点可以拿到它的父节点,换句话说,就是对父节点的引用
// 创建文件夹构造器
var Folder = function(name){
this.parent = null; // 增加this.parent属性,指向父节点
this.name = name;
this.files = [];
}
Folder.prototype.add = function(file){
this.parent = this; // 设置父对象
this.files.push(file);
}
// 增加删除文件夹的方法
Folder.prototype.remove = function(name){
if(!this.parent){
return;
}
for(let i=this.parent.files.length - 1;i>=0;i--){
if(this.parent.files[i] === name){
this.parent.files.splice(i,1);
}
}
}
Folder.prototype.scan = function(){
console.log('开始扫描文件夹' + this.name);
for(let i=0;i<this.files.length;i++){
this.files[i].scan();
}
}
// 创建文件构造器
var File = function(name){
this.parent = null;
this.name = name;
}
File.prototype.add = function(){
throw new Error('文件下面不能添加文件')
}
// 增加删除文件的方法
File.prototype.remove = function(){
if(!this.parent){
return;
}
for(let i=this.parent.files.length - 1;i>=0;i--){
if(this.parent.files[i] === this){
this.parent.files.splice(i,1);
}
}
}
File.prototype.scan = function(){
console.log('开始扫描文件' + this.name);
}
// 生成文件夹和文件
var folder1 = new Folder('学子资料');
var f1 = new File('react');
var f2 = new File('vue');
folder1.add(f1);
folder1.add(f2);
// 扫描文件
folder1.scan();
// 开始扫描文件夹学子资料
// 开始扫描文件react
// 开始扫描文件vue
- 组合模式适用场景
场景一:表示对象的部分-整体层次结构,只需要请求树的最顶层对象,就可以对整颗树做统一的操作。
场景二:用户希望统一对待树中的所有对象,忽略叶对象和组合对象的区别,不再使用if、else分别处理它们。
享元模式
享元模式的核心是运用共享技术来有效支持大量细粒度的对象。如果系统中因为创建了大量类似的对象而导致内存占用量过高,就可以使用享元模式优化了。
- 内部状态和外部状态
内部状态:存储与对象内部,可以被一些对象共享,通常不会改变;
外部状态:取决于具体的场景,并根据场景而变化,不能被共享;
我们称剥离了外部状态的对象为共享对象,外部状态在必要时被传入共享对象,从而组件一个完整对象。享元模式是一种用时间换空间的优化模式。
享元模式并非必须同时包含两种状态,有其一也是一种享元模式。
- 适用场景特点
- 一个程序中使用了大量的相似对象,且由此导致内存开销过大
- 对象的大多数状态都可以变为外部状态
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代之前的完整对象
- 使用享元模式
假设现在有一个需求,需要渲染100个按钮,每个按钮的文本和绑定事件不一致,其他均相同。通过享元模式实现的话,首先提取内部状态:类型Button、样式CSS、布局Layout,外部状态:绑定事件onClick、文本innerHTML。代码略(自行脑补即可)
- 对象池
对象池指的是维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是从池子中获取。如果对象池中没有空闲对象,则创建一个新的对象,当获取的对象完成职责后,将其放入池子中,供后续操作使用。
// 创建对象池的函数
var objectPoolFunc = function(createObj){
var objectPool = [];
return {
create: function(){ // 获取对象
var obj = objectPool.length === 0 ? createObj.apply(this,arguments): objectPool.shift();
return obj;
},
recover:function(obj){ // 回收对象
objectPool.push(obj);
}
}
}
// 创建对象池
var iframeFactory = objectPoolFunc(function(){
var iframe = document.createElement('iframe');
document.appendChild(iframe);
iframe.onload = function(){
iframe.onload = null;
iframeFactory.recover(iframe); // 自动回收对象
}
return iframe;
})
var iframe1 = iframeFactory.create();
iframe1.src = 'http://baidu.com';
var iframe2 = iframeFactory.create();
iframe2.src = 'http://qq.com';
- 对象池与享元模式对比
对象池也是一种性能优化方式,它和享元模式的区别在于,其没有区分内部状态和外部状态这个过程。
外观模式
外观模式(Facade Pattern)是一种结构型设计模式,其主要目的是简化复杂系统的接口并提供一个更高级别的接口以供外部使用。
很多我们常用的框架和库基本都遵循了外观设计模式,比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。
其实在平时工作中我们也会经常用到外观模式进行开发,只是我们不自知而已。
- 绑定事件
let addMyEvent = function (el, ev, fn) {
if (el.addEventListener) {
el.addEventListener(ev, fn, false)
} else if (el.attachEvent) {
el.attachEvent('on' + ev, fn)
} else {
el['on' + ev] = fn
}
};
- 封装接口
let myEvent = {
// ...
stop: e => {
e.stopPropagation();
e.preventDefault();
}
};
- 优缺点
优点:
- 减少系统相互依赖:将客户端和子系统解耦,客户端只与外观对象交互,不需要了解底层子系统的实现细节
- 提高灵活性:提供一个更简单的接口,使客户端可以更容易地使用系统中的功能
- 提高安全性:外观对象可以控制客户端对子系统的访问,并提供一个安全的接口
缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适
3. 行为型
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
发布-订阅模式
- 定义
发布-订阅模式又称观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来代替传统的发布-订阅模式。
- 作用
- 发布-订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。使用者无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点;
- 发布-订阅模式可以取代对象之间硬编码的通知机制,一个对象不用显式的调用另外一个对象的某个接口;
- 实现发布-订阅模式
第一步:指定发布者
第二步:创建缓存列表,存放回调函数以便通知订阅者
第三步:发布者遍历列表,依次触发订阅者的回调函数
可以在回调函数中增加“key”,使订阅者只订阅所需的消息。
按照上面的步骤,首先把发布-订阅的功能封装在一个event对象中
var event = {
clientList:{}, // 缓存队列
listen:function(fn,key){ // 添加监听者的回调函数
if(!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn);
},
trigger:function(){ // 调用监听者的回调函数
var key = Array.prototype.shift.call(arguments);
var fns = this.clientList[key];
if(!fns || fns.length === 0){
return false;
}
for(let i=0;i<fns.length;i++){
fns[i].apply(this,arguments);
}
},
remove:function(fn,key){ // 移除监听者的回调函数
var fns = this.clientList[key];
if(!fns){
return false;
}
if(!fn){
if(fns) fns = [];
}else{
for(let i=fns.length-1;i>=0;i--){
if(fns[i] === fn){
fns.splice(i,1);
}
}
}
}
}
再定义一个installEvent函数,这个函数可以给所有的对象都动态安装发布-订阅功能
var installEvent = function(obj){
for(let i in event){
obj[i] = event[i];
}
}
- 项目中发布-订阅模式使用
第一种场景:项目中顶栏、导航栏、侧边栏可能都需要使用当前用户的个人信息,而个人信息需要通过ajax异步从服务器获取。
第二种场景:实时通信、状态管理
// 包装为发布-订阅模式
var login = installEvent({});
// 添加监听者的回调函数
login.listen('loginSucc',[nav.setAvatar,header.setAvatar]);
// 监听信息的各个对象
var header = {
setAvatar:function(avatar){
console.log('设置header模块的头像');
}
};
var nav = {
setAvatar:function(avatar){
console.log('设置nav模块的头像');
}
};
// 信息更新后,触发监听者的回调函数
$.ajax('http://xxx.com?login',function(data){
login.trigger('loginSucc',data.avatar);
});
第二种场景:通过闭包实现全局Event对象的发布-订阅模式
var Event = (function(){
var clientList = {};
var listen = function(fn,key){
if(!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn);
};
var trigger = function(){
var key = Array.prototype.shift.call(arguments);
var fns = this.clientList[key];
if(!fns || fns.length === 0){
return false;
}
for(let i=0;i<fns.length;i++){
fns[i].apply(this,arguments);
}
};
var remove = function(fn,key){
var fns = this.clientList[key];
if(!fns){
return false;
}
if(!fn){
if(fns) fns = [];
}else{
for(let i=fns.length-1;i>=0;i--){
if(fns[i] === fn){
fns.splice(i,1);
}
}
}
};
return {
listen,
trigger,
remove,
}
})();
全局的发布-订阅模式可以实现模块间的通信功能,例如firEvent的使用等。
- 发布和订阅的先后顺序
- 先订阅再发布
标准实现方式,如上述的几种实现都是先订阅再发布
- 先发布再订阅
可以通过堆栈实现此功能。首先创建一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存放入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。
这种模式实现的先发布再订阅模式中,事件只能被执行一次。
- 发布-订阅模式的两种方式
- 推模型:事件发生时,发布者一次性把所有更改的状态和数据都推送给订阅者
- 拉模型:订阅者可以按需获取数据和状态
- 发布-订阅模式优缺点
- 优点:时间解耦,对象解耦
- 缺点:创建订阅者本身就要消耗一定的内存和时间,弱化了对象之间的联系,导致程序难以跟踪维护和理解
模版方法模式
模版方法模式是一种只需使用继承就可以实现的非常简单的模式。
模版方法模式由两部分组成,第一部分是抽象父类,第二部分是具体的实现子类。抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承抽象父类,同时继承了算法框架和执行顺序,也可以重写父类的方法。
模版方式模式的核心就是将相同部分上移至父类,将不同留在子类。
- 实现一个简单的咖啡和茶的案例
咖啡与茶是一个经典案例,冲咖啡的顺序是“烧水、冲咖啡、倒入杯子、加入牛奶和糖”,泡茶的顺序是“烧水、泡茶、倒入杯子、加入柠檬汁”,两种操作的共同点很多,可以提取成一个抽象类,不同点靠具体子类实现。
第一步:提取抽象父类
// 抽象父类
var Beverage = function(){};
Beverage.prototype.boilWater = function(){
console.log('把水烧开');
}
Beverage.prototype.brew = function(){} // 空方法,子类实现
Beverage.prototype.pourInCup = function(){} // 空方法,子类实现
Beverage.prototype.addCondiments = function(){} // 空方法,子类实现
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
第二步:子类实现具体区别
var Coffee = function(){};
Coffee.prototype = new Beverage();
Coffee.prototype.brew = function(){
console.log('用沸水冲咖啡');
}
Coffee.prototype.pourInCup = function(){
console.log('把咖啡倒入水杯中');
}
Coffee.prototype.addCondiments = function(){
console.log('加入牛奶和糖');
}
上面的Beverage.prototype.init就是模版方法,init方法中封装了子类算法框架,它作为算法的模版,指导子类以何种顺序去执行哪些方法。
- 钩子方法
上述咖啡与茶的代码实现中,模版方法一旦开启,就会按照相同的顺序调用子类算法,如果有某个子类,由四步实现改为三步呢?
钩子方法可以解决此问题,在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,由子类决定。钩子方法的返回结果决定了模版方法后面代码是否执行。
// 增加钩子方法,设置默认值为true
Beverage.prototype.customWantsCondiment = function(){
return true; // 默认需要
}
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
if(this.customWantsCondiment()){
this.addCondiments();
}
}
// 创建新的子类
var CoffeeWithHook = function(){};
CoffeeWithHook.prototype = new Beverage();
CoffeeWithHook.prototype.brew = function(){
console.log('用沸水冲咖啡');
}
CoffeeWithHook.prototype.pourInCup = function(){
console.log('把咖啡倒入水杯中');
}
CoffeeWithHook.prototype.addCondiments = function(){
console.log('加入牛奶和糖');
}
// 子类“挂钩”
CoffeeWithHook.prototype.customWantsCondiment = function(){
return window.confirm('请问需要调料么?');
}
- 好莱坞原则
允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式访问底层组件,高层组件对待底层组件的方式被称为“好莱坞原则”。
好莱坞原则意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。
好莱坞原则不仅体现在模版方法模式中,还体现在发布-订阅模式、回调函数中等。
- 避开继承实现模版方法模式
// 创建抽象类函数,参数决定执行顺序
var Beverage = function (param) {
var boilWater = function () {
console.log("把水烧开");
};
var brew =
param.brew ||
function () {
throw new Error("必须传递该方法");
};
var pourInCup =
param.pourInCup ||
function () {
throw new Error("必须传递该方法");
};
var addCondiments =
param.addCondiments ||
function () {
throw new Error("必须传递该方法");
};
var F = function () {};
F.prototype.init = function () {
boilWater();
brew();
pourInCup();
addCondiments();
};
return F;
};
// 调用抽象类函数,创建子类并获取模版方法
var Coffee = Beverage({
brew:function(){
console.log("用沸水冲咖啡");
},
pourInCup:function(){
console.log("把咖啡倒入水杯中");
},
addCondiments:function(){
console.log("加入牛奶和糖");
}
});
// 调用模版方法
Coffee.init();
策略模式
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
策略模式的程序至少包含两部分,一部分是一组策略类,策略类封装了具体的算法,并负责具体的实现过程。另外一部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类。
利用策略模式实现一个员工年终奖计算功能
var strategies = {
"S":function(salary){
return salary * 4;
},
"A":function(salary){
return salary * 3;
},
"B":function(salary){
return salary * 2;
}
}
// level绩效 salary工资数额
var calculateBonus = function( level, salary ){
return strategies[level](salary);
}
console.log(calculateBonus('S',20000));
console.log(calculateBonus('A',14000));
利用策略模式实现业务中表单验证功能
第一步:创建策略对象
var strategies = {
isNonEmpty: function(value,errorMsg){
if(value === ''){
return errorMsg;
}
},
minLength:function(value,length,errorMsg){
if(value.length < length){
return errorMsg;
}
},
isMobile:function(value,errorMsg){
if(!/(^1[3|5|8][0-9]{9}$)/.test(value)){
return errorMsg;
}
}
}
第二步:创建Validator类
var Validator = function(){
this.cache = [];
}
Validator.prototype.add = function( dom, rules ){
var self = this;
for(let i = 0;i < rules.length;i++){
(function(rule){
var strateAry = rules[i].strategy.split(':'); // 检验规则名称
var errorMsg = rules[i].errorMsg; // 错误提示
self.cache.push(function(){
var strategy = strateAry.shift();
strateAry.unshift(dom.value);
strateAry.push(errorMsg);
return strategies[strategy].apply(dom,strateAry);
})
})(rules[i]);
}
}
Validator.prototype.start = function(){
for(let i = 0;i < this.cache.length;i++){
var errorMsg = this.cache[i]();
if(errorMsg){
return errorMsg;
}
}
}
第三步:客户端调用
var validatorData = function(){
var validator = new Validator();
validator.add(form.useName,[
{
strategy:'isNonEmpty',
errorMsg:'不能为空'
},
{
strategy:'minLength:6',
errorMsg:'长度最小为6位'
}
]);
validator.add(form.useId,[
{
strategy:'isNonEmpty',
errorMsg:'不能为空'
},
])
validator.add(form.phoneNumber,[
{
strategy:'isNonEmpty',
errorMsg:'不能为空'
},
{
strategy:'isMobile',
errorMsg:'手机号格式不正确'
}
]);
const result = validator.start();
return result;
}
策略模式优缺点:
- 优点:避免多重条件选择语句,符合开放-封闭原则,扩展性良好
- 缺点:策略类或策略对象占用内存,违反了最少知识原则
适用场景:封装不同的处理逻辑,并根据不同条件选择相应的策略
职责链模式
职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求发送者和接收者之间的耦合关系。
职责模式会将多个对象连成一条链,并沿着这条链传递请求,直到有一个对象能处理这个请求为止。
- 通过职责链模式实现一个商城订金与代金券消费规则
支付订金为500元获取100元代金券,支付订金为200元获取50元代金券,支付0元无代金券可用
第一步:定义职责链节点函数
var order500 = function(orderType,pay,stock){
if(orderType === 1 && pay === true){
console.log('500元订金获得100元代金券');
}else{
return 'nextSuccessor';
}
}
var order200 = function(orderType,pay,stock){
if(orderType === 2 && pay === true){
console.log('200元订金获得50元代金券');
}else{
return 'nextSuccessor';
}
}
var orderNormal = function(orderType,pay,stock){
if(stock > 0){
console.log('普通购买无代金券');
}else{
console.log('手机库存不足');
}
}
第二步:定义一个构造函数,在调用构造函数是传递需要包装的函数
var Chain = function(fn){
this.fn = fn;
this.successor = null; // 下一个节点函数
}
Chain.prototype.setNextSuccesstor = function(successor){ // 指定下一个节点函数
return this.successor = successor;
}
Chain.prototype.passRequest = function(){ // 传递请求
var ret = this.fn.apply(this,arguments);
if(ret === 'nextSuccessor'){
return this.successor && this.successor.passRequest.apply(this.successor,arguments);
}
return ret;
}
第三步:包装职责链节点,指定节点顺序,传递请求给第一个节点
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
chainOrder500.setNextSuccesstor(chainOrder200);
chainOrder200.setNextSuccesstor(chainOrderNormal);
chainOrder500.passRequest(1, true, 500);
- 用AOP实现职责链
AOP:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术
Function.prototype.after = function(fn){
var self = this;
return function(){
var ret = self.apply(this,arguments);
if(ret === 'nextSuccessor'){
return fn.apply(this,arguments);
}
return ret;
}
}
var order = order500.after(order200).after(orderNormal);
order(1,true,500);
- 职责链模式优缺点
优点:
- 解耦请求发送者和请求接收者之间的复杂关系
- 职责链中的节点对象可以灵活替换和重组
- 支持自动指定某个开始节点,不一定是第一个节点
缺点:
- 不能保证某个请求一定会被职责链处理,需要在最后加入保底方法
- 会在程序中增加一些额外的节点对象,占用内存
状态模式
- 定义
状态模式的定义是:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
前一部分的含义:将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。
后一部分的含义:从客户的角度看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用委托实现的效果。
状态模式的关键是区分事物内部的状态,事物内部的状态的改变往往会带来事物的行为改变。
状态模式的实现是把事物每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部。
- 通用模式
定义一个通用的类Context,Context持有每一个状态类的引用,以便把请求委托给状态对象。
- 状态模式优缺点
优点:
- 定义了状态与行为的关系,并将其封装在一个类中
- 状态逻辑的切换被封装在状态类中,避免Context无限膨胀
- 用对象代替字符串,增强语义化
缺点:需要增加很多状态类,占用内存
- 状态模式与策略模式区别
策略模式:用户需要知道每个策略的细节
状态模式:用户不需要了解每个状态,状态的切换被封装在状态类中
- JavaScript的状态机
var FSM = {
off:{
buttonWasPressed:function(){
console.log('已关灯');
this.button.innerHTML = '下一次按我是开灯';
this.currState = FSM.on
}
},
on:{
buttonWasPressed:function(){
console.log('已开灯');
this.button.innerHTML = '下一次按我是关灯';
this.currState = FSM.off
}
}
}
var Light = function(){
this.currState = FSM.off; // 当前状态
this.button = null;
}
Light.prototype.init = function(){
var self = this;
var button = document.getElementById('button');
button.innerHTML = '已关灯';
this.button = document.body.appendChild(button);
this.button.onclick = function(){
self.currState.buttonWasPressed.call(self); // 把请求委托给FSM状态机
}
}
迭代器模式
迭代器模式的定义是:提供一种方法,顺序访问一个聚合对象中的各个元素,而又不需要暴露对象内部表示。
一个迭代器通常包含两个接口:hasNext()和next()
const item = [1, 'red', false, 3.14];
function Iterator(items) {
this.items = items;
this.index = 0;
}
Iterator.prototype = {
hasNext: function () { // 判断迭代是否结束
return this.index < this.items.length;
},
next: function () { // 查找并返回下一个元素
return this.items[this.index++];
}
}
const iterator = new Iterator(item);
while(iterator.hasNext()){
console.log(iterator.next()); // 1, red, false, 3.14
}
- 迭代器分类
- 内部迭代器:外界不需要关心迭代器内部的实现,和迭代器的交互仅仅是一次初始调用
- 外部迭代器:必须显示的请求迭代下一个元素,可以由外部控制迭代的过程或者顺序
- 可迭代的数据结构
在JavaScript中,只要满足以下两个条件就可以使用迭代器进行迭代:
- 拥有length属性
- 可以通过下标访问
目前可迭代的数据结构包括:Array、String、Map、Set、arguments类数组对象、NodeList类数组对象
- 使用迭代器实现一个根据系统自动选择文件上传方式的功能
var iteratorUploadObj = function(){
for(let i=0;i<arguments.length;i++){
var fn = arguments[i];
var uploadObj = fn();
if(uploadObj){
return uploadObj;
}
}
}
var uploadObj = iteratorUploadObj(getActiveUploadObj,getFlushUploadObj,getFormUploadObj);
- 迭代器模式的优缺点
- 优点:简化聚合类设计
- 缺点:抽象迭代器的设计挑战
中介模式
中介模式的作用是解除对象与对象之间的紧耦合关系。增加一个中介者,所有的对象都通过中介者来通信,而不是相互引用,当一个对象改变时,只需要通知中介者即可。中介者使各个对象之间的耦合松散,而且可以随意的改变它们之间的交互。中介者模式使网状多对多关系变成简单的一对多关系。
- 中介者模式实现商城页面交互
在一个手机购买页面,可以选择手机颜色、购买数量两个选择,页面会同时展示库存数量,只有满足库组不为0和购买数量不为0时,才可以点击按钮下单。
页面节点包括以下5个:
颜色color下拉选择框Select,绑定事件onSelect;
数量number输入框Input,绑定事件onChange;
展示选择的手机颜色colorText;
展示输入的手机购买数量numberText;
下单按钮Button,绑定事件onClick;
拟定的库存数量如下:
var goods={
red:4,
yellow:6
}
思考:onSelect事件会触发影响colorText和onClick,onChange事件会触发影响numberText和onClick,如果有需要增加一个内存下拉选项呢,这个关系图就会形成复杂的多对多。加入引入中介者,所有事件只影响中介者,不管触发哪个事件,仅仅是通知中介者它们被改变了,同时把自身当作参数传递给中介者,以便使中介者辨别是谁发生了改变。对于其他元素的影响由中介者完成。
第一步:引入中介者
var mediator = (function(){
var colorSelect = document.getElementById('colorSelect');
var numberInput = document.getElementById('numberInput');
var colorText = document.getElementById('colorText');
var numberText = document.getElementById('numberText');
var sumbitBtn = document.getElementById('sumbitBtn');
return{
changed:function(obj){
var color = colorSelect.value;
var number = numberInput.value;
var stock = goods[color];
if(obj === colorSelect){
colorText = colorSelect.value;
}else if(obj === numberInput){
numberText = numberInput.value;
}
if(!color){
sumbitBtn.disabled = true;
sumbitBtn.innerHTML = '请选择手机颜色';
return;
}
if(number > stock){
sumbitBtn.disabled = true;
sumbitBtn.innerHTML = '手机库存不足';
}
sumbitBtn.disabled = false;
sumbitBtn.innerHTML = '下单购买';
}
}
})();
第二步:将对象传递给中介者
var onSelect = function(){
mediator.changed(this);
}
var onChange= function(){
mediator.changed(this);
}
- 中介者模式特点
中介者模式是迎合迪米特法则的一种实现,迪米特法则也叫做最少知识原则,是指一个对象应该尽量少的了解另外一个对象。对象之间互不清楚,只能通过中介者实现相互之间的影响。
中介者模式的也缺点很明显,庞大的中介者会占用内存空间。
- 中介者模式适合场景
如果对象之间的复杂耦合关系确实导致调用和维护出现了困难,而且这些耦合度随着项目扩展成指数形式增长,那么就可以考虑使用中介者模式进行优化。
访问者模式
访问者模式是一种将算法与对象结构分离的设计模式,通俗点讲就是:访问者模式让我们能够在不改变一个对象结构的前提下能够给该对象增加新的逻辑,新增的逻辑保存在一个独立的访问者对象中。访问者模式常用于拓展一些第三方的库和工具
- Visitor Object:访问者对象,拥有一个 visit() 方法
- Receiving Object:接收对象,拥有一个 accept() 方法
- visit(receivingObj):用于Visitor接收一个Receiving Object
- accept(visitor):用于Receving Object接收一个Visitor,并通过调用Visitor的 visit() 为其提供获取Receiving Object数据的能力
function Employee(name, salary) {
this.name = name;
this.salary = salary;
}
Employee.prototype = {
getSalary: function () {
return this.salary;
},
setSalary: function (salary) {
this.salary = salary;
},
accept: function (visitor) {
visitor.visit(this);
}
}
function Visitor() { }
Visitor.prototype = {
visit: function (employee) {
employee.setSalary(employee.getSalary() * 2);
}
}
const employee = new Employee('bruce', 1000);
const visitor = new Visitor();
employee.accept(visitor);
console.log(employee.getSalary()); // 2000
适用场景:
- 对象的属性很少改变,但经常需要在此对象上定义新的属性
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"对象的类,也不希望在增加新操作时修改这些类
优点:
- 符合单一职责原则
- 优秀的扩展性
- 灵活性
缺点:
- 具体元素对访问者公布细节,违反了迪米特原则
- 违反了依赖倒置原则,依赖了具体类,没有依赖抽象
- 具体元素变更比较困难
三. 6大设计原则
1. 单一职责原则SRP
单一职责原则的定义是一个对象(方法)只做一件事。
开发过程中尽量做到每个程序只做一件事,但是当满足以下两个条件之一时,可以用分离职责。一方面,如果随着需求的变化,有两个职责总是同时改变,则不用分离;另一方面,职责的变化轴线仅当它们确定发生变化时才具有意义,即使两个职责已经耦合,但只要没有发生变化的征兆,就可以不用分类。
单一职责的优点是降低了但各类或者对象的复杂度,按照职责把对象分解成更小的粒度,有助于代码复用和单元测试。
单一职责的缺点是会增加编写代码的复杂度,增大了对象之间相互联系的难度。
2. 里氏替换原则LSP
定义:子类可以扩展父类的功能,但是不能修改父类原有的功能
优点:增强程序的健壮性
3. 依赖倒置原则DIP
定义:面向接口编程,依赖于抽象类和接口,而不是具体类
优点:减少需求变化带来的工作量,使并行开发更友好
4. 接口隔离原则ISP
定义:使用多个小的专门接口,避免使用一个大的接口
优点:减少对外耦合度
5. 最少知识原则
最少知识原则的定义是一个软件实体应当尽可能少的与其他实体发生相互作用。
最少知识原则也叫做迪米特法则,其优点是减少了对象之间的依赖,降低耦合度。缺点是有可能会引入一个庞大到难以维护的第三者对象。
6. 开放-封闭原则
开放-封闭原则的定义是软件实体应该是可以扩展的,但是不可以修改。
开放-封闭原则的思想是当需要改变一个程序的功能或者给这个程序增加新的功能的时候,可以使用增加代码的方式,但是不允许改动程序的源码。
实现开放-封闭原则的方式很多,包括动态装饰函数、利用对象多态性消除条件语句、找到变化的地方放置挂钩、回调函数等。
开放-封闭原则是编写一个好程序的目标,其他设计原则都是达到这个目标的过程。
如果想在项目中满足开放-封闭原则,可以尝试以下做法:
第一步:挑选出最容易发生变化的地方,然后构造抽象来封闭变化
第二步:在不可避免发生修改的时候,尽量修改那些相对容易修改的地方,比如配置文件等
高内聚,低耦合
高层模块不依赖底层模块,即为依赖反转原则。
内部修改关闭,外部扩展开放,即为开放封闭原则。
聚合单一功能,即为单一功能原则。
低知识要求,对外接口简单,即为迪米特法则。
耦合多个接口,不如独立拆分,即为接口隔离原则。
合成复用,子类继承可替换父类,即为里式替换原则。