JavaScript设计模式学习笔记(一)

一、基础知识

什么是多态?

同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果,换句话说,给不同的对象发送同一个消息时,这些对象会根据这个消息分别给出不同的结果。

/*
 * 对象的多态性
 * 改进:将不变的部分抽离出来(所有动画都会叫 makeSound),然后把可变的部分各自封装起来
 */

var makeSound = function (animal)  {
  animal.sound()
}

var Duck = function () {}
Duck.prototype.sound = function () {
  console.log('嘎嘎嘎')
}

var Chicken = function () {}
Chicken.prototype.sound = function () {
  console.log('咯咯咯')
}

makeSound(new Duck())
makeSound(new Chicken())

Js的操作比较直接了,所有的对象都实现同一个方法,然后统一调用就好了

多态在面向对象的程序设计中有什么作用?

多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。(确实很厉害,当转化为多态的时候,按照局部性原理,条件分支少了,其实也就提高了程序的执行效率,NB)

/*
 * 优化前 
 */
var googleMap = {
  show: function () {
    console.log('开始渲染谷歌地图')
  }
}

var baiduMap = {
  show: function () {
    console.log('开始渲染百度地图')
  }
}

var renderMap = function (type) {
  if (type === 'google') {
    googleMap.show()
  } else if (type === 'baidu') {
    baiduMap.show()
  }
}

renderMap('google') // 输出:开始渲染谷歌地图
renderMap('baidu') // 输出:开始渲染百度地图


/*
 * 优化后 
 * 对象的多态性提示我们:“做什么”和“怎么去做”是可以分开的
 */
var googleMap = {
  show: function () {
    console.log('开始渲染谷歌地图')
  }
}

var baiduMap = {
  show: function () {
    console.log('开始渲染百度地图')
  }
}

/*
 * 后续增加soso地图,renderMap 函数不需要做出任何改变
 */
var sosoMap = {
  show: function () {
    console.log('开始渲染soso地图')
  }
}

var renderMap = function (map) {
  if (map.show instanceof Function) {
    map.show()
  }
}

renderMap(googleMap) // 输出:开始渲染谷歌地图
renderMap(baiduMap) // 输出:开始渲染百度地图

原型模式和基于原型继承的JavaScript对象系统

JavaScript的函数既可以作为普通函数调用,也可以作为构造函数被调用。区别在于,是否使用new运算符电泳函数。当使用new运算符调用函数时,此时的函数就是一个构造器。

用new运算符来创建对象的过程,实际上也是先克隆Object.prototype 对象,再进行一些其他的额外操作的过程。

// new 运算符创建对象过程的代码模拟
function Person(name) {
  this.name = name
}

Person.prototype.getName = function () {
  return this.name
}

let objectFactory = function () {
  let obj = new Object() // 从 Object.prototype 上克隆一个空的对象
  // shift 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值
  // call 方法用于函数调用,调用时必须传递一个参数,空的可以用null代替
  let Constructor = [].shift.call(arguments) // 取得外部传入的构造器
  obj.__proto__ = Constructor.prototype // 指向正确的原型,而不是Object.prototype
  let ret = Constructor.apply(obj, arguments)
  return typeof ret === 'object' ? ret : obj // 确保构造器总是返回对象
}

let a = objectFactory(Person, 'seven')
console.log(a)
console.log(a.name)
console.log(a.getName())
console.log(Object.getPrototypeOf(a) === Person.prototype)

JavaScript中的原型继承

JavaScript中的__proto__ 属性默认会指向它的构造函数的原型对象

let a = new Object()
console.log(a.__proto__ === Object.prototype) // true

最常用的原型继承方式

let obj = {name: 'seven'}

let A = function () {}
A.prototype = obj

let a = new A()
console.log(a.name) // seven

虽然属说ECMAScript 6 带来的新的class语法不过是语法糖,但作为一个从其他面向对象语言转过来的同学,我还是更愿意使用class语法,而不是prototype

对于 this、call、apply 的一些简单理解

this的指向

