结构型模式主要用于处理类和对象的组合,对应思维导图:
对接口二次封装隐藏其复杂性,并简化其使用。 外观模式包含如下角色:
Facade
: 外观角色SubSystem
: 子系统角色使用时机
当我们将系统分成多个子系统时,我们会降低代码复杂性。编程时的最佳实践是最小化子系统之间的通信和依赖关系。实现这一目标的一个好方法是引入一个facade
对象,为子系统提供单一且统一的接口。
要保证处理事件的代码在大多数浏览器下一致运行,需要关注冒泡阶段。
在做跨浏览器网站时,你已经不经意间使用了外观模式:
var addMyEvent = function( el,ev,fn ){
if( el.addEventListener ){//存在DOM2级方法,则使用并传入事件类型、事件处理程序函数和第3个参数false(表示冒泡阶段)
el.addEventListener( ev,fn, false );
}else if(el.attachEvent){ // 为兼容IE8及更早浏览器,注意事件类型必须加上"on"前缀
el.attachEvent( "on" + ev, fn );
}else{
el["on" + ev] = fn;//其他方法都无效,默认采用DOM0级方法,使用方括号语法将属性名指定为事件处理程序
}
};
我们都熟悉$(document).ready(..)
。在源码中,这实际上是一个被调用的方法提供的bindReady()
:
加载事件共用两种方法
:window.onload()
和$(document).ready()
bindReady: function() {
...
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 );
Facade
外观模式大量应用于 jQuery
库以让其更容易被使用。譬如我们使用
jQuery
的$(el).css()
或 $(el).animate()
等方法 。
使我们不必手动在jQuery
内核中调用很多内部方法以便实现某些行为,也同时避免了手动与 DOM API
交互。
类似的还有
D3.js
JS
: 可额外适配两个及以上代码库、前后端数据等。使用时机 通常使用适配器的情况:
// 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');
内部实现为:
get: function( elem, computed ) {
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 || "";
style.zoom = 1;
// 如果将不透明度设置为1,则移除其他過濾器
//exist - attempt to remove filter attribute #6652
if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" ) {
style.removeAttribute( "filter" );
if ( currentStyle && !currentStyle.filter ) {
return;
}
}
// otherwise, set new filter values
style.filter = ralpha.test( filter ) ?
filter.replace( ralpha, opacity ) :
filter + " " + opacity;
}
};
yck - 《前端面试之道》
在 Vue
中,我们其实经常使用到适配器模式。
比如父组件传递给子组件一个时间戳属性,组件内部需要将时间戳转为正常的日期显示,一般会使用 computed
来做转换这件事情,这个过程就使用到了适配器模式。
为其他对象提供一种代理以便控制对这个对象的访问。
可以详细控制访问某个类(对象)的方法,在调用这个方法前作的前置处理(统一的流程代码放到代理中处理)。调用这个方法后做后置处理。
例如:明星的经纪人,租房的中介等等都是代理
使用代理模式的意义是什么?
特点:
分类:
Remote Proxy
):为一个位于不同的地址空间的对象提供一个本地的代理对象Virtual Proxy
):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。Protect Proxy
):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。Cache Proxy
):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。Smart Reference Proxy
):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。缺点::
前端用得最多的是 虚拟代理、保护代理、缓冲代理
ES6
所提供Proxy
构造函数能够让我们轻松的使用代理模式:
// target: 表示所要代理的对象,handler: 用来设置对所代理的对象的行为。
let proxy = new Proxy(target, handler);
目前一般的网站都会有图片预加载机制,也就是在真正的图片在被加载完成之前用一张菊花图(转圈的gif图片)表示正在加载图片。
const img = new Image();
img.src = '/some/big/size/image.jpg';
document.body.appendChild(img);
创建虚拟图片节点virtualImg
并构造创建代理函数:
// 图片懒加载: 虚拟代理
const createImgProxy = (img, loadingImg, realImg) => {
let hasLoaded = false;
const virtualImg = new Image();
virtualImg.src = realImg;
virtualImg.onload = () => {
Reflect.set(img, 'src', realImg);
hasLoaded = true;
}
return new Proxy(img, {
get(obj, prop) {
if (prop === 'src' && !hasLoaded) {
return loadingImg;
}
return obj[prop];
}
});
最后是将原始的图片节点替换为代理图片进行调用:
const img = new Image();
const imgProxy = createImgProxy(img, '/loading.gif', '/some/big/size/img.jpg');
document.body.appendChild(imgProxy);
如,前后端分离,向后端请求分页的数据的时候,每次页码改变时都需要重新请求后端数据,我们可以将页面和对应的结果进行缓存,当请求同一页的时候,就不再请求后端的接口而是从缓存中去取数据。
const getFib = (number) => {
if (number <= 2) {
return 1;
} else {
return getFib(number - 1) + getFib(number - 2);
}
}
const getCacheProxy = (fn, cache = new Map()) => {
return new Proxy(fn, {
apply(target, context, args) {
const argsString = args.join(' ');
if (cache.has(argsString)) {
// 如果有缓存,直接返回缓存数据 console.log(`输出${args}的缓存结果: ${cache.get(argsString)}`);
return cache.get(argsString);
}
const result = fn(...args);
cache.set(argsString, result);
return result;
}
})
}
const getFibProxy = getCacheProxy(getFib);
getFibProxy(40); // 102334155getFibProxy(40); // 输出40的缓存结果: 102334155
事件代理就用到了代理模式。
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector('#ul')
ul.addEventListener('click', (event) => {
console.log(event.target);
})
</script>
通过给父节点绑定一个事件,让父节点作为代理去拿到真实点击的节点。
在不改变原对象的基础上,通过对其进行包装拓展(添加属性或者方法)使原有对象可以满足用户更复杂的需求
装饰器类似于高阶函数的概念。装饰器将基本形式作为参数,并在其上添加处理并将其返回。 优点:
问题:
在JavaScript
中:
核心就是缓存上一次的函数
举一个简单的例子:
var xiaoming = function () {
this.run = function () {
return '跑步'
},
this.eat = function () {
return: '吃饭'
}
}
// 小明可以跑步,也可以吃饭
// 下面是一个装饰类,给小明进行装饰
var decor = function (xiaoming) {
this.run = function () {
return xiaoming.run + '很快'
}
this.eat = function () {
return xiaoming.eat + '很多'
}
}
通过一个装饰类,实现了对小明类的装饰。
“@”,与其说是修饰函数倒不如说是引用、调用它修饰的函数。
或者用句大白话描述:@: "下面的被我包围了。"
举个栗子,下面的一段代码,里面两个函数,没有被调用,也会有输出结果:
test(f){
console.log("before ...");
f()
console.log("after ...");
}
@test
func(){
console.log("func was called");
}
直接运行,输出结果:
before ...
func was called
after ...
在React
中,装饰器模式随处可见:
import React, { Component } from 'react';
import {connect} from 'react-redux';
class App extends Component {
render() {
//...
}
}
// const mapStateToProps
// const actionCreators
export default connect(mapStateToProps,actionCreators)(App);
Ant Design
中创建表单的最后一步其实也算装饰器模式
class CustomizedForm extends React.Component {}
CustomizedForm = Form.create({})(CustomizedForm);
桥接模式将实现层与抽象次层解耦分离,使两部分可以独立变化。 该模式包含如下角色:
Abstraction
(抽象类)RefinedAbstraction
(扩充抽象类)Implementor
(实现类接口)ConcreteImplementor
(具体实现类)常用于应用程序(客户端)和数据库驱动程序(服务):
应用程序写入定义的数据库API,例如ODBC
,但在此API之后,会发现每个驱动程序的实现对于每个数据库供应商(SQL Server,MySQL,Oracle
等)都是完全不同的。
JavaScript
中很少见。在大型网站中,不同模块可能会有不同主题,也有分白天/黑夜 或 用户自主选择的主题。
这时为每个主题创建每个页面的多个副本明显不合理,而桥接模式是更好的选择:
javascript-design-patterns-for-human
不同模块:
class About{
constructor(theme) {
this.theme = theme
}
getContent() {
return "About page in " + this.theme.getColor()
}
}
class Careers{
constructor(theme) {
this.theme = theme
}
getContent() {
return "Careers page in " + this.theme.getColor()
}
}
以及不同主题:
class DarkTheme{
getColor() {
return 'Dark Black'
}
}
class LightTheme{
getColor() {
return 'Off white'
}
}
class AquaTheme{
getColor() {
return 'Light blue'
}
}
生成主题:
const darkTheme = new DarkTheme()
const about = new About(darkTheme)
const careers = new Careers(darkTheme)
console.log(about.getContent() )// "About page in Dark Black"
console.log(careers.getContent() )// "Careers page in Dark Black"
该模式包含以下角色:
Component
- 声明组合中对象的接口并实现默认行为(基于Composite
)Leaf
- 表示合成中的原始对象Composite
- 在Component
接口中实现与子相关的操作,并存储Leaf(primitive)
对象。计算机文件结构是组合模式的一个实例。
如果你删除某个文件夹,也将删除该文件夹的所有内容,是吗?这实质上就是组合模式运行原理。你
你可以调用结构树上较高层次的组合对象,消息将沿这一层次结构向下传输。
Javascript设计模式理论与实战:组合模式
HTML
文档的DOM
结构就是天生的树形结构,最基本的元素醉成DOM树,最终形成DOM
文档,非常适用适用组合模式。
我们常用的jQuery
类库,其中组合模式的应用更是频繁,例如经常有下列代码实现:
$(".test").addClass("noTest").removeClass("test");
不论$(“.test”)
是一个元素,还是多个元素,最终都是通过统一的addClass
和removeClass
接口进行调用。
我们简单模拟一下addClass
的实现:
var addClass = function (eles, className) {
if (eles instanceof NodeList) {
for (var i = 0, length = eles.length; i < length; i++) {
eles[i].nodeType === 1 && (eles[i].className += (' ' + className + ' '));
}
}
else if (eles instanceof Node) {
eles.nodeType === 1 && (eles.className += (' ' + className + ' '));
}
else {
throw "eles is not a html node";
}
}
addClass(document.getElementById("div3"), "test");
addClass(document.querySelectorAll(".div"), "test");
对于NodeList
或者是Node
来说,客户端调用都是同样的使用了addClass
这个接口,这个就是组合模式的最基本的思想,使部分和整体的使用具有一致性。
享元(flyweight
)模式是一种用于性能优化的模式,“fly
”在这里是苍蝇的意思,意为蝇量级。
享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在JavaScript
中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非常有意义的事情。
享元模式有以下角色:
在下面的例子中,我们创建了一个“Book”类来处理有关特定书籍,然后创建一个“BookFactory
”类来控制如何创建这些Book对象。
为了获得更好的内存性能,如果同一对象被实例化两次,则会重用这些对象。
class Book {
constructor(title, isbn, author, ratings) {
this.title = title;
this.isbn = isbn;
this.author = author;
this.ratings = ratings;
}
getAverageReview() {
let averageReview = (this.ratings.reduce((a,b) => a+b)) / this.ratings.length
return averageReview;
}
}
class BookFactory {
constructor() {
this._books = [];
}
createBook(title, isbn, author, ratings) {
let book = this.getBookBy(isbn);
if (book) { //重用对象
return book;
} else {
const newBook = new Book(title, isbn, author, ratings);
this._books.push(newBook);
return newBook;
}
}
getBookBy(attr) {
return this._books.find(book => book.attr === attr);
}
}
打开谷歌在线表格,提取打印其节点元素。
可以看到就算是滚动至千行,它们都只是共用两个视图。
用的就是享元模式,来防止无限滚动造成卡顿。
以下是模拟实现:
首先是HTML
<section id="app">
<table id="table"></table>
<div class="controls">
<input type="range" name="scroll" id="scroll" value="0">
</div>
</section>
样式:
#app {
position: relative;
padding: 30px 0 30px 10px;
#table {
padding: 20px;
border-radius: 10px;
min-width: 450px;
transition: background 0.5s;
background: rgba(73, 224, 56, 0.1);
&.low-range {
background: rgba(73, 224, 56, 0.47);
td {
border-bottom: 1px solid rgba(73, 224, 56, 0.9)
}
}
&.mid-range {
background: rgba(224, 196, 56, 0.47);
td {
border-bottom: 1px solid rgba(224, 196, 56, 0.9)
}
}
&.high-range {
background: rgba(224, 56, 56, 0.47);
td {
border-bottom: 1px solid rgba(224, 56, 56, 0.9)
}
}
&.ultra-high-range {
background: rgba(224, 56, 56, 0.9);
td {
border-bottom: 1px solid black
}
}
td {
border-bottom: 1px solid black;
padding: 10px;
font-weight: bold;
}
}
.controls {
padding-top: 20px;
#scroll {
width: 450px;
box-sizing: border-box;
}
}
}
逻辑实现,请配合注释食用:
// 生成单元格实例
const makeRowCells = data => data.map(value => new Cell(value));
// 定义常量
const scrollViewport = 10; // 当前表格视图大小
const tableSize = 2000; // 行数
let scrollIndex = 0; // 初始滚动索引
let DATA = []; // 初始数据集
while (DATA.length < scrollViewport) {
const unit = DATA.length * 10;
DATA.push('12345678'.split('').map(() => unit));
}
/**
* cell类 - 列
*/
class Cell {
constructor(content) {
this.content = content;
}
// 更新列
updateContent(content) {
this.content = content;
this.cell.innerText = content;
}
// 渲染列
render() {
const cell = document.createElement('td');
this.cell = cell;
cell.innerText = this.content;
return cell;
}
}
/**
* row类 - 行
*/
class Row {
constructor(cellItems) {
this.cellItems = cellItems;
}
// 更新行
updateRowData(newData) {
this.cellItems.forEach((item, idx) => {
item.updateContent(newData[idx]);
});
}
// 渲染行
render() {
const row = document.createElement('tr');
this.cellItems.forEach(item => row.appendChild(item.render()));
return row;
}
}
/**
* 表格类
*/
class Table {
constructor(selector) {
this.$table = document.querySelector(selector);
}
// 添加行
addRows(rows) {
this.rows = rows;
this.rows.forEach(row => this.$table.appendChild(row.render()));
}
// 更新table数据
updateTableData(data) {
this.rows.forEach((row, idx) => row.updateRowData(data[idx]));
}
}
// 实例化新表
const table = new Table('#table');
// 匹配滚动条的DOM
const scrollControl = document.querySelector('#scroll');
// 在table下添加单元格行
table.addRows(
DATA.map(dataItem => new Row(makeRowCells(dataItem))));
const onScrollChange = event => {
// 为视图准备新数据
DATA = DATA.map((item, idx) => item.map(cell => parseInt(event.target.value, 10)*10 + idx*10));
// 更新当前table的数据
table.updateTableData(DATA);
// 添加颜色区别样式
scrollIndex = event.target.value;
if (event.target.value >= 0) {
table.$table.classList = 'low-range';
}
if (event.target.value > tableSize * 0.4) {
table.$table.classList = 'mid-range';
}
if (event.target.value > tableSize * 0.7) {
table.$table.classList = 'high-range';
}
if (event.target.value > tableSize * 0.9) {
table.$table.classList = 'ultra-high-range';
}
};
// 设置滚动条最小和最大范围
scrollControl.setAttribute('min', 0);
scrollControl.setAttribute('max', tableSize);
// 添加滚动事件
scrollControl.addEventListener('input', onScrollChange);
// 初始化事件
const event = {target: {value: 0}};
onScrollChange(event);
至此,结构型设计模式已经讲(水)完了,其中享元模式值得单独拿出来写一篇博客。
参考文章
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙: