注:本文主要记录了创建一个PWA应用的过程,方便日后再次开发类似应用时参考。
github链接:shenlvmeng/Distance: 一次PWA的尝试

准备

安装http-server方便本地测试。

本地开辟文件夹,加入.editorconfig.gitignore(根据样例适当修改)

1
2
3
4
5
6
7
8
9
10
11
# http://editorconfig.org

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

创建index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<title>Away from home</title>
<link rel="stylesheet" type="text/css" href="./main.css">
</head>
<body>
<h1>My first PWA Application</h1>
<p>See console for more!</p>
</body>
</html>

在目标文件夹路径下执行http-server,bingo!第一步完成

清单文件

创建清单文件manifest.json,描述应用添加到主屏幕需要的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "Distance from Home",
"short_name": "Distance",
"display": "standalone",
"start_url": "/",
"theme_color": "#8888ff",
"background_color": "#aaaaff",
"icons": [
{
"src": "compass.png",
"sizes": "144x144",
"type": "image/png"
}
]
}

并在index.html中引入:

1
<link rel="manifest" href="manifest.json">

添加Service Worker

Service Worker这个东西可以实现页面的缓存和离线访问,让应用逼近app的体验。

可以在HTML里插入<script>标签引入,这里单独定义一个app.js。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app.js

// Registering ServiceWorker
if ( 'serviceWorker' in navigator ) {
navigator.serviceWorker.register( 'sw.js' ).then(function(registration) {

// Registration was successful
console.log( 'ServiceWorker registration successful. Scope: ' + registration.scope )
}).catch(function(err) {

// Registration failed with error
console.log( 'ServiceWorker registration failed' + err);
});
}

index.html中引用。

下面实现service worker。主要是三个时机:

  • 脚本安装时,写入缓存
  • 脚本获取数据时,先查找缓存
  • 缓存更新版本时,删除原先版本的缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 脚本安装时
self.addEventListener('install', event => {
event.waitUntil(
caches.open(cacheStorageKey)
.then(cache => cache.addAll(cacheList))
// 保证在页面更新过程中,新的Service Worker脚本能立即激活生效
.then(() => self.skipWaiting())
)
});

// 通过脚本fetch数据
self.addEventListener('fetch', event => {
event.respondWith(
// 先在cache中找
caches.match(event.request).then(res => {
// cache中没有再使用fetch
return res || fetch(event.request.url);
})
)
});

// 更新静态资源
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(cacheNames.map(name => {
if (name !== cacheStorageKey) {
return caches.delete(name);
}
}));
})
);
});

其中,有下面一些名词:

  • self: Service Worker作用域, 也是全局变量
  • caches: 缓存接口
  • waitUntilExtendableEvent.waitUntil()方法——这会确保Service Worker 不会在waitUntil()里面的代码执行完毕之前安装完成。
  • skipWait,表示强制当前处在waiting状态的脚本进入activate状态

在浏览器中打开localhost:8080/即可,注意PWA必须运行在HTTPS的环境下

加入百度地图支持

参考官方API接入支持

添加文件index.js,写入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const url4PC = 'https://api.map.baidu.com/api?v=2.0&ak=你的秘钥&callback=init';
const init = window.init;
let map = null;

// 为了能成功调用到callback
window.init = () => {
map = new BMap.Map('map');
map.centerAndZoom(new BMap.Point(121.491, 31.233), 11);
map.enableScrollWheelZoom(true);
window.init = init;
}

function loadMap() {
const script = document.createElement('script');
script.src = url4PC;
document.body.appendChild(script);
}

window.onload = loadMap;

当然需要添加一点CSS,这里从略。

Service Worker支持跨域

接下来问题来了,打开网页测试,页面刷新时Service Worker会抛出跨域相关的错误。

Ahb0hR.png

再正常不过了,未经配置的情况下,fetch是不允许跨域的。注意sw.js中的一段:

1
2
3
4
caches.match(event.request).then(res => {
// cache中没有再使用fetch
return res || fetch(event.request.url);
})

这里我们不仅有跨域请求,还使用着JSONP的方式。后续地图数据和地图图片的展示请求也比较复杂。再经过多次加入respondWith()响应块失败后,干脆跳过这部分跨域的处理。不过还是留下了一个函数,方便后面拓展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function isCORSRequest(url) {
return url.search(host) === -1;
}

// 并存该函数为日后拓展所用
function handleFetchRequest(req) {
const request = isCORSRequest(req.url) ?
new Request(req.url, { mode: 'cors' })
: req;
fetch(req);
}

// 通过脚本fetch数据
self.addEventListener('fetch', event => {
if (isCORSRequest(event.request.url)) {
return;
}
event.respondWith(
// 先在cache中找
caches.match(event.request).then(res => {
// cache中没有再使用fetch
return res || handleFetchRequest(event.request);
})
);
});

目标点管理

核心功能之一是,可以新增计算距离的目标点。在我的设计里,有两种用户友好的添加方式:

  • 找不到地点时,可以搜索出地点,通过搜索结果设置
  • 明确地图上位置时,直接通过鼠标操作设置

前面的利用AutoComplete类,完成自动补全和搜索功能。值得注意的一个坑是,在初次设置好map对象后,每次的搜索结果都会以第一次初始化地点为中心搜索结果。解决方案是,每次<input>框聚焦时,重新构造一个AutoComplete类。

1
2
3
4
<div class="search">
<input class="searchbox" type="text" id="position" size="20" placeholder="输入地点或在地图上双击点选" />
<div id="searchBtn" class="searchBtn"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function initSearchLocation() {
// 工厂方法,每次聚焦时新创建一个AutoComplete对象
function setAutoComplete() {
const lastVal = doc.getElementById('position').value.trim();
// 建立一个自动完成的对象
const searchBox = new BMap.Autocomplete({
input: "position",
location: map
});
doc.getElementById('position').value = lastVal;

// 鼠标点击下拉列表后的事件
searchBox.addEventListener("onconfirm", event => {
const value = event.item.value;
const searchWord = `${value.province}${value.city}${value.district}${value.street}${value.business}`;
setPlace(searchWord);
});
}

function setPlace(searchWord){
map.clearOverlays();
// 搜索
const cleverSearch = new BMap.LocalSearch(map, {
onSearchComplete: () => {
pauseUpdate();
if (!cleverSearch.getResults().getPoi(0)) {
alert('未找到指定地点,换个关键词试试?');
return;
}
// 获取第一个智能搜索的结果
const res = cleverSearch.getResults().getPoi(0).point;
const marker = new BMap.Marker(res);
map.centerAndZoom(res, 18);
map.addOverlay(marker);
addToBeacons(marker);
}
});
cleverSearch.search(searchWord);
}

// 不使用自动补全,直接回车或点击图标
doc.getElementById("position").addEventListener("focus", event => {
setAutoComplete();
});
// 省略了其他的绑定事件
}

下一步是支持标记点(BeaconNodes)的管理,离线持久化采用localStorage,技术上倒没有什么难度。用户交互上,考虑用右键点击(for PC)和长按操作(PC Mobile)添加,点击标记删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 从页面新增目标点
function addToBeacons(point) {
let tag = win.prompt("给Beacon点起个名字吧");
if (tag === null) {
return;
}
tag = tag.trim().substr(0, 20) || `未知地点#${seq}`;
// 第一个点默认活跃
beaconNodes.push({
id: seq,
point,
tag,
isActive: !beaconNodes.length
});
addBeacon(seq, point, tag, beaconNodes.length === 1);
paintDropdown();
localStorage.setItem(STORAGE_KEY, JSON.stringify(beaconNodes));
}

function removeFromBeacon(id) {
if (!beaconNodes.length) {
return;
}
beaconNodes.splice(beaconNodes.findIndex(node => node.id == id), 1);
paintDropdown();
localStorage.setItem(STORAGE_KEY, JSON.stringify(beaconNodes));
}

// 在地图上新增目标点,方便复用
function addBeacon(id, point, tag, isActive) {
const icon = new BMap.Icon(`imgs/location-${2-isActive}.png`, new BMap.Size(50, 50));
const mark = new BMap.Marker(point, { icon, enableMassClear: false });
const label = new BMap.Label(tag,{ offset: new BMap.Size(32, 2) });
label.setStyle({
border: "#dedede",
padding: "3px 5px",
"font-size": "14px",
"font-weight": "bold"
});
mark.setLabel(label);
mark.getLabel().setTitle(tag);
mark.addEventListener('click', event => {
if (win.confirm(`确认要删除Beacon:${mark.getLabel().getTitle()}`)) {
map.removeOverlay(mark);
removeFromBeacon(id);
}
});
map.addOverlay(mark);
seq = id + 1;
}

// ...
function initMap() {
// 根据storage填充地图
// 注意这里必须使用Point类
beaconNodes.forEach(node => {
addBeacon(node.id, new BMap.Point(node.point.lng, node.point.lat), node.tag, node.isActive);
});
// ...

// 避免和其他事件冲突,使用暂无占用的右键点击
map.addEventListener("rightclick", event => {
addToBeacons(event.point);
});
// 支持移动设备,长按
map.addEventListener("longpress", event => {
addToBeacons(event.point);
})
}

在管理上,新增了下拉菜单管理当前所有节点,这里需要将离线的BeaconNodes映射成DOM结构,考虑到文件已略大,且这一部分比较繁琐,和业务关系不大,故单独抽出一个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// dom.js
/*
* 无状态,与业务弱相关
* 只渲染下拉菜单
*/

function renderDropdown(obj, dom, isInit) {
if (!obj) {
return;
}
dom.innerText = "";
// render过程
const fragment = document.createDocumentFragment();
obj.map(node => {
const div = document.createElement('DIV');
div.className = `node_wrapper${node.isActive ? " active" : ""}`;
const innerHTML = `<span>#${node.id}</span><span class="node-tag">${node.tag}</span>`;
div.innerHTML = innerHTML;
const operateBtn = document.createElement('DIV');
operateBtn.className = "operateBtn";
if (node.isActive) {
operateBtn.className += " active"
operateBtn.innerText = "Deactive!"
} else {
operateBtn.innerText = "Active!"
}
div.appendChild(operateBtn);
// 方便统一事件绑定
div.dataset.nid = node.id;
return div;
}).forEach(div => {
fragment.appendChild(div);
});
dom.appendChild(fragment);

// 多次调用只监听一次
if (isInit) {
// 下拉事件
dom.parentNode.children[0].addEventListener("click", event => {
if (dom.parentNode.className.search("hide") !== -1) {
dom.parentNode.className = "dropdown dropdown_container";
} else {
dom.parentNode.className += " hide";
}
});
}
}

HTML和CSS部分就不再赘述了。

在下拉菜单下,可以点击条目跳转到相应位置,以及点击按钮切换当前活动的BeaconNode。这部分实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function initDropdown() {
paintDropdown(true);
// 事件监听
const dropdown = doc.getElementById("dropdown");
dropdown.addEventListener("click", event => {
const nid = event.target.parentNode.dataset.nid || event.target.dataset.nid;
const node = beaconNodes.find(node => node.id == nid);
if (event.target.tagName.toLowerCase() === "div") {
if (node) {
const prev = beaconNodes.find(node => node.isActive);
if (prev) {
prev.isActive = false;
beaconMarkers[prev.id].setIcon(new BMap.Icon("imgs/location-2.png", new BMap.Size(40, 40)));
}
node.isActive = true;
paintDropdown();
beaconMarkers[nid].setIcon(new BMap.Icon("imgs/location-1.png", new BMap.Size(40, 40)));
}
} else {
node && map.panTo(new BMap.Point(node.point.lng, node.point.lat));
}
});
}