除去不常用的情况(with、eval等)

  • 在普通函数中调用:this指向全局对象(浏览器中则为window对象,严格模式下,是undefined)
  • 在对象中使用:this 指向该对象
  • 构造函数中调用:指向返回的对象
  • Function.prototype.call 或 Function.prototype.apply 调用:动态改变传入函数的this,若第一个参数为null,则函数内的this 会指向默认的宿主对象(Global对象),但是如果是严格模式,则this 仍未null。对于目的不在于指定this指向,而是借用其他对象的方法,那么可以传入null来代替某个具体的对象: Math.max.apply(null, [1, 2, 3])

Function.prototype.bind

模拟bind函数

// bind 的一个特性就是 bind 之后不能再次更改this
Function.prototype.bind = function (context) {
  let self = this;
  return function () {
    return self.apply(context, arguments)
  }
}

Obj.func.call(db) : this 指向db,调用函数

Obj.func.apply(db): this指向db

高阶函数和闭包

高阶函数是至少满足下列条件之一的函数:

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出

高阶函数实现AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样可以保持业务逻辑模块的纯净和高内聚性,其次是方便复用日志统计等功能模块。

Function.prototype.before = function (beforefn) {
  let self = this // 保持原函数的引用
  return function () {
    beforefn.apply(this, arguments) // 执行新函数, 修正this
    return self.apply(this, arguments) // 执行原函数
  }
}

Function.prototype.after = function (afterfn) {
  let self = this
  return function () {
    let ret = self.apply(this, arguments)
    afterfn.apply(this, arguments)
    return ret
  }
}

let func = function () {
  console.log(2)
}

func = func.before(function () {
  console.log(1)
}).after(function () {
  console.log(3)
})

func() // 1 2 3

有点类似于中间件或者注入之类的,在Python里面有装饰器也可以实现类似功能,不知道JS里面有没有装饰器

高阶函数的其他应用

  1. currying
  2. 函数节流throttle
  3. 分时函数
  4. 惰性加载函数

currying

currying 又称部分求值。一个currying的函数首先会接收一些参数,函数不会立即求值,而是返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来,待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值

/**
 * 通用的 function currying
 */

let currying = function (fn) {
  let args = []
  return function () {
     if (arguments.length === 0) {
      return fn.apply(this, args)
     } else {
      [].push.apply(args, arguments)
      return arguments.callee
     }
  }
}

/**
 * 月底计算本月花了多少钱,并不需要每天计算,只需最后求值即可
 * 
 */

let costs = (function () {
  let money = 0
  return function () {
    for (let i = 0, l = arguments.length; i < l; i++) {
      money += arguments[i]
    }
    return money
  }
})();

let cost = currying(costs) // 转化为currying函数
cost(100)
cost(200)
cost(300)

console.log(cost())

节流函数

/**
 * throttle
 */

let throttle = function (fn, interval) {
  let __self = fn // 保存需要被延迟执行的函数引用
  let timer // 定时器初始化
  let firstTime = true // 是否是第一次调用

  return function () {
    let args = arguments
    let __me = this

    if (firstTime) { // 如果是第一次调用,不需要延迟执行
      __self.apply(__me, args)
      return firstTime = false
    }

    if (timer) { // 如果定时器还在,说明前一次延迟执行还没有完成
      return false
    }

    timer = setTimeout(function () { // 延迟一段时间执行
      clearTimeout(timer)
      timer = null
      __self.apply(__me, args)
    }, interval || 500)
  }
}

window.onresize = throttle(function () {
  console.log(1)
}, 500)

分时函数

对于耗时任务的解决方案之一就是下面的timeChunk函数,timeChunk函数让创建节点的工作分批进行

timeChunk 函数接受3个参数,第一个参数是总数据,第2个参数是封装了处理逻辑的函数,第3个参数是每一批处理的数量

/**
 * 分式函数
 * 
 */

