- 子类如果要扩展这个唯一的实例,客户可以不用修改代码就能使用这个扩展后的实例。
关于第二点,可以参考如下的实例,我们需要这样编码:
mySingleton.getInstance = function(){ if ( this._instance == null ) { if ( isFoo() ) { this._instance = new FooSingleton(); } else { this._instance = new BasicSingleton(); } } return this._instance;};
在这里,getInstance 有点类似于工厂方法,我们不需要去更新每个访问单例的代码。FooSingleton可以是BasicSinglton的子类,并且实现了相同的接口。
尽管单例模式有着合理的使用需求,但是通常当我们发现自己需要在javascript使用它的时候,这是一种信号,表明我们可能需要去重新评估自己的设计。
这通常表明系统中的模块要么紧耦合要么逻辑过于分散在代码库的多个部分。单例模式更难测试,因为可能有多种多样的问题出现,例如隐藏的依赖关系,很难去创建多个实例,很难清理依赖关系,等等。
4、观察者模式
观察者模式是这样一种设计模式:一个被称作被观察者的对象,维护一组被称为观察者的对象,这些对象依赖于被观察者,被观察者自动将自身的状态的任何变化通知给它们。
当一个被观察者需要将一些变化通知给观察者的时候,它将采用广播的方式,这条广播可能包含特定于这条通知的一些数据。
当特定的观察者不再需要接受来自于它所注册的被观察者的通知的时候,被观察者可以将其从所维护的组中删除。在这里提及一下设计模式现有的定义很有必要。这个定义是与所使用的语言无关的。
通过这个定义,最终我们可以更深层次地了解到设计模式如何使用以及其优势。在四人帮的《设计模式:可重用的面向对象软件的元素》这本书中,是这样定义观察者模式的:
一个或者更多的观察者对一个被观察者的状态感兴趣,将自身的这种兴趣通过附着自身的方式注册在被观察者身上。当被观察者发生变化,而这种便可也是观察者所关心的,就会产生一个通知,这个通知将会被送出去,最后将会调用每个观察者的更新方法。当观察者不在对被观察者的状态感兴趣的时候,它们只需要简单的将自身剥离即可。
我们现在可以通过实现一个观察者模式来进一步扩展我们刚才所学到的东西。这个实现包含一下组件:
-
被观察者:维护一组观察者, 提供用于增加和移除观察者的方法。
-
观察者:提供一个更新接口,用于当被观察者状态变化时,得到通知。
-
具体的被观察者:状态变化时广播通知给观察者,保持具体的观察者的信息。
-
具体的观察者:保持一个指向具体被观察者的引用,实现一个更新接口,用于观察,以便保证自身状态总是和被观察者状态一致的。
首先,让我们对被观察者可能有的一组依赖其的观察者进行建模:
function ObserverList(){ this.observerList = [];}ObserverList.prototype.Add = function( obj ){ return this.observerList.push( obj );};ObserverList.prototype.Empty = function(){ this.observerList = [];};ObserverList.prototype.Count = function(){ return this.observerList.length;};ObserverList.prototype.Get = function( index ){ if( index > -1 && index < this.observerList.length ){ return this.observerList[ index ]; }};ObserverList.prototype.Insert = function( obj, index ){ let pointer = -1; if( index === 0 ){ this.observerList.unshift( obj ); pointer = index; }else if( index === this.observerList.length ){ this.observerList.push( obj ); pointer = index; } return pointer;};ObserverList.prototype.IndexOf = function( obj, startIndex ){ let i = startIndex, pointer = -1; while( i < this.observerList.length ){ if( this.observerList[i] === obj ){ pointer = i; } i++; } return pointer;};ObserverList.prototype.RemoveAt = function( index ){ if( index === 0 ){ this.observerList.shift(); }else if( index === this.observerList.length -1 ){ this.observerList.pop(); }};// Extend an object with an extensionfunction extend( extension, obj ){ for ( let key in extension ){ obj[key] = extension[key]; }}
接着,我们对被观察者以及其增加,删除,通知在观察者列表中的观察者的能力进行建模:
function Subject(){ this.observers = new ObserverList();}Subject.prototype.AddObserver = function( observer ){ this.observers.Add( observer );}; Subject.prototype.RemoveObserver = function( observer ){ this.observers.RemoveAt( this.observers.IndexOf( observer, 0 ) );}; Subject.prototype.Notify = function( context ){ let observerCount = this.observers.Count(); for(let i=0; i < observerCount; i++){ this.observers.Get(i).Update( context ); }};
我们接着定义建立新的观察者的一个框架。这里的update 函数之后会被具体的行为覆盖。
// The Observerfunction Observer(){ this.Update = function(){ // … };}
在我们的样例应用里面,我们使用上面的观察者组件,现在我们定义:
-
一个按钮,这个按钮用于增加新的充当观察者的选择框到页面上
-
一个控制用的选择框 , 充当一个被观察者,通知其它选择框是否应该被选中
-
一个容器,用于放置新的选择框
我们接着定义具体被观察者和具体观察者,用于给页面增加新的观察者,以及实现更新接口。通过查看下面的内联的注释,搞清楚在我们样例中的这些组件是如何工作的。
html
Add New Observer checkbox
Javascript
// 我们DOM 元素的引用let controlCheckbox = document.getElementById(“mainCheckbox”), addBtn = document.getElementById( “addNewObserver” ), container = document.getElementById( “observersContainer” );// 具体的被观察者//Subject 类扩展controlCheckbox 类extend( new Subject(), controlCheckbox );//点击checkbox 将会触发对观察者的通知controlCheckbox[“onclick”] = new Function(“controlCheckbox.Notify(controlCheckbox.checked)”);addBtn[“onclick”] = AddNewObserver;// 具体的观察者function AddNewObserver(){ //建立一个新的用于增加的checkbox let check = document.createElement( “input” ); check.type = “checkbox”; // 使用Observer 类扩展checkbox extend( new Observer(), check ); // 使用定制的Update函数重载 check.Update = function( value ){ this.checked = value; }; // 增加新的观察者到我们主要的被观察者的观察者列表中 controlCheckbox.AddObserver( check ); // 将元素添加到容器的最后 container.appendChild( check );}
在这个例子里面,我们看到了如何实现和配置观察者模式,了解了被观察者,观察者,具体被观察者,具体观察者的概念。
观察者模式和发布/订阅模式的不同
观察者模式确实很有用,但是在javascript时间里面,通常我们使用一种叫做发布/订阅模式的变体来实现观察者模式。这两种模式很相似,但是也有一些值得注意的不同。
观察者模式要求想要接受相关通知的观察者必须到发起这个事件的被观察者上注册这个事件。
发布/订阅模式使用一个主题/事件频道,这个频道处于想要获取通知的订阅者和发起事件的发布者之间。
这个事件系统允许代码定义应用相关的事件,这个事件可以传递特殊的参数,参数中包含有订阅者所需要的值。这种想法是为了避免订阅者和发布者之间的依赖性。
这种和观察者模式之间的不同,使订阅者可以实现一个合适的事件处理函数,用于注册和接受由发布者广播的相关通知。
这里给出一个关于如何使用发布者/订阅者模式的例子,这个例子中完整地实现了功能强大的publish(), subscribe() 和 unsubscribe()。
// 一个非常简单的邮件处理器// 接受的消息的计数器let mailCounter = 0;// 初始化一个订阅者,这个订阅者监听名叫"inbox/newMessage" 的频道// 渲染新消息的粗略信息let subscriber1 = subscribe( “inbox/newMessage”, function( topic, data ) { // 日志记录主题,用于调试 console.log( "A new message was received: ", topic ); // 使用来自于被观察者的数据,用于给用户展示一个消息的粗略信息 $( “.messageSender” ).html( data.sender ); $( “.messagePreview” ).html( data.body );});// 这是另外一个订阅者,使用相同的数据执行不同的任务// 更细计数器,显示当前来自于发布者的新信息的数量let subscriber2 = subscribe( “inbox/newMessage”, function( topic, data ) { $(‘.newMessageCounter’).html( mailCounter++ );});publish( “inbox/newMessage”, [{ sender:“hello@google.com”, body: “Hey there! How are you doing today?”}]);// 在之后,我们可以让我们的订阅者通过下面的方式取消订阅来自于新主题的通知// unsubscribe( subscriber1, );// unsubscribe( subscriber2 );
这个例子的更广的意义是对松耦合的原则的一种推崇。不是一个对象直接调用另外一个对象的方法,而是通过订阅另外一个对象的一个特定的任务或者活动,从而在这个任务或者活动出现的时候的得到通知。
优点
观察者和发布/订阅模式鼓励人们认真考虑应用不同部分之间的关系,同时帮助我们找出这样的层,该层中包含有直接的关系,这些关系可以通过一些列的观察者和被观察者来替换掉。
这中方式可以有效地将一个应用程序切割成小块,这些小块耦合度低,从而改善代码的管理,以及用于潜在的代码复用。
使用观察者模式更深层次的动机是,当我们需要维护相关对象的一致性的时候,我们可以避免对象之间的紧密耦合。例如,一个对象可以通知另外一个对象,而不需要知道这个对象的信息。
两种模式下,观察者和被观察者之间都可以存在动态关系。这提供很好的灵活性,而当我们的应用中不同的部分之间紧密耦合的时候,是很难实现这种灵活性的。
尽管这些模式并不是万能的灵丹妙药,这些模式仍然是作为最好的设计松耦合系统的工具之一,因此在任何的JavaScript 开发者的工具箱里面,都应该有这样一个重要的工具。
缺点
事实上,这些模式的一些问题实际上正是来自于它们所带来的一些好处。在发布/订阅模式中,将发布者共订阅者上解耦,将会在一些情况下,导致很难确保我们应用中的特定部分按照我们预期的那样正常工作。
例如,发布者可以假设有一个或者多个订阅者正在监听它们。比如我们基于这样的假设,在某些应用处理过程中来记录或者输出错误日志。如果订阅者执行日志功能崩溃了(或者因为某些原因不能正常工作),因为系统本身的解耦本质,发布者没有办法感知到这些事情。
另外一个这种模式的缺点是,订阅者对彼此之间存在没有感知,对切换发布者的代价无从得知。因为订阅者和发布者之间的动态关系,更新依赖也很能去追踪。
让我们看一下最小的一个版本的发布/订阅模式实现。这个实现展示了发布,订阅的核心概念,以及如何取消订阅。
let pubsub = {};(function(q) { let topics = {}, subUid = -1; q.publish = function( topic, args ) { if ( !topics[topic] ) { return false; } let subscribers = topics[topic], len = subscribers ? subscribers.length : 0; while (len–) { subscribers[len].func( topic, args ); } return this; }; q.subscribe = function( topic, func ) { if (!topics[topic]) { topics[topic] = []; } let token = ( ++subUid ).toString(); topics[topic].push({ token: token, func: func }); return token; }; q.unsubscribe = function( token ) { for ( let m in topics ) { if ( topics[m] ) { for ( let i = 0, j = topics[m].length; i < j; i++ ) { if ( topics[m][i].token === token) { topics[m].splice( i, 1 ); return token; } } } } return this; };}( pubsub ));
我们现在可以使用发布实例和订阅感兴趣的事件,例如:
let messageLogger = function ( topics, data ) { console.log( "Logging: " + topics + ": " + data );};let subscription = pubsub.subscribe( “inbox/newMessage”, messageLogger );pubsub.publish( “inbox/newMessage”, “hello world!” );// orpubsub.publish( “inbox/newMessage”, [“test”, “a”, “b”, “c”] );// orpubsub.publish( “inbox/newMessage”, { sender: “hello@google.com”, body: “Hey again!”});// We cab also unsubscribe if we no longer wish for our subscribers// to be notified// pubsub.unsubscribe( subscription );pubsub.publish( “inbox/newMessage”, “Hello! are you still there?” );
观察者模式在应用设计中,解耦一系列不同的场景上非常有用,如果你没有用过它,我推荐你尝试一下今天提到的之前写到的某个实现。这个模式是一个易于学习的模式,同时也是一个威力巨大的模式。
5、中介者模式
如果系统组件之间存在大量的直接关系,就可能是时候,使用一个中心的控制点,来让不同的组件通过它来通信。中介者通过将组件之间显式的直接的引用替换成通过中心点来交互的方式,来做到松耦合。这样可以帮助我们解耦,和改善组件的重用性。
在现实世界中,类似的系统就是,飞行控制系统。一个航站塔(中介者)处理哪个飞机可以起飞,哪个可以着陆,因为所有的通信(监听的通知或者广播的通知)都是飞机和控制塔之间进行的,而不是飞机和飞机之间进行的。一个中央集权的控制中心是这个系统成功的关键,也正是中介者在软件设计领域中所扮演的角色。
5.1基础的实现
中间人模式的一种简单的实现可以在下面找到,publish()和subscribe()方法都被暴露出来使用:
let mediator = (function(){ let topics = {}; let subscribe = function( topic, fn ){ if ( !topics[topic] ){ topics[topic] = []; } topics[topic].push( { context: this, callback: fn } ); return this; }; let publish = function( topic ){ let args; if ( !topics[topic] ){ return false; } args = Array.prototype.slice.call( arguments, 1 ); for ( let i = 0, l = topics[topic].length; i < l; i++ ) { let subscription = topics[topic][i]; subscription.callback.apply( subscription.context, args ); } return this; }; return { publish: publish, subscribe: subscribe, installTo: function( obj ){ obj.subscribe = subscribe; obj.publish = publish; } };}());
优点 & 缺点
中间人模式最大的好处就是,它节约了对象或者组件之间的通信信道,这些对象或者组件存在于从多对多到多对一的系统之中。由于解耦合水平的因素,添加新的发布或者订阅者是相对容易的。
也许使用这个模式最大的缺点是它可以引入一个单点故障。在模块之间放置一个中间人也可能会造成性能损失,因为它们经常是间接地的进行通信的。由于松耦合的特性,仅仅盯着广播很难去确认系统是如何做出反应的。
这就是说,提醒我们自己解耦合的系统拥有许多其它的好处,是很有用的——如果我们的模块互相之间直接的进行通信,对于模块的改变(例如:另一个模块抛出了异常)可以很容易的对我们系统的其它部分产生多米诺连锁效应。这个问题在解耦合的系统中很少需要被考虑到。
在一天结束的时候,紧耦合会导致各种头痛,这仅仅只是另外一种可选的解决方案,但是如果得到正确实现的话也能够工作得很好。
6、原型模式
原型模式是指通过克隆的方式基于一个现有对象的模板创建对象的模式。
我们能够将原型模式认作是基于原型的继承中,我们创建作为其它对象原型的对象.原型对象自身被当做构造器创建的每一个对象的蓝本高效的使用着.如果构造器函数使用的原型包含例如叫做name的属性,那么每一个通过同一个构造器创建的对象都将拥有这个相同的属性。
我们可以在下面的示例中看到对这个的展示:
let myCar = { name: “Ford Escort”, drive: function () { console.log( “Weeee. I’m driving!” ); }, panic: function () { console.log( “Wait. How do you stop this thing?” ); }};let yourCar = Object.create( myCar );console.log( yourCar.name );// Ford Escort
Object.create也允许我们简单的继承先进的概念,比如对象能够直接继承自其它对象,这种不同的继承.我们早先也看到Object.create允许我们使用 供应的第二个参数来初始化对象属性。例如:
let vehicle = { getModel: function () { console.log( “The model of this vehicle is…” + this.model ); }};let car = Object.create(vehicle, { “id”: { value: “1”, // writable:false, configurable:false by default enumerable: true }, “model”: { value: “Ford”, enumerable: true }});
这里的属性可以被Object.create的第二个参数来初始化,使用一种类似于Object.defineProperties和Object.defineProperties方法所使用语法的对象字面值。
在枚举对象的属性,和在一个hasOwnProperty()检查中封装循环的内容时,原型关系会造成麻烦,这一事实是值得我们关注的。
如果我们希望在不直接使用Object.create的前提下实现原型模式,我们可以像下面这样,按照上面的示例,模拟这一模式:
let vehiclePrototype = { init: function ( carModel ) { this.model = carModel; }, getModel: function () { console.log( “The model of this vehicle is…” + this.model); }};function vehicle( model ) { function F() {}; F.prototype = vehiclePrototype; let f = new F(); f.init( model ); return f;}let car = vehicle( “Ford Escort” );car.getModel();
注意:这种可选的方式不允许用户使用相同的方式定义只读的属性(因为如果不小心的话vehicle原型可能会被改变)。
原型模式的最后一种可选实现可以像下面这样:
let beget = (function () { function F() {} return function ( proto ) { F.prototype = proto; return new F(); };})();
7、命令模式
命名模式的目标是将方法的调用,请求或者操作封装到一个单独的对象中,给我们酌情执行同时参数化和传递方法调用的能力.另外,它使得我们能将对象从实现了行为的对象对这些行为的调用进行解耦,为我们带来了换出具体的对象这一更深程度的整体灵活性。
具体类是对基于类的编程语言的最好解释,并且同抽象类的理念联系紧密。抽象类定义了一个接口,但并不需要提供对它的所有成员函数的实现。它扮演着驱动其它类的基类角色.被驱动类实现了缺失的函数而被称为具体类.。命令模式背后的一般理念是为我们提供了从任何执行中的命令中分离出发出命令的责任,取而代之将这一责任委托给其它的对象。
实现明智简单的命令对象,将一个行为和对象对调用这个行为的需求都绑定到了一起.它们始终都包含一个执行操作(比如run()或者execute()).所有带有相同接口的命令对象能够被简单地根据需要调换,这被认为是命令模式的更大的好处之一。
为了展示命令模式,我们创建一个简单的汽车购买服务:
(function(){ let CarManager = { requestInfo: function( model, id ){ return “The information for " + model + " with ID " + id + " is foobar”; }, buyVehicle: function( model, id ){ return "You have successfully purchased Item " + id + ", a " + model; }, arrangeViewing: function( model, id ){ return "You have successfully booked a viewing of " + model + " ( " + id + " ) "; } };})();
看一看上面的这段代码,它也许是通过直接访问对象来琐碎的调用我们CarManager的方法。在技术上我们也许都会都会对这个没有任何失误达成谅解.它是完全有效的Javascript然而也会有情况不利的情况。
例如,想象如果CarManager的核心API会发生改变的这种情况.这可能需要所有直接访问这些方法的对象也跟着被修改.这可以被看成是一种耦合,明显违背了OOP方法学尽量实现松耦合的理念.取而代之,我们可以通过更深入的抽象这些API来解决这个问题。
现在让我们来扩展我们的CarManager,以便我们这个命令模式的应用程序得到接下来的这种效果:接受任何可以在CarManager对象上面执行的方法,传送任何可以被使用到的数据,如Car模型和ID。
这里是我们希望能够实现的样子:
CarManager.execute( “buyVehicle”, “Ford Escort”, “453543” );
按照这种结构,我们现在应该像下面这样,添加一个对于"CarManager.execute()"方法的定义:
CarManager.execute = function ( name ) { return CarManager[name] && CarManager[name].apply( CarManager, [].slice.call(arguments, 1) );};
最终我们的调用如下所示:
CarManager.execute( “arrangeViewing”, “Ferrari”, “14523” );CarManager.execute( “requestInfo”, “Ford Mondeo”, “54323” );CarManager.execute( “requestInfo”, “Ford Escort”, “34232” );CarManager.execute( “buyVehicle”, “Ford Escort”, “34232” );
8、外观模式
当我们提出一个门面,我们要向这个世界展现的是一个外观,这一外观可能藏匿着一种非常与众不同的真实。这就是我们即将要回顾的模式背后的灵感——门面模式。
这一模式提供了面向一种更大型的代码体提供了一个的更高级别的舒适的接口,隐藏了其真正的潜在复杂性。
把这一模式想象成要是呈现给开发者简化的API,一些总是会提升使用性能的东西。
为了在我们所学的基础上进行构建,门面模式同时需要简化一个类的接口,和把类同使用它的代码解耦。这给予了我们使用一种方式直接同子系统交互的能力,这一方式有时候会比直接访问子系统更加不容易出错。
门面的优势包括易用,还有常常实现起这个模式来只是一小段路,不费力。
让我们通过实践来看看这个模式。这是一个没有经过优化的代码示例,但是这里我们使用了一个门面来简化跨浏览器事件监听的接口。我们创建了一个公共的方法来实现,此方法能够被用在检查特性的存在的代码中,以便这段代码能够提供一种安全和跨浏览器兼容方案。
let addMyEvent = function( el,ev,fn ){ if( el.addEventListener ){ el.addEventListener( ev,fn, false ); }else if(el.attachEvent){ el.attachEvent( “on” + ev, fn ); }else{ el[“on” + ev] = fn; }};
门面不仅仅只被用在它们自己身上,它们也能够被用来同其它的模式诸如模块模式进行集成。如我们在下面所看到的,我们模块模式的实体包含许多被定义为私有的方法。门面则被用来提供访问这些方法的更加简单的API:
let module = (function() { let _private = { i:5, get : function() { console.log( “current value:” + this.i); }, set : function( val ) { this.i = val; }, run : function() { console.log( “running” ); }, jump: function(){ console.log( “jumping” ); } }; return { facade : function( args ) { _private.set(args.val); _private.get(); if ( args.run ) { _private.run(); } } };}());module.facade( {run: true, val:10} );// “current value: 10” and “running”
在这个示例中,调用module.facade()将会触发一堆模块中的私有方法。但再一次,用户并不需要关心这些。我们已经使得对用户而言不需要担心实现级别的细节就能消受一种特性。
9、工厂模式
工厂模式是另外一种关注对象创建概念的创建模式。它的领域中同其它模式的不同之处在于它并没有明确要求我们使用一个构造器。
取而代之,一个工厂能提供一个创建对象的公共接口,我们可以在其中指定我们希望被创建的工厂对象的类型。
下面我们通过使用构造器模式逻辑来定义汽车。这个例子展示了Vehicle 工厂可以使用工厂模式来实现。
function Car( options ) { this.doors = options.doors || 4; this.state = options.state || “brand new”; this.color = options.color || “silver”;
}function Truck( options){ this.state = options.state || “used”; this.wheelSize = options.wheelSize || “large”; this.color = options.color || “blue”;}function VehicleFactory() {}VehicleFactory.prototype.vehicleClass = Car;VehicleFactory.prototype.createVehicle = function ( options ) { if( options.vehicleType === “car” ){ this.vehicleClass = Car; }else{ this.vehicleClass = Truck; } return new this.vehicleClass( options );
};let carFactory = new VehicleFactory();let car = carFactory.createVehicle( { vehicleType: “car”, color: “yellow”, doors: 6 } );console.log( car );
何时使用工厂模式
当被应用到下面的场景中时,工厂模式特别有用:
-
当我们的对象或者组件设置涉及到高程度级别的复杂度时。
-
当我们需要根据我们所在的环境方便的生成不同对象的实体时。
-
当我们在许多共享同一个属性的许多小型对象或组件上工作时。
-
当带有其它仅仅需要满足一种API约定(又名鸭式类型)的对象的组合对象工作时.这对于解耦来说是有用的。
何时不要去使用工厂模式
当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性.除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。
由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。
抽象工厂
了解抽象工厂模式也是非常实用的,它的目标是以一个通用的目标将一组独立的工厂进行封装.它将一堆对象的实现细节从它们的一般用例中分离。
抽象工厂应该被用在一种必须从其创建或生成对象的方式处独立,或者需要同多种类型的对象一起工作,这样的系统中。
简单且容易理解的例子就是一个发动机工厂,它定义了获取或者注册发动机类型的方式。抽象工厂会被命名为AbstractVehicleFactory。抽象工厂将允许像"car"或者"truck"的发动机类型的定义,并且构造工厂将仅实现满足发动机合同的类.(例如:Vehicle.prototype.driven和Vehicle.prototype.breakDown)。
let AbstractVehicleFactory = (function () { let types = {}; return { getVehicle: function ( type, customizations ) { var Vehicle = types[type]; return (Vehicle ? new Vehicle(customizations) : null); }, registerVehicle: function ( type, Vehicle ) { let proto = Vehicle.prototype; // only register classes that fulfill the vehicle contract if ( proto.drive && proto.breakDown ) { types[type] = Vehicle; } return AbstractVehicleFactory; } };})();
AbstractVehicleFactory.registerVehicle( “car”, Car );AbstractVehicleFactory.registerVehicle( “truck”, Truck );
let car = AbstractVehicleFactory.getVehicle( “car” , { color: “lime green”, state: “like new” } );
let truck = AbstractVehicleFactory.getVehicle( “truck” , { wheelSize: “medium”, color: “neon yellow” } );
10、Mixin 模式
mixin模式指一些提供能够被一个或者一组子类简单继承功能的类,意在重用其功能。
子类划分
子类划分是一个参考了为一个新对象继承来自一个基类或者超类对象的属性的术语.在传统的面向对象编程中,类B能够从另外一个类A处扩展。这里我们将A看做是超类,而将B看做是A的子类。如此,所有B的实体都从A处继承了其A的方法,然而B仍然能够定义它自己的方法,包括那些重载的原本在A中的定义的方法。
B是否应该调用已经被重载的A中的方法,我们将这个引述为方法链.B是否应该调用A(超类)的构造器,我们将这称为构造器链。
为了演示子类划分,首先我们需要一个能够创建自身新实体的基对象。
let Person = function( firstName , lastName ){ this.firstName = firstName; this.lastName = lastName; this.gender = “male”;};
接下来,我们将制定一个新的类(对象),它是一个现有的Person对象的子类.让我们想象我们想要加入一个不同属性用来分辨一个Person和一个继承了Person"超类"属性的Superhero.由于超级英雄分享了一般人类许多共有的特征(例如:name,gender),因此这应该很有希望充分展示出子类划分是如何工作的。
let clark = new Person( “Clark” , “Kent” );let Superhero = function( firstName, lastName , powers ){ Person.call( this, firstName, lastName ); this.powers = powers;};SuperHero.prototype = Object.create( Person.prototype );let superman = new Superhero( “Clark” ,“Kent” , [“flight”,“heat-vision”] );console.log( superman );
Superhero构造器创建了一个自Peroson下降的对象。这种类型的对象拥有链中位于它之上的对象的属性,而且如果我们在Person对象中设置了默认的值,Superhero能够使用特定于它的对象的值覆盖任何继承的值。
Mixin(织入目标类)
在Javascript中,我们会将从Mixin继承看作是通过扩展收集功能的一种途径.我们定义的每一个新的对象都有一个原型,从其中它可以继承更多的属性.原型可以从其他对象继承而来,但是更重要的是,能够为任意数量的对象定义属性.我们可以利用这一事实来促进功能重用。
Mix允许对象以最小量的复杂性从它们那里借用(或者说继承)功能.作为一种利用Javascript对象原型工作得很好的模式,它为我们提供了从不止一个Mix处分享功能的相当灵活,但比多继承有效得多得多的方式。
它们可以被看做是其属性和方法可以很容易的在其它大量对象原型共享的对象.想象一下我们定义了一个在一个标准对象字面量中含有实用功能的Mixin,如下所示:
let myMixins = {
moveUp: function(){ console.log( “move up” ); },
moveDown: function(){ console.log( “move down” ); },
stop: function(){ console.log( “stop! in the name of love!” ); }
};
然后我们可以方便的扩展现有构造器功能的原型,使其包含这种使用一个 如下面的score.js_.extends()方法辅助器的行为:
function carAnimator(){ this.moveLeft = function(){ console.log( “move left” ); };}function personAnimator(){ this.moveRandomly = function(){ /…/ };}.extend( carAnimator.prototype, myMixins );.extend( personAnimator.prototype, myMixins );let myAnimator = new carAnimator();myAnimator.moveLeft();myAnimator.moveDown();myAnimator.stop();
如我们所见,这允许我们将通用的行为轻易的"混"入相当普通对象构造器中。
在接下来的示例中,我们有两个构造器:一个Car和一个Mixin.我们将要做的是静Car参数化(另外一种说法是扩展),以便它能够继承Mixin中的特定方法,名叫driveForwar()和driveBackward().这一次我们不会使用Underscore.js。
取而代之,这个示例将演示如何将一个构造器参数化,以便在无需重复每一个构造器函数过程的前提下包含其功能。
let Car = function ( settings ) { this.model = settings.model || “no model provided”; this.color = settings.color || “no colour provided”;};// Mixinlet Mixin = function () {};Mixin.prototype = { driveForward: function () { console.log( “drive forward” ); }, driveBackward: function () { console.log( “drive backward” ); }, driveSideways: function () { console.log( “drive sideways” ); }};function augment( receivingClass, givingClass ) { if ( arguments[2] ) { for ( var i = 2, len = arguments.length; i < len; i++ ) { receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]]; } }else { for ( let methodName in givingClass.prototype ) { if ( !Object.hasOwnProperty(receivingClass.prototype, methodName) ) { receivingClass.prototype[methodName] = givingClass.prototype[methodName]; } } }}augment( Car, Mixin, “driveForward”, “driveBackward” );let myCar = new Car({ model: “Ford Escort”, color: “blue”});myCar.driveForward();myCar.driveBackward();
augment( Car, Mixin );let mySportsCar = new Car({ model: “Porsche”, color: “red”});mySportsCar.driveSideways();
优点 & 缺点
Mixin支持在一个系统中降解功能的重复性,增加功能的重用性.在一些应用程序也许需要在所有的对象实体共享行为的地方,我们能够通过在一个Mixin中维护这个共享的功能,来很容易的避免任何重复,而因此专注于只实现我们系统中真正彼此不同的功能。
也就是说,对Mixin的副作用是值得商榷的.一些开发者感觉将功能注入到对象的原型中是一个坏点子,因为它会同时导致原型污染和一定程度上的对我们原有功能的不确定性.在大型的系统中,很可能是有这种情况的。
但是,强大的文档对最大限度的减少对待功能中的混入源的迷惑是有帮助的,而且对于每一种模式而言,如果在实现过程中小心行事,我们应该是没多大问题的。
11、装饰器模式
装饰器是旨在提升重用性能的一种结构性设计模式。同Mixin类似,它可以被看作是应用子类划分的另外一种有价值的可选方案。
典型的装饰器提供了向一个系统中现有的类动态添加行为的能力。其创意是装饰本身并不关心类的基础功能,而只是将它自身拷贝到超类之中。
装饰器模式并不去深入依赖于对象是如何创建的,而是专注于扩展它们的功能这一问题上。不同于只依赖于原型继承,我们在一个简单的基础对象上面逐步添加能够提供附加功能的装饰对象。它的想法是,不同于子类划分,我们向一个基础对象添加(装饰)属性或者方法,因此它会是更加轻巧的。
向Javascript中的对象添加新的属性是一个非常直接了当的过程,因此将这一特定牢记于心,一个非常简单的装饰器可以实现如下:
示例1:带有新功能的装饰构造器
function vehicle( vehicleType ){ this.vehicleType = vehicleType || “car”; this.model = “default”; this.license = “00000-000”;}let testInstance = new vehicle( “car” );console.log( testInstance );// vehicle: car, model:default, license: 00000-000
let truck = new vehicle( “truck” );truck.setModel = function( modelName ){ this.model = modelName;};truck.setColor = function( color ){ this.color = color;};truck.setModel( “CAT” );truck.setColor( “blue” );console.log( truck );// vehicle:truck, model:CAT, color: blue
let secondInstance = new vehicle( “car” );console.log( secondInstance );// vehicle: car, model:default, license: 00000-000
示例2:带有多个装饰器的装饰对象
function MacBook() { this.cost = function () { return 997; }; this.screenSize = function () { return 11.6; };}function Memory( macbook ) { let v = macbook.cost(); macbook.cost = function() { return v + 75; };}function Engraving( macbook ){ let v = macbook.cost(); macbook.cost = function(){ return v + 200; };}function Insurance( macbook ){ let v = macbook.cost(); macbook.cost = function(){ return v + 250; };}
let mb = new MacBook();Memory( mb );Engraving( mb );Insurance( mb );console.log( mb.cost() );// 1522console.log( mb.screenSize() );// 11.6
在上面的示例中,我们的装饰器重载了超类对象MacBook()的 object.cost()函数,使其返回的Macbook的当前价格加上了被定制后升级的价格。
这被看做是对原来的Macbook对象构造器方法的装饰,它并没有将其重写(例如,screenSize()),我们所定义的Macbook的其它属性也保持不变,完好无缺。
优点 & 缺点
因为它可以被透明的使用,并且也相当的灵活,因此开发者都挺乐意去使用这个模式——如我们所见,对象可以用新的行为封装或者“装饰”起来,而后继续使用,并不用去担心基础的对象被改变。在一个更加广泛的范围内,这一模式也避免了我们去依赖大量子类来实现同样的效果。
然而在实现这个模式时,也存在我们应该意识到的缺点。如果穷于管理,它也会由于引入了许多微小但是相似的对象到我们的命名空间中,从而显著的使得我们的应用程序架构变得复杂起来。这里所担忧的是,除了渐渐变得难于管理,其他不能熟练使用这个模式的开发者也可能会有一段要掌握它被使用的理由的艰难时期。
足够的注释或者对模式的研究,对此应该有助益,而只要我们对在我们的应程序中的多大范围内使用这一模式有所掌控的话,我们就能让两方面都得到改善。
12、亨元模式
享元模式是一个优化重复、缓慢和低效数据共享代码的经典结构化解决方案。它的目标是以相关对象尽可能多的共享数据,来减少应用程序中内存的使用(例如:应用程序的配置、状态等)。
此模式最先由Paul Calder 和 Mark Linton在1990提出,并用拳击等级中少于112磅体重的等级名称来命名。享元(“Flyweight”英语中的轻量级)的名称本身是从以帮以助我们完成减少重量(内存标记)为目标的重量等级推导出的。
实际应用中,轻量级的数据共享采集被多个对象使用的相似对象或数据结构,并将这些数据放置于单个的扩展对象中。我们可以把它传递给依靠这些数据的对象,而不是在他们每个上面都存储一次。
使用享元
有两种方法来使用享元。第一种是数据层,基于存储在内存中的大量相同对象的数据共享的概念。第二种是DOM层,享元模式被作为事件管理中心,以避免将事件处理程序关联到我们需要相同行为父容器的所有子节点上。享元模式通常被更多的用于数据层,我们先来看看它。
享元和数据共享
对于这个应用程序而言,围绕经典的享元模式有更多需要我们意识到的概念。享元模式中有一个两种状态的概念——内在和外在。内在信息可能会被我们的对象中的内部方法所需要,它们绝对不可以作为功能被带出。外在信息则可以被移除或者放在外部存储。
带有相同内在数据的对象可以被一个单独的共享对象所代替,它通过一个工厂方法被创建出来。这允许我们去显著降低隐式数据的存储数量。
个中的好处是我们能够留心于已经被初始化的对象,让只有不同于我们已经拥有的对象的内在状态时,新的拷贝才会被创建。
我们使用一个管理器来处理外在状态。如何实现可以有所不同,但针对此的一种方法就是让管理器对象包含一个存储外在状态以及它们所属的享元对象的中心数据库。
经典的享元实现
近几年享元模式已经在Javascript中得到了深入的应用,我们会用到的许多实现方式其灵感来自于Java和C++的世界。
我们来看下来自维基百科的针对享元模式的 Java 示例的 Javascript 实现。
在这个实现中我们将要使用如下所列的三种类型的享元组件:
-
享元对应的是一个接口,通过此接口能够接受和控制外在状态。
-
构造享元来实际的实际的实现接口,并存储内在状态。构造享元须是能够被共享的,并且具有操作外在状态的能力。
-
享元工厂负责管理享元对象,并且也创建它们。它确保了我们的享元对象是共享的,并且可以对其作为一组对象进行管理,这一组对象可以在我们需要的时候查询其中的单个实体。如果一个对象已经在一个组里面创建好了,那它就会返回该对象,否则它会在对象池中新创建一个,并且返回之。
这些对应于我们实现中的如下定义:
-
CoffeeOrder:享元
-
CoffeeFlavor:构造享元
-
CoffeeOrderContext:辅助器
-
CoffeeFlavorFactory:享元工厂
-
testFlyweight:对我们享元的使用
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
最后
前端CSS面试题文档,JavaScript面试题文档,Vue面试题文档,大厂面试题文档
CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
器
-
CoffeeFlavorFactory:享元工厂
-
testFlyweight:对我们享元的使用
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-kR6rezrf-1711910524732)]
[外链图片转存中…(img-xtjxuUTj-1711910524733)]
[外链图片转存中…(img-Fyc4AWT5-1711910524734)]
[外链图片转存中…(img-eNgh9sjk-1711910524734)]
[外链图片转存中…(img-rVMEfmwj-1711910524735)]
[外链图片转存中…(img-uwaJA5GC-1711910524736)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-80UhzMfz-1711910524736)]
最后
前端CSS面试题文档,JavaScript面试题文档,Vue面试题文档,大厂面试题文档
CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
[外链图片转存中…(img-4R8I3un5-1711910524737)]
[外链图片转存中…(img-sWdxFGr2-1711910524737)]