当然了,标记点的增删改查还剩下改没有实现,目前来看,和“改”相关的业务是能拖动标记点,方便用户随时更改标记点位置。在addBeacon()函数中新增相关代码。

1
2
3
4
5
6
mark.enableDragging();
// ...
mark.addEventListener('dragend', event => {
editToBeacons(id, { point: event.point });
paintDistance(lastPoint);
});

editToBeacons()函数设计如下:

1
2
3
4
5
6
function editToBeacons(id, newProps) {
const index = beaconNodes.findIndex(node => node.id == id);
Object.assign(beaconNodes[index], newProps);
localStorage.setItem(STORAGE_KEY, JSON.stringify(beaconNodes));
// 不是所有的属性更改都有重绘,交给调用者处理重绘
}

计算距离

计算距离包括距离和方向两部分,既然能拿到两点的经纬度信息,这两个值肯定可以计算出来。计算距离上利用百度地图的API,计算角度上,通过Math.atan2换算得到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function paintDistance(currentPoint) {
let info;
let target = beaconNodes.find(node => node.isActive);
if (!target || !currentPoint) {
info = "N/A , 未知方向";
} else {
let distance = map.getDistance(currentPoint, target.point) || "N/A";
if (distance > 10000) {
distance = `${(distance / 1000).toFixed(2)}km`;
} else if (distance > 100) {
distance = `${distance.toFixed(0)}m`;
} else {
distance = `就在附近...`
}
// 工具函数位于utils.js
const deg = getDirection(target.point, currentPoint);
info = `${distance} , ${deg}点钟方向`
}
// ...
}

function getDirection(p1, p2) {
const pi = Math.PI;
const [x2, y2, x1, y1] = [p1.lng, p1.lat, p2.lng, p2.lat];
let rad = Math.atan2(y2 - y1, x2 - x1);
let absRad = Math.abs(rad);
let clockPointer;
if (absRad < pi / 12) {
clockPointer = 3;
} else if (absRad < pi / 4) {
clockPointer = rad > 0 ? 2 : 4;
} else if (absRad < pi * 5 / 12) {
clockPointer = rad > 0 ? 1 : 5;
} else if (absRad < pi * 7 / 12) {
clockPointer = rad > 0 ? 12 : 6;
} else if (absRad < pi * 3 / 4) {
clockPointer = rad > 0 ? 11 : 7;
} else if (absRad < pi * 11 / 12) {
clockPointer = rad > 0 ? 10 : 8;
} else {
clockPointer = 9;
}
return clockPointer;
}

参考

全文参考《设计模式之禅》Head First Design Pattern这本也不错。

准备

JavaScript学久了,在看设计原则时,顺带温习了下OOP中的一些传统概念。

类图

  • 一>: 关联,类定义中的相互引用,通常使用类的属性表达。 比如客户类和订单类
  • -->: 依赖,最弱的关系,对象间的临时关联,用函数参数、局部变量、返回值等表达
  • 一▷: 继承
  • --▷: 实现
  • 一◇: has-a关系,表示整体与局部,但不稳定。比如公司类和雇员类
  • 一◆: contains-a关系,表示整体与局部,部分不能脱离整体而存在。

override和overload

区别 覆写 重载
单词 OverLoading Override
概念 方法名称相同,参数的类型或个数不同 方法名称相同,参数的类型或个数相同,返回值类型相同
范围 发生在一个类之中 发生在类的继承关系中
权限 一个类中所重载多个方法可以不同的权限 被子类所覆写的方法不能拥有比父类更严格的访问控制权限

接口和抽象类的不同

两者都为“面向契约编程”而存在。总体来说,接口是对能力的抽象,抽象类是对事物的抽象。从而有着下面的不同:

  • 接口被类实现,抽象类被子类继承。
  • 接口只做方法声明,抽象类中可以做方法声明,也可以做方法实现。
  • 接口里定义的变量只能是公共静态常量,抽象类中的变量可以是普通变量。
  • 抽象类里可以没有抽象方法,接口是设计的结果,抽象类是重构的结果。
  • Java中接口可继承接口,并可多继承接口,但类只能单继承。

它们还有以下特点:

  • 在实现时必须全部实现,否则仍是接口/抽象类
  • 抽象类中可以没有抽象方法

设计6原则

SOLID原则:

  • 单一职责(接口细分到单一业务)
  • 里氏替换(实现都按接口来)
  • 依赖倒置(多使用抽象概念)
  • 接口隔离原则(接口尽量细分)
  • 迪米特法则(低耦合)
  • 开闭原则(高内聚,低耦合)

总结来说,就是好好设计、合理拆分接口,一旦确定,避免更改;减少接口耦合;面向接口编程;使用中间层应对需求变更

常见设计模式

单例模式

单例模式是最简单的设计模式。即一个类只有一个实例,或一个构造函数只能初始化一次(JS版本),且自行实例化,并像整个系统提供整个实例。常用在下面这些场景下,解决特定的问题:

  • 整个项目需要一个共享访问点或共享数据
  • 包含了大量静态常量(通常是配置数据)和静态方法的工具类
  • 创建一个对象需要消耗的资源过多

实现上,分懒汉型和饿汉型。它们的主要区别在初始化单一实例的时机在类创建时还是访问时。懒汉型需要注意线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 饿汉型
public class Singleton {
private static final Singleton singleton = new Singleton();

// 限制外部访问
private Singleton() {
}

// 暴露的public方法
public static Singleton getInstance() {
return singleton;
}

// 其他方法
// ...
}

// 懒汉型
public class Singleton {
private static Singleton singleton = null;

// 限制外部访问
private Singleton() {
}

// 暴露的public方法
public static sychronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}

JavaScript中就灵活多了。最常用的字面量变量就是最简单的单例,强行使用构造函数,也可以借助闭包的特点实现。另外,JavaScript是单线程,不需要考虑线程安全的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 字面量变量
const singleton = {
// 一些属性
foo: 'bar',
// 一些方法
baz() {
console.log('Hello world!');
}
}

// 闭包
function Singleton() {
const singleton = this;
this.foo = 'bar';
this.baz = () => { console.log('Hello world!'); };
Singleton = () => singleton;
}

拓展

当放开单例限制,编程多例模式时,可以通过加入计数器来限制。但不常用,下面给出JavaScript版本例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Singleton() {
const singleton = this = [];
// 比如限制为3
const limit = 3;
// 一些初始化工作
for (let i = 0; i < limit; i++) {
// ...
[].push({
// ...
});
}
Singleton = () => singleton;
}

工厂模式

工厂模式意思是,将创建对象的过程封装起来,在OOP语言中体现在定义一个创建一类对象的接口,让实例化过程在子类中完成。浅显的来讲,就是让工厂类将具有共性、创建过程复杂的一类对象的创建过程封装起来,便于业务类使用,同时也便于日后拓展。业务类只需要交给工厂类需要的对象类名(Java)或别的标志就可以得到所需对象。

使用场景上,它是new模式的替代品,在任何需要对象的场景下都可以使用,但是只有下面这些情况下是比较合适的:

  • 需要灵活解耦的框架
  • 产品类创建过程复杂、有差异性且有共性。比如连接邮箱客户端的三种协议POP3、IMAP、HTTP的构造过程抽象成一个IConnectMail接口,再设计各自的工厂类。在日后有新的连接邮箱客户端协议时,只需新增一个工厂类即可。

Java中工厂类可以使用反射等方法创建新对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 产品类
public abstract class abstractProduct {
// 共有方法
public void method() {}
// 抽象方法
public abstract void abstractMethod();
}

public class product1 extends abstractProduct() {
public abstractMethod() {
// 差异化方法
}
}

public class product2 extends abstractProduct() {
public abstractMethod() {
// 差异化方法
}
}

public abstract class Factory {
// 入参根据场景调整
public abstract <T extends abstractProduct> T createProduct(Class<T> c);
}

public class Factory1 extends Factory {
public <T extends Product> T createProduct(Class<T> c) {
Product p = null;
try {
p = (Product)Class.forName(c.getName()).newInstance();
} catch (Exception e) {
}
}
}

JavaScript中,工厂模式的实现更加轻量级,因为构造对象的方式更加简单,使用函数将多个产品类间的共性抽出来就行了。样例略。

抽象工厂模式

抽象模式在工厂模式的基础上又抽象了一层,产品类接口下是多个产品族抽象类,产品族下才是明确的产品类。相对应的,工厂接口下的多个工厂方法根据最细节的产品类生产对象。它的优势在:

  • 可以不公开地控制产品族间的约束
  • 更好地组织多维度(更多是2维)上多个产品间的生产

缺点也很明显,产品族的修改将会直接影响工厂接口和产品抽象类,这是开闭原则的大忌。因此,在产品维度固定,且有必要从多维度上划分产品时,才会使用抽象工厂模式。比如Windows、Linux、Mac OS上的文本编辑器与图像处理程序就是两个维度的多个产品。

样例略。

模板方法模式

模板方法模式比较好理解,就是将子类中共有的算法框架抽象到抽象类中实现,注意是框架,而不是具体的步骤。子类可以根据自己的需要,在不改变框架的基础上,重定义算法的某些步骤,得到不同的结果。下面举个例子就能更方便地看明白了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const student = {
study() {
this.gotoPrimarySchool();
this.gotoMiddleSchool();
this.gotoUniversity();
}
};

const stu1 = Object.assign(Object.create(Student), {
gotoPrimarySchool() {
console.log("子弟学校 ");
}
gotoMiddleSchool() {
console.log("人大附中 ");
}
gotoUniversity() {
console.log("清华大学 ");
}
});

const stu2 = Object.assign(Object.create(Student), {
gotoPrimarySchool() {
console.log("子弟学校 ");
}
gotoMiddleSchool() {
console.log("华师一附中 ");
}
gotoUniversity() {
console.log("中科大 ");
}
});

stu1.study(); //子弟学校 人大附中 清华大学
stu2.study(); //子弟学校 华师一附中 中科大

可以看到,同样是调用学习方法studystu1stu2可以再不影响公用算法流程下,定义自己的算法步骤。使用Java实现是一样的思路:

  • 定义抽象类,声明可以差异化的基本方法,实现模板方法,在模板方法中调用可以差异化的基本方法
  • 子类根据需要,实现自己的基本方法

模板方法模式核心就在于封装不变部分,开放可变部分,共有的算法步骤也较容易维护。因此,使用在下面的场景里:

  • 子类共有相同算法流程
  • 将核心算法设计为模板方法,细节功能由子类补充

建造者模式

建造者模式和工厂模式类似,意思是,讲一个复杂对象的构建表示分离,使同样的构建过程可以有不同的表示。其中的构建强调的是不同基本方法的调用顺序安排,而不是基本方法的实现(这也是它和工厂方法的最大区别);表示是指产品子类对于基本方法的差异性实现。

