Another RayJune

JavaScript 中的设计模式

偷懒是程序员的优良品质,模式则是先人们总结的偷懒招式。 Stoyan Stefanov 的这本书,从 JavaScript 的实际使用场景出发,提炼了不少可以让前端们偷懒的实用招式。模式的探索、创新,将永远是程序员自我提升的一条修炼之道。

Design Patterns: 设计模式

Although these design patterns are language-independent and implementation-agnostic, they have been studied for many years, mainly from the perspective of strongly typed static-class languages, such as CPP and Java.
尽管这些设计模式跟语言和具体的实现方式无关,但它们多年来被关注到的方面仍然主要是在强类型静态语言比如 C++ 和 Java 中的应用。

JavaScript, being an untyped dynamic prototype-based language, sometimes makes it surprisingly easy, even trivial, to implement some of these patterns.
JavaScript 作为一种基于原型的弱类型动态语言,使得有些时候实现某些模式时相当简单,甚至不费吹灰之力。

Let’s start with the first example of how things are different in JavaScript compared to a static class-based language—the singleton pattern.
让我们从第一个例子——单例模式——来看一下在 JavaScript 中和静态的基于类的语言有什么不同

Singleton: 单例模式

The idea of the singleton pattern is to have only one instance of a specific class. This means that the second time you use the same class to create a new object, you should get the same object that was created the first time.
单例模式的核心思想是让指定的类只存在唯一一个实例。这意味着当你第二次使用相同的类去创建对象的时候,你得到的应该和第一次创建的是同一个对象

And how does this apply to JavaScript? In JavaScript there are no classes, just objects. When you create a new object, there’s actually no other object like it, and the new object is already a singleton. Creating a simple object using the object literal is also an example of a singleton:
这如何应用到 JavaScript 中呢?在 JavaScript 中没有类,只有对象当你创建一个对象时,事实上根本没有另一个对象和它一样,这个对象其实已经是一个单例。使用对象字面量创建一个简单的对象也是一种单例的例子:

1
2
3
var obj = {
myprop: 'my value'
};

In JavaScript, objects are never equal unless they are the same object, so even if you create an identical object with the exact same members, it won’t be the same as the first one:
在 JavaScript 中,对象永远不会相等,除非它们是同一个对象,所以即使你创建一个看起来完全一样的对象,它也不会和前面的对象相等:

1
2
3
4
5
6
7
8
9
10
var obj2 = {
myprop: 'my value'
};
obj === obj2; // false
obj == obj2; // false

// But
var obj3 = obj2;
obj3 == obj2 //true
obj3 === obj2 //true

So you can say that every time you create an object using the object literal, you’re actually creating a singleton, and there’s no special syntax involved.
所以你可以说当你每次使用对象字面量创建一个对象的时候就是在创建一个单例,并没有特别的语法迁涉进来。

Note that sometimes when people say “singleton” in a JavaScript con- text, they mean the module pattern discussed in Chapter 5.
需要注意的是,有的时候当人们在 JavaScript 中提出“单例”的时候,它们可能是在指第 5 章讨论过的“模块模式”。

使用new

JavaScript doesn’t have classes, so the verbatim definition for singleton doesn’t technically make sense. But JavaScript has the new syntax for creating objects using con- structor functions, and sometimes you might want a singleton implementation using this syntax. The idea is that when you use new to create several objects using the same constructor, you should get only new pointers to the exact same object.
JavaScript 没有类,所以一字一句地说单例的定义并没有什么意义。但是 JavaScript 有使用 new、通过构造函数来创建对象的语法,有时候你可能需要这种语法下的一个单例实现。这也就是说当你使用 new、通过同一个构造函数来创建多个对象的时候,你应该只是得到同一个对象的不同引用

Usefulness warning: The following discussion is not so useful as a practical pattern but more as a theoretical exercise in imitating the workarounds for issues related to the designs of some (statically, strongly typed) class-based languages in which functions are not first-class objects.
温馨提示:从一个实用模式的角度来说,下面的讨论并不是那么有用,只是更多地在实践模拟一些语言中关于这个模式的一些问题的解决方案。这些语言主要是(静态强类型的)基于类的语言,在这些语言中,函数并不是“一等公民”。

ideal result:

1
2
3
var uni = new Universe(); 
var uni2 = new Universe();
uni === uni2; // true

You need your Universe constructor to cache the object instance this when it’s created and then return it the second time the constructor is called. You have several options to achieve this:
当对象实例 this 被创建时,你需要在 Universe 构造函数中缓存它,以便在第二次调用的时候返回。有几种选择可以达到这种效果:

  • You can use a global variable to store the instance. This is not recommended because of the general principle that globals are bad. Plus, anyone can overwrite this global variable, even by accident. So let’s not discuss this option any further. 你可以使用一个全局变量来存储实例。不推荐使用这种方法,因为通常我们认为使用全局变量是不好的。而且,任何人都可以改写全局变量的值,甚至可能是无意中改写。所以我们不再讨论这种方案。
  • You can cache in a static property of the constructor. Functions in JavaScript are objects, so they can have properties. You can have something like Universe.instance and cache the object there. This is a nice, clean solution with the only drawback that the instance property is publicly accessible, and code outside of yours might change it, so you lose the instance. 你也可以将对象实例缓存在构造函数的属性中。在JavaScript中,函数也是对象,所以它们也可以有属性。你可以写一些类似Universe.instance的属性来缓存对象。这是一种漂亮干净的解决方案,不足之处是 instance 属性仍然是可以被公开访问的别人写的代码可能修改它,这样就会失去这个实例
  • You can wrap the instance in a closure. This keeps the instance private and not available for modifications outside of your constructor at the expense of an extra closure. 你可以将实例包裹在闭包中。这可以保持实例是私有的,不会在构造函数之外被修改,代价是一个额外的闭包

