TokenYangForever / NewProject

0 stars 0 forks source link

四个你应该知道的JS设计模式 #36

Closed TokenYangForever closed 6 years ago

TokenYangForever commented 6 years ago

Exposer.first(); // Output: This is a method I want to expose! Exposer.second(); // Output: Inside a private method! Exposer.methodToExpose; // undefined

* 虽然代码看起来更加整洁,一个明显的缺点就是无法直接引用私有方法。这样在进行单元测试时会遇到些挑战,同样的,公有行为也是不可重写的。
#### 原型设计模式(Prototype Design Pattern)
* 可能有的js开发者既搞不清楚原型继承和关键字**prototype**,也不会在代码中应用到原型。原型设计模式依赖于JavaScript的原型链继承。大多数情况下,原型主要用来创建对象。
* 对象创建的过程是对传递过去的原对象进行浅克隆。原型设计模式的其中一个使用实例就是:执行一个大量的数据操作来创建一个供应用其他部分使用的对象。如果另一个程序要使用这个对象,和执行大量的数据操作相比,直接克隆之前创建好的对象就有利的多了。
![image](https://user-images.githubusercontent.com/25222844/38015624-e50514aa-329e-11e8-8711-cb054725e04a.png)
* 上图介绍了一个原型接口如何用来克隆的具体应用
* 克隆一个对象时,必须要有一个构造函数来实例化第一个对象。然后,再通过使用prototype关键字来把变量和方法绑定到对象结构上去。来看下面这个基础的例子:

var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; }