对比上面模板方法模式来看,就是study的顺序对于不同人不一样,这个顺序有另外的建造类描述并实现。可以看到,建造者模式主要的使用场景是:

  • 相同的执行方法,不同的执行顺序,产生不同的结果
  • 产品类中,不同的构建顺序会有不同的结果
  • 用户希望执行次序可控

在实现时,建造类通过传入新状态或其他方式影响产品类的模板方式执行次序。在建造类和产品类之上,使用导演类可以起到封装的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 产品类
public class Product {
public void method() {
// 差异化业务
}
}

// 抽象建造类
public abstract class Builder {
// 设置构建的次序,以获得不同的结果
public abstract void setSequence();
// 建造
public abstract Product build();
}

// 具体建造类
public class Builder1 extends Builder {
private Product product = new Product();
public void setSequence() {
// 差异化逻辑
}
public Product buildProduct() {
return product;
}
}

// 导演类
public class Director {
private Builder builder = new Builder1();
public Product getProductA() {
builder.setSequence();
return builder.build();
}
}

代理模式

代理模式即为其他对象提供一个代理来控制对这个对象的访问,浅显易懂。利用代理模式还可以拦截原始请求,做额外的事情。应用很广泛。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public interface Object {
// 作为示例的一个方法
public void request();
}

public class RealObject implements Object {
public void request() {
// 业务逻辑
}
}

public class Proxy implements Object {
// 代理的对象
private Object object = null;

public Proxy() {
this.subject = new Proxy();
}

// 传递代理者
public Proxy(Object o) {
}

public void request() {
this.before();
this.object.request();
this.after();
}

// 预处理
public void before() {
}

// 善后处理
public void after() {
}
}

除了普通代理,还有强制代理即只能通过被代理对象对象访问到代理。另外,AOP(Aspect Oriented Progarmming)模式也是建立在代理模式的基础上的。ES6中的proxy和ES7中的装饰器就是AOP概念下的产物。

原型模式

原型模式即不通过new而是通过对象复制生产同类对象。非常好理解。在Java中,一个实现了cloneable接口的对象即使用了原型模式。而JavaScript更是天生使用原型模式实现对象的继承和拓展。

1
2
3
4
5
6
7
8
9
10
11
12
public class PrototypeClass implements Cloneable {
@Override
public PrototypeClass clone() {
PrototypeClass p = null;
try {
p = (PrototypeClass)super.clone();
// 其他操作
} catch(CloneNotSupportedException e) {
}
return p;
}
}

它的优势在于更加轻量级,避免了构造函数的约束。问题在于,第一会跳过构造函数(这个JavaScript没有),第二是深浅拷贝的问题,第三在Java中,clone带有final成员的类会抛出异常。

中介者模式

中介者模式主要用在多个对象那个间有比较复杂的交互场景下,用一个中介对象封装对象间的一系列交互,中介往往与各对象都有交互,从而使其耦合松散,符合迪米特法则。从类图上看,它把原先的蛛网状结构简化为了星型结构。

它的优点是减少了类间依赖,缺点是有些时候中介者会膨胀的很大。使用场景上,在有协调概念出现的场景都有它的发挥空间:

  • 机场调度中心
  • MVC框架中的Controller
  • 媒体网关,中介服务

因为应用场景广泛,这里不举样例。

命令模式

命令模式即将一个用对象组织每一个请求,从而允许使用请求完成一系列操作,同时还可以对请求排队或记录日志或撤销以及恢复。模式主要包括三个角色:

  • 接受者,完成请求内操作的角色
  • 命令,封装好的系列操作
  • 调用者,接受、执行命令的角色

这种设计模式在存在上面三种角色的场景很适用,易于封装常用的命令,且很容易拓展,当命令间共同点较多时,还可以结合模板方法模式进行改进。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public abstract class Receiver {
// 定义所有接受者必须完成的业务
public abstract void work() {
}
}

public class Receiver1 extends Receiver {
public void work() {
}

// 差异化业务
public void otherWork1() {
}
}

public class Receiver2 extends Receiver {
public void work() {
}

// 差异化业务
public void otherWork2() {
}
}

public abstract class Command {
public abstract void exec();
}

public class Command1 extends Command {
// 对特定接受者命令
private Receiver receiver;

public Command1(Receiver _receiver) {
this.receiver = _receiver;
}

public void exec() {
this.receiver.work();
}
}

public class Command2 extends Command {
// 对特定接受者命令
private Receiver receiver;

public Command2(Receiver _receiver) {
this.receiver = _receiver;
}

public void exec() {
this.receiver.work();
}
}

// 调用者
public class Invoker {
private Command command;

public void setCommand(Command _command) {
this.command = _command;
}

public void react() {
this.command.exec();
}
}

责任链模式

责任链模式重点在“链”,将有机会处理请求的所有对象组成链,沿着这条链传递请求直到有对象处理它为止。它和Express和Redux中的中间件的概念有相似之处,区别在于责任链上一般只会有一个或部分对象处理请求。它替换了场景代码中的大堆if elseswitch语句。

一个狭义的抽象处理者像下面这样。使用模板方法模式,将抽象的业务逻辑外的处理流程实现在抽象类,细节的业务逻辑放在子类里完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public abstract class Handler {
// 下一个处理者
private Handler next;
// 处理逻辑
public final Response handle(Request req) {
Response res = null;
if (this.getLevel().equals(req.getLevel())) {
// 只处理属于自己的level
res = this.exec(req);
} else {
// 如果有下一个处理者,交给它
if (this.next != null) {
res = this.next.handle(req);
} else {
// 自行处理
}
}
return res;
}

// 设置下一个处理者
public void setNext(Handler _handler) {
this.next = _handler;
}

// 处理者有自己的处理级别
protected abstract Level getLevel();

// 处理者有自己的处理逻辑
protected abstract Response exec(Request req);
}

// 子类示例
public classHandler1 extends Handler {
// 自己的处理逻辑
protected Response exec(Request req) {
return null;
}

// 自己的处理级别
protected Level getHandlerLevel() {
return null;
}
}

public class Level {
// 定义一个请求和处理等级
}

public class Request {
// 获取请求等级
public Level getRequestLevel() {
return null;
}
}

public class Response {
// 定义返回的数据
}

它的优点在解耦了请求与处理者,使系统更加灵活。问题在责任链比较长时,遍历整个责任链会带来性能问题。Express中类似的中间件则不会,因为处理后可以决定是否执行next(),跳到下一个中间件。

上面Java的实现通过next属性连接所有的处理者(类似链表),在JavaScript的工程实现上,一般在上层又有一层封装,用数组保存所有处理者,再建立之间的连接。这种动态的连接支持动态增删处理者,甚至改变他们的处理顺序。

装饰模式

装饰模式也是一种比较常见的模式,它可以动态地为一个对象增加一些额外的功能,使用装饰器类返回对象比定义子类继承要来得更加灵活。在设计上,有下面几个角色:

  • 抽象构件,即被修饰的对象抽象
  • 具体构件,被修饰的客体
  • 装饰器,一般是一个抽象类,将具体装饰器的共性抽出来,其必有一个private属性指向原始的抽象构件
  • 具体装饰器,装饰具体构件的原有方法,一般来说需要有所有与具体构件public方法同名的方法,且在方法内会使用到而非单纯替换原同名方法(类似滚雪球的过程)。

它的使用类似下面:

1
2
3
4
5
6
7
8
9
10
11
12
// 场景类
public class Scene {
public static void main(String[] args) {
Component c = new Component();
// 装饰
c = new Decorator1(c);
// 再次装饰
c = new Decorator2(c);
// 执行
c.exec();
}
}

JavaScript中的Object.create()Object.assign()和装饰模式有几分相似。

它的优势在于装饰类间不相互耦合,且装饰次序可以灵活修改,可以很好地重构、替换继承关系。劣势在于包装层次过多时,不利于调试时发现错误。装饰模式一般用于:

  • 动态增强一个类、对象的功能
  • 批量为一批对象或类改装或增加功能

总之就是使用继承总感觉小题大做的场合下(OOP语言中)。JS下扩展功能要容易很多。

策略模式