Let’s take a look at an example implementation of the second and third options.
我们主要来看一下第二种方案(将实例放到静态属性中缓存)和第三种方案(将实例放到闭包中)的实现实例。

Instance in a Static Property: 将实例放到静态属性中

Here’s an example of caching the singular instance in a static property of the Universe constructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Universe() {
// do we have an existing instance?
if (typeof Universe.instance === 'object') {
return Universe.instance; // 利用新创建的instance属性来缓存第一个实例
}
// proceed as normal
this.startTime = 0;
this.bang = 'Big';
// cache
Universe.instance = this;
// implicit return: // 用new + 构造函数会自动隐式返回this
// return this;
}

// testing
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true

As you see, this is a straightforward solution with the only drawback that instance is public. It’s unlikely that other code will change it by mistake (much less likely than if instance was a global) but still possible.
如你所见,这是一种直接有效的解决方案,唯一的缺陷是 instance 是可被公开访问的。一般来说它被其它代码误删改的可能是很小的(起码比全局变量 instance 要小得多),但是仍然是有可能的。

比如下面的例子:

1
2
3
4
Universe.instance = {a: 1};
var u3 = new Universe();
console.log(u3); // {a: 1};
u3 === u2; // false;
Instance in a Closure: 将实例放到闭包中

Another way to do the class-like singleton is to use a closure to protect the single instance. You can implement this by using the private static member pattern discussed in Chapter 5. The secret sauce here is to rewrite the constructor:
另一种实现基于类的单例模式的方法是使用一个闭包来保护这个唯一的实例。你可以通过第 5 章讨论过的“私有静态成员模式”来实现。唯一的秘密就是重写构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Universe() {
// the cached instance
var instance = this;

// proceed as normal
this.start_time = 0;
this.bang = 'big';

// rewrite the constructor
Universe = function singleUniverse() {
return instance;
};
}

// testing
var u1 = new Universe();
var u2 = new Universe();
u1 === u2; // true

This implementation is actually another example of the self-defining function pattern from Chapter 4. The drawback, as we discussed there, is that the rewritten function (in this case the constructor Universe()) will lose any properties added to it between the moment of initial definition and the redefinition. In our specific case anything you add to the prototype of Universe() will not have a live link to the instance created with the original implementation.
这个实现实际上也是第 4 章讨论的自定义函数的又一个例子。如我们讨论过的一样,这种模式的缺点是被重写的函数(在这个例子中就是构造函数 Universe() )将丢失那些在初始定义和重新定义之间添加的属性。在这个例子中,任何添加到 Universe()的原型上的属性将不会被链接到使用原来的实现创建的实例上。(注:这里的“原来的实现”是指实例是由未被重写的构造函数创建的,而 Universe()则是被重写的构造函数。)

Here’s how you can see the problem with some testing:

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
function Universe() {
// the cached instance
var instance = this;
// proceed as normal
this.start_time = 0;
this.bang = 'big';
// rewrite the constructor
Universe = function singleUniverse() {
return instance;
};
}

// adding to the prototype
Universe.prototype.nothing = true;

var uni = new Universe();

// again adding to the prototype
// after the initial object is created
Universe.prototype.everything = true;

var uni2 = new Universe();

// Testing:
// only the original prototype was
// linked to the objects
uni.nothing; // true
uni2.nothing; // true
uni.everything; // undefined
uni2.everything; // undefined

// that sounds right:
uni.constructor.name; // "Universe"

// but that's odd:
uni.constructor === Universe; // false

The reason that uni.constructor is no longer the same as the Universe() constructor is because uni.constructor still points to the original constructor, not the redefined one.
uni.constructor 不再和 Universe()相同的原因是 uni.constructor 仍然是指向原来的构造函数(即原来的老函数),而不是被重新定义的那个。

If getting the prototype and the constructor pointer working as expected is a requirement, it’s possible to achieve this with a few tweaks:
如果一定被要求让 prototype 和 constructor 的指向像我们期望的那样,可以通过一些调整来做到:

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
function Universe() {
// the cached instance
var instance;

// rewrite the constructor
Universe = function Universe() {
return instance;
};

// carry over the prototype properties
Universe.prototype = this;

// the instance
instance = new Universe();

// reset the constructor pointer
instance.constructor = Universe;

// all the functionality
instance.start_time = 0;
instance.bang = 'Big';
return instance;
}

// 现在所有的测试结果都可以像我们期望的那样了:
// update prototype and create instance
Universe.prototype.nothing = true; // true
var uni = new Universe();
Universe.prototype.everything = true; // true
var uni2 = new Universe();

// it's the same single instance
uni === uni2; // true

// all prototype properties work
// no matter when they were defined

uni.nothing && uni.everything && uni2.nothing && uni2.everything; // true // the normal properties work
uni.bang; // "Big"
// the constructor points correctly
uni.constructor === Universe; // true

