JavaScript 12种设计模式汇总

我们的方法是有名字空间限制的,因此在我们代码的测试部分,我们需要给所有函数调用前面加上模块的名字(例如"testModule")。

当使用模块模式时,我们会发现通过使用简单的模板,对于开始使用模块模式非常有用。下面是一个模板包含了命名空间,公共变量和私有变量。

let myNamespace = (function () { let myPrivateVar, myPrivateMethod; myPrivateVar = 0; myPrivateMethod = function( foo ) { console.log( foo ); }; return { myPublicVar: “foo”, myPublicFunction: function( bar ) { myPrivateVar++; myPrivateMethod( bar ); } };})();

看一下另外一个例子,下面我们看到一个使用这种模式实现的购物车。这个模块完全自包含在一个叫做basketModule 全局变量中。

模块中的购物车数组是私有的,应用的其它部分不能直接读取。只存在与模块的闭包中,因此只有可以访问其域的方法可以访问这个变量。

let basketModule = (function () { let basket = []; function doSomethingPrivate() { //… } function doSomethingElsePrivate() { //… } return { addItem: function( values ) { basket.push(values); }, getItemCount: function () { return basket.length; }, doSomething: doSomethingPrivate, getTotal: function () { let q = this.getItemCount(), p = 0; while (q–) { p += basket[q].price; } return p; } };}());

上面的方法都处于basketModule 的名字空间中。

请注意在上面的basket模块中 域函数是如何在我们所有的函数中被封装起来的,以及我们如何立即调用这个域函数,并且将返回值保存下来。这种方式有以下的优势:

  • 可以创建只能被我们模块访问的私有函数。这些函数没有暴露出来(只有一些API是暴露出来的),它们被认为是完全私有的。

  • 当我们在一个调试器中,需要发现哪个函数抛出异常的时候,可以很容易的看到调用栈,因为这些函数是正常声明的并且是命名的函数。

  • 这种模式同样可以让我们在不同的情况下返回不同的函数。我见过有开发者使用这种技巧用于执行测试,目的是为了在他们的模块里面针对IE专门提供一条代码路径,但是现在我们也可以简单的使用特征检测达到相同的目的。

2.3Import mixins(导入混合)

这个变体展示了如何将全局(例如 jQuery, Underscore)作为一个参数传入模块的匿名函数。这种方式允许我们导入全局,并且按照我们的想法在本地为这些全局起一个别名。

let myModule = (function ( jQ, _ ) { function privateMethod1(){ jQ(“.container”).html(“test”); } function privateMethod2(){ console.log( _.min([10, 5, 100, 2, 1000]) ); } return{ publicMethod: function(){ privateMethod1(); } };}( jQuery, _ ));// 将JQ和lodash导入myModule.publicMethod();

2.4Exports(导出)

这个变体允许我们声明全局对象而不用使用它们。

let myModule = (function () { let module = {}, privateVariable = “Hello World”; function privateMethod() { // … } module.publicProperty = “Foobar”; module.publicMethod = function () { console.log( privateVariable ); }; return module;}());

2.5其它框架特定的模块模式实现

Dojo:

Dojo提供了一个方便的方法 dojo.setObject() 来设置对象。这需要将以".“符号为第一个参数的分隔符,如:myObj.parent.child 是指定义在"myOjb"内部的一个对象“parent”,它的一个属性为"child”。

使用setObject()方法允许我们设置children 的值,可以创建路径传递过程中的任何对象即使这些它们根本不存在。

例如,如果我们声明商店命名空间的对象basket.coreas,可以使用如下方式:

let store = window.store || {};

if ( !store[“basket”] ) { store.basket = {};}

if ( !store.basket[“core”] ) { store.basket.core = {};}

store.basket.core = { key:value,};

Extjs:

// create namespaceExt.namespace(“myNameSpace”);// create applicationmyNameSpace.app = function () { // do NOT access DOM from here; elements don’t exist yet // private variables let btn1, privVar1 = 11; // private functions let btn1Handler = function ( button, event ) { console.log( “privVar1=” + privVar1 ); console.log( “this.btn1Text=” + this.btn1Text ); }; // public space return { // public properties, e.g. strings to translate btn1Text: “Button 1”, // public methods init: function () { if ( Ext.Ext2 ) { btn1 = new Ext.Button({ renderTo: “btn1-ct”, text: this.btn1Text, handler: btn1Handler }); } else { btn1 = new Ext.Button( “btn1-ct”, { text: this.btn1Text, handler: btn1Handler }); } } };}();

jQuery:

因为jQuery编码规范没有规定插件如何实现模块模式,因此有很多种方式可以实现模块模式。Ben Cherry 之间提供一种方案,因为模块之间可能存在大量的共性,因此通过使用函数包装器封装模块的定义。

在下面的例子中,定义了一个library 函数,这个函数声明了一个新的库,并且在新的库(例如 模块)创建的时候,自动将初始化函数绑定到document的ready上。

function library( module ) { $( function() { if ( module.init ) { module.init(); } }); return module;}let myLibrary = library(function () { return { init: function () { // module implementation } };}());

优点:

既然我们已经看到单例模式很有用,为什么还是使用模块模式呢?首先,对于有面向对象背景的开发者来讲,至少从javascript语言上来讲,模块模式相对于真正的封装概念更清晰。

其次,模块模式支持私有数据-因此,在模块模式中,公共部分代码可以访问私有数据,但是在模块外部,不能访问类的私有部分(没开玩笑!感谢David Engfer 的玩笑)。

缺点:

模块模式的缺点是因为我们采用不同的方式访问公有和私有成员,因此当我们想要改变这些成员的可见性的时候,我们不得不在所有使用这些成员的地方修改代码。

我们也不能在对象之后添加的方法里面访问这些私有变量。也就是说,很多情况下,模块模式很有用,并且当使用正确的时候,潜在地可以改善我们代码的结构。

其它缺点包括不能为私有成员创建自动化的单元测试,以及在紧急修复bug时所带来的额外的复杂性。根本没有可能可以对私有成员打补丁。

相反地,我们必须覆盖所有的使用存在bug私有成员的公共方法。开发者不能简单的扩展私有成员,因此我们需要记得,私有成员并非它们表面上看上去那么具有扩展性。

3、单例模式


单例模式之所以这么叫,是因为它限制一个类只能有一个实例化对象。经典的实现方式是,创建一个类,这个类包含一个方法,这个方法在没有对象存在的情况下,将会创建一个新的实例对象。如果对象存在,这个方法只是返回这个对象的引用。

在JavaScript语言中, 单例服务作为一个从全局空间的代码实现中隔离出来共享的资源空间是为了提供一个单独的函数访问指针。

我们能像这样实现一个单例:

let mySingleton = (function () { // Instance stores a reference to the Singleton let instance; function init() { // 单例 // 私有方法和变量 function privateMethod(){ console.log( “I am private” ); } let privateVariable = “Im also private”; let privateRandomNumber = Math.random(); return { // 共有方法和变量 publicMethod: function () { console.log( “The public can see me!” ); }, publicProperty: “I am also public”, getRandomNumber: function() { return privateRandomNumber; } }; }; return { // 如果存在获取此单例实例,如果不存在创建一个单例实例 getInstance: function () { if ( !instance ) { instance = init(); } return instance; } };})();

let myBadSingleton = (function () { // 存储单例实例的引用 var instance; function init() { // 单例 let privateRandomNumber = Math.random(); return { getRandomNumber: function() { return privateRandomNumber; } }; }; return { // 总是创建一个新的实例 getInstance: function () { instance = init(); return instance; } };})();

// 使用:let singleA = mySingleton.getInstance();let singleB = mySingleton.getInstance();console.log( singleA.getRandomNumber() === singleB.getRandomNumber() ); // true

let badSingleA = myBadSingleton.getInstance();let badSingleB = myBadSingleton.getInstance();console.log( badSingleA.getRandomNumber() !== badSingleB.getRandomNumber() ); // true

创建一个全局访问的单例实例 (通常通过 MySingleton.getInstance()) 因为我们不能(至少在静态语言中) 直接调用 new MySingleton() 创建实例. 这在JavaScript语言中是不可能的。

在四人帮(GoF)的书里面,单例模式的应用描述如下:

  • 每个类只有一个实例,这个实例必须通过一个广为人知的接口,来被客户访问。

  • 子类如果要扩展这个唯一的实例,客户可以不用修改代码就能使用这个扩展后的实例。

关于第二点,可以参考如下的实例,我们需要这样编码:

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中的对象添加新的属性是一个非常直接了当的过程,因此将这一特定牢记于心,一个非常简单的装饰器可以实现如下:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)


后记


总结一下这三次面试下来我的经验是:

  1. 一定不要死记硬背,要理解原理,否则面试官一深入就会露馅!

  2. 代码能力一定要注重,尤其是很多原理性的代码(之前两次让我写过Node中间件,Promise.all,双向绑定原理,被虐的怀疑人生)!

  3. 尽量从面试官的问题中表现自己知识的深度与广度,让面试官发现你的闪光点!

  4. 多刷面经!

我把所有遇到的面试题都做了一个整理,并且阅读了很多大牛的博客之后写了解析,免费分享给大家,算是一个感恩回馈吧,有需要的朋友【点击我】免费获取。祝大家早日拿到自己心怡的工作!

篇幅有限,仅展示部分内容



因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-EB5wNyMD-1713498260903)]

[外链图片转存中…(img-NACqvFdP-1713498260903)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-rLVwvu4U-1713498260904)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

[外链图片转存中…(img-k5U4V634-1713498260904)]
后记


总结一下这三次面试下来我的经验是:

  1. 一定不要死记硬背,要理解原理,否则面试官一深入就会露馅!

  2. 代码能力一定要注重,尤其是很多原理性的代码(之前两次让我写过Node中间件,Promise.all,双向绑定原理,被虐的怀疑人生)!

  3. 尽量从面试官的问题中表现自己知识的深度与广度,让面试官发现你的闪光点!

  4. 多刷面经!

我把所有遇到的面试题都做了一个整理,并且阅读了很多大牛的博客之后写了解析,免费分享给大家,算是一个感恩回馈吧,有需要的朋友【点击我】免费获取。祝大家早日拿到自己心怡的工作!

篇幅有限,仅展示部分内容



  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值