策略模式意为定义一组算法,将每个算法单独封装为可以相互替换的模块。在使用时,有三个主要角色:

  • 策略模块,被封装好的算法策略,屏蔽了高层逻辑对策略的直接访问(高内聚
  • 抽象策略,抽出策略共性的接口,如下面的
  • 具体策略,具体的算法策略,包含具体的算法

在算法间可以自由切换、最好屏蔽算法细节时常用,比如Web编程中常见的表格验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const strategy = {
notEmpty: val => val.length > 0,
name: val => val.length > 1 && val.length < 10,
age: val => +val > 0,
password: val => val.match(/\d+{6,}/g)
}

const validator = {
rules: strategy,
verify: (rule, val) => this.rules[rule](val)
}

function formValidate(formData) {
const { name, age, password, introduction } = formData;
return validator.verify('name', name) &&
validator.verify('age', age) &&
validator.verify('password', password) &&
validator.verify('notEmpty', introduction);
}

策略模式的优势在扩展性良好,同时避免了if else以及switch语句。它的问题也很明显,策略类增多时,会不好维护,同时所有策略类都需要对外暴露。当策略类数目膨胀时需要考虑到这些问题。同时,现在在哪些时机如何组合策略并没有严格定义,实际实现时,会参考建造者模式里一样,定义一个导演类,把常用的组合方式定义出来。减少策略类的暴露。

适配器模式

适配器模式即,把一个类的接口变成客户端期待的另一个接口,从而使原先不匹配的两个类能够一起工作。简单点说,就是加了一个中间层做翻译工作。这种模式下有三种角色:

  • 目标角色,即期望接口
  • 源角色,即原始接口
  • 适配器角色,即转换类

在实现上,通常使用一个同时继承两个类的类作为中转。因为对象很轻量级,JS中就更容易实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface Target {
public void request();
}

public class Target1 extends Target {
public void request() {
// 目标逻辑
}
}

public class Adaptee {
// 原有逻辑
public void foo() {
// ...
}
}

public class Adapter extends Adaptee implements Target {
public void request() {
super.foo();
}
}

它的优点在可以将两个不相关的类在一起运行,提高了类的复用程度,它通常用来救火,完成拓展。由于实际工程中,业务变更较常出现,适配器模式也很常用。

拓展

如需要多适配一的场景,此时无法多继承(Java),在适配类中引用多个类即可。

迭代器模式

迭代器模式,顾名思义是提供一种方法按顺序访问容器中的每个元素,而无需暴露容器的细节。在实现时,通常要自己实现一个迭代器。Java中通过拓展java.util.Iterator实现,JavaScript中,则通过封装数组实现。实现时,要考虑下面几个方法:

  • 判断是否到达尾部
  • 返回下一个元素
  • 删除当前元素

像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
// 迭代器
public interface Iterator {
public Object next();
public boolean hasNext();
public boolean remove();
}
// 容器
public interface Demo {
public void add(Object o);
public void remove(Object o);
public Iterator iterator();
}

现在所有高级语言基本都有这个接口或基础实现。这个模式已经很少用到。

组合模式

组合模式用在表示树状结构的数据中,使用户对单个对象和组合对象使用具有一致性。组合模式下有三种角色:

  • Component,节点抽象角色,参与组合对象的共有方法和属性
  • Leaf,叶子对象,遍历的最小单位
  • Composite,树枝节点

用JavaScript表示,就像下面这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const node = {
commonMethod() {}
}

const branchNode = Object.create(node);
const leafNode = Object.create(node);

branchNode = Object.create(branch, {
children: [],
add(node) {
children.push(node);
},
remove(index) {
children.splice(index,1);
},
print() {
children.forEach(child => { console.log(child); });
}
})

leafNode = Object.create(leafNode, {
// ...重写node的同名方法
})

组合模式在表示整合局部的关系时很有用,但是由于树枝节点和叶子节点的使用方式不同,在使用两种节点时,需要直接调用接口的实现类,这一点违背了依赖导致原则(面向接口编程)。使用上分为透明模式和安全模式,后者就像上面一样,在实现类上区分开树枝和叶子节点,透明模式下,所有的方法均抽象到抽象类中实现,而在叶子节点调用不合法方法时抛出异常。

综上来看,组合模式即使用用数据结构描述一颗多叉树。

观察者模式

观察者模式,也叫发布订阅模式,可以说是前端最熟悉也是最常见的一种设计模式了。小到页面事件监听,大到Vue的设计原理都能看到观察者模式的影子。它的内涵在将信息流从原来的pull变成push。从而不需要使用whilesetInterval这种很消耗资源的方式。代价是,需要硬编码到被监听者中,在状态改变时,push信息到监听者那里。通常实现时,这个过程可以抽象到很底层完成,如Vue使用Object.defineProperty,并不影响整体实现。另外,为了实现多对多的监听,往往需要在被监听者和监听者之间增加spy(或者叫probe)这样的角色进行中转和广播通知。

这时候可以定义Observable接口,当然了,它需要可以增删监听者和在发生事件后提醒监听者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public interface Observable {
public void add(Observer o);
public void delete(Observer o);
public void notify(String text);
}

public interface IFoo {
public void work();
public void sleep();
}

public interface Observer {
public void update(String text);
}

public class Victim implements Observable, IFoo {
private ArrayList<Observer> oList = new ArrayList<Observer>();
public void add(Observer o) {
this.oList.add(o);
}

public void delete(Observer o){
this.oList.remove(o);
}

public void notify(String text) {
for (Observer o: oList) {
o.update(text);
}
}

public void work() {
// ...
this.notify("Working...");
}

public void sleep() {
// ...
this.notify("Sleeping...");
}
}

一个简单的JavaScript实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var EventUtil = {
// 全局事件管理
var events = {},
// 注册事件
on = function (type, handler) {
if (events[type]) {
events[type].push(handler);
} else {
events[type] = [handler];
}
},
// 触发事件
emit = function (type) {
if (!events[type]) return;
for (var i = 0, len = events[type].length; i < len; i++) {
events[type][i];
}
};
};

观察者模式的优势在于在两个对象有频繁信息交互或希望监听特定时机时很有用,当信息很多时,可以考虑增加中间层,设计消息队列处理。Java本身也提供java.util.Observerjava.util.Observable用来实现这种模式。

建立在观察者模式的基础上,有响应式编程这样新的编程范式出现,ReactiveX就是在这种范式基础上推出的多语言框架。JavaScript版本的叫做RxJS,相信看完这个简介对你会非常有帮助。

门面模式

门面模式又叫外观模式,它要求所有子系统与外部通信时必须使用统一的对象进行,提供高层的接口,尽量掩盖不必要的业务细节,使得子系统更易用。因为实现起来重点在于统一通信数据格式和封装业务细节。这种模式也非常常用。比如在通常的前后端协调时,后端回传前端请求的数据通常都是统一的格式,避免错误同时减少前端工作量。比如下面这样。同样,有时后端也会要求前端在请求时使用统一的数据格式(不常见)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const successRet = {
// 错误码
code: 0,
// 返回数据
data: {
userInfo: {
id: 0000001,
// ...
}
}
}

const errorRet = {
code: 0002,
// 错误原因
message: "请求过于频繁!"
}

门面模式可以极大地提升封装性,增加系统的高内聚,同时也减少了系统内外的耦合,提高了灵活度和安全性。劣势在于对扩展不利,所有的改动几乎都要对应到门面(Facade)类的硬编码。因此门面模式的使用场景是:

  • 为一个复杂的系统或模块提供对外接口
  • 子系统间相对独立

通常情况下,门面类只负责聚合,不参与具体的子系统逻辑。另外,在系统庞大时,很可能有不止一个门面入口。后端接口微服务化的趋势下,在系统内,拆分原来庞大的接口,同时面向前端不同设备,设计不同的服务汇合接入点正是门面模式的体现。

备忘录模式

备忘录模式即,在不破坏封装性的前提下,捕获一个对象的内部状态,在对象外保存,并在合适的时候可以将对象恢复到保存的状态。这个概念很简单,涉及到三个角色:

  • 发起人,需要记录状态的对象
  • 备忘录, 用来储存状态
  • 备忘录管理者,对备忘录进行管理,保存和恢复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Memorial {
state: ''

memorize() {
return new Memento(this.state);
}
restore(memento) {
this.state = memento.state;
}
}

class Memento {
state: ''

constructor(state) {
this.state = state;
}
}

const Manager = {
memento: null
}

const m = new Memorial();
Manager.memento = Memorial.memorize();
Memorize.restore(Manager.memento);

它的优点很明显,可以保存和回复状态,支持回滚事务。但在使用时,通常是结合了别的设计模式的变种。

拓展

结合原型模式,可以直接用clone对象的方式保存状态。这么做问题是当状态对象较大时,会有时间和空间的开销,优势是可以直接将状态存储在类内部,避免了其余类的定义。

在多状态存储上,Java可以借用BeanUtil工具类(书中所说),JavaScript中就灵活很多了(还是因为轻量级的对象)。同样的多备忘录模式就不再赘述。另外,需要保证备忘录的保密性时,封装成业务类的内置类,设置权限为private即可,JS中同理。

访问者模式

访问者模式指,封装作用在数据结构上各元素的操作,它可以在不改变数据的前提下定义新的对于元素的操作。实现原理上,

  • 被访问类新增访问方法(如accept),注入访问类,同时将自己交给访问类
  • 访问类根据得到的被访问类对象,执行想要的操作
  • 场景类中通过调用访问方法访问被访问类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class Element {
// ...
public void accept(IVisitor v);
}

public class Element {
public void foo() {
}

// 注入访问类
public void accept(IVisitor v) {
visitor.visit(this);
}
}

public interface IVisitor() {
// 通过重载对不同元素定义不同的访问方式
public void visit(Element e);
public void visit(OtherElement e);
}

当然,为了保证依赖倒置原则,被访问类和访问类都可以再抽象出抽象类和接口。另外,访问者模式通常和组合模式、迭代器模式一同出现。访问者模式的优点:

  • 符合单一职责原则
  • 拓展性优秀

缺点在于,被访问者要暴露细节给访问者,通常会增加很多约定,使代码不稳定。另外重载中依赖的是具体元素违背了依赖倒置原则。

访问器模式的应用场景通常是使用迭代器模式已经不能满足的场合。比如对不同的元素有不同的遍历操作,甚至涉及到元素内部的逻辑。使用访问器模式可以封装和掩盖这种差异性。

拓展

在访问器模式和迭代器模式一同出现时,可以增加统计功能,在每次访问元素时收集统计信息。在往深处拓展,甚至可以抽象访问器为接口,从而拓展不同类型的访问器,如用来展示数据和用来统计数据的。

状态模式

这种模式就很好理解了。即将客体抽象成一个有限状态机,客体有一个初始状态,在某特定时机下会跃迁到另一状态,且状态间的跳转是有规律可循的。这种模式在编程中非常常见,自然语言分析、所有可以用马尔科夫过程描述的事物变化都可以抽象成状态模式实现。在实现时,主要要完成三方面工作:

  • 定义所有状态,根据状态的薄厚程度,用常量或类定义
  • 定义修改状态的行为,在方法内往往要根据上一时刻状态做判断,这些行为定义在状态内部
  • 在上下文中调用这些行为

在实现时,为了避免switch语句,会使用一个上下文类,托管当前状态,在状态切换时,先调用状态类中的行为,再通知上下文类更换托管的状态。两者相互注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public abstract class State {
// 注入上下文对象
protected Context c;
// 设置上下文
public void setContext(Context c) {
this.context = c;
}
// 行为
public abstract action1();
public abstract action2();
// ...
}

public class State1 {
@Override
public void action1() {
// ...
}

@Override
public void action2() {
super.context.setState(Context.STATE2);
// 切换到state2
super.context.handleAction2();
}
}

public class Context {
// 注入所有状态
public final static state STATE1 = new State1();
public final static state STATE2 = new State2()

private State CurrState;

public State getState() {
return this.CurrState;
}

// 设置当前状态
public void setState(State currState) {
this.currState = currState;
// 切换当前状态
this.CurrState.setContext(this);
}

// 行为委托
public void handleAction1() {
this.CurrState.action1();
}
public void handleAction2() {
this.CurrState.action2();
}
}

增加了上下文类Context后,避免了大量的switch语句,问题是,状态较多时,定义的类也会较多。状态模式在工作流开发中很常用。

解释器模式

解释器模式顾名思义,即定义一个解释器,去按文法解释一种语言中的句子。当然这个语言以科学运算和编程语句居多。它的应用场景比较特殊,即需要语句解析介入的场景,比如自然语言分析、或者真的编写一个语言的解释器。通常开发解释器工程量和难度都较大,且会遇到效率问题。一般应用较少。

在这个模式下。主要有下面这些角色;

  • 抽象解释器,用来派生具体的表达式解释器
  • 终结符解释器,即不需要解释的,字面意义的符号,比如1a
  • 非终结符解释器,和两边表达式相关联的符号解释器,比如+*
  • 上下文角色

享元模式

享元模式是一种重要的池技术,原理上指使用共享对象支持大量的细粒度的对象。我们可以将这些对象内的状态拆分成可共享状态和不可共享状态。对象往往可以按可共享状态拆分为细粒度较大的若干部分,放在共享对象池中,再贴上自己的不可共享状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 享元
public abstract class SharedObject {
private String intrinsic;
// 共享的状态作为享元的key
protected final String Extrinsic;
// 共享的状态需要可以设置
public SharedObject(String E) {
this.Extrinsic = E;
}
// 业务方法
public abstract foo();
// 不可共享状态的getter/setter
public String getIntrinsic() {
return intrinsic;
}
public void setIntrinsic(String intrinsic) {
this.intrinsic = intrinsic;
}
}

// 享元工厂
public class SharedObjectFactory {
// 共享池
private static HashMap<String, SharedObject> pool = new HashMap<String, SharedObject>();
// 工厂方法
public static SharedObject getSharedObject(String Extrinsic) {
SharedObject o = null;
// 从池中寻找
if (pool.containsKey(Extrinsic)) {
o = pool.get(Extrinsic);
} else {
o = new SharedObject1(Extrinsic);
// 放到池中
pool.put(Extrinsic, o);
}
return o;
}
}

在共享对象池中,建议使用可共享状态的简单组合构成池内元素的key值(最好是基本类型),一方面减少编程负担,另一方面还可以提高工作效率。享元模式主要使用在下面场景下:

  • 系统中存在大量相似对象
  • 对象具备相近的外部状态和与环境无关的内部状态

桥梁模式

桥梁模式又叫桥接模式,是比较轻量级的一种设计模式,意为将抽象和实现解耦,使两者可以独立变化。角色上,桥梁模式分为主体和客体,在主体内注入客体的接口/抽象类,并在主体的方法内使用,提供setter或构造函数方法。这样继承自主体的类就可以根据传入setter/构造函数的客体实现类的不同得到不同的实现结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public interface Object {
// 基本方法
public void foo()
}

public class Object1 implements Object {
public void foo() {
// 自己的逻辑
}
}

public class Object2 implements Object {
public void foo() {
// 自己的逻辑
}
}

public abstract class Subject {
// 必须有一个注入的客体
private Object o;

// 必须可以通过构造函数/setter注入
public Subject(Object o) {
this.o = o;
}

// 获得客体行为
public void bar() {
this.o.foo();
}

// 获得客体
public Object getObject() {
return o;
}
}

public class Subject1 extends Subject {
// 覆写构造函数
public Subject1(Object o) {
super(o);
}
// 自身业务
@Override
public void bar() {
super.bar();
// ... 其余业务
}
}

public class Scene {
public static void main(String[] args) {
Object o = new Object1();
Subject s = new Subject1(o);
s.bar();
}
}

桥梁模式的扩展能力很强,它是对普通的继承的一种灵活的补充,避免了父类对子类的强侵入,可以将父类可能会变化的部分抽出去,通过注入的方式引入,方便子类随时更换。

设计模式的比较

创建类

和创建类相关的模式:

  • 工厂模式
  • 抽象工厂模式
  • 建造者模式
  • 单例模式
  • 原型模式

后两者容易理解。重点对比上面三个模式:

  • 工厂模式注重整体的构建过程,成产出的对象具有很强的相似性
  • 建造者模式注重建造的过程,希望在每一步最好都可定制,根据步骤的不同生产出差异化的对象,通常伴生导演类
  • 抽象工厂模式通常和产品族关系密切,尤其是一组事物有很明显的两个或多个划分方法时使用,其余等同工厂模式

结构类

结构类指调整或组合类产生更大结构来适应更高层次逻辑需求,大多通过增加中间层解决问题,有下面的相关模式:

  • 适配器模式
  • 桥梁模式
  • 组合模式
  • 装饰模式
  • 门面模式
  • 享元模式
  • 代理模式

其中简单明了的有桥梁模式、组合模式、门面模式、享元模式,下面对比下其他的几种模式:

  • 代理模式即在原对象和用户间增加了一个中间层,在不改变原接口的情况下,增加准入和限定操作
  • 装饰模式是代理模式的加强,装饰类并不起中间层的作用,不做准入判断,它单纯地在原接口上增强或削弱功能
  • 适配器模式和它们差别较大,它也起包装作用,作用于两个不同的对象,重点在伪装和转换

行为类

这一批模式重点在修饰类的行为:

  • 责任链模式
  • 命令模式
  • 解释器模式
  • 迭代器模式
  • 中介者模式
  • 备忘录模式
  • 观察者模式
  • 状态模式
  • 策略模式
  • 模板方法模式
  • 访问者模式

下面比较一些类似的模式

  • 命令模式强调把动作解耦,将其分为执行对象和执行行为,在行为类内部注入执行对象,使用执行者操作命令
  • 策略模式强调包装对等的可替换的多个算法,通常有一个上下文类,封装所有的算法

比如在表单验证时,策略模式会将所有的验证规则对等地定义出来,再由一个表单验证上下文对象包裹起来;命令模式下,首先要明确所有验证规则的接受者(Receiver),再定义所有的验证“命令”,最后由执行者(Invoker)操作命令完成工作,具体工作是在接受者那里完成的,命令只负责组织。

关于策略模式和状态模式,

  • 策略模式没有状态的概念,虽然有上下文类Context,但是切换的状态只是不同的算法而已
  • 状态模式重点关注状态,它同样有上下文类Context,但相同的行为在不同的状态下产生的结果不同。在实现上体现在,上下文类内保存了当前状态,虽然状态间都有相同方法,但实现不同。

至于观察者模式和责任链模式,

  • 观察者模式重点在观察和被观察的关系(想想事件监听),被观察者中需要注入监听者(Observable),再由监听者告知观察者(Observer),整条链是有回调的,链上传递的信息可以自由变化,即最后返回给用户数据的总是第一个被观察者
  • 责任链模式重点在事务链条化处理的过程(想想中间件),每个处理者都必须通过next属性明确指定下一个目标,整条链是责任链,链上角色相互平等,传递的信息一般不会改变结构,最终由最后一个角色返回结果

其他

首先先来比较策略模式和桥梁模式。它俩的共同点在都有一个注入依赖关系。策略模式中算法封装被注入到上下文类Context中,桥梁模式中差异化继承的部分被单独抽出再注入父类里。它们的区别主要在:

  • 策略模式着重于封装一系列不同的行为
  • 桥梁模式在不破坏封装的情况下将差异化的实现部分从抽象部分抽取出来,因此必然有抽象化角色和实现化角色

门面模式和中介者模式就比较好区分了,它们的应用场景有很大不同;

  • 门面模式用来掩盖下面复杂的子系统,提供统一的高层接口(“金玉其外”),它并不管下面的子系统间怎样耦合(“败絮其中”)
  • 中介者模式要用在同事对象间(通常至少3个)复杂交互时,化网状结构为星型结构,减少耦合

最后,代理模式、装饰模式、适配器模式(不严格)、桥梁模式、门面模式都可以总结为包装模式,它们并没有为原来的类增加新的功能,只是增加了新的包装或插件。

设计模式的组合

shell命令解释demo

主要采用命令模式、责任链模式、模板方法模式。

银行扣款demo

主要采用策略模式、工厂方法模式、门面模式

产品消费事件demo

产品创建时摄影工厂模式,保证产品和工厂的紧耦合,避免创建事件不触发的可能性

新模式

MVC

MVC其实算不上一种新模式,但是从上世纪90年代起到现在实在是太流行了。它的目的是通过C(Controller)将模型M(Model)和视图V(View)分离开。书中具体在讲MVC在Java Web开发中的实现,这里从略。

规格书模式

规格书模式多用在描述一个规范或条件的场合下,比如“性别男年龄大于20且位于北京”。通过定义抽象类以及ANDORNOT等的组合,可以得到更复杂的规格书对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public interface ISpec() {
public boolean isSatisfied(Object o);
public ISpec and(ISpec s);
public ISpec or(ISpec s);
public ISpec not(ISpec s);
}

public abstract Spec implements ISpec {
public abstract isSatisfied(Object o);

public ISpec and(ISpec s) {
return new AndSpec(this, spec);
}

public ISpec or(ISpec s) {
return newOrSpec(this, spec);
}

public ISpec not() {
return notSpec(this);
}
}

public class AndSpec extends Spec {
private ISpec left;
private ISpec right;

public AndSpec(ISpec left, ISpec right) {
this.left = left;
this.right = right;
}

@Override
public boolean isSatisfied(Object o) {
return left.isSatisfied(o) && right.isSatisfied(o);
}
}

public class OrSpec extends Spec {
private ISpec left;
private ISpec right;

public OrSpec(ISpec left, ISpec right) {
this.left = left;
this.right = right;
}

@Override
public boolean isSatisfied(Object o) {
return left.isSatisfied(o) || right.isSatisfied(o);
}
}

public class NotSpec extends Spec {
private ISpec spec;

public NotSpec(ISpec s) {
this.spec = s;
}

@Override
public boolean isSatisfied(Object o) {
return !this.spec.isSatisfied(o);
}
}

public class bizSpec extends Spec {
private Object obj;

public bizSpec(Object o) {
this.obj = o;
}

public boolean isSatisfied(Object o) {
// 根据业务逻辑决定真值判断
// ...
}
}

规格模式应用场景比较局限,在LINQ(Language INtegrated Query,语言集成查询)中常见,用来构造WHERE子句。

对象池模式

对象池模式和享元模式相似,都是循环使用对象,但是对象池模式是整个直接取出到池中,避免初始化和释放资源时的消耗。如连接池和线程池就是常见的例子。

雇工模式

雇工模式是常见的“接口-实现”的倒转实现。比如,小学老师教小学学生,中学老师教中学学生,大学老师教大学学生。在实现时,我们先定义学习接口,再实现所有的学生类。再实现老师时,直接调用学生接口的学习方法就完成了教学的实现。雇工模式为一组类提供通用的功能,不需要类实现这些功能,而在客体上实现功能。它和命令模式有相似之处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IService {
public void serving();
}

public class Object implements IService {
public void serving() {
// 服务完成
}
}

public class Servant {
public void service(IService s) {
s.serving();
}
}

黑板模式

黑板模式是观察者模式的拓展,它允许消息有多个读写者,且同时进行。消息生产者通过“黑板”这个总线,将消息传递给对应的消息消费者。这可以是一种宏观的设计理念,使用数据库或者消息队列当做“黑板”都是可以的。

空对象模式

空对象通过实现一个无意义的默认类避免程序出现null值。

1
2
3
4
5
class NullAnimal implements Animal {
public void makeSound {
// 什么都不写
}
}

–END–

攻略向游记,以流水账为主,记念宿舍的毕业旅行

Day1 - Day4:成都

我们一共在成都玩耍了3天,逛的景点大多数为名人古迹,其中有杜甫草堂武侯祠,门票分别是30和60,不过景点里面也没什么好逛的。由于时间有限,都江堰这样的比较偏远的景点只能放弃。

AhhRKJ.jpg

除此以外,还有几个有名的商业步行街和老街,都有历史,可以去逛逛:

  • 锦里,小吃纪念品一条街,有一整片街区,在武侯祠旁边
  • 春熙路,商业步行街,类似于万达广场,龙抄手总店就在这里。位于天府广场东南侧
  • 宽窄巷子,类似锦里,南锣鼓巷的感觉,但路面非常干净。在人民公园附近

AhhfbR.jpg

另外值得一去的就是熊猫基地了。4A级景区,虽然门票要快60元,但是能尽情吸(熊)猫,感觉还是超棒的。不建议在节假日去,人山人海的吸猫体验很差。熊猫基地的位置离市中心较远,建议单独安排一天去。

Ahh5Ux.jpg

除此之外,我们还顺路去四川博物馆逛了下,可看的内容还挺多的。上面提到的这些地方除了熊猫基地基本都离市中心不远,我们几个住在如家人民公园店,每次出行都很方便。

除了玩,体验四川的美食也是少不了的。我们分别在第一和第二天去了玉林串串香龙抄手的总店,不用说口味都很经典。其中龙抄手点的每人78的套餐,可以尝到各种成都的小吃,比较推荐。其余的如甜水面勾魂面等没有尝试,不做评价。在小吃上,极力推荐军屯锅盔,外焦里嫩的味道,加上花椒和肉沫的味道和香气,让我欲罢不能。一个5-7元,还比较实惠。

Ahh4V1.jpg

Day4 - Day7:重庆

在重庆,我们住在三峡广场舍友的家附近,布丁比较坑,我们住了两天换到了更远一点的7天。重庆的城市景观很有特色,有着“山城”,“桥都”的美称,3D的城市景观、穿楼钻山的轻轨2号线、赛博朋克感极强的高楼建筑都是山赋予的。另外,嘉陵江和长江穿城而过,滨江的风景和长江索道吸引了许多游客。

在老街区上,有沙坪坝区的磁器口和渝中区的洪崖洞。我们第一天晚上去的磁器口,它位于嘉陵江南岸,也是一整片的街区,以美食为主,值得一提的是陈昌银麻花的总店就位于此,山寨较多需要仔细辨认。洪崖洞也位于嘉陵江南岸,但距离长江和朝天门更近,它曾经是棚户区。依山而建形成了它独特的层次景观,每一楼都有单独的步行街,顶楼和1楼还分别是两条公路。从下往上仰视和从上往下俯瞰都有非常棒的景观。不过,推荐晚上来看,亮灯后的画面很壮观。在渝中区附近,解放碑八一广场也可以顺便逛逛。

AhhTPK.jpg

长江索道也在渝中区,它在长江的北岸,门票单程20,往返30,值得一坐。曾经作为交通工具的长江索道,现在更多承担着旅游观光的作用,同样不推荐在节假日来乘坐,人会非常多,我们大概排了1个小时的队才坐上。在索道上俯瞰长江和渝中区山上层次感极强的居民楼,别有一种感觉。

AhhI56.jpg

重庆的轻轨2号线很有特点,作为重庆内开通的第一条地铁/轻轨线路,2号线最初是以观光为主的,票价曾经高达25元一人。作为在《火锅英雄》等影片中出现过的轻轨线路,在李子坝黄花园两站能感受轻轨穿楼和钻山的神奇。沿途的风景很不错。

AhhH2D.jpg

另外,重庆还有一些红色景点,如白公馆渣滓洞,在沙坪坝区歌乐山上,和红岩纪念馆在一起,不需要门票,有时间可以去看看。

吃上,重庆的美食小吃毫不逊色成都。毛血旺小面肯定是不能错过的。其中,毛血旺我们是在磁器口吃的,原来正宗的毛血旺是煮着吃的。小面的话,就不限地点了。洪崖洞还有别的很多重庆小吃,可以一试,如凉糕什么的。

还有一个不能少的是重庆火锅,作为本地人,舍友推荐南山区的枇杷园火锅,味道真得很正宗。油碟和火锅是标配,麻酱完全是北方人的吃法,涮料的主角也不是肉片,而是鸭肠、黄喉、毛肚、jun花、鸭血等下水。重庆火锅往往不只是辣,还有麻,对于无辣不欢的人来说,简直是飨宴。要提醒一点的是,重庆的特色美食比较重口,建议中间穿插一天较清淡的饮食,以免把肠胃吃坏。

Ahh78O.jpg

重庆人普遍健谈而热情,舍友的招待就让我们感到了扑面而来的欢迎和宾至如归感。总的来说,重庆很值得一去。

Day7 - Day9:武汉

武汉是我们旅程的最后一站。作为湖北人,深刻感受到湖北菜似乎没有什么值得一说的特色。倒是一些小吃,像是热干面三鲜豆皮蛋酒糯米鸡等比较有名。由于江多、湖多,这边做有着不错的手艺。如果想尝试湖北菜,我们去过的清江人家湖锦酒楼两个饭店还可以。

黄鹤楼的门票居然要80元1人,有点小贵。在主楼上可以眺望到长江大桥和江景,只可惜那天江面雾很大,桥只能看见一半。

Ahhbxe.jpg

湖北省博物馆还是很值得一去的,曾侯乙编钟表演票价20元1人,绝对超值,谁看谁知道。整个演出大约半小时,钟、罄、埙、排箫等的表演实在精彩。东湖风景也挺不错,如果天气好,推荐环湖骑行,共享单车就可以。

Day10:各奔东西

19日上午我们5人分散在武汉、汉口、武昌三站各回各家(除了能哥回北京写推荐信)。

这次旅行我主要有两点感想:

  • 我们很幸运,这10天里,老天很给面子地一场雨没下。
  • 只有本地人才能把城市里最有特点和最真实的一面展示出来

说起来是游记,但以流水账为主,谨记念第一次长途骑行和观光游

去海南的主意不是我出的,最初我和C是打算去些更远的地方骑行。那还是2016年的下半年的事情,C在学苑超市偶遇我,突然说起要不要去台湾骑行。对于长期只在北京市里转来转去的我来说,实在太有诱惑力,于是我俩一拍即合。只是那会儿我既没实习更没工作,长途骑行也只能是计划。

这一计划就是一年多,转眼就是17年的12月,时机成熟,我叫上凯哥。我们最终定下了5天的行程,从海口到三亚,中间在文昌、博鳌、兴隆和海棠湾停靠(这也是经典的环岛东线)。整个5天除了第1天,每天都有很贴近海边的路,风景还是不错的。路上遇到了蛮多骑友,他们来自不同的地方,有的甚至是从甘肃、辽宁这种地方骑过来的。除了晒伤和胳膊肘的轻伤外,这次难得的旅行还留下了下面这些经历。

下图是我在首都机场候机的间隙拍的,窗外那架海航的波音747就是我们要乘坐的那架。

AhhMDA.jpg

我们到海口时已经是晚上了,第一晚住在租车的517驿站,环境比事先设想的要差劲一些,所幸也只住这一晚。租车的人并不是很多,听老板说寒假的时候是旺季,可能是都来避寒了吧。我们的行程是5天,刚好满足一个5日套餐,学生证还可以8.8折,可以说相当划算了。

Ahh1Et.jpg

Day1: 海口 ——> 龙楼

第一天是整个行程里相对比较无趣的,并看不到海。路书里甚至有一条没法骑行的小路。最后从小路绕上213省道时,我们三个都累坏了。若没有凯哥的麦丽素,后果恐怕难以设想。

AhhnjH.jpg

在大致坡镇休息时,C瞥见了一家叫“琼海炒冰清补凉”的店,作为曾经在广东上了4年学的C,竟然也没断清楚句。拜这好奇所赐,我尝到了一道极好的甜点。

这里的清补凉以椰奶为底,炒冰为料,辅以红豆、绿豆、薏米、椰果、葡萄干、莲子、仙草冻等。入口冰爽,糖水的味道清而不腻,实在美味。要是夏天品尝,应该更是大大提升幸福感。(右边的菜汤请忽略)

AhhQHI.jpg

另外,由于我们三个的速度有所差距,为了避免走失的情况。在大致坡时,我们威逼利诱凯哥下载了行者,加入了我们“海南养生骑行群”,从而能共享队友的位置。这在后面几天派上了大用场。

海南的人们大多比较慵懒,穿着拖鞋。很少见到穿着其他鞋的。海南的摩托和电动车非常多,十字路口过街的车流相当壮观。

Day2: 龙楼 ——> 博鳌

在龙楼歇了一夜后,我们向博鳌出发。沿着213省道骑行十几公里后左拐,东郊椰林中成片椰树的画面就出现在面前。在后面的几天骑行里,我们逐渐发现椰树似乎是这边主要的木本植物。

AhhKud.jpg

现在似乎是淡季,这边的野海滩几乎没有人。灰暗的天空营造出一种凄凉的既视感。

Ahh84f.jpg

离开东郊椰林后,我们坐船过江来到文昌市内,码头边停着许多渔船,散发着浓重的腥味。现在可能是休渔期,船上没看到什么人。

AhbZX8.jpg

我们在文昌市吃过午饭,从海边延乡道绕回S213省道上,半路下起了小雨。不过不影响兴致,反倒驱散了骑车的燥热。等再次骑上县道时,离博鳌已经不远了。

Ahh3UP.jpg

这边的鸡、牛、猪的养殖方式很有特点。在路的两边,很容易发现它们没人看管,自由踱步地觅食。有的甚至直接走上公路。

AhhY8S.jpg

C的估计真的非常准,我和他准时在下午5点半到达酒店(凯哥要慢一点)。我们晚上去了一家叫“海的故事”的酒吧,它建在海边,夜晚的海浪咆哮着涌上海岸,海风也比文昌那里凶猛些。享受着海风拂面的同时,我和C进行了一番恋爱观的探讨,然而都是纸上谈兵。

AhhJC8.jpg

Day3: 博鳌 ——> 兴隆

海南的东北人很多,新建的楼盘也很多,当然同样没人住的楼盘也挺多。

Ahhauj.jpg

博鳌附近的省道修得极好,很适合骑车。不知道是不是因为亚洲论坛的缘由。

AhhNvQ.jpg

在到达万宁市稍作休整后,我们转向海边的旅游公路进发,“旅游公路”这名字名副其实,海边的风景很不错。

Ahhtgg.jpg

路况同样很棒。

AhhdDs.jpg

在桥上还能看到某小河的入海口。

Ahhwbn.jpg

AhhBEq.jpg

晚上5点多时,我们很幸运地赶上了夕阳,旅游公路上的远眺美不胜收。

AhhDU0.jpg

Ahhr5V.jpg

AhhyCT.jpg

由于拍照耽误了些时间,我们到达兴隆时已经比较晚了。吃了顿正宗的海(dong)南(bei)饺子,便结束了一天的行程。并没有泡到兴隆温泉。

Day4: 万宁 ——> 三亚海棠湾

海南的丘陵比较多,一路上很多上上下下的起伏。不过,真正意义上的爬坡出现在万宁和陵水的分界岭,是一个海拔100多米的爬坡。在下坡的路上,还看见了一群5、60岁大爷组成的车队,着实是老当益壮。

Ahhc2F.jpg

这一天的午餐在陵水进行,早听人说海南的粉很出名,四大粉中就有陵水酸粉的一个位置。这份是加了沙虫的,味道很赞。

Ahh68U.jpg

接下来又是S213上的狂奔,海棠湾位于三亚市区东北边,离蜈支洲岛比较近,有着相当不错的海景。我们入住的民宿正对着海(当然旁边的所有民宿都是这样)。

Ahhgv4.jpg

天台上的风景也是不错的。

AhbQts.jpg

灯台旁的防波堤上,海浪有耐心地拍打。

AhbMkj.jpg

Day5: 三亚海棠湾 ——> 三亚

我们出发去三亚时已经是12月31号的事情了,这段路很近,只有30多公里。还车的时候,还没到正午。

在三亚跨年是C设计好的环节,似乎在一个陌生的地方,哦不,严格意义上来说是在一个陌生的热闹的地方跨年,会很有feel。尽管对此我持保留意见,但我倒是也觉得这会是有趣的体验。

1年说过就过去了。12月31号的这个青旅里,十几个来自世界各地的朋友玩着抽牌喝酒的游戏,和我们一样,等待2018的来临。唯一的不同是他们似乎真的很期待这一刻的到来。临到跨年的前几分钟,更是热情地拉着我们一同去海边看烟花。“打冬海”,她们有点激动地说。

AhhWr9.jpg

沿着海边的街直走,我才发现,三亚的跨年比想象中热闹。街头遍是从遥远的俄罗斯赶来的毛子兄弟。海边的跨年party上,他们更是在倒计时中载歌载舞。这恐怕是我这辈子见过最密集的毛子群体了。

海水拍打的沙滩上,邀请我们的外国友人相互祝着“Happy new year”。恍惚间,一瓶劲酒被塞进手里。“Take a dip,let’s celebrate!”,一个褐发碧眼的年轻姑娘朗声笑着。盛情难却,我们一人一口,干完了最后的酒。

远处有人放起烟花,仿佛春节似的。

用一个这样的跨年作为环岛东线的结尾,挺好的。

原作:You-Dont-Know-JS
本文的99.9%的内容都来自《You dont know JS》的电子中文版

传送门:《You don’t know JS》 上(入门&作用域&对象)

类型和文法

内建类型

  • 7种类型
  • 值才有类型,变量没有
  • undefined ≠ is not defined(undeclared)。undefined表示定义却没有赋值的变量类型。然而typeof一个未声明的变量也会返回undefined,这是typeof的安全机制,它给了我们更多空间检查变量是否可用。

由于JS里String的只读性,所有String的相关方法都是返回一个新字符串。

使用二进制浮点数的最出名(臭名昭著)的副作用是(记住,这是对 所有 使用 IEEE 754 的语言都成立的 —— 不是许多人认为/假装 仅 在 JavaScript 中存在的问题):0.1 + 0.2 === 0.3 // false。不过可以用Number.EPSILON做最小误差得到足够精确的近似结果。ES6下已经可以用Number.isInteger()Number.isSafeInteger()检查数字是不是整数/安全整数。

特殊值

undefinednull是JS里比较特殊的两类值,它们既是类型又是唯一的值。更加不幸的是,在非strict模式下,undefined还可以作为标识符,像下面这样:

1
undefined = 2;

另外,在特别需要undefined时,void操作符会变得很有用。

Infinity / Infinity == undefined。

针对一些特殊的等价情况(NaN和-0),ES6使用Object.is()判断其相等性。

值与引用

在JS中没有指针,只有引用,同时页没有语法上的提示可以控制值和引用的赋值/传递。取而代之的是,值的类型用来唯一控制值是通过值拷贝,还是引用拷贝来赋予(复合值)。引用指向的是值本身而不是变量,不能使用一个引用来改变另一个引用所指向的值。

底层的基本标量值是不可变的(String和Boolean也一样)。比如一个Number对象持有一个基本标量值2,那么这个Number对象就永远不能再持有另一个值;你只能用一个不同的值创建一个全新的Number对象:

1
2
3
4
5
6
7
8
9
10
function foo(x) {
x = x + 1;
x; // 3
}

var a = 2;
var b = new Number( a ); // 或等价的 `Object(a)`

foo( b );
console.log( b ); // 2, 不是 3

在其中x = x + 1这一步,包装值内的x被取出+1后,赋值给x,将其从一个引用变成一个基本标量值3。

类型转换

对于最简单的值,JSON字符串化行为基本上和toString()转换是相同的,在对String字符串化时,结果也会包含"",如JSON.stringify("11") // ""11""。另外,对于JSON不安全值(即不能移植到消费JSON的语言中),有下面的处理:

  • 忽略undefinedfunctionsymbol
  • Array中遇到这种类型的值,会被替换为null(避免修改位置信息)
  • Object的属性中遇到时,属性会被简单的忽略掉
  • 带有循环引用时,JSON.stringify()会报错

另外,对于有toJSON()方法的对象,JSON字符串化会优先使用该方法。JSON.stringify()的第二个参数可以指定Array或Function说明可以编辑的对象属性。第三个参数是填充符,填充在各级开头,用来友好展示结果,最多取入参的前10个字符。

在对象上使用toNumbertoString方法,首先会找到其原始类型(toPrimitives()),即使用其valueOf()toString()方法(也会在[[prototype]]上寻找)。

-> Number

可以用Date.now()代替+new Date()获取更好的语义。

~除了可以用来检查-1这个特殊的值,还可以通过~~对小数取整,因为执行位操作时会先将数字转为Int32类型。

parseInt以及parseFloat+Number()强制类型转换存在区别。它们的作用是,从字符串中解析出一个number出来。两者是不能相互替换的。后者是不能容忍非数字字符的。另外,**请在字符串上使用parseIntparseFloat**,这也是它们的设计目的。对非字符串类型使用它们可能得到意外的结果:

1
parseInt( 1/0, 19 ); // 18,惊不惊喜,意不意外

原因是,parseInt会把第一个参数toString(这不能责怪它,因为它本来就是设计对String使用的)。类似的例子还能举出很多:

1
2
3
4
5
6
7
parseInt( 0.000008 );       // 0   ("0" from "0.000008")
parseInt( 0.0000008 ); // 8 ("8" from "8e-7")
parseInt( false, 16 ); // 250 ("fa" from "false")
parseInt( parseInt, 16 ); // 15 ("f" from "function..")

parseInt( "0x10" ); // 16
parseInt( "103", 2 ); // 2

另外,parseInt会通过前缀试图猜测数字进制,默认是10进制。以0x开头表示16进制,以0b开头表示2进制,以0o开头表示8进制。

-> Boolean

使用!!强制转换类型。

&&||在JS中的逻辑和C++以及Java中的不大一样,它并不一定返回boolean类型的值,而是根据比较的两个数判断返回哪一个。其中&&可以用来进行短路操作。

另外,对于Symbol来说,只能通过String()的形式转为String类型,却不能转为Boolean类型。

等价

等价分为=====

StringNumber进行比较时,会对String使用强制类型转换(类似+Number());

在和Boolean比较时,会首先把Boolean类型转为Number类型,再进行比较。这会产生下面这样比较迷惑的情况:

1
2
"42" == true  // false
"42" == false // false

Object和非Object比较时,会先对Object进行toPrimtives,即先使用valueOf()看能否转成基本类型,再使用toString()

下面有一些疯狂的例子,但却可以由上面的规则解释:

1
2
3
4
5
6
7
8
"0" == false    // true
false == [] // true
0 == [] // true
[] == ![]; // true
2 == [2]; // true
"" == [] // true
"" == [null]; // true
0 == "\n" // true

通过上面的坑可以看到,等号的两边总有[]""false0。建议在这些情况使用===

下面是由Alex Dorey(@dorey on GitHub)制作的一个方便的表格,将各种比较进行了可视化:

大小关系比较

首先对值进行toPrimitives转换,如果有一个不是String,则使用Number类型比较。见下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// toNumber
var a = [ 42 ];
var b = [ "43" ];

a < b; // true

// toString
var a = { b: 42 };
var b = { b: 43 };

a < b; // false
a == b; // false
a > b; // false

a <= b; // true
a >= b; // true

在下面的例子里,a和b在比较时,都会转成”[object Object]”。而等价比较上会比较引用是否相同。因此都返回false,而JS中的<=>=操作会去对应计算><的结果再取反,从而会得到两个true

语法

语句和表达式

JS中的表达式都有一个隐式的返回值,但是它只会打印在控制台中,并不会真实返回。块语句的返回值是块中最后一个语句的返回值。ES7中可能会引入do语句显式地使用这个返回值。

JS中的++和C风格类似,表示自增,有前后之分。但是++a++这种用法是不合法的。

赋值表达式的返回是赋予的值,这在链式赋值时很好用:

1
2
var a, b, c;
a = b = c = 42;

这里,c = 42被求值得出42(带有将42赋值给c的副作用),然后b = 42被求值得出42(带有将42赋值给b的副作用),而最后a = 42被求值(带有将42赋值给a的副作用)。

另一种用法是直接将之放在&&||的前后,检查赋值语句的真值。

上下文

{}包裹的内容作为表达式结果可以直接赋值给变量,但是直接声明时会被当做代码块,但是可能仍然是合法的,如:

1
2
3
{
foo: bar()
}

因为,JS中允许使用语句标签,便于breakcontinue跳转(JS中没有goto)。而JSON中带有""的键则不会被这么解释,因为语句标签不允许出现引号。

我们现在来解决下面的问题:

1
2
[] + {}; // "[object Object]"
{} + []; // 0

为什么交换顺序会对+的规则有影响?在第一个式子里一切都是正常的,[]转换成""{}转换成[object Object];在第二个式子里,{}被理解成一个空代码块儿,[]被强制转换为0。

操作符优先级

  • &&优先于||这里有完整的表格。
  • &&||有短接的特点,即当第一个表达式为true或false时直接返回结果。
  • 赋值表达式和三元表达式? : 一样是从右向左结合的

ASI(自动分号)

尽量避免ASI,只在确认没有歧义的地方依赖ASI。

错误

  • JS有早期错误一说,即运行前编译期间的错误
  • let会造成块域内的TDZ(Temporal Dead Zone,时间死区),typeof在此时会报错,而不会返回undefined。TDZ是指变量还没到能使用它的时候,还需要初始化。下面还有一个例子:
    1
    2
    3
    4
    5
    var b = 3;

    function foo( a = 42, b = a + b + 5 ) {
    // ..
    }
  • ES6提供了剩余参数来代替原有的arguments对象,这更加安全。

finally子句

try catch在和finally一起使用时,finally的语句一定会被执行,而且一定会在try语句执行完后立即执行,即使try中有return或者throwcontinue等控制语句。可以在finally中修改try中的结果,但是最后不要这么做,因为会影响程序可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 function foo() {
try {
throw 42;
}
finally {
console.log( "Hello" );
}

console.log( "never runs" );
}

console.log( foo() );
// Hello
// Uncaught Exception: 42

宿主环境

由于浏览器的遗留行为,使用id属性创建DOM元素会创建同名的全局变量。

1
<div id="foo"></div>
1
2
3
4
5
if (typeof foo == "undefined") {
foo = 42; // 永远不会运行
}

console.log( foo ); // HTML元素
  • 永远不要修改内建类型。
  • JS的函数和变量声明提升只在同一<script>标签内

保留字

Let this long package float, Goto private class if short. While protected with debugger case, Continue volatile interface. Instanceof super synchronized throw, Extends final export throws.

Try import double enum?

False, boolean, abstract function, Implements typeof transient break! Void static, default do, Switch int native new. Else, delete null public var In return for const, true, char …Finally catch byte.

来自StackOverflow用户“art4theSould”创造性的一首小诗

另外,在ES6+中,可以使用保留字作为对象字面量中的属性名或键。

异步与性能

JS引擎对时间没有天生的感觉,只是一个任意JS代码段的按需执行环境。是周围的宿主环境在不停地安排“事件”(JS代码的执行)。举例来说,当你的JS程序发起一个从服务器取得数据的Ajax请求时,你在一个函数(通常称为回调)中建立好“应答”代码,然后JS引擎就会告诉宿主环境,“嘿,我就要暂时停止执行了,但不管你什么时候完成了这个网络请求,而且你还得到一些数据的话,请回来调这个函数。”

然后浏览器就会为网络的应答设置一个监听器,当它有东西要交给你的时候,它会通过将回调函数插入事件轮询来安排它的执行。

关于事件轮询队列,之前也有过一些介绍。

异步概览

异步≠并行。异步本质上还是串行的。工作依然有先后之分,没有线程、线程池的概念。从而,在JS中的函数都是原子的,即不会与别的函数的代码相互穿插(除非使用Generator)。

并发

并发是当两个或多个“进程”(或任务)在同一时间段内同时执行,而不管构成它们的每个操作是不是同时进行的。在JS中,单线程事件轮询是并发的一种表达。

不互动

当程序中运行多个“进程”(或任务),如果它们之间没有逻辑联系,那么不互动是完全可以接受的。

1
2
3
4
5
6
7
8
9
10
11
12
13
var res = {};

function foo(results) {
res.foo = results;
}

function bar(results) {
res.bar = results;
}

// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

互动

相反,如果它们之间有依赖关系,或者前后次序而产生互动时,let it alone就会出事。

1
2
3
4
5
6
7
8
9
var res = [];

function response(data) {
res.push( data );
}

// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

协调

跳过使用全局变量等丑陋的协作手段,有一种方法,将长时间处理的任务打断成多个小段的请求外加setTimeout,以便将任务穿插完成。

Jobs

ES6在事件轮询队列之上引入了一层新概念,称为“工作队列(Job queue)”。它和轮询队列的关系类似于Macrotask和Microtask。

回调

顺序的大脑

回调不符合正常思维逻辑顺序 & 回调地狱。

信任问题

(本人并不完全赞同)回调遭受着控制反转的蹂躏,它们隐含地将控制权交给第三方(通常第三方工具不受你控制!)来调用你程序的延续。

Promise

Promise的thencatch

可靠的Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var p3 = new Promise( function(resolve,reject){
resolve( "B" );
} );

var p1 = new Promise( function(resolve,reject){
resolve( p3 );
} );

var p2 = new Promise( function(resolve,reject){
resolve( "A" );
} );

p1.then( function(v){
console.log( v );
} );

p2.then( function(v){
console.log( v );
} );

// A B <-- 不是你可能期望的 B A

这是因为p1由p3解析的结果所解析,这个过程是异步地。

作者认为Promise在很大程度上,解决了下面的问题:

  • 调的太早/太晚(本人并不赞同)
  • 根本不调回调(勉强成立),Promise通知状态改变是由编程者自己代码控制的,用resolvereject(用户只能借助外部环境API发起异步操作,resolve一样要么放在传统的回调,要么转交给第三方完成)。
  • 调太少或太多次(成立),一个Promise一旦resolve或者reject,状态就不再发生变化
  • 没能传入任何参数/环境(勉强成立),原因与第二条相同
  • 吞掉所有错误和异常(勉强成立),Promise中在catch字句里捕获异常。

Promise.resolve(p)会把thenable的入参p转换为合法的Promise。这里猜测下这个resolve(p)的实现(个人猜想):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Promise.resolve = thenable => {
// if `thenable` is a promise, just return it
// ...

// if `thenable` is plain, just resolve
if (typeof thenable.then != 'function') {
return new Promise(resolve => {
resolve(thenable);
});
}

return new Promise((resolve, reject) => {
thenable.then(resolve, reject);
});
}

// 一个thenable的例子
const p = {
then(cb, err) {
Math.random < 0.5 ? cb(42) : err("oops! Something bad happens.");
}
}

链式调用

看看RxJS的Introduction。就可以很轻松地理解Promise的流程和链式过程了。

Promise模式

Promise.all()Promise.race()。除了这两个官方钦定的方法外,其他的Promise库还实现了像是any()none()first()last()这样的方法,看看RxJS的operators会有更多选择。

Promise的限制

  • 顺序的错误处理
  • 只能传单一的值(其实就是状态改变不可逆)
  • 单次解析(同上),文章也在惰性的上方提到了观察者模式的RxJS,的确在设计时间概念的领域,RxJS要厉害多了
  • 惰性(生产生产Promise函数的工厂函数)
  • 不可反悔(即不能中途撤销)
  • 性能

Generator

使用同步风格书写异步代码的基础在Generator。关于这部分的更详细介绍见本人之前参考阮一峰大神写的博文

打破运行至完成

generator(生成器)是一个可以和别的代码穿插执行的非原子的特殊函数。使用new构造generator得到的只是一个迭代器,迭代器在执行到yield时会让出执行权。真正执行这个迭代器需要用调用或者执行器的方式。

yield和next是generator可以和外部甚至是其他generator双向通信。但是generator只是声明了自己将要以什么样的形式去执行。还需要一个下面这样的帮助函数去推动它执行:

1
2
3
4
5
6
7
8
9
function step(gen) {
var it = gen();
var last;

return function() {
// 不论`yield`出什么,只管在下一次时直接把它塞回去!
last = it.next( last ).value;
};
}

生成器

  • 可以把generator像状态机一样使用。
  • for of需要迭代器的实现
  • 可以在generator上使用for of
  • 使用return而非next可以终止生成器执行

在异步流程中使用generator

generator的yield暂停特性不仅意味着我们可以从异步的函数调用那里得到看起来同步的return值。

带有promise的generator

在ES6的世界中最棒的就是将generator(看似同步的异步代码)与Promise(可靠性和可组合性)组合起来。

co与koa。

ES7中的await和async

像下面这样,没有run函数,没有生成器函数的*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}