An alternative solution would also be to wrap the constructor and the instance in an immediate function. The first time the constructor is invoked, it creates an object and also points the private instance to it. From the second invocation on, the constructor simply returns the private variable. All the tests from the previous snippet will work as expected, too, with this new implementation:
另一种可选的解决方案是将构造函数和实例包在一个立即执行的函数中。当构造函数第一次被调用的时候,它返回一个对象并且将私有的 instance 指向它。在后续调用时,构造函数只是简单地返回这个私有变量。在这种新的实现下,前面所有的测试代码也会和期望的一样:

RayJune注:这种方法看起来逻辑更为清晰

1
2
3
4
5
6
7
8
9
10
11
12
13
var Universe; 
(function handleSinglePattern() {
var instance;
Universe = function Universe() {
if (instance) {
return instance;
}
instance = this;
// all the functionality
this.start_time = 0;
this.bang = 'Big';
};
}());

Factory: 工厂模式

The purpose of the factory is to create objects. It’s usually implemented in a class or a static method of a class, which has the following purposes:
使用工厂模式的目的就是创建对象。它通常在类或者类的静态方法中实现,目的是:

  • Performs repeating operations when setting up similar objects 执行在建立相似的对象时进行的一些重复操作
  • Offers a way for the customers of the factory to create objects without knowing the specific type (class) at compile time 让工厂的使用者在编译阶段创建对象时不必知道它的特定类型(类)

The second point is more important in static class languages in which it may be nontrivial to create instances of classes, which are not known in advance (in compile time). In JavaScript, this part of the implementation is quite easy.
第二点在静态的基于类的语言中更重要,因为在(编译阶段)提前不知道类的情况下,创建类的实例是不普通的行为。但在 JavaScript 中,这部分的实现却是相当容易的事情。

The objects created by the factory method (or class) are by design inheriting from the same parent object; they are specific subclasses implementing specialized functionality. Sometimes the common parent is the same class that contains the factory method.
使用工厂方法(或类)创建的对象被设计为从同一个父对象继承;它们是特定的实现一些特殊功能的子类。有些时候这个共同的父对象就是包含工厂方法的同一个类。

Let’s first see how the finished implementation will be used:

1
2
3
4
5
6
var corolla = CarMaker.factory('Compact'); 
var solstice = CarMaker.factory('Convertible');
var cherokee = CarMaker.factory('SUV');
corolla.drive(); // "Vroom, I have 4 doors"
solstice.drive(); // "Vroom, I have 2 doors"
cherokee.drive(); // "Vroom, I have 17 doors"

This part:

1
var corolla = CarMaker.factory('Compact');

is probably the most recognizable in the factory pattern. You have a method that accepts a type given as a string at runtime and then creates and returns objects of the requested type. There are no constructors used with new or any object literals in sight, just a function that creates objects based on a type identified by a string.
这一段可能是工厂模式中最知名的。你有一个方法可以在运行时接受一个表示类型的字符串,然后它创建并返回了一个和请求的类型一样的对象。这里没有使用 new 的构造函数,也没有看到任何对象字面量,仅仅只有一个函数根据一个字符串指定的类型创建了对象

Let’s first see how the finished implementation will be used:

1
2
3
4
5
6
var corolla = CarMaker.factory('Compact'); 
var solstice = CarMaker.factory('Convertible');
var cherokee = CarMaker.factory('SUV');
corolla.drive(); // "Vroom, I have 4 doors"
solstice.drive(); // "Vroom, I have 2 doors"
cherokee.drive(); // "Vroom, I have 17 doors"

This part:

1
var corolla = CarMaker.factory('Compact');

It is probably the most recognizable in the factory pattern. You have a method that accepts a type given as a string at runtime and then creates and returns objects of the requested type. There are no constructors used with new or any object literals in sight, just a function that creates objects based on a type identified by a string.
可能是工厂模式中最知名的。你有一个方法可以在运行时接受一个表示类型的字符串,然后它创建并返回了一个和请求的类型一样的对象。这里没有使用 new 的构造函数,也没有看到任何对象字面量,仅仅只有一个函数根据一个字符串指定的类型创建了对象

Here’s an example implementation of the factory pattern that would make the code in preceding snippet work:

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
// parent constructor 
function CarMaker() {}

// a method of the parent
CarMaker.prototype.drive = function () {
return "Vroom, I have " + this.doors + " doors";
};

// the static factory method
CarMaker.factory = function factoryCarMaker(type) {
var constr = type; // 保存传入字符串
var newcar;
// error if the constructor doesn't exist
if (typeof CarMaker[constr] !== 'function') {
throw {
name: 'Error',
message: constr + " doesn't exist"
};
}
// at this point the constructor is known to exist
// let's have it inherit the parent but only once

// FIXME:对这一句暂时不理解
if (typeof CarMaker[constr].prototype.drive !== 'function') {
CarMaker[constr].prototype = new CarMaker();
}
// FIXME

// create a new instance
newcar = new CarMaker[constr]();
// optionally call some methods and then return...
return newcar;
};
// define specific car makers
CarMaker.Compact = function () {
this.doors = 4;
};
CarMaker.Convertible = function () {
this.doors = 2;
};
CarMaker.SUV = function () {
this.doors = 24;
};

