logo

JavaScript设计模式(一)

Constructor(构造器)模式

javascript 不支持类的概念,但是它支持通过对构造器函数使用 new 运算符来生成实例化对象,实例通过原型继承实例方法属性。
new Foo()运算符的执行过程:

  1. 创建一个继承Foo.prototype的新空对象
  2. 将构造函数中的this绑定到新创建的对象,执行构造函数内代码
  3. 如果构造函数没有return关键字,则返回新对象,否则返回return返回的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name = "Tony", age = "18") {
this.name = name;
this.age = age;
}
Person.prototype = {
getName() {
return this.name;
},
getAge() {
return this.age;
}
};
new Person().getName();
//"Tony"

Module(模块)模式

javascript 中有集中用于实现模块的方法,包括:

  • 对象字面量表示法
  • Module 模式
  • AMD 模块
  • CommonJS 模块
  • ECMAScript Harmony(ES6)模块

对象字面量

使用对象字面量有利于组织与封装代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
let moduleA = {
propertyA: "123",
config: {
language: "en",
useCaching: true
},
setConfig(config) {
this.config = Object.assign(this.config, config);
}
};
moduleA.setConfig({ language: "zh" });
moduleA.config.language;
//"zh"

Module(模块)模式

在 javascript 中,Module 模式用于进一步模拟类的概念,是一个单独的对象拥有 共有/私有 方法和变量,从而避免污染全局作用域,只返回一个共有 API,降低代码冲突的可能性。
通过闭包的我们可以实现私有方法和变量。私有方法无法直接被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let counterModule = (function() {
//私有变量
let counter = 0;
//私有方法
let log = function() {
console.log(counter);
};

//返回共有方法
return {
incrementCounter() {
return ++counter;
},
resetCounter() {
counter = 0;
//调用私有方法
log();
}
};
})();

这种模式的缺点:

  • 如果想改变方法变量的公有/私有性,则需要修改每一个调用该模块方法的地方。
  • 对于私有方法属性无法创建自动化单元测试。

Revaling Module(揭示模块)模式

揭示模块模式与模块模式主要区别在于,所有方法变量都定义在私有作用于范围,只在底部指定共有方法名称与引用,改善代码可读性,一致性。私有方法可以更简洁的引用公有方法(无需重复模块变量名),暴露的方法变量名更加灵活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let person = (function() {
let age = "18";
function privateLogAge() {
console.log(publicGetAge());
}
function publicGetAge() {
return age;
}
function publicLogAge() {
privateLogAge();
}
return {
getAge: publicGetAge,
logAge: publicLogAge
};
})();
person.logAge;
//18

Singleton(单例)模式

Singleton 模式限制了一个类只能实例化一次。如果类实例不存在则创建,存在则返回该实例。

1
2
3
4
5
6
7
8
9
10
11
function Singleton() {
if (typeof Singleton.instance === "object") {
return Singleton.instance;
}
this.name = "singleton";
this.data = {};
Singleton.instance = this;
}

console.log(new Singleton() === new Singleton());
//true
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
var singleton = (function() {
var instance;

//在init中初始化对象
function init() {
//随机生成年龄
var age = Math.round(Math.floor(Math.random() * 100));
return {
getAge() {
return age;
}
};
}

return {
getInstance() {
if (!instance) {
//调用方法时初始化,避免资源浪费
instance = init();
}
return instance;
}
};
})();
singleton.getInstance().getAge();
//17
singleton.getInstance().getAge();
//17

Singleton 的存在往往表明系统中的模块要么是紧密耦合,要么是逻辑过于分散。由于 Singleton 模式下依赖隐藏,创建多个实例的难度,Singleton 的测试会更加困难。

Observer(观察者)模式

在 Observer 模式下,一个对象(subject)维持一系列依赖于它的(观察者)对象,将对象(subject)有关状态的变更自动通知给它们。

Observer pattern

  • Subject(目标),维护一系列观察者,方便添加或删除观察者
  • Observer(观察者),为那些在目标状态发生改变时需获得通知的对象提供一个更新接口。
  • ConcreteSubject(具体目标),状态发生改变时,向 Observer 发出通知,存储 ConcreteObserver 的状态
  • ConcreteObserver(具体观察者),存储一个指向 ConcreteSubject 的引用,实现 Observer 的更新接口,以使自身状态与目标保持一致。
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
79
80
81
/* ObserverList */
function ObserverList() {
this.observerList = [];
}
ObserverList.prototype = {
add(obj) {
this.observerList.push(obj);
},
remove(obj) {
let index = this.observerList.indexOf(obj);
if (index !== -1) {
this.observerList.splice(index, 1);
}
},
empty() {
this.observerList = [];
},
getListLength() {
return this.observerList.length;
},
getByIndex(index) {
return this.observerList[index];
}
};