let timeChunk = function (ary, fn, count) {
  let obj
  let t 
  let len = ary.length

  let start = function () {
    for (let i = 0; i < Math.min(count || 1, ary.length); i++) {
      let obj = ary.shift()
      fn(obj)
    }
  }

  return function () {
    t = setInterval(function () {
      if (ary.length === 0) { // 如果全部数据已处理完成
        return clearInterval(t)
      }
      start()
    }, 200)
  }
}

惰性加载

// 惰性载入函数
let addEvent = function (elem, type, handler) {
  if (window.addEventListener) {
    addEvent = function (elem, type, handler) {
      elem.addEventListener(type, handler, false)
    }
  } else if (window.attachEvent) {
    addEvent = function (elem, type, handler) {
      elem.attachEvent('on' + type, handler)
    }
  }

  addEvent(elem, type, handler)
}

二、单例模式

1.什么是单例模式?

单例模式也称为单体模式,规定一个类只有一个实例,并且具有可提供全局访问的特点。

单体模式是一个用来划分命名空间并将一批属性和方法组织在一起的对象,如果它被实例化,那么它只能被实例化一次。

单例模式的优点:

  • 可以用来划分命名空间,减少全局变量的数量
  • 使用单体模式可以使代码组织的更为一致,使代码容易阅读和维护
  • 可以被实例化,且实例化一次

JS中的对象字面量结构:

// 对象字面量
let Singleton = {
  attr1: 1,
  attr2: 2,
  attr3: new Date().getTime(),
  method1: function () {
    return this.attr1;
  },
  method2: function () {
    return this.attr2;
  },
  method3: function () {
    return this.attr3;
  }
};

对象字面量与单例模式定义的差别在于以下几点:

  • 单例模式可以被实例化有且仅有一次,但字面量是不能被实例化的一个类

字面量是用来创建单体模式的方法之一????

2. 单体模式的基本结构

// 单体模式
let Singleton = function (name) {
  this.name = name;
  this.instance = null;
}

Singleton.prototype.getName = function () {
  return this.name;
}

// 获取实例对象
function getInstance(name) {
  if (!this.instance) {
    this.instance = new Singleton(name);
  }
  return this.instance;
}

let getInstance = (function() {
  let instance = null;
  return function (name) {
    if (!instance) {
      instance = new Singleton(name);
    }
    return instance;
  } 
})();

// 测试单例模式的实例
let a = getInstance("aa");
let b = getInstance("bb");
console.log(a)
console.log(b)
console.log(a === b)

也就是说,两个instance 其实是相同的

3. 通用的惰性单例模式

// 通用的惰性单例

var getSingle = function (fn) {
  var result
  return function () {
    return result || (result = fn.apply(this, arguments))
  }
}

var createLoginLayer = function () {
  var div = document.createElement('div')
  div.innerHTML = '我是登录浮窗'
  div.style.display = 'none'
  document.body.appendChild(div)
  return div
}

var createSingleLoginLayer = getSingle(createLoginLayer)

document.getElementById('loginBtn').onclick = function () {
  var loginLayer = createSingleLoginLayer()
  loginLayer.style.display = 'block'
}

4. 使用场景

比如我们想要创建一个弹窗

// 实现弹窗
var createWindow = function(){
    var div = document.createElement("div");
    div.innerHTML = "我是弹窗内容";
    div.style.display = 'none';
    document.body.appendChild('div');
    return div;
};
document.getElementById("Id").onclick = function(){
    // 点击后先创建一个div元素
    var win = createWindow();
    win.style.display = "block";
}

缺点如下:

  • 每次点击都会创建一次div,频繁点击下就会创建大量的div元素
  • 如果写了移除弹出代码,就会频繁创建并删除DOM元素,极大的影响性能
  • DOM频繁的操作会引起重绘等,从而影响性能

解决方法当然就是使用单例模式来实现了

// 实现单例模式弹窗
let createWindow = (function () {
  let div;
  return function () {
    if (!div) {
      div = document.createElement("div");
      div.innerHTML = "我是窗体";
      div.style.display = 'none';
      document.body.appendChild(div);
    }
    return div;
  }
})();

