JS核心理论之《面向对象设计原则与常见设计模式》

370 阅读7分钟

面向对象设计原则

面向对象设计的六个设计原则:

中文名称 定义
开闭原则 一个软件实体如类、模块和函数面向扩展开放,面向修改关闭
单一职责原则 一个类只允许有一个职责,即只有一个导致该类变更的原因
里氏替换原则 超类存在的地方,子类是可以替换的
迪米特法则(最少知道原则) 一个软件实体应当尽可能少的与其他实体发生相互作用
接口分离原则 应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口
依赖倒置原则 实现尽量依赖抽象,不依赖具体实现

单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。 实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。

适用场景:一个单一对象。

  • 引用第三方库(多次引用只会使用一个库引用,如 jQuery)
  • 弹窗(登录框,信息提升框)
  • 购物车 (一个用户只有一个购物车)
  • 全局态管理 store (Vuex / Redux)

优点:适用于单一对象,只生成一个对象实例,避免频繁创建和销毁实例,减少内存占用。

缺点:不适用动态扩展对象,或需创建多个相似对象的场景。

最简版示例:

let Singleton = function(name){
  this.name = name
  this.instance = null
}
Singleton.prototype.getName = function() {
  console.log(this.name)
}
Singleton.getInstance = function(name) {
  if(this.instance){
    return this.instance
  }
  return this.instance = new Singleton(name)
}

let a = Singleton.getInstance('a');
let b = Singleton.getInstance('b');

console.log(a === b); // true
console.log(a.getName());  // 'a'
console.log(b.getName());  // 'a'

上述单例无法使用new进行实例化,且管理单例的操作与功能代码耦合在一起,不符合"单一职责原则",因此我们做如下改造。

代理版示例:

let Singleton = function(name){
  this.name = name
}
Singleton.prototype.getName = function() {
  console.log(this.name)
}

let ProxySingletonCreator = (function() {
  let instance;
  return function(name) {
    if(instance){
      return instance;
    }
    return instance = new Singleton(name)
  }
})()

let a = new ProxySingletonCreator('a');
let b = new ProxySingletonCreator('b');

console.log(a === b); // true
console.log(a.getName());  // 'a'
console.log(b.getName());  // 'a'

代理模式

定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

适用场景

  • 懒加载:比如图片懒加载机制,先通过一张loading图占位,然后通过异步的方式加载图片,等图片加载好了再把完成的图片加载到img标签里面
  • 缓存代理:可以将一些开销很大的方法的运算结果进行缓存,再次调用该函数时,若参数一致,则可以直接返回缓存中的结果,而不用再重新进行运算。
  • 验证代理:比如实现表单验证器 另外,还有保护代理,比如当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用。

ES6所提供Proxy构造函数能够让我们轻松的使用代理模式:

var proxy = new Proxy(target, handler);

Proxy构造函数传入两个参数,第一个参数target表示所要代理的对象,第二个参数handler也是一个对象用来设置对所代理的对象的行为。 handler支持的拦截操作一共有13种。

  • get(target, propKey, receiver) //拦截对象属性的读取,比如proxy.foo和proxy['foo']
  • set(target, propKey, value, receiver) //拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值
  • apply(target, object, args) //拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)
  • construct(target, args) //拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
  • has(target, propKey)
  • ownKeys(target)
  • getPrototypeOf(target)
  • setPrototypeOf(target, proto)
  • isExtensible(target)
  • preventExtensions(target)
  • getOwnPropertyDescriptor(target, propKey)
  • defineProperty(target, propKey, propDesc)
  • deleteProperty(target, propKey)

示例:

const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : 37;
    }
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b);      // 1, undefined
console.log('c' in p, p.c); // false, 37

懒加载示例:

const getLayLoadProxy = (obj) => {
  return new Proxy(obj, {
    set: function(target, prop, value) {
      const defaultImage = 'https://img-blog.csdn.net/20150603165425396' //或者本地图片loading.gif
      if (!value) {
        return Reflect.set(target, prop, value)
      }
      target[prop] = defaultImage
      const img = new Image()
      img.src = value
      img.onload = function() {
        target[prop] = value
      }
    },
  })
}

let img = document.createElement('img')
document.body.appendChild(img)
let imgPrxoy = getLayLoadProxy(img)
imgPrxoy.src = 'https://i.ibb.co/SK5TGGN/1280-800.jpg'

缓存代理示例:

//原函数
const getFib = (number) => {
  if (number <= 2) {
    return 1;
  } else {
    return getFib(number - 1) + getFib(number - 2);
  }
}

