享元模式(flyweight)

核心思想是通过减少对象的创建数量来节约内存。
实现方式主要是通过:
优化重复、缓慢且低效地共享数据的代码
与相关对象(例如应用程序配置、状态等)共享尽可能多的数据来做到最大限度地减少应用程序中内存的使用。
在享元模式下,有 3 个核心概念:享元工厂、接口和具体享元。它们的关系如下图所示。

内在数据(intrinic)和外在数据(extrinsic)

内在数据是在对象内部存储和调用的。
外部信息则是存储在外部的可被删除的数据。
具有相同内在数据的对象可以由工厂方法创建单个共享对象。
享元接口(flyweight interface)可以接收和作用于外部状态。
具体享元(concrete flyweight)是接口的实际实现,它负责存储可分享的内在状态,并且可以控制外部状态。
享元工厂(flyweight factory)负责管理和创建享元对象。我们用一个租车的例子来解释,在租车行里,同一款车(car)可能有好几辆,这几款车的车型(model)、制造商(maker)、识别码(vin)  都是一样的,那我们可以说这些就是内在数据,可以通过单个共享对象 cars 存储。但是每个车目前出租的状态(availability)、租金 (sales) 是不同的,这些我们就可以作为外在数据存储。当通过 addCar 加进来新车时,createCar 可以判断是创建新的车型,还是基于 vin 返回已有的车型就可以了。所以下面我们虽然创建了 5 条车辆记录,但只有 3 个车型的实例。

// 储存车型的独立对象
class Car {
  constructor(model, maker, vin) {
    this.model = model;
    this.maker = maker;
    this.vin = vin;
  }
}

// 储存具体车型对象的容器
var cars = new Map();

// 如果车型已知,就返回vin;未知就创建
var createCar = (model, maker, isbn) => {
  var existingCar = cars.has(vin);

  if (existingCar) {
    return cars.get(vin);
  }

  var car = new Car(model, maker, vin);
  cars.set(vin, car);

  return car;
};

// 存储租赁车的容器
var carList = [];

// 登记租赁车到列表
var addCar = (model, maker, vin, availability, sales) => {
  var car = {
    ...createCar(model, maker, vin),
    sales,
    availability,
    vin
  };

  carList.push(car);
  return car;
};

addCar("911", "Porsche", "FR345", true, 2300);
addCar("911", "Porsche", "FR345", false, 2400);
addCar("Togun", "VW", "AZ567", false, 800);
addCar("C-Class", "Mercedes-Benz", "AS356", false, 1200);
addCar("C-Class", "Mercedes-Benz", "AS356", true, 1100);

上边是以数据角度分析,我们也可以从从另外一个事件处理的角度看看享元模式的应用。
DOM(文档对象模型)是层层嵌套的,所以一个单一的,比如点击事件,可能会被多个 DOM 层级中的事件处理程序处理。一种是自顶向下的事件捕获,一种是自底向上的事件冒泡。

对于享元模式来说,我们可以将一个享元加到元素外部的容器上,自上而下地侦听来自下方的事件,然后根据需要使用相应逻辑来处理这些事件。而不是像冒泡那样,将点击绑定到多个元素。
下面的例子是用享元构建一个非常基本的 accordion。在这里,jQuery 用于将初始点击绑定到 container div 上,把许多独立的行为转化为共享的行为。

var stateManager = {
  flyweight() {
    var self = this;
    $('#container')
        .unbind()
        .on('click', 'div.toggle', ({
            target
        }) => {
            self.handleClick(target);
        });
  }
};

Facebook 的詹姆斯·帕德奥尔西(James Padolsey)提出过另外一个 jQuery 中用到享元的概念。
在用 jQuery 的一些工具方法时,最好使用内部的 jQuery.methodName 底层方法,例如 jQuery.text;而不是用对外暴露的 jQuery.fn.methodName 外部方法,例如 jQuery.fn.text。
jQuery.fn.methodName 的底层方法。使用它可以减少一层抽象避免创建新的jQuery方法。因此詹姆斯提出了一个 jQuery.single 的想法,每次调用 jQuery.single ,意味着多个对象的数据被整合到一个中心化共享的数据结构中,所以它也算是一种享元。

jQuery.single = (o => {
var collection = jQuery([1]);
    return element => {
        // Give collection the element:
        collection[0] = element;
        // Return the collection:
        return collection;
    };
})();

$('div').on('click', function() {
  var html = jQuery
    .single(this)
    .next()
    .html();
  console.log(html);
});

门面模式(facade)

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

打个比方,我们平时用的搜索引擎可以说是“门面”,它的界面和操作简单的不能再简单。但是背后的实现逻辑是非常复杂的,这里牵扯到了调度、网络信息的爬取、解析、索引等等,最后呈现出来的才是搜索。

