JS中的设计模式

单例模式

一个对象其实就是一个单例,每当我们创建一个新的对象,实际上它就是一个单例对象,class 也是单例的实现方式。

工厂模式

工厂可以批量创建单例,根据传入的数据不同而生成不同的单例对象返回,更加灵活和可以处理复杂的逻辑。

工厂模式,是使用工厂函数来创建对象的。它可以使我们调用工厂,而不是直接使用 new 运算符或 Object.create() 从类中创建新对象。在 JavaScript 中,工厂模式只不过是一个不使用 new 关键字就返回对象的函数。
工厂允许我们将对象的创建与其实现分开。本质上,工厂包装了新实例的创建,从而为我们提供了更多的灵活性和控制权。在工厂内部,我们可以选择使用 new 运算符创建类的新实例,或者利用闭包动态构建有状态的对象字面量,甚至可以根据特定条件返回不同的对象类型。工厂的消费者完全不知道如何创建实例。
工厂模式在 JavaScript 中有很多体现。比如 Object() 本身就像工厂。因为它根据输入创建不同的对象。如果你将数字传递给它,它可以在后台使用 Number() 构造函数创建一个对象。类似的还有字符串和布尔值。任何其他值,包括空值,都将创建一个普通对象。下面我们可以看一个例子。Object() 也是一个工厂这一事实并没有什么实际用途,只是通过这个例子,你能看到工厂模式在 JavaScript 中是无处不在。

var o = new Object(),
    n = new Object(1),
    s = Object('1'),
    b = Object(true);
// test
o.constructor === Object;  // true
n.constructor === Number;  // true
s.constructor === String;  // true
b.constructor === Boolean; // true

原型模式

很多其他语言实现原型模式是使用class,js则有天然的优势,原型继承和创建原型共享属性还是比较熟悉了。

原型模式对于 JavaScript 来说不算陌生了,原型就是在许多相同类型的对象之间共享属性。

代理模式 & 响应式编程 & Vue的实现

代理对象在调用者和主体对象之间,主要起到的作用是保护和控制调用者对主体对象的访问。代理会拦截所有或部分要在主体对象上执行的操作,有时会增强或补充它的行为。
一般代理和主体具有相同的接口,这对调用者来说是透明的。代理将每个操作转发给主体,通过额外的预处理或后处理增强其行为。这种模式可能看起来像“二道贩子”,但存在即合理,代理特别是在性能优化方面还是起到了很大作用的。

  1. 延迟初始化
    代理接受初始化请求,在明确调用者确实会使用主体之前不会传递给它。客户端发出初始化请求并且代理先做响应,但实际上并没有将消息传递给主体对象,直到客户端明显需要主体完成一些工作,只有这样的情况下,代理才会将两条消息一起传递。
    就比如数据校验、安全验证、放进队列、确定比对修改点不同、浅比较之类的操作,可以通过代理拦截和延迟。
  2. 缓存
    代理除了起到延迟初始化的作用外,还可以增加一层缓存。当客户端第一次访问的时候,代理会合并请求给主体,并且把结果先缓存,再分开返回给客户端。在客户端第二次发起二次请求的时候,代理可以直接从缓存中读取信息,不需要访问主体,就直接返回给客户端。

实现方式

代理模式在 JavaScript 中有很多种实现方式。其中包含了:1. 对象组合或对象字面量加工厂模式;2. 对象增强;3. 使用从 ES6 开始自带的内置的 Proxy。

组合模式

组合模式,通过继承类的属性和方法进行覆写,达到代理的效果。不会改变传入的主体对象。
优点在于:不会暴露主体,不会改变主体,它可以达到延迟初始化、缓存的效果。
缺点在于:父类的每一个方法都要重写。

  • 类的方式:
class Calculator {
  constructor () {
    /*...*/
  }
  plus () { /*...*/ }
  minus () { /*...*/ }
}

class ProxyCalculator {
  constructor (calculator) {
    this.calculator = calculator
  }
  // 代理的方法
  plus () { return this.calculator.divide() }
  minus () { return this.calculator.multiply() }
}

var calculator = new Calculator();
var proxyCalculator = new ProxyCalculator(calculator);
  • 函数方式
function factoryProxyCalculator (calculator) {
  return {
    // 代理的方法
    plus () { return calculator.divide() },
    minus () { return calculator.multiply() }
  }
}

var calculator = new Calculator();
var proxyCalculator = new factoryProxyCalculator(calculator);
对象增强

第二种模式对象增强(Object Augmentation),对于对象增强来说,它的优点就是不需要 delegate 所有方法。但是它最大的问题是改变了主体对象。用这种方式确实是简化了代理创建的工作,但弊端是会造成函数式编程思想中的“副作用”,因为在这里,主体不再具有不可变性。

function patchingCalculator (calculator) {
  var plusOrig = calculator.plus
  calculator.plus = () => {
    // 额外的逻辑
    // 委托给主体
    return plusOrig.apply(calculator)
  }
  return calculator
}
var calculator = new Calculator();
var safeCalculator = patchingCalculator(calculator);
内置 Proxy

从 ES6 之后,JavaScript 便支持了 Proxy。它结合了对象组合和对象增强各自的优点,我们既不需要手动的去 delegate 所有的方法,也不会改变主体对象,保持了主体对象的不变性。
但是它也有一个缺点,就是它几乎没有 polyfill。也就是说,如果使用内置的代理,就要考虑在兼容性上做出一定的牺牲。真的是鱼和熊掌不能兼得。