async function main() {
try {
var text = await foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}

main();

yield委托

使用yield * foo可以把其他的生成器函数整合进当前生成器中。除了生成器外,还可以委托一个非generator的iterator。错误可以委托,promise可以委托,委托还可以递归。

结合yield可以很方便地协调多个generator

thunk

同步的thunk即包装了所有预设形参的函数执行的函数。异步thunk指需要指定callback的包装所有其他预设形参异步函数的函数。像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 同步thunk
function foo(x,y) {
return x + y;
}

function fooThunk() {
return foo( 3, 4 );
}

// 异步thunk
function foo(x,y,cb) {
setTimeout( function(){
cb( x + y );
}, 1000 );
}

function fooThunk(cb) {
foo( 3, 4, cb );
}

一旦来说会有一个工具thunkify帮你完成制造函数thunk的工作(放心,总会有人这么做的)。它的用法是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var fooThunkory = thunkify( foo );

var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );

// 稍后

fooThunk1( function(sum) {
console.log( sum ); // 7
} );

fooThunk2( function(sum) {
console.log( sum ); // 11
} );

包装函数生产一个thunkory,之后指定除cb以外的其他参数得到thunk函数。

thunk和Promise本质上其实是等价的。只不过是回调所在的为之不一样罢了。所以使用Promise.wrap包装得到的promise还是thunkify包装得到的thunk函数其实都可以yield出来。因为,它们都能通过指定回调来让generator进一步推动下去。