同样的,比如我们常用的 jQuery 的 $() 查询器做的就是把很多复杂的用来接收和解析多种类型的查询功能在后端通过 Sizzle 引擎处理,呈现给开发者的是一套更加简便的选择器。
下面我们可以看一个 $(document).ready(…) 的例子,在背后,它是基于一个 bindReady 的函数来实现的。

function bindReady() { 
  // ... 
  if (document.addEventListener) { 
    // Use the handy event callback 
    document.addEventListener('DOMContentLoaded', DOMContentLoaded, false); 
    // A fallback to window.onload, that will always work 
    window.addEventListener('load', jQuery.ready, false); 
    // If IE event model is used 
  } else if (document.attachEvent) { 
    document.attachEvent('onreadystatechange', DOMContentLoaded); 
    // A fallback to window.onload, that will always work 
    window.attachEvent('onload', jQuery.ready); 
  } 
} 

门面模式对于 jQuery 的使用者来说提供了很多方便,但这也不是没有代价的。它虽然降低了开发成本,但在一定程度上牺牲了性能。对于一些简单的页面开发,很多开发者还是会选择使用它,原因呢就是因为这些应用中页面开发的要求远不到工业级,但是通过 jQuery 能节省的开发成本确是指数级的
在开发的时候,我们除了要关注设计模式能带来什么好处以外,更要注意使用的场景,在开发效率和性能之间做出平衡。

组合模式(composite)

组合模式(composite)指的是可以通过同一种方式处理单个或一组对象。

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

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

延伸:什么是包装器模式。

在设计模式中,装饰器(decorator)和适配器(adaptor)通常是起到包装的作用。
装饰器(decorator)
当我们不想直接修改一个组件时,装饰器在这时就派上了用场。举个例子:化妆就是对对象的包装

在 jQuery 当中,装饰者可以用 extend() 来实现。
适配器(adaptor)
比如我们购买了一个英标的电子产品,如果在国内使用,是找不到合适的插座的。因为标准不同,孔型也不一样,就无法插入。但是如果我们使用一个转换接头,这个问题就迎刃而解了。

适配器的例子在 jQuery 中也是无处不见,比如 CSS 中关于透明度的 get 和 set,只需要通过以下方式就可以使用了,看起来是不是很方便呢:

// 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');

但其实在背后,jQuery 做了很多的工作。

get: function( elem, computed ) {
  // IE uses filters for opacity
  return ropacity.test( (
        computed && elem.currentStyle ?
            elem.currentStyle.filter : elem.style.filter) || "" ) ?
    ( parseFloat( RegExp.$1 ) / 100 ) + "" :
    computed ? "1" : "";
},
set: function( elem, value ) {
  var style = elem.style,
    currentStyle = elem.currentStyle,
    opacity = jQuery.isNumeric( value ) ?
          "alpha(opacity=" + value * 100 + ")" : "",
    filter = currentStyle && currentStyle.filter || style.filter || "";
  // IE has trouble with opacity if it does not have layout
  // Force it by setting the zoom level
  style.zoom = 1;
  // if setting opacity to 1, and no other filters
  //exist - attempt to remove filter attribute #6652
  if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" ) {
    // Setting style.filter to null, "" & " " still leave
    // "filter:" in the cssText if "filter:" is present at all,
    // clearType is disabled, we want to avoid this style.removeAttribute
    // is IE Only, but so apparently is this code path...
    style.removeAttribute( "filter" );
    // if there there is no filter style applied in a css rule, we are done
    if ( currentStyle && !currentStyle.filter ) {
      return;
    }
  }
  // otherwise, set new filter values
  style.filter = ralpha.test( filter ) ?
    filter.replace( ralpha, opacity ) :
    filter + " " + opacity;
}
};

装饰器还是适配器?

区别: 在于不同使用场景的包装方式不同,装饰器更多是通过包装嵌套添加一些不同的特征,适配器的包装更多是一个对象和另一个对象之间的接口映射。
相同: 在于它们都是在无法直接改变主体对象的情况下,加了一层包装。
我们什么时候应该避免使用装饰器和适配器呢?
如果我们可以控制整个实现(也就是说我们拥有自己的库)的话,就可以通过更改基本代码而不是通过包装使接口变得复杂化来实现相同的实用程序了。此外,与任何设计模式一样,如果非要使用包装模式的话,要确保实际的结果和影响比原始的、无模式的代码更简单且更易于理解。

此文章为2月Day3学习笔记,内容来源于极客时间《Jvascript进阶实战课》,大家共同进步💪💪

豪猪
4 声望4 粉丝

undefined