/* Subject */
function Subject() {
this.observers = new ObserverList();
}
Subject.prototype = {
addObserver(observer) {
this.observers.add(observer);
},
removeObserver(observer) {
this.observers.remove(observer);
},
notifyObservers(context) {
let observerCount = this.observers.getListLength();
for (var i = 0; i < observerCount; i++) {
this.observers.getByIndex(i).update(context);
}
}
};

/* Observer */
function Observer(updateFn = function() {}) {
/* 私有更新方法 */
this.update = updateFn;
}

/* ConcreteSubject */
let subject = new Subject();
let obj = {
_a: 1,
get a() {
subject.notifyObservers("getting a");
// return this._a
},
set a(val) {
subject.notifyObservers("setting a");
this._a = val;
return val;
}
};

var log1 = [];
/* ConcreteObserver */
var observer1 = new Observer(function(val) {
log1.push("log1:" + val);
});
var log2 = [];
var observer2 = new Observer(function(val) {
log2.push("log2:" + val);
});

subject.addObserver(observer1);
subject.addObserver(observer2);

obj.a = obj.a * 2;
console.log(log1, log2);
/*["log1:getting a", "log1:setting a"] ["log2:getting a", "log2:setting a"]*/

Observer(观察者)模式和 Publish/Subscribe(发布/订阅)模式的区别

在 javascaript 中,Observer 模式更常见的是一种变体实现 Publish/Subscribe 模式。
Observer 模式要求 Observer 置入 Subject 的 ObserverList 中(订阅 Subject),以便于 Subject 触发这些 Observer,这样 Subject,Observer 就存在依赖关系。
Publish/Subscribe 模式区别在 Publisher 与 Subscriber 之间添加了一个 Topic/EventChannel 作为中间层,这样我们就解耦了 Publisher 与 Subscriber。在中间层我们可以自定义事件与传递的参数,允许订阅者通过自定义方法来注册事件,接收事件。

Observer 与 Pub/Sub 优点:

  • 帮助我们将程序分解为更小,更松散耦合的块,改进代码管理与复用性
  •  我们无需再去维护 ConcerteObserver 对象的状态,因为 ConcerteSubject 会执行 ConcerteObserver.update 自动更新其状态,解耦了我们的代码

Observer 与 Pub/Sub 缺点:

  • 由于系统解耦,发布者  无法确保  订阅者代码正确执行
  • 订阅者之间没有关联,无法感知替换发布者的成本。订阅者与发布者之间的动态关系,难以跟踪更新依赖。

Pub/Sub 实现

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
var pubsubModule = (function() {
/* 订阅token累加器 */
let subId = -1,
/* 中间层,事件通道topics */
topics = {};

return {
/* 发布,传入发布事件类型 */
publish(topic, ...arg) {
if (!topics[topic]) {
return false;
}
topics[topic].forEach(item => item.fn(arg));
return this;
},
/* 订阅,传入订阅事件类型,回调函数 */
subscribe(topic, fn) {
if (!topics[topic]) {
topics[topic] = [];
}
topics[topic].push({
/* 存入token,回调 */
token: ++subId,
fn
});
return subId;
},
/* 取消订阅,根据token取消订阅 */
unsubscribe(token) {
for (let key in topics) {
let index = topics[key].findIndex(item => item.token === token);
if (index !== -1) {
topics[key].splice(index, 1);
return token;
}
}
return this;
}
};
})();

var subscriberA = pubsubModule.subscribe("you/is/five", data =>
console.log("subscriberA:i get you/is/five boradcast", "data:" + data)
);
var subscriberB = pubsubModule.subscribe("you/is/five", data =>
console.log("subscriberB:i get you/is/five boradcast", "data:" + data)
);
pubsubModule.publish("you/is/five", "just a joke!");
pubsubModule.unsubscribe(subscriberA);
pubsubModule.publish("you/is/five", "is there subscriberA?nope");

/*
subscriberA:i get you/is/five boradcast data:just a joke!
pubsub:42 subscriberB:i get you/is/five boradcast data:just a joke!
pubsub:42 subscriberB:i get you/is/five boradcast data:is there subscriberA?nope
*/

Mediator(中介者)模式

Mediator 模式解耦代码的方式是:确保组件交互通过中心点 Mediator 来处理,而不是直接操作其他组件。
Vuex,Redux处理全局状态,部分组件之间通过Store来通信,可以看作Mediator模式的实现。

  • 优点:将系统组件之间的通信从多对多改为一对多,解耦程度高。
  • 缺点:无法通过仅关注发布广播来来确定系统作何反应,因为无法确定该广播有哪些订阅者。
    Mediator模式与Observer模式的差异:
  • Observer模式不存在封装单一对象来维持发布/订阅关系,Observer/Subject合作才能维持关系。
  • Mediator模式严格限制通过Mediator进行通信。

参考链接

JavaScript 设计模式 - Addy Osmani