当然了无论是在可组合性还是错误处理上,Promise都有更胜一筹。所以,thunk通常作为替代性的前ES6方案。

前ES6的Generator

当然了Generator也是可以通过其他方式实现的。

性能

Web Worker

近HTML5时代被加入web平台的特性,称为“Web Worker”。这是一个浏览器(也就是宿主环境)特性,而且几乎和JS语言本身没有任何关系。这里简单说了下它和Service Worker的区别。

asm.js

asm.js”是可以被高度优化的JavaScript语言子集的标志。通过小心地回避那些特定的很难优化的(垃圾回收,强制转换,等等)机制和模式,asm.js风格的代码可以被JS引擎识别,而且用主动地底层优化进行特殊的处理。

基准分析(BenchMark)和调优

  • Benchmark.js用统计学的方式避免时间戳测量语句性能时的不准确
  • jsPerf.com基于Benchmark.js的代码性能测试平台

编写好的测试

  • 注意上下文的影响
  • “过早的优化是万恶之源”
  • 尾部调用优化

ES6与未来

ES?现在与未来

  • polyfill与转译

语法

尽管ES6算是JS最新的官方特性,下面说的大部分特性已经被很经常地使用了。

  • 块作用域(之前的部分已经提到过了)
  • 扩散、剩余,...操作符,用在函数入参,数组和对象中
  • 函数默认参数值(是不是很神奇),默认参数值可以是合理的表达式甚至是函数调用
  • 解构赋值,也可以有默认参数值
  • 对象字面量拓展,简约声明/简约方法/getter,setter/计算型属性名/__proto__/super
  • 模板字面量
  • 箭头函数,词法this
  • for of和iterator
  • 正则表达式拓展
    • Unicode标识
    • 粘性标志
  • 数字,八进制
  • Unicode
    • 合理的string长度,String.prototype.normalize()
    • charCodeAt => codePointAt
    • fromCharCode => fromCodePoint
    • Unicode标识符名称
  • Symbol,新的基本类型,它是一个新的包装器对象,可以认为每个EVT_LOGIN持有一个不能被其他任何值所(有意或无意地)重复的值。
    • Symbol.for()先查询是否有一个同名的Symbol,如果有就返回,没有就创建一个