There’s nothing particularly difficult about the implementation of the factory pattern. All you need to do is look for the constructor function that creates an object of the required type. In this case a simple naming convention was used to map object types to the constructors that create them. The inheritance part was just an example of a common repeating piece of code that could be put into the factory method instead of repeated for every constructor type.
工厂模式的实现中没有什么是特别困难的。你需要做的仅仅是寻找请求类型的对象的构造函数。在这个例子中,使用了一个简单的名字转换以便映射对象类型和创建对象的构造函数。继承的部分只是一个公共的重复代码片段的示例,它可以被放到工厂方法中而不是被每个构造函数的类型重复(通过原型继承的 代码可以在 factory 方法以外执行,而不是放到 factory 中每调用一次 都要执行一次。)

Built-in Object Factory: 内置对象工厂

And for an example of “factory in the wild,” consider the built-in global Object() con- structor. It also behaves as a factory, because it creates different objects, depending on the input. If you pass it a primitive number, it can create an object with the Number() constructor behind the scenes. The same is true for the string and boolean values. Any other values, including no input values, will create a normal object.

作为一个“野生的工厂”的例子,我们来看一下内置的全局构造函数 Object()。它的行为很像工厂,因为它根据不同的输入创建不同的对象。如果传入一个数字,它会使用 Number()构造函数创建一个对象。在传入字符 串和布尔值的时候也会发生同样的事情。任何其它的值(包括空值)将会创建一个正常的对象。

Here are some examples and tests of the behavior. Note that Object can be called with or without new:
下面是这种行为的例子和测试,注意 Object 调用时可以不用加 new:

1
2
3
4
5
6
7
8
9
10
var o = new Object();
var n = new Object(1);
var s = Object('1');
var b = Object(true);

// test
o.construcotr === Object; //true
n.constructor == Number; //true
s.constructor === String; //true
b.constructor === Boolean; //true

The fact that Object() is also a factory is of little practical use, just something worth mentioning as an example that the factory pattern is all around us.

Iterator: 迭代器模式

In the iterator pattern, you have an object containing some sort of aggregate data. This data may be stored internally in a complex structure, and you want to provide easy access to each element of that structure. The consumer of your object doesn’t need to know how you structure your data; all they need is to work with the individual elements.
在迭代器模式中,你有一些含有有序聚合数据的对象。这些数据可能在内部用一种复杂的结构存储着,但是你希望提供一种简单的方法来访问这种结构 的每个元素。数据的使用者不需要知道你是怎样组织你的数据的,他们只需要操作一个个独立的元素。

In the iterator pattern, your object needs to provide a next() method. Calling next() in sequence must return the next consecutive element, where it’s up to you to decide what “next” means in your particular data structure.
在迭代器模式中,你的对象需要提供一个 next()方法。按顺序调用 next() 方法必须返回序列中的下一个元素,但是“下一个”在你的特定的数据结构中指什么是由你自己来决定的。

Assuming that your object is called agg, you could access each data element by simply calling next() in a loop like so:

1
2
3
4
5
var element;
while (element = agg.next()) {
// do something with the element ...
console.log(element);
}

In the iterator pattern, the aggregate object usually also provides a convenience has Next() method, so the users of the object can determine if they’ve reached the end of your data. Another way to access all elements sequentially, this time using hasNext(), would be the like the following:
在迭代器模式中,聚合对象通常也会提供一个方便的方法 hasNext(),这样 对象的使用者就可以知道他们已经获取到你数据的最后一个元素。当使用另 一种方法——hasNext()——来按顺序访问所有元素时,是像这样的:

1
2
3
4
while (agg.hasNext()) {
// do something with the next element...
console.log(agg.next());
}

Decorator: 装饰器模式

In the decorator pattern, additional functionality can be added to an object dynamically, at runtime. When dealing with static classes, this could be a challenge. In Java- Script, objects are mutable, so the process of adding functionality to an object is not a problem in itself.
在装饰器模式中,一些额外的功能可以在运行时被动态地添加到一个对象中。在静态的基于类的语言中,处理这个问题可能是个挑战,但是在 JavaScript 中,对象本来就是可变的,所以给一个对象添加额外的功能本身并不是什么问题。

A convenient feature of the decorator pattern is the customization and configuration of the expected behavior. You start with your plain object, which has some basic functionality. Then you pick and choose from an available pool of decorators which ones you want to use to enhance your plain object and in which order, if the order is important.
装饰器模式的一个很方便的特性是可以对我们需要的特性进行定制和配置。刚开始时,我们有一个拥有基本功能的对象,然后可以从可用的装饰器中去挑选一些需要用到的去增加这个对象,甚至如果顺序很重要的话,还可以指定增强的顺序。

Usage: 用法

Let’s take a look into an example usage of the pattern. Say you’re working on a web application that sells something. Every new sale is a new sale object. The sale “knows” about the price of the item and can return it by calling the sale.getPrice() method. Depending on the circumstances, you can start decorating this object with extra functionality. Imagine a scenario where the sale for a customer is in the Canadian province of Québec. In this case the buyer needs to pay a federal tax and a provincial Québec tax. Following the decorator pattern, you’ll say that you “decorate” the object with a federal tax decorator and a Québec tax decorator. You can then also decorate the object with price formatting functionality. This scenario could look like the following:
我们来看一下这个模式的示例用法。假设你正在做一个卖东西的 web 应用,每个新交易是一个新的 sale 对象。这个对象“知道”交易的价格并且可以通过调用 sale.getPrice()方法返回。根据环境的不同,你可以开始用一些额 外的功能来装饰这个对象。假设一个场景是这笔交易是发生在加拿大的一个省 Québec,在这种情况下,购买者需要付联邦税和 Québec 省税。根据装饰器模式的用法,你需要指明使用联邦税装饰器和 Québec 省税装饰器来装饰这个对象。然后你还可以给这个对象装饰一些价格格式的功能。这个场景的使用方式可能是像这样:

1
2
3
4
5
var sale = new Sale(100);           // the price is 100 dollars 
sale = sale.decorate('fedtax'); // add federal tax
sale = sale.decorate('quebec'); // add provincial tax
sale = sale.decorate('money'); // format like money
sale.getPrice(); // "$112.88"

In another scenario the buyer could be in a province that doesn’t have a provincial tax, and you might also want to format the price using Canadian dollars, so you can do:
在另一种场景下,购买者在一个不需要交省税的省,并且你想用加拿大元的格式来显示价格,你可以这样做:

1
2
3
4
var sale = new Sale(100);           // the price is 100 dollars 
sale = sale.decorate('fedtax'); // add federal tax
sale = sale.decorate('cdn'); // format using CDN
sale.getPrice(); // "CDN$ 105.00"

As you can see, this is a flexible way to add functionality and tweak an object at runtime. Let’s see how to approach an implementation of the pattern.
如你所见,这是一种在运行时很灵活的方法来添加功能和调整对象。我们来看一下如何来实现这种模式。

Implementation: 实现

One way to implement the decorator pattern is to have each decorator be an object containing the methods that should be overwritten. Each decorator actually inherits the object enhanced so far after the previous decorator. Each decorated method calls the same method on the uber (the inherited object) and gets the value and proceeds with doing something in addition.
一种实现装饰器模式的方法是让每个装饰器成为一个拥有应该被重写的方法的对象。每个装饰器实际上是继承自已经被前一个装饰器增强过的对象。装饰器的每个方法都会调用父对象(继承自的对象)的同名方法并取得值,然后做一些额外的处理

1
2
3
4
5
6
7
8
function Sale(price) {
this.price = price || 100;
}
Sale.prototype.getPrice = function () {
return this.price;
};

Sale.decorators = {};

Let’s see one example decorator. It’s an object that implements the customized get Price() method. Note that the method first gets the value from the parent method and then modifies that value:
我们来看一个装饰器的例子。这是一个对象,实现了一个自定义的 getPrice() 方法。注意这个方法首先从父对象的方法中取值然后修改这个值

1
2
3
4
5
6
7
Sale.decorators.fedtax = {
getPrice: function () {
var price = this.uber.getPrice(); // 从父对象的方法中取值
price += price * 5 / 100; // 再修改这个值
return price;
}
};

Similarly we can implement other decorators, as many as needed. They can be extensions to the core Sale() functionality, implemented like plugins. They can even “live” in additional files and be developed and shared by third-party developers:
使用类似的方法我们可以实现任意多个需要的其它装饰器。他们的实现方式像插件一样来扩展核心的 Sale()的功能。他们甚至可以被放到额外的文件中,被第三方的开发者来开发和共享:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Sale.decorators.quebec = {
getPrice: function () {
var price = this.uber.getPrice();
price += price * 7.5 / 100;
return price;
}
};

Sale.decorators.money = {
getPrice: function () {
return '$' + this.uber.getPrice().toFixed(2);
}
};

Sale.decorators.cdn = {
getPrice: function () {
return 'CDN$ ' + this.uber.getPrice().toFixed(2);
}
};

Finally let’s see the “magic” method called decorate() that ties all the pieces together. Remember it will be called like:

1
sale = sale.decorate('fedtax');

To do the inheritance part, let’s use the temporary constructor pattern from the previous chapter. We also set the uber property of newobj so the children have access to the parent. Then we copy all the extra properties from the decorator to the newly decorated object newobj. At the end, newobj is returned and, in our concrete usage example, it becomes the new updated sale object:
实现这一部分需要用到前面章节中提到的临时构造函数模式。我们也设置一个 uber 属性给 newobj 以便子对象可以访问到父对象。然后我们从装饰器中复制所有额外的属性到被装饰的对象 newobj 中。最后,在我们的例子中,newobj 被返回并且成为被更新过的 sale 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Sale.prototype.decorate = function (decorator) {
var F = function () {};
var overrides = this.constructor.decorators[decorator];
var i;
var newobj;
F.prototype = this;
newobj = new F();
newobj.uber = F.prototype;
for (i in overrides) {
if (overrides.hasOwnProperty(i)) {
newobj[i] = overrides[i];
}
}
return newobj;
};

whole code:

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
function Sale(price) {
this.price = price || 100;
}
Sale.prototype.getPrice = function () {
return this.price;
};
Sale.decorators = {};

Sale.decorators.fedtax = {
getPrice: function () {
var price = this.__proto__.getPrice(); // 从父对象的方法中取值
price += price * 5 / 100; // 再修改这个值
return price;
}
};
Sale.decorators.quebec = {
getPrice: function () {
var price = this.__proto__.getPrice();
price += price * 7.5 / 100;
return price;
}
};
Sale.decorators.money = {
getPrice: function () {
return '$' + this.__proto__.getPrice().toFixed(2);
}
};
Sale.decorators.cdn = {
getPrice: function () {
return 'CDN$ ' + this.__proto__.getPrice().toFixed(2);
}
};