const getCacheProxy = (func, caches = new Map()) =>{
   return new Proxy(func,{
      apply: function(target, object, args) {
        const args_ = args.join('-')
        if(caches.has(args_)){
           return caches.get(args_)
        } 
        const result = func(...args)
        caches.set(args_, result)
        return result
      }
   })
}
//代理函数
const getFibProxy = getCacheProxy(getFib);
console.log(getFibProxy(10))  // 50
console.log(getFibProxy(10))  // 50 from cache

验证代理示例:

const userForm = {
  password: '',
}
const validators = {
  password: (value) => {
    return {
      valid: value.length >= 6,
      error: '密码长度不能少于6位',
    }
  }
}
const getValidProxy = (obj, validators) => {
  return new Proxy(obj, {
    set: function(target, prop, value) {
      if (!value) return target[prop] = false
      const result = validators[prop](value)
      if (result.valid) {
        return Reflect.set(target, prop, value)
      } else {
        console.error(result.error)
        return target[prop] = false
      }
    },
  })
}

const userFormProxy = getValidProxy(userForm, validators)
userFormProxy.password = '12345' // 控制台输出:密码长度不能少于6位

策略模式

定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。策略模式的目的就是将算法的使用与算法的实现分离开来。

适用场景:某个“类”中包含有大量的条件性语句,比如if...else 或者 switch。每一个条件分支都会引起该“类”的特定行为以不同的方式作出改变。 与其维护一段庞大的条件性语句,不如将每一个行为划分为多个独立的对象。每一个对象被称为一个策略。

示例(未使用策略模式前):

var calculateBonus = function( performanceLevel, salary ){
    if ( performanceLevel === 'S' ){
        return salary * 4;
    }
    if ( performanceLevel === 'A' ){
        return salary * 3;
    }
    if ( performanceLevel === 'B' ){
        return salary * 2;
    }
};

calculateBonus( 'B', 20000 ); // 输出:40000
calculateBonus( 'S', 6000 ); // 输出:24000

示例(使用策略模式后):

var strategies = {
    "S": function( salary ){
        return salary * 4;
    },
    "A": function( salary ){
        return salary * 3;
    },
    "B": function( salary ){
        return salary * 2;

    }
};
var calculateBonus = function( level, salary ){
    return strategies[ level ]( salary );
};

console.log( calculateBonus( 'S', 20000 ) ); // 输出:80000
console.log( calculateBonus( 'A', 10000 ) ); // 输出:30000

装饰器模式

定义:装饰者模式的定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。

适用场景:日志记录、性能统计、安全控制、事务处理、异常处理等等。

面向切面编程(Aspect-oriented programming,AOP)是一种编程范式。做后端 Java web 的同学,特别是用过 Spring 的同学肯定对它非常熟悉。AOP 是 Spring 框架里面其中一个重要概念。 在ES6之前,要使用装饰器模式,通常通过高阶函数:Function.prototype.before做前置装饰,和Function.prototype.after做后置装饰。

示例:

Function.prototype.before = function(beforeFn) {
  var self = this
  return function() {
    beforeFn.apply(this,arguments)
    return self.apply(this,arguments)
  }
}
Function.prototype.after = function(afterFn) {
  var self = this
    return function() {
      var result = self.apply(this,arguments)
      afterFn.apply(this,arguments)
      return result
    }
}

var func = function() {
    console.log('func');
}
var func1 = function() {
    console.log('beforeFunc');
}
var func3 = function() {
    console.log('afterFunc');
}
func = func.before(func1).after(func3)
func()  // beforeFunc  func   afterFunc

ES6中,装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。装饰器是一种函数,写成@ + 函数名。它可以放在类和类方法的定义前面。 但不能用于函数,因为存在函数提升。如果一定要装饰函数,可以上面介绍的采用高阶函数的形式直接执行。

@frozen 
class Foo {
  @configurable(false)
  @enumerable(true)
  method() {}

  @throttle(500)
  expensiveMethod() {}
}

上面代码一共使用了四个装饰器,一个用在类本身,另外三个用在类方法。它们不仅增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改类的功能。

示例:

class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

function log(target, name, descriptor) { 
  var oldValue = descriptor.value;

  descriptor.value = function() {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(this, arguments);
  };

  return descriptor;
}

const math = new Math();

// passed parameters should get logged now
math.add(2, 4);

上面代码中,@log 装饰器的作用就是在执行原始的操作之前,执行一次console.log,从而达到输出日志的目的。