document.getElementById('app').onclick = function () {
  // 点击后创建一个div元素
  let win = createWindow();
  win.style.display = 'block';
}

这样的方式下,创建过的实例对象就会存储在createWindow的属性里面,你下次再创建的时候就可以直接调用它了,而不需要重复创建

5.基于ES6的单例模式

'use strict';

let __instance = (function () {
  let instance;
  return (newInstance) => {
    if (newInstance) instance = newInstance;
    return instance
  }
})();

class Universe {
  constructor () {
    if (__instance()) return __instance();
    this.foo = 'bar';
    __instance(this);
  }
}

let u1 = new Universe();
let u2 = new Universe();
console.log(u1)
console.log(u2)
console.log(u1 === u2)
class SingletonApple {
  constructor(name, creator, products) {
    //首次使用构造器实例
    if (!SingletonApple.instance) {
      this.name = name;
      this.creator = creator;
      this.products = products;
      //将this挂载到SingletonApple这个类的instance属性上
      SingletonApple.instance = this;
    }
    return SingletonApple.instance;
  }
}

let appleCompany = new SingletonApple('苹果公司', '乔布斯', ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = new SingletonApple('苹果公司', '阿辉', ['iPhone', 'iMac', 'iPad', 'iPod']);

console.log(appleCompany === copyApple);  //true

利用ES6静态方法优化代码

class SingletonApple {
  constructor(name, creator, products) {
      this.name = name;
      this.creator = creator;
      this.products = products;
  }
  //静态方法
  static getInstance(name, creator, products) {
    if(!this.instance) {
      this.instance = new SingletonApple(name, creator, products);
    }
    return this.instance;
  }
}

let appleCompany = SingletonApple.getInstance('苹果公司', '乔布斯', ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = SingletonApple.getInstance('苹果公司', '阿辉', ['iPhone', 'iMac', 'iPad', 'iPod'])

console.log(appleCompany === copyApple); //true

三、工厂模式

定义

工厂化模式是为了解决过个类似对象声明的问题,也就是为了解决实例化对象产生重复的问题。

复杂的功能模式定义:将其成员对象的实例化推迟到子类中,子类可以重写父类接口方法以便创建的时候指定自己的对象类型。

可优化代码示例

function CreatePerson(name,age,sex) {
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sex = sex;
    obj.sayName = function(){
        return this.name;
    }
    return obj;
}
var p1 = new CreatePerson("longen",'28','男');
var p2 = new CreatePerson("tugenhua",'27','女');
console.log(p1.name); // longen
console.log(p1.age);  // 28
console.log(p1.sex);  // 男
console.log(p1.sayName()); // longen

console.log(p2.name);  // tugenhua
console.log(p2.age);   // 27
console.log(p2.sex);   // 女
console.log(p2.sayName()); // tugenhua

// 返回都是object 无法识别对象的类型 不知道他们是哪个对象的实列
console.log(typeof p1);  // object
console.log(typeof p2);  // object
console.log(p1 instanceof Object); // true

工厂模式示例代码

以自行车店为例,每个店铺有多重型号的自行车出售

// 定义自行车的构造函数
let BicycleShop = function () {}

BicycleShop.prototype = {
  constructor: BicycleShop,
  /**
   * 买自行车的方法
   */
  sellBicycle: function (model) {
    let bicycle = this.createBicycle(model)
    // 执行A业务逻辑
    bicycle.A()

    // 执行 B业务逻辑
    bicycle.B()

    return bicycle.B()
  },
  createBicycle: function (model) {
    throw new Error("父类是抽象类不能直接调用,需要子类重写该方法")
  }
}

// 实现原型继承
function extend(Sub, Sup) {
  // Sub 表示子类, Sup表示超类
  // 首先定义一个空函数
  var F = function (){};

  // 设置空函数的原型为超类的圆心
  F.prototype = Sup.prototype

  // 实例化空函数,并把超类原型引用传递给子类
  Sub.prototype = new F();

  // 在子类中保存超类的原型,避免子类与超类耦合
  Sub.sup = Sup.prototype;

  if (Sup.prototype.constructor === Object.prototype.constructor) {
    // 检测超类原型的构造器是否为原型自身
    Sup.prototype.constructor = Sup;
  }
}

let BicycleChild = function (name) {
  this.name = name;
  BicycleShop.call(this, name)
}

// 子类继承父类原型方法
extend(BicycleChild, BicycleShop);

// BicycleChild 子类重写父类的方法
BicycleChild.prototype.createBicycle = function () {
  let A = function () {
    console.log("执行A业务逻辑");
  }

  let B = function () {
    console.log("执行B业务操作");
  }

  return {A, B}
}

let childClass = new BicycleChild("龙恩")
console.log(childClass)

工厂模式的优点

  • 可以在父类中实现一些相同的方法
  • 具体的业务逻辑可以放在子类中,或通过重写该父类的方法,去实现自己的业务逻辑
  • 弱化对象间的耦合,防止代码的重复
  • 重复性的代码可以放在父类中去编写,子类继承父类的所有成员属性和方法,子类只专注于自实现自己的业务逻辑

基于ES6实现的工厂模式

简单工厂模式

//User类
class User {
  //构造器
  constructor(opt) {
    this.name = opt.name;
    this.viewPage = opt.viewPage;
  }

  //静态方法
  static getInstance(role) {
    switch (role) {
      case 'superAdmin':
        return new User({ name: '超级管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理'] });
        break;
      case 'admin':
        return new User({ name: '管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据'] });
        break;
      case 'user':
        return new User({ name: '普通用户', viewPage: ['首页', '通讯录', '发现页'] });
        break;
      default:
        throw new Error('参数错误, 可选参数:superAdmin、admin、user')
    }
  }
}