组织

迭代器

迭代器Iterator接口有一个必选接口next(),和两个可选接口return()throw(),它的result被规定为包括属性valuedone,下面是一个数组的迭代:

1
2
3
4
5
6
7
8
9
var arr = [1,2,3];

var it = arr[Symbol.iterator]();

it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }

it.next(); // { value: undefined, done: true }

但通常使用for of就足够了。我们可以依照这个接口,定义一个自己的迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var Fib = {
[Symbol.iterator]() {
var n1 = 1, n2 = 1;

return {
// 使迭代器成为一个可迭代对象
[Symbol.iterator]() { return this; },

next() {
var current = n2;
n2 = n1;
n1 = n1 + current;
return { value: current, done: false };
},

return(v) {
console.log(
"Fibonacci sequence abandoned."
);
return { value: v, done: true };
}
};
}
};

Generator

这个上一章已经提到了。它可以用来:

  • 生产一系列值,即状态机
  • 串行执行的任务队列,化异步同步

模块

importexport

  • ES6引入了元属性的概念,用new.target表示。在任意的构造器中,new.target总是指向new实际直接调用的构造器。

集合

ArrayBuffer

它表示一组比特位,但是这些比特的实际意义是由结构化数组控制的,由它表示这些比特上的“视图”究竟是8位有符号整数还是字符串。