Sale.prototype.decorate = function (decorator) {
var F = function () {};
var overrides = this.constructor.decorators[decorator];
var i;
var newobj;
F.prototype = this;
newobj = new F();
newobj.uber = F.prototype;
for (i in overrides) {
if (overrides.hasOwnProperty(i)) {
newobj[i] = overrides[i];
}
}
return newobj;
};

// testing
var sale = new Sale(100); // the price is 100 dollars
sale = sale.decorate('fedtax'); // add federal tax
sale = sale.decorate('cdn'); // format using CDN
sale.getPrice(); // "CDN$ 105.00"

Implementation Using a List

Let’s explore a slightly different implementation, which benefits from the dynamic nature of JavaScript and doesn’t need to use inheritance at all. Also instead of having each decorated method call the method previously in the chain, we can simply pass the result of the previous method as a parameter to the next method.
我们来看另一个明显不同的实现方法,受益于 JavaScript 的动态特性,它完全不需要使用继承。同时,我们也可以简单地将前一个方面的结果作为参数传给下一个方法,而不需要每一个方法都去调用前一个方法。

Such implementation could also allow for easy undecorating or undoing a decoration, which means simply removing an item from the list of decorators.
这样的实现方法还允许很容易地反装饰(undecorating)或者撤销一个装饰,这仅仅需要从一个装饰器列表中移除一个条目

The usage example will be slightly simpler because we don’t assign the return value from decorate() to the object. In this implementation, decorate() doesn’t do anything to the object, it simply appends to a list:

1
2
3
4
var sale = new Sale(100); // the price is 100 dollars sale.decorate('fedtax');  // add federal tax 
sale.decorate('quebec'); // add provincial tax
sale.decorate('money'); // format like money
sale.getPrice(); // "$112.88"

The Sale() constructor now has a list of decorators as an own property:

1
2
3
4
function Sale(price) {
this.price = (price > 0) || 100;
this.decorators_list = [];
}

The avaliable decorators are once again implemented as properties of Sale.decorators. Note that the getPrice() methods are now simpler because they don’t call the parent getPrice() to get the intermediate result; this result is passed to them as a parameter:
可用的装饰器仍然被实现为 Sale.decorators 的属性。注意 getPrice()方法现在更简单了,因为它们不需要调用父对象的 getPrice()来获取结果,结果已经作为参数传递给它们了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Sale.decorators = {};
Sale.decorators.fedtax = {
getPrice: function (price) {
return price + price * 5 / 100;
}
};

Sale.decorators.quebec = {
getPrice: function (price) {
return price + price * 7.5 / 100;
}
};

Sale.decorators.money = {
getPrice: function (price) {
return '$' + price.toFixed(2);
}
};

The interesting part happens in the parent’s decorate() and getPrice() methods. In the previous implementation, decorate() was somewhat complex and getPrice() was quite simple. In this implementation it’s the other way around: decorate() just appends to a list while getPrice() does all the work. The work includes going through the list of currently added decorators and calling each of the getPrice() methods, passing the result from the previous:
最有趣的部分发生在父对象的 decorate()和 getPrice()方法上。在前一种实现方式中,decorate()还是多少有些复杂,而 getPrice()十分简单。在这种实现方式中事情反过来了:decorate()只需要往列表中添加条目而getPrice()做了所有的工作。这些工作包括遍历现在添加的装饰器的列表,然后调用它们的 getPrice()方法并将结果传递给前一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Sale.prototype.decorate = function (decorator) {
this.decorators_list.push(decorator);
};
Sale.prototype.getPrice = function () {
var price = this.price;
var i;
var max = this.decorators_list.length;
var name;
for (i = 0; i < max; i += 1) {
name = this.decorators_list[i];
price = Sale.decorators[name].getPrice(price);
}
return price;
};

This second implementation of the decorator pattern is simpler, and there’s no inheritance involved. The decorating methods are also simpler. All the work is done by the method that “agrees” to be decorated. In this sample implementation, getPrice() is the only method that allows decoration. If you want to have more methods that can be decorated, then the part of going through the list of decorators should be repeated by each additional method. However, this can be easily abstracted into a helper method that takes a method and makes it “decoratable.” In such an implementation the deco rators_list property would become an object with properties named after the methods and values being arrays of decorator objects.
装饰器模式的第二种实现方式更简单一些,并且没有引入继承。装饰的方法也会简单。所有的工作都由“同意”被装饰的方法来做。在这个示例实现中, getPrice()是唯一被允许装饰的方法。如果你想有更多可以被装饰的方法,那遍历装饰器列表的工作就需要由每个方法重复去做。但是,这可以很容易地被抽象到一个辅助方法中,给它传一个方法然后使这个方法“可被装饰”。如果这样实现的话,decorators_list 属性就应该是一个对象,它的属性名字是方法名,值是装饰器对象的数组。

Strategy: 策略模式

The strategy pattern enables you to select algorithms at runtime. The clients of your code can work with the same interface but pick from a number of available algorithms to handle their specific task depending on the context of what they are trying to do.
策略模式允许在运行的时候选择算法。你的代码的使用者可以在处理特定任 务的时候根据即将要做的事情的上下文来从一些可用的算法中选择一个。

