精通JavaScript--06设计模式:结构型
本章主要学习结构性设计模式,前一章介绍的创建型设计模式侧重于对象的处理,而结构型设计模式则有助于把多个对象整合为一个更大型的、更有组织的代码库。它们具有灵活性,可维护性,可扩展性,并能够确保当系统中的某一部分发生变更时,不必完全重写其余部分进行适应。结构型模式还可用于帮助我们与其他代码结构(在我们的应用程序中需要简易地实现与这些代码结构的协同运作)
6.1 适配器模式
适配器(adapter)模式是一种很有用的设计模式。当需要关联两个或更多代码组件时便可应用此模式,否则这些代码将无法正常关联在一起。相类似地,当一个你之前开的API出现更新而不能再按相同的方法进行调用时,此模式也能帮上大忙。适配器提供了旧版本与新版本API之间的对接,能够帮助那些使用你的API的用户迁移至新API,使得用户在分享你改进后的代码的同时而又不必更改其原来的旧代码。代码清单6-1演示了如果使用此模式来为代码创建一个适配器,以将新的API映射至旧API。
1 //假设以下节课深藏在你庞大的代码库中,用于通过HTTP发出Ajax请求 2 var http = { 3 makeRequest: function(type, url, callback, data) { 4 var xhr = new XMLHttpRequest(), 5 STATE_LOADED = 4, 6 STATUS_OK = 200; 7 8 xhr.onreadystatechange = function() { 9 if(xhr.readyState !== STATE_LOADED) { 10 return; 11 } 12 13 if(xhr.status === STATUS_OK) { 14 callback(xhr.responseText); 15 } 16 }; 17 xhr.open(type.toUpperCase(), url); 18 xhr.send(data); 19 } 20 }; 21 22 //以上定义的http.makeRequest()方法可按如下方式进行调用,以对系统中的ID为“12345”的用户的数据进行获取和更新 23 http.makeRequest("get", "/user/12345", function(response) { 24 alert("HTTP GET response received,User data:" + response); 25 }); 26 27 http.makeRequest("post", "/user/12345", function(response) { 28 alert("HTTP POST response received,New User data:" + response); 29 }, "company=AKQA&name=wing"); 30 31 //现在,假设你要对项目进行重构,你决定引入一个新的结构,使用命名空间,并把makeRequest()方法划分为 32 //两个独立的方法来发出HTTP GET和POST请求 33 var myProject = { 34 data: { 35 ajax: (function() { 36 function createRequestObj(callback) { 37 var xhr = new XMLHttpRequest(), 38 STATE_LOADED = 4, 39 STATUS_OK = 200; 40 xhr.onreadystatechange = function() { 41 if(xhr.readyState !== STATE_LOADED) { 42 return; 43 } 44 45 if(xhr.status === STATUS_OK) { 46 callback(xhr.responseText); 47 } 48 }; 49 return xhr; 50 } 51 return { 52 get: function(url, callback) { 53 var requestObj = createRequestObj(callback); 54 requestObj.open("GET", url); 55 requestObj.send(); 56 57 }, 58 post: function(url, data, callback) { 59 var requestObj = createRequestObj(callback); 60 requestObj.open("POST", url); 61 requestObj.send(data); 62 } 63 64 }; 65 }()) 66 } 67 }; 68 69 //新的get()和post()方法可按如下方式调用 70 myProject.data.ajax.get("/user/12345",function(response){ 71 alert("Refactored HTTP GET response received.User data:"+response); 72 }); 73 74 myProject.data.ajax.post("/user/12345","company=AKQA&name=wing",function(response){ 75 alert("Refactored HTTP GET response received.New User data:"+response); 76 }); 77 78 //为了避免在代码库中的其余部分重写每一个对http.makeRequest()方法的调用,你可以创建一个适配器来映射 79 //旧接口至新方法。配置器需要使用与所要替换掉的原方法相同的输入参数,并在适配器内部调用新方法 80 function httpToAjaxAdapter(type,url,callback,data){ 81 if(type.toLowerCase()==="get"){ 82 myProject.data.ajax.get(url,callback); 83 }else if(type.toLowerCase()==="post"){ 84 myProject.data.ajax.post(url,data,callback); 85 } 86 } 87 88 //最后,应用配置器来代替员原来的方法 89 //这样,它将会映射旧接口至新方法,而不需要同时重写整个代码的其余部分 90 http.makeRequest=httpToAjaxAdapter; 91 92 //按照原方法的使用方式使用该新的适配器————在内部,它将调用新的代码,但在外部, 93 //它看起来有与旧的makeRequest()方法一模一样 94 http.makeRequest("get","/user/123456",function(response){ 95 alert("Refactored HTTP GET response received.User data:"+response); 96 }); 97 98 http.makeRequest("post","/user/12345",function(response){ 99 alert("Refactored HTTP GET response received.New User data:"+response); 100 },"company=AKQA&name=wing");
当需要把不同的代码进行关联,否则这些代码无法兼容在一起工作时,使用适配器模式最为合适。例如,当某个外部API进行了更新时,可以创建一个是适配器来映射各新方法至旧方法,以避免更改依赖这些方法的其余代码。
6.2 组合模式
组合(composite)模式为一个或多个对象创建了一个接口,使终端用户不需要知道他们所处里对象的个数。当你希望能够简化其他开发者对你的函数的访问方法时,该模式很有帮助。无论他人向同一方法传入的是一个单独对象还是一个由对象组成的数组,都不需要区别对待。代码清单6-2展示了组合模式的一个简单例子。用户可以添加class标签特性名称至一个或多个DOM节点,而不需要知道是否应将一个或多个DOM节点传给该方法。
代码清单6-2 组合模式
1 var elements = { 2 //定义一个方法按tag名称获取DOM元素。如果只发现一个元素,则它作为一个单独的节点返回, 3 //如果发现多个元素,则返回这些元素所组成的数组 4 get: function(tag) { 5 var elems = document.getElementsByTagName(tag), 6 elemsIndex = 0, 7 elemsLength = elems.length, 8 output = []; 9 10 //把所找到的元素结构转化为一个标准数组 11 for(; elemsIndex < elemsLength; elemsIndex++) { 12 output.push(elems[elemsIndex]); 13 } 14 //如果只找到一个元素,则返回该独立元素,否则返回所找到的各个元素所组成的数组 15 return output.length === 1 ? output[0] : output; 16 }, 17 18 //定义一个组合方法,用于为一个或多个元素添加class标签特性class名称,无论在执行时有多少个元素被传入都可实现 19 addClass: function(elems, newClassName) { 20 var elemIndex = 0, 21 elemLength = elems.length, 22 elem; 23 24 //判断所传入的元素究竟是数组还是一个单独对象 25 if(Object.prototype.toString.call(elems) === "[object Array]") { 26 //如果是数组,循环遍历每一个元素并为每个元素都增加class标签特性class名称 27 for(; elemIndex < elemLength; elemIndex++) { 28 elem = elems[elemIndex]; 29 elem.className += (elem.className === "" ? "" : " ") + newClassName; 30 } 31 } else { 32 //如果传入的是单独元素,则为其增加class标签特性class名称值 33 elems.className += (elems.className === "" ? "" : " ") + newClassName; 34 } 35 } 36 }; 37 38 //使用该elements.get()方法来找出当前页面的单独的<body>元素,已经<a>元素(可能有很多个) 39 var body = elements.get("body"), 40 links = elements.get("a"); 41 42 //该组合方式elements.addClass()为单独元素和多个元素给出了相同的使用接口,很明显地简化了该方法的使用 43 elements.addClass(body, "has-js"); 44 elements.addClass(links, "custom-link");
若不希望那些正与你的方法进行交互的开发者操心需要传入多少个对象作为方法参数,使用组合模式最为合适,这样可以简化方法的调用。
6.3 装饰模式
装饰(decorator)模式用于为某个“类”创建的对象扩展和定制额外的方法和属性,避免了因创建大量的子类而变得难以维护。其实现方法时,通过有效地将对象包装在另一个对象中,此另一个对象实现了相同的公共方法,根据我们所要增加的行为对相关方法进行重写。
代码清单6-3演示了一个例子,当中创建了若干装饰者,每个装饰者都会对一个已存在的对象增加额外的属性和行为。
//定义一个类,用于构建一个对象来表示一个简单的表单域 var FormField = function(type, displayText) { //使用构造函数的输入参数来确定该表单域的类型,默认为一个简单的文本域以及它的placeholder文本 this.type = type || "text"; this.displayText = displayText || ""; } FormField.prototype = { createElement: function() { this.element = document.createElement("input"); this.element.setAttribute("type", this.type); this.element.setAttribute("placeholder", this.displayText); return this.element; }, isValid: function() { return this.element.value !== ""; } }; //表单域装饰者,它实现了与FormField相同的公共方法 var FormFieldDecorator = function(formField) { this.formField = formField; } FormFieldDecorator.prototype = { createElement: function() { this.formField.createElement(); }, isValid: function() { return this.formField.isValid(); } }; var MaxLengthFieldDecorator = function(formField, maxLength) { FormFieldDecorator.call(this, formField); this.maxLength = maxLength || 100; } MaxLengthFieldDecorator.prototype = new FormFieldDecorator(); MaxLengthFieldDecorator.prototype.createElement = function() { var element = this.formField.createElement(); element.setAttribute("maxlength", this.maxLength); return element; } var AutoCompleteFieldDecorator = function(formField, autocomplete) { FormFieldDecorator.call(this, formField); this.autocomplete = autocomplete || "on"; } AutoCompleteFieldDecorator.prototype = new FormFieldDecorator(); AutoCompleteFieldDecorator.prototype.createElement = function() { var element = this.formField.createElement(); element.setAttribute("autocomplete", this.autocomplete); return element; }
代码清单6-3所创建的装饰者可以在代码清单6-4所示的情况下使用,用于生成表示表单当中的表单域的对象,使用这些装饰者而非通过子类来为对象添加属性和行为
代码清单6-4 使用装饰模式
1 //创建一个空的<form>标签,一个代表<input type="search"/>表单域的新FormField对象 2 var form = document.createElement("form"), 3 formField = new FormField("search", "Enter your search term"); 4 5 //使用装饰者为所生成的表单域元素添加maxlength和autocomplete属性,实现对formField对象的扩展。注意 6 //我们是如果一次把经扩展的formField对象传入每个装饰者的。在这里,装饰者又对该对象进行了进一步扩展 7 formField = new MaxLengthFieldDecorator(formField, 255); 8 formField = new AutoCompleteFieldDecorator(formField, "off"); 9 10 //创建该HTML表单域元素并将其添加至<form>元素 11 form.appendChild(formField.createElement()); 12 13 //添加一个事件处理函数至该<form>标签的submit事件,防止在我们添加的这个表单域当中没有值的 14 //情况下提交表单 15 form.addEventListener("submit", function(e) { 16 //屏蔽表单的默认提交动作 17 e.preventDefault(); 18 19 //检测该表单域是否通过验证,例如是否包含值 20 if(formField.isValid()) { 21 22 //如果是,继续运行并提交表单 23 form.submit(); 24 } else { 25 26 //如果否,提示用户有错误存在,用户需要进行验证 27 alert("Please correct the issues in the form field."); 28 } 29 }, false); 30 31 32 window.addEventListener("load", function() { 33 document.body.appendChild(form); 34 }, false);
当需要快速且简便地为从一个"类"所创建的对象实例增加行为,而又不借助于创建一系列的从继承于该“类”的子类来实现时,使用装饰者模式最为合适。
6.4 外观模式
外观(facade)模式是很常见的。其实它就是通过编写一个单独的函数,来简化对一个或多个更大型的、可能更为复杂的函数的访问。有争论说那些直接调用另一个函数的函数其实就是这种模式的一个例子,但我认为最好视之为一种简化某种内容的手段,不然这些内容的实现会需要多个步骤;又或者看作是为一个更庞大的系统提供了一个单独方法入口,使得其他开发者能更加容易地访问该系统。代码清单6-5展示了一个简单的外观模式,该模式用包裹器简化了跨浏览器Ajax调用。
代码清单6-5 外观模式
1 //定义一个函数,作为facade来简化和帮助实现跨浏览器的Ajax调用,所支持的浏览器可回溯至Internet Explorer 5 2 function ajaxCall(type, url, callback, data) { 3 4 //根据当前浏览器获取对Ajax连接对象的引用 5 var xhr = (function() { 6 try{ 7 //所有现代浏览器所使用的标准方法 8 return new XMLHttpRequest(); 9 }catch(e){} 10 11 //较老版本的Internet Explorer使用用户机器上安装的一个ActiveX对象 12 try{ 13 return new ActiveXObject("Msxml2.XMLHTTP.6.0"); 14 }catch(e){} 15 16 try{ 17 return new ActiveXObject("Msxml2.XMLHTTP.3.0"); 18 }catch(e){} 19 20 21 try{ 22 return new ActiveXObject("Microsoft.XMLHTTP"); 23 }catch(e){} 24 25 26 }()), 27 STATE_LOADED = 4, 28 STATUS_OK = 200; 29 //一旦从服务器收到表示成功的响应信息,则执行所给定的回调方法 30 xhr.onreadystatechange=function(){ 31 if(xhr.readyState!==STATE_LOADED){ 32 return; 33 } 34 if(xhr.status===STATUS_OK){ 35 callback(xhr.responseText); 36 } 37 }; 38 //使用浏览器的Ajax连接对象来向所给定的URL发出相关的调用 39 xhr.open(type.toUpperCase(),url); 40 xhr.send(data); 41 } 42 //外观模式的示例见代码清单6-6,掩盖了跨浏览器Ajax操作后的复杂性
代码清单6-6 使用外观模式
1 //ajaxCall()外观函数可实现跨浏览器的Ajax调用,使用方法如下 2 ajaxCall("get","/user/12345",function(response){ 3 4 alert("HTTP GET response received.User data:"+response); 5 }); 6 ajaxCall("get","/user/12345",function(response){ 7 8 alert("HTTP POST response received.User data:"+response); 9 },"name=wing");
当需要通过一个单独的函数或方法来访问一系列的函数或方法调用,以简化代码库的其余内容,使得代码更易于跟踪管理并因而在日后更具可维护性和伸缩性时,使用外观模式最为合适。
6.5 享元模式
享元(flyweight)模式是一种关于优化的模式。对于那些需要创建大量的相似对象却因此而消耗大量的内存代码来说,应用享元模式时很有帮助的。它使用少量可共享的对象来代替这些大量相似对象,使得代码的运行占用内存更少而更为高效。因此,此模式被名为flyweight。该名称来源于拳击界,它指得是那些最轻量级别的选手,那些最为敏捷灵活的选手。代码清单6-7演示了一个在设计中应用享元模式来处理问题(即多个对象的低效存储)的例子。
代码清单6-7 低效的多个对象实例
1 //创建一个类来保存员工数据,这些员工工作于一个或多个不同的公司 2 function Employee(data) { 3 4 //表示组织结构内员工的ID 5 this.employeeId = data.employeeId || 0; 6 7 //表示员工的社会安全号码 8 this.ssId = data.ssId || "000-000"; 9 10 //表示员工的名称 11 this.name = data.name || ""; 12 13 //表示员工的职业 14 this.occupation = data.occupation || ""; 15 16 //表示员工的公司名称,地址和国籍 17 this.companyName = data.companyName || ""; 18 this.companyAddress = data.companyAddress || ""; 19 this.companyCountry = data.companyCountry || ""; 20 } 21 22 //建立3个方法,用于从保存数据的对象中获取员工的名称、职业和公司详细信息 23 Employee.prototype.getName = function() { 24 return this.name; 25 }; 26 27 Employee.prototype.getOccupaction = function() { 28 return this.occupation; 29 }; 30 31 Employee.prototype.getCompany = function() { 32 return [this.companyName, this.companyAddress, this.companyCountry].join(","); 33 }; 34 35 //创建4个员工对象。注意,有两个员工对象具有相同的公司信息,而另两个员工对象具有相同的ssId和name 36 //随着所创建对象的增加,数据出现重复的数量也将增加,这种实现方法的低效消耗了更多的内存 37 var denOdell = new Employee({ 38 employeeId: 1456, 39 ssId: "1234-567", 40 name: "wing", 41 occupation: "Head of web Development", 42 companyName: "baidu", 43 companyAddress: "beijing", 44 companyCountry: "CN" 45 }), 46 billGates = new Employee({ 47 employeeId: 1, 48 ssId: "7754-344", 49 name: "Bill Gates", 50 occupation: "Founder", 51 companyName: "microsoft", 52 companyAddress: "New York", 53 companyCountry: "US" 54 });
享元模式的应用是通过拆解一个已存在的“类”来实现的。这样,可以使该“类”的对象实例中的重复数据所占用的内存实现最小化。这是通过研究所有对象实例,找出其中的重复数据,
并创建若干独立的“类”来代表这些数据而实现的。这样,就可以用一个单独的对象实例来表示这些重复的数据,进而实现从原来的“类”的多个对象实例中对此对象的引用,
使得需要存储的数据变得更少,从而减少应用程序的内存占用。
当前对象实例中的数据核(data core)(不是指核心数据)被称作该“类”的内部状态数据(intrinsic data),而那些可以被提取出来进行单独存储的、并能从原对象对此独立存储的数据进行引用的数据则被称为
外部状态数据(extrinsic data)。在代码清单6-7中,对于员工信息来说,内部数据(本质上来说就是那些具有唯一性的数据)就是员工的employeeId和员工的occupation。而对于公司数据,
它目前在多个Employee对象重复出现,则可以被提出出来单独存放。同样地,每个单独的员工个人身份信息(如name和ssId)也可以被提取出来单独存放。因此,员工信息就可通过4项属性表示出来:employeeId、occupation、company、person。最后两个属性参见其他对象实例。
享元模式的应用分为3个阶段,如代码清单5-8所示。首先,创建各个新“类”来表示外部状态数据;然后,应用工厂模式来确保之前已创建的对象不会再次被创建;最后,编写代码,使得对象的创建可以按原来的方式进行,同时允许所有实现享元的繁重任务都在后台实现。
代码清单6-8 享元模式
1 //实现享元模式的第一阶段是,从我们想要实现更高效的内存存储的对象的原来的外部状态数据 2 //(原Employee的各项属性)中提取出要成为内部状态的数据 3 // 4 //在代码清单的Employee对象中,有2组外部状态数据——人员数据和公司数据。 5 //让我们创建2个类来表示这些类型的数据 6 // 7 //Person对象表示独立的社会安全号码以及人员的名称 8 function Person(data) { 9 this.ssId = data.ssId || ""; 10 this.name = data.name || ""; 11 } 12 13 //Company对象表示公司的名称、地址和国家详细内容 14 function Company(data) { 15 this.name = data.name || ""; 16 this.address = data.address || ""; 17 this.country = data.country || ""; 18 } 19 20 //实现享元模式的第二阶段是,要确保代码唯一外部状态数据的对象,在下方employee[data.employeeId]中的对象直接量中的person对象和company对象,只被创建一次并保存起来以后供后续使用。 21 //这是通过对每一个新的外部状态数据“类”应用工厂模式来抽象对象实例的创建过程来实现的。 22 //这样,如果找到一个已经存在的对象,就会返回该对象而不是创建一个新的实例 23 var personFactory = (function() { 24 //创建一个变量,用于按照ssId保存Person“类”的所有对象 25 var people = {}, 26 personCount = 0; 27 28 return { 29 //建立一个方法,根据输入数据所提供的给定的ssId、如果还不存在该ssId对应的Person“类”的实例, 30 //则创建一个实例;如果已经存在、则返回该对象而不是创建一个新对象 31 createPerson: function(data) { 32 var person = people[data.ssId], 33 newPerson; 34 //如果该给定的ssId所对应的人员已经存在于本地数据存储区中,则返回该人员对象实例; 35 //否则,使用所提供的数据创建一个新的对象 36 if(person) { 37 return person; 38 } else { 39 newPerson = new Person(data); 40 people[newPerson.ssId] = newPerson; 41 personCount++; 42 43 return newPerson; 44 } 45 }, 46 47 //建立一个方法,以便获悉已经创建了多少多个Person类的对象 48 getPersonCount: function() { 49 return personCount; 50 } 51 } 52 }()), 53 54 //为Company类的对象创建一个相似的工厂,按name保存公司数据 55 companyFactory = (function() { 56 var companies = {}, 57 companyCount = 0; 58 59 return { 60 createPerson: function(data) { 61 var company = companies[data.name], 62 newCompany; 63 if(company) { 64 return company; 65 } else { 66 newCompany = new Person(data); 67 companies[newCompany.name] = newCompany; 68 companyCount++; 69 70 return newCompany; 71 } 72 }, 73 74 //建立一个方法,以便获悉已经创建了多少多个Person类的对象 75 getCompanyCount: function() { 76 return companyCount; 77 } 78 } 79 }()), 80 //实现享元模式的第三阶段是,使得对象的创建可以按照代码清单6-7中的相同方法进行, 81 //实现了最高效的数据存储处理,且从终端用户的角度看来,这种处理是透明的 82 // 83 //创建一个对象,当中提供了保存员工数据的方法以及按员工的employeeId来返回每个对象的数据方法。 84 //这简化了终端开发者的代码,因为他们不需要直接访问对象中的底层方法,只需要按此处理函数的交互方法进行使用即可 85 employee = (function() { 86 //声明一个变量,作为数据存储区来存放所有创建的员工对象 87 var employees = {}, 88 employeeCount = 0; 89 return { 90 //建立一个方法,用于往数据存储区添加员工对象,把参数所提供的数据传入Person和Company所 91 //对应的工厂,保存所生成指由对象直接量生成,而非工厂生成的对象(此对象由employeeId、occupation、person对象的引用和company对象的引用组成)至本地数据存储区 92 add: function(data) { 93 94 //根据所提供的参数数据、相应地创建或查找Person类的对象、Company类的对象 95 var person = personFactory.createPerson({ 96 ssId: data.ssId, 97 name: data.name 98 }), 99 company = companyFactory.createPerson({ 100 name: data.name, 101 address: data.address, 102 country: data.country 103 }); 104 //保存此新对象至本地数据存储区,此对象含有employeeId、occupation,以及员工所任职的公司, 105 //还有员工的唯一个人信息数据,其中包含其名称和社会安全号码 106 employee[data.employeeId] = { 107 employeeId: data.employeeId, 108 occupation: data.occupation, 109 person: person, 110 company: company 111 }; 112 employeeCount++; 113 }, 114 //建立一个方法,用于根据员工的employeeId来返回员工的名称————从相关的Person对象中获取人名数据 115 getName: function(employeeId) { 116 return employees[employeeId].person.name; 117 }, 118 //建立一个方法,用于根据员工的employeeId来返回员工的职位 119 getOccupation: function(employeeId) { 120 return employees[employeeId].occupation; 121 }, 122 //建立一个方法(方法名改成getCompany会好一点),用于返回员工所任职公示的地址————从相关联Company对象中获取有关数据 123 getCountry: function(employeeId) { 124 var company = employees[employeeId].company; 125 return [company.name, company.address, company.country].join(","); 126 }, 127 //建立一个功能性方法来获悉已经创建的员工的数量 128 getTotalCount: function() { 129 return employeeCount; 130 } 131 132 }; 133 }());
代码清单6-8中的享元模式代码可以如代码清单6-9般进行使用,当中实现了与代码清单6-7相同的行为。应用享元模式后,原来很耗内存的各个对象(旧Employee类的各个对象)中所重复的数据量越多,现在可共享的对象(新Person类元对象和新Company类元对象)就越多,从而减少了应用程序的内存占用。这体现了该设计模式的有用之处。
代码清单6-9 使用享元模式
1 //创建4个员工对象。注意,其中有两个对象具有相同的公司信息。有两个对象具有相同的ssId和name。 2 //在后台的处理上,代码清单6-8中的享元模式可以确保重复的人员信息和公司数据尽量以最高的方式存储 3 var denOdell = employee.add({ 4 employeeId: 1456, 5 ssId: "1234-567", 6 name: "wing", 7 occupation: "Head of web Development", 8 companyName: "baidu", 9 companyAddress: "beijing", 10 companyCountry: "CN" 11 }), 12 billGates =employee.add({ 13 employeeId: 1, 14 ssId: "7754-344", 15 name: "Bill Gates", 16 occupation: "Founder", 17 companyName: "Microsoft", 18 companyAddress: "New York", 19 companyCountry: "US" 20 }), 21 billGatesPhilanthropist = employee.add({ 22 employeeId:31, 23 ssId: "7754-344", 24 name: "Bill Gates", 25 occupation: "Founder", 26 companyName: "Microsoft", 27 companyAddress: "New York", 28 companyCountry: "US" 29 }); 30 //我们已经以ssId和name创建了2个对象来表示人员:wing,Bill gates, 31 alert(personFactory.getPersonCount());//2 32 //我们已经以name,address和country创建了2个对象来表示公司:baidu,Microsoft 33 alert(companyFactory.getCompanyCount());//2 34 35 //我们已经创建了4个对象来表示员工,其中,有两个唯一属性和另两个属性分别指向自己已经存在的person和company对象。 36 //有着相同人员和公司数据的员工对象创建得越多,应用程序所需保存的数据就越少,越能体现享元模式的高效性 37 alert(employee.getTotalCount());//3
当面对大量的对象时,若在这些对象里面有着一些相似的共有属性“名称-值”对,这些属性“名称-值”对可以分离成更小的对象,可通过对这些小对象的引用来实现数据共享,使代码的内存占的更少,使代码运行更为高效,则此时使用享元模式最为合适。
6.6 掺和模式
掺和(Mixin)模式通过快速并简易地从一个对象中把一组方法和属性直接应用至其他对象,或应用至“类”的prototype,使得该类的所有对象实例都能够访问这些属性和方法,从而避免了产生大量的子类和继承链的需要。虽然这听起来可能会像hack(作弊),特别是对于那些有着传统面向对象背景而现在逐步接触JavaScript的开发人员来说,这种模式直接发挥了JavaScript语言以及它所使用的Prototype的优势,它并不是使用其他语言所应用的严格类继承。只要能够做到小心使用,此模式可以简化开发及代码维护工作。代码清单6-10显示了如果使用掺和模式来简易迅速地应用一组通用方法至一些对象中。
代码清单6-10 掺和模式
1 //定义一个mixin,可以借助它来实现调试日志记录,可将其应用至任何对象或“类” 2 var loggingMixin = { 3 //声明一个存储数组,用于存放各条日志记录 4 logs: [], 5 //定义一个方法,用于存储消息到日志中 6 log: function(message) { 7 this.logs.push(message); 8 }, 9 //定义一个方法,用于读取所存储的日志 10 readLog: function() { 11 return this.logs.join("\n"); 12 } 13 }, 14 element, 15 header, 16 textField, 17 emailField; 18 //定义一个函数,用于将一个对象中的方法和属性应用到另一个对象中, 19 //我们将使用该函数来应用mixin至其他对象 20 function extendObj(obj1, obj2) { 21 var obj2key; 22 23 for(obj2key in obj2) { 24 if(obj2.hasOwnProperty(obj2key)) { 25 obj1[obj2key] = obj2[obj2key]; 26 } 27 } 28 return obj1; 29 } 30 //定义一个单例,我们将应用该mixin于其上(虽然没有该mixin此单例也能正常运作) 31 element = { 32 allElements: [], 33 create: function(type) { 34 var elem = document.createElement(type); 35 this.allElements.push(elem); 36 37 //使用mixin的log()方法,确保该方法存在才调用它。如果该mixin尚未应用,则该方法仍然将正常运作 38 if(typeof this.log === "function") { 39 this.log("Created an element of type:" + type); 40 } 41 return elem; 42 }, 43 getAllElements: function() { 44 return this.allElements; 45 } 46 }; 47 //定义一个简单“类”,我们将应用该mixin于其上 48 function Field(type, displayText) { 49 this.type = type || ""; 50 this.displayText = displayText || ""; 51 52 //确保该mixin的log()方法存在才调用它. 53 if(typeof this.log === "function") { 54 this.log("Created an instance of Field"); 55 } 56 } 57 58 Field.prototype = { 59 getElemengt: function() { 60 var field = document.createElement("input"); 61 field.setAttribute("type", this.type); 62 field.setAttribute("placeholder", this.displayText); 63 if(typeof this.log === "function") { 64 this.log("Created a DOM element with placeholder text:" + this.displayText); 65 } 66 return field; 67 } 68 }; 69 70 //直接应用该mixin至element对象,实质时从该mixin复制各方法和属性至该单例 71 element = extendObj(element, loggingMixin); 72 //应用该mixin至Field“类”的Prototype,使得该mixin的方法为每一个从该“类”实例化出来的对象所拥有 73 Field.prototype = extendObj(Field.prototype, loggingMixin); 74 //使用element.create()方法来创建一个新的DOM元素 75 header = element.create("header"); 76 //创建两个对象实例,两者都从Prototype中获得了getElement方法 77 textField = new Field("text", "Enter the first line of your address"); 78 emailField = new Field("email", "Enter your email address"); 79 80 // //将这些对象中所保存的元素添加至当前页面 81 // document.body.appendChild(textField.getElemengt()); 82 // document.body.appendChild(emailField.getElemengt()); 83 // 84 // //输出通过该mixin所保存的日志记录 85 // alert(loggingMixin.readLog()); 86 // //输出如下内容,注意该mixin的每一次使用所录得的所有日志信息如何保存在一起的 87 /* 88 Created an element of type:header 89 Created an instance of Field 90 Created an instance of Field 91 Created a DOM element with placeholder text:Enter the first line of your address 92 Created a DOM element with placeholder text:Enter your email address 93 Created a DOM element with placeholder text:Enter the first line of your address 94 Created a DOM element with placeholder text:Enter your email address 95 */
如果研究代码清单6-10中的代码,可能会注意到一些意想不到的事情:尽管mixin是独立地应用至单例和“类“的,但所有记录的日志数据却是存储在一起的。在任何包含着readLog()方法的对象上调用该方法都将得到相同的输出。这是因为,当extendObj()函数从某一对象复制有对象特征的属性至另一个对象时,比如本例中的logs数组(记住,在JavaScript中,数组是一种对象),这个复制动作是通过引用来实现的,而不会出现真正的数据复制。每当该属性被其他任何对象访问时,就会使用相同的属性,即loggingMixin对象的原始属性。
对于本例这种情况,我们希望一次就能看到所有的日志,因而是很有用的;然而,当你在自己的代码中使用此模式时,这可能不是你所需要的结果。如果你希望所复制的属性用于创建单独的副本,则应如代码清单6-11所示般对extendObj()函数进行更改。
代码清单6-11 更改extendObj()函数以真正复制属性而不是通过引用实现复制
1 function extendObj(obj1, obj2) { 2 var obj2key, 3 value; 4 for(obj2key in obj2) { 5 if(obj2.hasOwnProperty(obj2key)) { 6 value = obj2[obj2key]; 7 8 //如果所复制的值是数组,则使用slice()方法来复制出该数组的副本 9 if(Object.prototype.toString.apply(value) === "[object Array]") { 10 obj1[obj2key] = value.slice(); 11 //否则,如果所复制的值是对象,而不是数组,则使用本函数的递归调用来复制出该对象的副本 12 } else if(typeof obj2[obj2key] == "object") { 13 obj1[obj2key] = extendObj({}, value); 14 //否则,像之前那样复制该值 15 } else { 16 obj1[obj2key] = value; 17 } 18 } 19 } 20 return obj1; 21 }
当希望能够快速地直接从一个对象把一组属性和方法应用至另一对象,或应用至一个“类”以供它所有对象实例使用,而不需要借助复杂的子类和继承来实现时,使用掺和模式最为合适。
6.7 模块模式
模块(module)模式可能是专业JavaScript开发者使用最为普遍的一个模式。事实上,在之前的章节我已经两次介绍过这种模式的基础。第一次是在第1章讨论public、private和Protected变量时介绍的,而第二次是在第4章讨论提高JavaScript代码压缩程度的方法时介绍的。这都基于自执行函数闭包,它使得我们可以建立一个代码的安全沙箱区域,在其中可以访问全局变量和函数,但不会把在它里面声明的变量和函数暴露给它周围的作用域,触发是明确地使用return语句。
一个自执行函数的最简单例子如下所示:
(function(){ //在此函数内声明的任何变量和函数都不能在它的外部进行访问 }());
我们可以使用此模式来把我们的代码库划分为若干更小的、相关联的代码块,我们称之为模块。模块模式因此得名。每一个这样的模块要明确列出对代码的其他部分的依赖(如果有的话)、此依赖要作为参数传入该函数,如下所示:
(function($){ //我们非常明确地声明jQuery作为module的依赖,使得jQuery可以在内部通过$变量进行访问 }(jQuery));
提示:访问函数内的JavaScript参数要快于访问在该函数作用域之外的全局变量,因为语言解释器不需要实施额外的步骤来离开当前函数的作用域去搜寻变量。
模块模式的基本形式是通过在函数闭包内使用return语句来返回所声明的代码来最终完成的。这些代码可被其他模块使用,也可由主应用程序本身使用。代码清单6-12展示了模块模式的完整形式,它基于第5章中的代码清单5-10。
代码清单6-12 模块模式
1 //模块模式是很有特色的,它综合使用了自执行的匿名函数闭包、作为参数传入的依赖,以及一个可选的return语句, 2 //此语句可使得在闭包中所编写的代码能够在外部进行访问 3 4 //这里的唯一一项依赖是document对象,在它当中包含有浏览器的cookie数据。作为一项附加的安全措施,我们 5 //可以使用一个明明为undefined的末位参数,但我们从不会向其中传入值。假如我们确实是不向该参数传入值的, 6 //此做法可确保该命名为undefined的变量总会包含着值undefined(这个值就是undefined,而不是指未定义的值)。 7 //不然无论是出于而已还是其他原因,其他代码都有可能会重写该值(因为undefined不是JavaScript语言的保留关键字), 8 //从而引发各种方式来破坏代码的正常行为 9 10 var cookie=(function(document,undefined){ 11 var allCookies=document.cookie.split(";"), 12 cookies={}, 13 cookiesIndex=0, 14 cookiesLength=allCookies.length, 15 cookie; 16 for(;cookiesIndex<cookiesLength;cookiesIndex++){ 17 cookie=allCookies[cookiesIndex].split("="); 18 cookies[unescape(cookie[0])]=unescape(cookie[1]); 19 } 20 //返回一些方法,属性或值,以便在代码库的其余部分进行访问使用。在本例中,return将通过cookie变量 21 //暴露出以下两个方法,完成单例的创建 22 return { 23 get:function(name){ 24 return cookies[name]||""; 25 }, 26 set:function(name,value){ 27 cookies[name]=value; 28 document.cookie=escape(name)+"="+escape(value); 29 } 30 }; 31 //在函数执行的时刻传入所有依赖 32 }(document));
在通过单例对象结构来实现命名空间的较大型代码库中,模块模式的使用方式与我们之前所见的稍有不同。在这种情况下,我们把依赖传入,然后在该函数闭包的最后返回经处理的依赖,使用模块来为单例增加新的属性和方法。代码清单6-13展示了如何应用模块模式来为命名空间增加内容,这是该模式的最常见用法之一。
代码清单6-13 使用模块模式增加命名空间
1 //定义一个命名空间,我们将会把一些代码模块放入此命名空间 2 var myData={}; 3 4 //这是Ajax模块,我们通过添加的方式将其加入到myData命名空间 5 //命名空间是作为参数传入的。一旦该命名空间被加入了新的方法,最后就返回此命名空间,使用此新的、 6 //增加了新的内容的命名空间重写原来的命名空间 7 myData=(function(myNamespace,undefined){ 8 //往命名空间加入一个ajax对象属性,并使用相关方法充实该属性 9 myNamespace.ajax={ 10 get:function(url,callback){ 11 var xhr=new XMLHttpRequest(), 12 STATE_LOADED=4, 13 STATUS_OK=200; 14 xhr.onreadystatechange=function(){ 15 if(xhr.readyState!==STATE_LOADED){ 16 return; 17 } 18 if(xhr.status===STATUS_OK){ 19 callback(xhr.responseText); 20 } 21 }; 22 xhr.open("GET",url); 23 xhr.send(); 24 } 25 }; 26 //返回该新的、增加了新内容的命名空间至myData变量 27 return myNamespace; 28 //我们可以使用以下保障机制,如果该myData命名空间对象尚不存在,则使用一个空对象。当你在一个大的命名 29 //空间下使用分散在多个文件下的多个模块,而且你又不确定所传入的命名空间是否在之前的其他地方已经被 30 //初始化过时,这种方法将很有帮助的 31 }(myData||{})); 32 33 34 //这是Cookie模块,我们通过添加的方式将其加入到myData命名空间和之前一样 35 //把命名空间传进来,增加内容,然后返回,重写原来的命名空间对象,此时, 36 //myData命名空间已包含了Ajax模块代码 37 myData=(function(myNamespace,undefined){ 38 //往命名空间加入一个cookies对象属性,并使用相关方法充实该属性 39 myNamespace.cookies={ 40 get:function(name){ 41 var output="", 42 escapedName=escape(name), 43 start=document.cookie.indexOf(escapedName+"="), 44 end=document.cookie.indexOf(";",start); 45 end=end===1?(document.cookie.length-1):end; 46 if(start>=0){ 47 output=document.cookie.substring(start+escapedName.length+1,end); 48 } 49 return unescape(output); 50 }, 51 set:function(name,value){ 52 document.cookie=escape(name)+"="+escape(value); 53 } 54 }; 55 return myNamespace; 56 }(myData||{})); 57 //直接通过myData命名空间对象来执行相关方法。现在,myData命名空间对象包含有Ajax和Cookies模块 58 myData.ajax.get("/user/12345",function(response){ 59 alert("HTTP GET response received.User data:"+response); 60 }); 61 myData.cookies.set("company","AKQA"); 62 myData.cookies.set("name","wing"); 63 64 myData.cookies.get("company");//AKQA 65 myData.cookies.get("name");//wing
当希望把大型的代码库分解为更小的,更易管理的、自我包含的多个组成部分,而每部分有明确的依赖项,每部分有清晰定义的使用目的时,使用模块模式最为合适。因为它们的沙箱特性,它们的自我执行函数代码块同时也是通过混淆和缩编来生成更小的文件的主要代码(自执行函数代码块中的内容可以更大程度地被混淆、缩编),此话题我们是在第4章讨论的。在第9章,我们将学习一个代替的方法,即使用异步模块定义(Asynchronous Module Defintion,简称AMD)来为JavaScript代码定义和加载模块。
更多内容:http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html
6.8 代理模式
代理(proxy)模式是通过定义一个代理(或替身)对象或方法来替换或增强一个已经存在的对象或方法,以提升其性能或增加额外的功能,但又不影响已经使用了该对象或方法的其余部分代码。我和许多专业JavaScript开发者使用该模式的最普遍方式是对一个已经存在的方法或函数进行包装,但不改变方法或函数的名称,如代码清单6-14所示。
代码清单6-14 代理模式
1 //为了对代码清单6-13中的myData.cookies.get()方法实现代理,我们先把当前方法保存在一个变量中 2 var proxiedGet = myData.cookies.get; 3 //使用一个新函数来重写get()方法,对原来的方法实现代理并增加它的行为 4 myData.cookies.get = function() { 5 //调用被代理(原来的)方法来获取它可能产生的值 6 var value = prefixes.apply(this.arguments); 7 //对被代理的方法所返回的值实施某种操作 8 value = value.toUpperCase(); 9 //对于实施了某种操作后的值,其类型要与被代理方法相同。使用该值作为返回值,则该新方法的使用就不会影响到原有的对该方法的调用 10 return value; 11 } 12 //代理模式的一个变体被称为虚拟代理(virtual proxy),通过延迟对象的实例化时间(这样构造函数的执行就会推迟),直至对象实例中的某个方法被真正调用之时,如代码清单6-15所示。
代码清单6-15 虚拟代理模式
1 //定义一个“类”,用于构建一个对象,来表示一个简单表单域 2 function FormField(type, displayText) { 3 this.type = type || "text"; 4 this.displayText = displayText || ""; 5 6 //创建并初始化一个表单域的DOM元素 7 this.element = document.createElement("input"); 8 this.element.setAttribute("type", this.type); 9 this.element.setAttribute("placeholder", this.displayText); 10 } 11 //定义2个方法以供对象实例来继承 12 FormField.prototype = { 13 getElement: function() { 14 return this.element; 15 }, 16 isValid: function() { 17 return this.element.value !== ""; 18 } 19 }; 20 //现在,使用实现了相同方法的代理来替换FormField“类”。它会延迟调用原来的构造函数,直至这些方法被真正调用。 21 //这样,节省了内存资源并提升了性能 22 //根据需要,可使用模块模式来使代理“类”的作用域实现局部化,传入原来的FormField“类”并返回它所经过的代理的版本 23 FormField = (function(FormField) { 24 //定义代理构造函数,类似于原来的FormField“类” 25 function FormFieldProxy(type, displayText) { 26 this.type = type; 27 this.displayText = displayText; 28 } 29 FormFieldProxy.prototype = { 30 //定义一个属性,用于在原来的“类”被实例化之后,保存对该实例的引用 31 formField: null, 32 //定义一个新的initialize方法、用于在FormFiled的对象实例尚未存在时执行原来的“类”的构造函数,以创建出该对象实例 33 initialize: function() { 34 if(!this.formField) { 35 this.formField = new FormField(this.type, this.displayText); 36 } 37 }, 38 39 //使用一些新方法对原来的各个方法进行代理、只有当这些新方法当中某一个被调用时,才会调用initialize()方法来实例化FormField“类” 40 getElement: function() { 41 this.initialize(); 42 return this.formField.getElemengt(); 43 }, 44 isValid: function() { 45 this.initialize(); 46 return this.formField.isValid(); 47 } 48 }; 49 //返回该代理“类”,替换原来的“类” 50 return FormFieldProxy; 51 }(FormField)); 52 //创建2个对象实例。它们实际调用的都是代理“类”而不是原来的“类”。这意味着,在此阶段,DOM元素并不会被创建, 53 //这样便节省了内存占用并提升了性能 54 var textField=new FormField("text","Enter the first line of your address"), 55 emailField=new FormField("email","Enter your email address"); 56 //当页面完成加载时,把保存在这些对象中的元素添加至当前页面。getElement()方法在此时被调用。它首先调用initialize(),然后创建原来的“类”的实例并执行该“类”的构造函数。 57 //此函数才是真正实施DOM元素创建过程的函数。此做法确保了用作保存DOM元素的内存只在需要使用该元素的确切时刻才被占用 58 // window.addEventListener("load",function(){ 59 // document.body.appendChild(textField.getElemengt()); 60 // document.body.appendChild(emailField.getElemengt()); 61 // },false); 62 //从代理中执行另一个方法。此时,原来的“类”的对象实例将不会被再次创建,而是会使用所保存的实例 63 alert(emailField.isValid());
通过延迟调用或为那些可能会在同一时间发出多个调用(比如Ajax请求或其他网络相关的调用)的对象进行调用组合,我们可以扩展代理模式,进一步提升性能并减少内存占用。
当需要重写某对象或“类”上的特定方法的行为,或者重写方法以提升某个已有的“类”的性能,使得直至该“类”的方法被调用时才真正对该“类”进行实例化时,使用代理模式最为合适。