var ProxyCalculatorHandler = {
  get: (target, property) => {
    if (property === 'plus') {
      // 代理的方法
      return function () {
        // 额外的逻辑
        // 委托给主体
        return target.divide();
      }
    }
    // 委托的方法和属性
    return target[property]
  }
}
var calculator = new Calculator();
var proxyCalculator = new Proxy(calculator, ProxyCalculatorHandler);
VUE 如何用代理实现响应式编程

Vue.js 通过代理创建了一种 Change Obsverver 的设计模式。
Vue.js 最显着的特点之一是无侵入的反应系统(unobtrusive reactivity system)。组件状态是响应式 JavaScript 对象,当被修改时,UI 会更新。这也是在前面说过很多次的在函数式编程思想中的副作用(side effect)。

响应式编程(Reactive Programming)是一种基于声明式编程的范式。如果要做到响应式编程,我们就会需要下面示例中这样一个 update 的更新功能。
这个功能会使得每次当 A0 或 A1 发生变化时,更新 A2 的值。这样做,其实就产生了副作用,update 就是这个副作用。A0 和 A1 被称为这个副作用的依赖。这个副作用是依赖状态变化的订阅者。whenDepsChange 在这里是个伪代码的订阅功能。

var A2;
function update() {
  A2 = A0 + A1;
}
whenDepsChange(update);

Vue.js 能做的,是拦截对象属性的读写。JavaScript 中有两种拦截属性访问的方法:getter/setter 和 Proxies。由于浏览器支持限制,Vue 2 仅使用 getter/setter。在 Vue 3 中,Proxies 用于响应式对象,getter/setter 用于通过属性获取元素的 refs。

// eg.
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key); // 触发对象的属性变化,将变化发布出去。
    }
  })
}
使用Proxy
  1. 对象虚拟化
    下面的例子中的 oddNumArr 单数数组就是一个虚拟的对象。我们可以查看一个单双数是不是在单数数组里,我们也可以获取一个单数,但是实际上这个数组里并没有储存任何数据。
const oddNumArr = new Proxy([], {
  get: (target, index) => index % 2 === 1 ? index : Number(index)+1,
  has: (target, number) => number % 2 === 1
})

console.log(4 in oddNumArr) // false
console.log(7 in oddNumArr) // true
console.log(oddNumArr[15])   // 15
console.log(oddNumArr[16])   // 17
  1. 运算符重载
    运算符重载就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。比如在下面的例子中,我们就是通过重载“.”这个符号,所以在执行 obj.count 时,我们看到它同时返回了拦截 get 和 set 自定义的方法,以及返回了计数的结果。
var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`获取 ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`设置 ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});

obj.count = 1; // 返回:设置 count!
obj.count; 
// 返回:获取 count!
// 返回:1

通过jQuery看结构型模式

享元模式

享元模式(flyweight)的核心思想是通过减少对象的创建数量来节约内存。
jQuery 用于将初始点击绑定到 container div 上,把许多独立的行为转化为共享的行为。(事件委托)

门面模式

门面模式(facade)是一种经常可以在 jQuery 等 JavaScript 库中看到的结构,它的特点是把很多的解决复杂的兼容性问题的实现隐藏在背后,只通过“门面”将对外的接口抽象提供给使用者。

比如我们常用的 jQuery 的 $() 查询器做的就是把很多复杂的用来接收和解析多种类型的查询功能在后端通过 Sizzle 引擎处理,呈现给开发者的是一套更加简便的选择器。

组合模式

组合模式(composite)指的是可以通过同一种方式处理单个或一组对象。
在 jQuery 中,可以用统一的方式处理单个元素以及一个元素的合集,因为它们返回的都是一个 jQuery 对象。可以为单个元素,比如具有唯一 ID 的元素,或具有相同标签名称元素类型或类属性的一组元素的两个选择添加同一个展示类类的属性。

// 单个元素
$( "#specialNote" ).addClass( "show" );
$( "#mainContainer" ).addClass( "show" );
// 一组元素
$( "div" ).addClass( "show" );
$( ".item" ).addClass( "show" );

装饰器模式

比如现在常用的类外面包裹的@方式装饰类(decorator),它不会改变原本类的属性和方法,只是新注入一些处理方法和属性。

如果我们从一个非自研的、不想或不能直接操作的组件中提取类,那么就可以用到装饰器。而且,装饰器可以让我们的程序中减少大量的子类。装饰器模式可以提供方便的一个特点,就是对预期行为的定制和配置。

适配器模式

比如 CSS 中关于透明度的 get 和 set,只需要通过以下方式就可以使用,实际上就是适配器模式。Jquery实际上在背后做了很多的适配操作。

// Cross browser opacity:
// opacity: 0.9; Chrome 4+, FF2+, Saf3.1+, Opera 9+, IE9, iOS 3.2+, Android 2.1+
// filter: alpha(opacity=90); IE6-IE8
// Setting opacity
$( ".container" ).css( { opacity: .5 } );
// Getting opacity
var currentOpacity = $( ".container" ).css('opacity');

它们之间的相同性在于它们都是在无法直接改变主体对象的情况下,加了一层包装。而区别在于不同使用场景的包装方式不同,装饰器更多是通过包装嵌套添加一些不同的特征,适配器的包装更多是一个对象和另一个对象之间的接口映射。

你可能感兴趣的:(JS,&,TS,javascript,设计模式,开发语言)