An example of using the strategy pattern would be solving the problem of form validation. You can create one validator object with a validate() method. This is the method that will be called regardless of the concrete type of form and will always return the same result—a list of data that didn’t validate and any error messages.
使用策略模式的一个例子是解决表单验证的问题。你可以创建一个 validator 对象,有一个 validate()方法。这个方法被调用时不用区分具体的表单类型,它总是会返回同样的结果——一个没有通过验证的列表和错误信息。

But depending on the concrete form and the data to be validated, the clients of your validator may choose different types of checks. Your validator picks the best strategy to handle the task and delegates the concrete data checks to the appropriate algorithm.
但是根据具体的需要验证的表单和数据,你代码的使用者可以选择进行不同类别的检查。你的 validator 选择最佳的策略来处理这个任务,然后将具体的数据检查工作交给合适的算法去做。

Let’s say you have the following piece of data, probably coming from a form on a page, and you want to verify whether it’s valid:

1
2
3
4
5
6
var data = {
first_name: "Super",
last_name: "Man",
age: "unknown",
username: "o_O"
};

For the validator to know what is the best strategy to use in this concrete example, you need to configure the validator first and set the rules of what you consider to be valid and acceptable.

Let’s say you will not require a last name and you’ll accept anything for first name, but you require the age to be a number and the username to have letters and numbers only and no special symbols. The configuration will be something like:
假设你不需要姓,名字可以接受任何内容,但要求年龄是一个数字,并且用 户名只允许包含字母和数字。配置可能是这样的:

1
2
3
4
5
validator.config = {
first_name: 'isNonEmpty',
age: 'isNumber',
username: 'isAlphaNum'
};

Now that the validator object is configured to handle your data, you call its validate() method and print any validation errors to the console:

1
2
3
4
validator.validate(data);
if (validator.hasErrors()) {
console.log(validator.messages.join("\n"));
}

This could print the following error messages:

1
Invalid value for *age*, the value can only be a valid number, e.g. 1, 3.14 or 2010 Invalid value for *username*, the value can only contain characters and numbers, no special symbols

Now let’s see how the validator is implemented. The available algorithms for the checks are objects with a predefined interface—they provide a validate() method and a one-line help information to be used in error messages:
现在我们来看一下这个 validator 是如何实现的。所有可用的用来检查的逻辑都是拥有一个 validate()方法的对象,它们还有一行辅助信息用来显示错 误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// checks for non-empty values 
validator.types.isNonEmpty = {
validate: function (value) {
return value !== "";
},
instructions: "the value cannot be empty"
};

// checks if a value is a number
validator.types.isNumber = {
validate: function (value) {
return !isNaN(value);
},
instructions: "the value can only be a valid number, e.g. 1, 3.14 or 2010"
};

// checks if the value contains only letters and numbers
validator.types.isAlphaNum = {
validate: function (value) {
return !/[^a-z0-9]/i.test(value);
},
instructions: "the value can only contain characters and numbers, no special symbols"
};

And finally the core of the validator object:

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
var validator = {
// all available checks
types: {},

// error messages in the current
// validation session
messages: [],

// current validation config // name: validation type
config: {},

// the interface method
// *data* is key => value pairs
validate: function (data) {
var i, msg, type, checker, result_ok;
// reset all messages
this.messages = [];

for (i in data) {
if (data.hasOwnProperty(i)) {
type = this.config[i];
checker = this.types[type];
if (!type) {
continue; // no need to validate
}
if (!checker) { // uh-oh
throw {
name: "ValidationError",
message: "No handler to validate type " + type
};
}
result_ok = checker.validate(data[i]); if (!result_ok) {
msg = "Invalid value for *" + i + "*, " + checker.instructions;
this.messages.push(msg);
}
}
}
return this.hasErrors();
},
// helper
hasErrors: function () {
return this.messages.length !== 0;
}
};

As you can see, the validator object is generic and could be kept like this for all validation use cases. The way to improve it would be to add more types of checks. If you use it on several pages, soon you’ll have a nice collection of specific checks. Then all you need to do for each new use cases is to configure the validator and run the validate() method.
如你所见,validator 对象是通用的,在所有的需要验证的场景下都可以保持这个样子。改进它的办法就是增加更多类型的检查。如果你将它用在很多页面上,每快你就会有一个非常好的验证类型的集合。然后在每个新的使用场景下你需要做的仅仅是配置 validator 然后调用validate()方法。

Façade: 外观模式

The façade is a simple pattern; it provides only an alternative interface to an object. It’s a good design practice to keep your methods short and not have them handle too much work. Following this practice you’ll end up with a greater number of methods than if you have uber methods with lots of parameters. Sometimes two or more methods may commonly be called together. In such cases it makes sense to create another method that wraps the repeating method calls.
外观模式是一种很简单的模式,它只是为对象提供了更多的可供选择的接口。 使方法保持短小而不是处理太多的工作是一种很好的实践。在这种实践的指导下,你会有一大堆的方法,而不是一个有着非常多参数的 uber 方法。有些时候,两个或者更多的方法会经常被一起调用。在这种情况下,创建另一个将这些重复调用包裹起来的方法就变得意义了。

The façade pattern is also helpful with redesign and refactoring efforts. When you want to replace an object with a different implementation, you have to do it over a period of time (it’s a complex object), while at the same time new code is being written that uses this object. You can start with thinking about the new object’s API and then proceed to create a façade in front of the old object that follows the new API. This way, when you get to fully replacing the old object, you’ll have less client code to modify because any recent client code will already use the new API.
外观模式在做一些重新设计和重构工作时也很有用。当你想用一个不同的实 现来替换某个对象的时候,你可能需要工作相当长一段时间(一个复杂的对象),与此同时,一些使用这个新对象的代码也在被同步编写。你可以先想好新对象的 API,然后使用新的 API 创建一个外观方法在旧的对象前面。使用这种方式,当你完全替换到旧的对象的时候,你只需要修改少量客户代码,因为新的客户代码已经是在使用新的 API 了。