//调用
let superAdmin = User.getInstance('superAdmin');
let admin = User.getInstance('admin');
let normalUser = User.getInstance('user');

四、代理模式

定义

代理顾名思义,在客户访问对象过程中加一个替身(proxy)就是代理模式了

两种代理模式:保护代理和虚拟代理

  • 保护代理:用于控制不同权限对象对目标对象的访问,但在JavaScript并不容易实现保护代理,因为无法判断谁访问了对象
  • 虚拟代理:把一些开销很大的对象,延迟到真正需要它的时候才创建

示例:使用虚代理实现图片的预加载

第一种方案:不使用代理的预加载图片函数

/*
 * 无代理,更常见的情况
 */
var MyImage = (function () {
  var imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  var img = new Image

  img.onload = function () {
    imgNode.src = img.src
  }

  return {
    setSrc: function (src) {
      imgNode.src = '***.gif'
      img.src = src
    }
  }
})

第二种方案:使用代理模式来编写预加载图片的代码如下

/*
 * 引入代码
 */
var myImage = (function () {
  var imgNode = document.createElement('img')
  document.body.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('***.gif')
      img.src = src
    }
  }
})()

proxyImage.setSrc('***.JPG')

从我自己的角度来说,很明显看到第二个方案将图片的预加载和图片的赋值分开来做了

优缺点分析

  • 第一种方案的代码耦合度太高,一个函数内负责做了几件事情,比如创建img元素,和实现给未加载图片完成之前设置loading加载状态等多项任务,未满足面向面向对象设计原则中的单一职责原则;并且当某个时候不需要预加载的时候,需要从myImage函数内把代码删掉,这样的代码耦合度太高
  • 第二种方案使用代理模式,其中myImage函数只负责做一件事,创建img元素加入到页面中,其中加载图片的功能交给代理函数ProxyImage去做,当图片加载成功后,代理函数ProxyImage会通知及执行myImage函数的方法,同时当以后不需要代理对象的话,我们直接可以调用本体对象的方法即可

ES6中的代理模式

缓存代理

没有经过任何优化的计算斐波拉契数列的函数

const Fib = number => {
  if (number <= 2) {
    return 1;
  } else {
    return Fib(number - 1) + Fib(number - 2);
  }
}