TeslaModelS.prototype.go = function() { // Rotate wheels }

TeslaModelS.prototype.stop = function() { // Apply brake pads }

* 构造函数用来产生一个单独的TeslaModelS对象,在创建一个新的TeslaModelS对象时,将会保持构造函数里声明的变量。另外,我们通过关键字`prototype`声明时,方法`go`和`stop`也会保留给对象实例。使用下面的代码也是同一个意思:

var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; }

TeslaModelS.prototype = { go: function() { // Rotate wheels }, stop: function() { // Apply brake pads } }

#### 揭示型原型模式
* 和模块模式类似,原型模式也有揭示型的变种。揭示型原型模式提供一个对私有成员和公有成员的封装,同时返回一个字面量对象
* 因为我们是要返回一个对象,所以我们要用一个函数来表达原型对象。通过对上述例子的扩展,在当前原型内部,可以选择想要暴露的属性或方法:

var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; }

TeslaModelS.prototype = function() {

var go = function() { // Rotate wheels };

var stop = function() { // Apply brake pads };

return { pressBrakePedal: stop, pressGasPedal: go }

}();

* 注意`go`和`stop`方法是被保护了的,因为他们不在所返回的对象所在的作用域内。因为js本来就支持原型继承,所以也不需要再重写底部特性了。
#### 观察者模式
* 很多时候当应用中的一部分代码变化时,要求其他部分也进行更新。在AngularJS中,当`$scope`对象更新时,就会触发一个事件去通知其他组件。观察者模式包含的内容就是:如果一个对象发生改变了,那么它就会**广播(broadcasts)**通知所有的依赖于此的对象。
* 另一个最简单的例子就是MVC结构(比如backbone框架);model改变时,view视图层就会更新。有个好处就在于解耦view和model,使代码依赖性降低。
![image](https://user-images.githubusercontent.com/25222844/38071879-6aa0a996-3356-11e8-9eec-d60a938638cb.png)
* 从上面的图表可以看出,必须要的对象是`subject`, `observer`, 和 `concrete`对象。subject包含了各个observer观察者实例的索引,从而能通知任何发生的改变。Observer是一个抽象类,允许观察者实例执行通知方法。
* 来看下用AngularJS通过事件管理来实现观察者模式的例子。

// Controller 1 $scope.$on('nameChanged', function(event, args) { $scope.name = args.name; }); ... // Controller 2 $scope.userNameChanged = function(name) { $scope.$emit('nameChanged', {name: name}); };

* 使用观察者模式时,区分subject和依赖对象是很重要的。
* 虽然观察者模式能提供很多好处,但还是有缺陷,比如随着观察者对象数量的增加,程序的性能会明显下降。一个臭名昭著的观察者就是**watchers**。在AngularJS中,我们可以**watch**监控变量、函数和对象。当作用域中的一个对象改变时,**$$digest**循环执行同时通知每一个相关观察者。
* 我们可以在JavaScript代码中创建观察者(Observers)和目标对象(Subjects)。我们来看下是如何实现的:

var Subject = function() { this.observers = [];

return { subscribeObserver: function(observer) { this.observers.push(observer); }, unsubscribeObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers.splice(index, 1); } }, notifyObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers[index].notify(index); } }, notifyAllObservers: function() { for(var i = 0; i < this.observers.length; i++){ this.observers[i].notify(i); }; } }; };

var Observer = function() { return { notify: function(index) { console.log("Observer " + index + " is notified!"); } } }

var subject = new Subject();

var observer1 = new Observer(); var observer2 = new Observer(); var observer3 = new Observer(); var observer4 = new Observer();

subject.subscribeObserver(observer1); subject.subscribeObserver(observer2); subject.subscribeObserver(observer3); subject.subscribeObserver(observer4);

subject.notifyObserver(observer2); // Observer 2 is notified!

subject.notifyAllObservers(); // Observer 1 is notified! // Observer 2 is notified! // Observer 3 is notified! // Observer 4 is notified!

#### 发布/订阅(Publish/Subscribe)
* 发布/订阅模式:在希望接收通知的对象(订阅者)和触发事件的对象(发布者)之间,搭建一个主题/事件的通道。这个事件系统运行代码定义具体的应用事件,来传递订阅者需要的参数值。这里的思想是在于,避免发布者和订阅者相依赖(即他们是通过事件来联系的,并没有直接的依赖)。
* 这和观察者模式的区别在于:订阅者执行一个适当的事件处理函数来进行注册,然后接收发布者的广播通知。
* 许多开发者将发布/订阅模式与观察者相结合,尽管两者可能有些区别。在发布/订阅模式中的订阅者,通过一些媒介信息得到通知,然而观察者是通过执行一个处理函数来得到通知。
#### 单例模式(Singleton)
* 一个单例(Singleton)类,只允许存在唯一一个实例。单例模式会禁止客户端创建多个实例对象,当第一个实例被创建后,后面就直接返回这一个实例本身,而不会去创建另一个实例。
* 在你没使用过单例模式时,是很难去发现单例的用例。举个例子,比如办公室的打印机,办公室里有十个人,他们分别有自己的电脑,然后共用一台打印机。这台打印机就相当于单例类的唯一实例,然后共享打印机的同时,共享着相同的资源。

var printer = (function () { var printerInstance; function create () { function print() { // underlying printer mechanics } function turnOn() { // warm up // check for paper } return { // public + private states and behaviors print: print, turnOn: turnOn }; } return { getInstance: function() { if(!printerInstance) { printerInstance = create(); } return printerInstance; } }; function Singleton () { if(!printerInstance) { printerInstance = intialize(); } }; })();

* create方法应该是私有的,因为我们不想客户端能够访问它。但是getInstance方法是公共的,每个办公室的职员想要使用打印机时,就会去调用getInstance方法来得到唯一实例,第一次创建成功后,后面再次调用就直接返回之前已经创建好的实例对象:

var officePrinter = printer.getInstance();


* 在AngularJS中,单例模式是很常见的,比如作为services, factories, 和 providers。因为他们在保持状态、提供可访问资源,如果创建两个实例就违背唯一共享services/factories/providers的设计理念了。
* 在多线程的应用中,当多个线程同时访问一个资源时就会产生竞态条件(race condition)。单例模式就很容易受到竞态条件的影响,比如一开始并没有创建实例对象,两个进程同时访问,然后同时创建了一个实例,就会产生了两个实例对象,而并不是先创建一个,然后再返回之前创建好的。这就违背了单例模式的本意了。所以开发者在多线程应用中使用单例时,要了解同步性。(然而js只是单线程的~~~)
#### 结论
* 设计模式在大型应用中经常会用到,理解其中一两种也是很有好处的,伴随着实践效果更佳。
* 在构造一个应用之前,你应该彻底思考清楚,一个模块应该如何和其他模块相联系。回顾熟悉上面四个设计模式之后,你应该能够在项目中识别出他们,并广泛的应用。