Proxy: 代理模式

In the proxy design pattern, one object acts as an interface to another object. It’s different from the façade pattern, where all you have is convenience methods that combine several other method calls. The proxy sits between the client of an object and the object itself and protects the access to that object.
在代理设计模式中,一个对象充当了另一个对象的接口的角色。它和外观模式不一样,外观模式带来的方便仅限于将几个方法调用联合起来。而代理对象位于某个对象和它的客户之间,可以保护对对象的访问。

This pattern may look like overhead but it’s useful for performance purposes. The proxy serves as a guardian of the object (also called a “real subject”) and tries to have the real subject do as little work as possible.
这个模式看起来开销有点大,但在出于性能考虑时非常有用。代理对象可以 作为对象(也叫“真正的主体”)的保护者,让真正的主体对象做尽量少的工作。

One example use would be something we can call lazy initialization. Imagine that initializing the real subject is expensive, and it happens that the client initializes it but never actually uses it. In this case the proxy can help by being the interface to the real subject. The proxy receives the initialization request but never passes it on until it’s clear that the real subject is actually used.
一种示例用法是我们称之为“懒初始化”(延迟初始化)的东西。假设初始 化真正的主体是开销很大的,并且正好客户代码将它初始化后并不真正使用 它。在 这种情况下,代理对象可以作为真正的主体的接口起到帮助作用。代理对象接收到初始化请求,但在真正的主体真正被使用之前都不会将它传递过去。

Proxy As a Cache: 使用代理对象做缓存
使用代理合并http请求

Mediator: 中介者模式

Applications—large and small—are made up of separate objects. All these objects need a way to communicate among themselves in a manner that doesn’t hurt maintenance and your ability to safely change a part of the application without breaking the rest of it. As the application grows, you add more and more objects. Then, during refactoring, objects are removed and rearranged. When objects know too much about each other and communicate directly (call each other’s methods and change properties) this leads to undesirable tight coupling. When objects are closely coupled, it’s not easy to change one object without affecting many others. Then even the simplest change in an application is no longer trivial, and it’s virtually impossible to estimate the time a change might take.
一个应用不论大小,都是由一些彼此独立的对象组成的。所有的对象都需要 一个通讯的方式来保持可维护性,即你可以安全地修改应用的一部分而不破 坏其它 部分。随着应用的开发和维护,会有越来越多的对象。然后,在重构 代码的时候,对象可能会被移除或者被重新安排。当对象知道其它对象的太 多信息并且直接通讯 (直接调用彼此的方法或者修改属性)时,会导致我们 不愿意看到的紧耦合。当对象耦合很紧时,要修改一个对象而不影响其它的 对象是很困难的。此时甚至连一个 最简单的修改都变得不那么容易,甚至连 一个修改需要用多长时间都难以评估。

The mediator pattern alleviates this situation promoting loose coupling and helping improve maintainability (see Figure 7-7). In this pattern the independent objects (col- leagues) do not communicate directly, but through a mediator object. When one of the colleagues changes state, it notifies the mediator, and the mediator communicates the change to any other colleagues that should know about it.
中介者模式就是一个缓解此问题的办法,它通过解耦来提升代码的可维护性。在这个模式中,各个彼此合作的对象并不直接通讯,而是通过一 个 mediator(中介者)对象通讯。当一个对象改变了状态后,它就通知中介者,然后中介者再将这个改变告知给其它应该知道这个变化的对象

Observer: 观察者模式

The observer pattern is widely used in client-side JavaScript programming. All the browser events (mouseover, keypress, and so on) are examples of the pattern. Another name for it is also custom events, meaning events that you create programmatically, as opposed to the ones that the browser fires. Yet another name is subscriber/publisher pattern.
观察者模式被广泛地应用于 JavaScript 客户端编程中。所有的浏览器事件 (mouseover,keypress 等)都是使用观察者模式的例 子。这种模式的另一 个名字叫“自定义事件”,意思是这些事件是被编写出来的,和浏览器触发 的事件相对。它还有另外一个名字叫“订阅者/发布者”模式。

The main motivation behind this pattern is to promote loose coupling. Instead of one object calling another object’s method, an object subscribes to another object’s specific activity and gets notified. The subscriber is also called observer, while the object being observed is called publisher or subject. The publisher notifies (calls) all the subscribers when an important event occurs and may often pass a message in the form of an event object.
使用这个模式的最主要目的就是促进代码触解耦。在观察者模式中,一个对 象订阅另一个对象的指定活动并得到通知,而不是调用另一个对象的方法。 订阅者 也被叫作观察者,被观察的对象叫作发布者或者被观察者 。当一个 特定的事件发生的时候,发布者会通知(调用)所有的订阅者,同时还可能 以事件对象的形式传递一些消息。

文章标题:JavaScript 中的设计模式

文章作者:RayJune

时间地点:下午9:30,于又玄图书馆

原始链接:https://www.rayjune.me/2017/09/16/design-patterns-from-JavaScript-Patterns/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。