console.log(Fib(50))

计算40以上就能明显感觉到延迟感,但其实我计算10的时候我的破电脑就有延迟感hhhh

基于缓存代理的斐波拉契数列计算

const Fib = number => {
  if (number <= 2) {
    return 1;
  } else {
    return Fib(number - 1) + Fib(number - 2)
  }
}

const getCacheProxy = (fn, cache = new Map()) => {
  return new Proxy(fn, {
    apply(target, context, args) {
      const argString = args.join(' ')
      if (cache.has(argString)) {
        // 如果有缓存,直接返回缓存数据
        console.log(`输出${args}的缓存结果:${cache.get(argString)}`)
        return cache.get(argString)
      }
      const result = fn(...args)
      cache.set(argString, result)

      return result
    }
  })
}

const getFibProxy = getCacheProxy(Fib);
console.log(getFibProxy(45))
console.log(getFibProxy(45))
// console.log(Fib(45))

原来缓存的意思真的只是缓存,所以并不会加快计算速度,而是会大大减少重复计算的时间

验证代理

const userForm = {
  account: '',
  password: '',
}

// 验证方法
const validators = {
  account(value) {
    // account 只允许为中文
    const re = /^[\u4e00-\u9fa5]+$/
    return {
      valid: re.test(value),
      error: '"account" is only allowed to be chinese'
    }
  },
  password(value) {
    // password 的长度应该大于6个字符
    return {
      valid: value.length >= 6,
      error: '"password" should more than 6 character'
    }
  }
}

// 基于Proxy实现的一个通用的表单验证器
const getValidateProxy = (target, validators) => {
  return new Proxy(target, {
    _validators: validators,
    set(target, prop, value) {
      if (value === '') {
        console.log(`"${prop}" is not allowed to be empty`)
        return target[prop] = false
      }
      const validResult = this._validators[prop](value)
      if (validResult.valid) {
        return Reflect.set(target, prop, value)
      } else {
        console.error(`${validResult.error}`)
        return target[prop] = false
      }
    }
  })
}

// 调用方式
const userFormProxy = getValidateProxy(userForm, validators)
userFormProxy.account = '123' // "account" is only allowed to be chinese
userFormProxy.password = 'he' // "password" should more than 6 character

实现私有属性

function getPrivateProps (obj, filterFunc) {
  return new Proxy(obj, {
    get(obj, prop) {
      if (!filterFunc(prop)) {
        let value = Reflect.get(obj, prop)
        // 如果是方法,将this指向修改原对象
        if (typeof value === 'function') {
          value = value.bind(obj)
        }
        return value
      }
    },
    set(obj, prop, value) {
      if (filterFunc(prop)) {
        throw new TypeError(`Can't set property "${prop}"`)
      }
      return Reflect.set(obj, prop, value)
    },
    has(obj, prop) {
      return filterFunc(prop) ? false : Reflect.has(obj, prop)
    },
    Ownkeys(obj) {
      return Reflect.ownkeys(obj).filter(prop => !filterFunc(prop))
    },
    getOwnPropertyDescriptor(obj, prop) {
      return filterFunc(prop) ? undefined : Reflect.getOwnPropertyDescriptor(obj, prop)
    }
  })
}

function propFilter (prop) {
  return prop.indexOf('_') === 0;
}


// 方法调用
const myObj = {
  public: 'hello',
  _private: 'secret',
  method: function () {
    console.log(this._private);
  }
},

myProxy = getPrivateProps(myObj, propFilter);

console.log(JSON.stringify(myProxy)); // {"public":"hello"}
console.log(myProxy._private); // undefined
console.log('_private' in myProxy); // false
console.log(Object.keys(myProxy)); // ["public", "method"]
for (let prop in myProxy) { console.log(prop); }    // public  method
myProxy._private = 1; // Uncaught TypeError: Can't set property "_private"

参考文档

暂无评论

发表评论

您的电子邮件地址不会被公开,必填项已用*标注。

相关推荐