1
2
3
4
var buf = new ArrayBuffer( 32 );
buf.byteLength; // 32字节
var arr = new Uint16Array( buf );
arr.length; // 16

一个单独的缓冲可以连接多个视图

1
2
3
4
5
6
7
8
var buf = new ArrayBuffer( 2 );

var view8 = new Uint8Array( buf );
var view16 = new Uint16Array( buf );

view16[0] = 3085;
view8[0]; // 13
view8[1]; // 12

在ES6中可以使用下面的类型化数组构造器:

  • Int8Array(8位有符号整数),Uint8Array(8位无符号整数)
  • Uint8ClampedArray(8位无符号整数,每个值都被卡在0 - 255范围内)
  • Int16Array(16位有符号整数),Uint16Array(16位无符号整数)
  • Int32Array(32位有符号整数),Uint32Array(32位无符号整数)
  • Float32Array(32位浮点数,IEEE-754)
  • Float64Array(64位浮点数,IEEE-754)

Maps

摆脱对象只能使用字符串做键值的限制。有getsetdeletehasclear等方法。类似地还有WeakMap,不过它只能使用对象做键。

Sets

一个集合。类似Map,不过set换成了add,且没有get。Set和Map都有自己的迭代器。也可以通过keysvaluesentries来访问里面的内容。

新增API & 元编程

略,参考原文

ES6以后

  • asnyc function
  • Object.observe
  • 指数运算符**
  • Array#includes替代~Array.indexOf(value)
  • SIMD(多个数据),用于多个元素的并行数学操作,参考下面
    1
    2
    3
    4
    5
    var v1 = SIMD.float32x4( 3.14159, 21.0, 32.3, 55.55 );
    var v2 = SIMD.float32x4( 2.1, 3.2, 4.3, 5.4 );

    SIMD.float32x4.mul( v1, v2 );
    // [ 6.597339, 67.2, 138.89, 299.97 ]
  • WASM(Web Assembly)

-END-

0%