广州域名企业网站建站哪家好电商网课
原文:Pro JavaScript Development
协议:CC BY-NC-SA 4.0
六、设计模式:结构型
在这一章中,我们将继续关注设计模式,重点是结构设计模式。我们在前一章中看到的创造性设计模式集中在对象创建上,而结构化设计模式帮助你将对象组合成一个更大、更结构化的代码库。它们是灵活的、可维护的、可扩展的,并且确保如果系统的一部分发生变化,您不需要完全重写其余部分来适应。结构化设计模式还可以用来帮助与其他代码结构进行交互,您需要在应用中轻松地使用这些代码结构。让我们一起来看看你可能会发现在你的代码中有用的八种结构设计模式,以及一些例子。
适配器模式
adapterpattern 是一种有用的设计模式,当您需要将两个或更多通常不会连接在一起的代码组件连接在一起时,可以使用它;类似地,当您开发的 API 被更新,不再以相同的方式调用时,它就变得有用了——提供了一个适配器来连接新旧版本,有助于 API 用户的迁移,他们可以利用您代码中的其他改进,而不会破坏他们的代码。清单 6-1 中的例子展示了如何使用这种模式为你的代码创建一个适配器来映射一个新的 API 接口到一个旧的接口。
清单 6-1。适配器模式
// Imagine the following interface exists deep in your large code base for making Ajax requests
// over HTTP
var http = {
makeRequest: function(type, url, callback, data) {
var xhr = new XMLHttpRequest(),
STATE_LOADED = 4,
STATUS_OK = 200;
xhr.onreadystatechange = function() {
if (xhr.readyState !== STATE_LOADED) {
return;
}
if (xhr.status === STATUS_OK) {
callback(xhr.responseText);
}
};
xhr.open(type.toUpperCase(), url);
xhr.send(data);
}
};
// The http.makeRequest() method defined above could be called as follows, for getting and
// updating user data in a system for a user with an ID of "12345":
http.makeRequest("get", "/user/12345", function(response) {
alert("HTTP GET response received. User data: " + response);
});
http.makeRequest("post", "/user/12345", function(response) {
alert("HTTP POST response received. New user data: " + response);
}, "company=AKQA&name=Den%20Odell");
// Now imagine in a refactor of your project, you decide to introduce a new structure using a
// namespace and splitting out the makeRequest() method into separate methods for HTTP GET
// and POST requests
var myProject = {
data: {
ajax: (function() {
function createRequestObj(callback) {
var xhr = new XMLHttpRequest(),
STATE_LOADED = 4,
STATUS_OK = 200;
xhr.onreadystatechange = function() {
if (xhr.readyState !== STATE_LOADED) {
return;
}
if (xhr.status === STATUS_OK) {
callback(xhr.responseText);
}
};
return xhr;
}
return {
get: function(url, callback) {
var requestObj = createRequestObj(callback);
requestObj.open("GET", url);
requestObj.send();
},
post: function(url, data, callback) {
var requestObj = createRequestObj(callback);
requestObj.open("POST", url);
requestObj.send(data);
}
};
}())
}
};
// These new get() and post() methods could be called as follows:
myProject.data.ajax.get("/user/12345", function(response) {
alert("Refactored HTTP GET response received. User data: " + response);
});
myProject.data.ajax.post("/user/12345", "company=AKQA&name=Den%20Odell", function(response) {
alert("Refactored HTTP POST response received. New user data: " + response);
});
// To avoid rewriting every call to the http.makeRequest() method in the rest of your code
// base, you could create an adapter to map the old interface to the new methods. The adapter
// needs to take the same input parameters as the original method it is designed to replace,
// and calls the new methods internally instead
function httpToAjaxAdapter(type, url, callback, data) {
if (type.toLowerCase() === "get") {
myProject.data.ajax.get(url, callback);
} else if (type.toLowerCase() === "post") {
myProject.data.ajax.post(url, data, callback);
}
}
// Finaly, apply the adapter to replace the original method. It will then map the old
// interface to the new one without needing to rewrite the rest of your code at the same time
http.makeRequest = httpToAjaxAdapter;
// Use the new adapter in the same way as the original method - internally it will call the
// newer code, but externally it will appear identical to the old makeRequest() method
http.makeRequest("get", "/user/12345", function(response) {
alert("Adapter HTTP GET response received. User data: " + response);
});
http.makeRequest("post", "/user/12345", function(response) {
alert("Adapter HTTP POST response received. New user data: " + response);
}, "company=AKQA&name=Den%20Odell");
适配器模式最适合在需要将本来不能组合在一起的代码连接在一起时使用,例如,当外部 API 被更新时——您创建一个适配器来将新方法映射到旧方法,以避免需要对依赖于这些方法的代码的其余部分进行更改。
要在线阅读有关适配器模式的更多信息,请查看以下资源:
- “JavaScript 设计模式:适配器”,作者 Joseph Zimmerman,Adobe Developer Connection(通过
http://bit.ly/adapter_pattern
) - 维基百科上的“适配器设计模式”(via
http://bit.ly/adapter_wiki
)
复合模式
复合模式为一个或多个对象创建了一个界面,而最终用户不需要知道他们正在处理多少个对象。当你想简化其他人访问你的函数的方式时,它就派上用场了;无论是将单个对象还是一组对象传递给同一个方法,都没有区别。清单 6-2 显示了复合模式的一个简单例子,允许用户向一个或多个 DOM 节点添加类名,而不需要知道他们是否需要向方法传递一个或多个 DOM 节点。
清单 6-2。复合模式
// Define a singleton containing methods to get references to page elements and to add
// class names to those elements
var elements = {
// Define a method to get DOM elements by tag name. If one element is found, it is
// returned as an individual node, or multiple elements are found, an array of those
// found elements are returned
get: function(tag) {
var elems = document.getElementsByTagName(tag),
elemsIndex = 0,
elemsLength = elems.length,
output = [];
// Convert the found elements structure into a standard array
for (; elemsIndex < elemsLength; elemsIndex++) {
output.push(elems[elemsIndex]);
}
// If one element is found, return that single element, otherwise return the array
// of found elements
return output.length === 1 ? output[0] : output;
},
// Define a composite method which adds an class name to one or more elements, regardless
// of how many are passed when it is executed
addClass: function(elems, newClassName) {
var elemIndex = 0,
elemLength = elems.length,
elem;
// Determine if the elements passed in are an array or a single object
if (Object.prototype.toString.call(elems) === "[object Array]") {
// If they are an array, loop through each elements and add the class name to each
for (; elemIndex < elemLength; elemIndex++) {
elem = elems[elemIndex];
elem.className += (elem.className === "" ? "" : " ") + newClassName;
}
} else {
// If a single element was passed in, add the class name value to it
elems.className += (elems.className === "" ? "" : " ") + newClassName;
}
}
};
// Use the elements.get() method to locate the single <body> element on the current page, and
// potentially numerous <a> elements
var body = elements.get("body"),
links = elements.get("a");
// The composite elements.addClass() method gives the same interface to single elements
// as it does to multiple elements, simplifying its use considerably
elements.addClass(body, "has-js");
elements.addClass(links, "custom-link");
当您不希望与您的方法交互的开发人员担心有多少对象作为参数传递给它们,从而简化方法调用时,最好使用复合模式。
要在线阅读关于复合模式的更多信息,请查阅以下资源:
- Joseph Zimmerman 在 Adobe Developer Connection 上发表的“JavaScript 设计模式:复合”(通过
http://bit.ly/composite_pattern
) - 维基百科上的“复合模式”(via
http://bit.ly/composite_wiki
)
装饰图案
装饰模式是一种扩展和定制从“类”创建的对象的方法和属性的方式,而不需要创建大量可能变得难以管理的子类。这是通过有效地将对象包装在另一个实现相同公共方法的对象中来实现的,其中相关方法根据我们试图增强的行为被覆盖。清单 6-3 中的代码展示了一个创建几个装饰器的例子,每个装饰器都用额外的属性和行为来扩充一个现有的对象。
清单 6-3。装饰图案
var FormField = function(type, displayText){
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 !== "";
}
};
// The form field deocorator, which implements the same public methods as 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。正在使用的装饰模式
// Create an empty <form> tag and a new FormField object to represent
// a <input type="search"> field
var form = document.createElement("form"),
formField = new FormField("search", "Enter your search term");
// Extend the formField object using our decorators to add maxlength and autocomplete properties
// to the resulting form field element. Note how we pass the extended formField object into each
// decorator in turn, which extends it further.
formField = new MaxLengthFieldDecorator(formField, 255);
formField = new AutoCompleteFieldDecorator(formField, "off");
// Create the HTML form field element and add it to the <form> element
form.appendChild(formField.createElement());
// Add an event handler to the <form> tag's submit event, preventing the form from submitting if
// the form field we added contains no value
form.addEventListener("submit", function(e) {
// Stop the form from submitting
e.preventDefault();
// Test to see if our form field is valid, i.e. that it contains a value
if (formField.isValid()) {
// If it does, go ahead and submit the form
form.submit();
} else {
// If it doesn't, alert the user that something is wrong and they need to correct it
alert("Please correct the issues in the form field.");
}
}, false);
// Add the <form> field to the current page once it has loaded
window.addEventListener("load", function() {
document.body.appendChild(form);
}, false);
当您需要快速简单地增加从一个“类”创建的对象实例的行为,而不必求助于从它创建一长串继承的子类时,最好使用装饰模式。要在线阅读关于装饰模式的更多信息,请查阅以下资源:
- Addy Osmani 的《探索 JavaScript 中的装饰模式》(via
http://bit.ly/decorator_pattern
) - 斯托扬·斯特凡诺夫关于多布斯博士的“JavaScript 中的装饰模式”(通过
http://bit.ly/decorator_js
)
立面图案
外立面图案很常见;它只是编写一个函数来简化对一个或多个更大、可能更复杂的函数的访问。可能有人会说,任何简单地调用另一个函数的函数都是这种模式的一个例子,但是我发现最好是从简化一些原本需要多个步骤的事情的角度来考虑,或者提供一个访问更大系统的单点,这将使其他开发人员访问该系统更加容易。清单 6-5 中的代码演示了一个简单的外观,它提供了一个包装器来简化跨浏览器 Ajax 调用。
清单 6-5。立面图案
// Define a function which acts as a façade to simplify and facilitate cross-browser Ajax calls,
// supporting browsers all the way back to Internet Explorer 5
function ajaxCall(type, url, callback, data) {
// Get a reference to an Ajax connection object relevant to the current browser
var xhr = (function() {
try {
// The standard method, used in all modern browsers
return new XMLHttpRequest();
}
catch(e) {}
// Older versions of Internet Explorer utilise an ActiveX object installed on the
// user's machine
try {
return new ActiveXObject("Msxml2.XMLHTTP.6.0");
}
catch(e) {}
try {
return new ActiveXObject("Msxml2.XMLHTTP.3.0");
}
catch(e) {}
try {
return new ActiveXObject("Microsoft.XMLHTTP");
}
catch(e) {}
// If no relevant Ajax connection object can be found, throw an error
throw new Error("Ajax not supported in this browser.");
}()),
STATE_LOADED = 4,
STATUS_OK = 200;
// Execute the given callback method once a succesful response is received from the server
xhr.onreadystatechange = function() {
if (xhr.readyState !== STATE_LOADED) {
return;
}
if (xhr.status === STATUS_OK) {
callback(xhr.responseText);
}
};
// Use the browser's Ajax connection object to make the relevant call to the given URL
xhr.open(type.toUpperCase(), url);
xhr.send(data);
}
清单 6-5 中的外观模式可以用在你的代码中,如清单 6-6 所示,掩盖了跨浏览器 Ajax 操作背后的复杂性。
清单 6-6。正在使用的立面图案
// The ajaxCall() facade function can make cross-browser Ajax calls as follows
ajaxCall("get", "/user/12345", function(response) {
alert("HTTP GET response received. User data: " + response);
});
ajaxCall("post", "/user/12345", function(response) {
alert("HTTP POST response received. New user data: " + response);
}, "company=AKQA&name=Den%20Odell");
当您希望通过单个函数或方法来提供对一系列函数或方法调用的访问,以便简化代码库的其余部分,使其更容易遵循,因此将来更易于维护和扩展时,最好使用外观模式。要在线阅读有关外观模式的更多信息,请查看以下资源:
- Joseph Zimmerman 在 Adobe Developer Connection 上的“JavaScript 设计模式:外观”(通过
http://bit.ly/facade_pattern
) - 卡尔·丹利的“正面图案”(经由
http://bit.ly/facade_js
)
轻量级模式
flyweight 模式是一种优化模式;这对于创建大量相似对象的代码非常有用,否则这些对象会消耗大量内存。它用一些共享对象代替了大量的相似对象,使得代码更轻,性能更好;因此得名,它来自拳击界,指的是最轻重量级的运动员,那些最敏捷的运动员。清单 6-7 显示了一个例子,这个例子说明了 flyweight 模式旨在解决的一个问题,即对象的低效存储。
清单 6-7。低效的对象实例
// Create a "class" to store data to related to employees working for one or more different
// companies
function Employee(data) {
// Represent an employee's ID within an organisation
this.employeeId = data.employeeId || 0;
// Represent an employee's social security number
this.ssId = data.ssId || "0000-000-0000";
// Represent an employee's name
this.name = data.name || "";
// Represent an employee's occupation
this.occupation = data.occupation || "";
// Represent an employee's company name, address and country
this.companyName = data.companyName || "";
this.companyAddress = data.companyAddress || "";
this.companyCountry = data.companyCountry || "";
}
// Create three methods to get the employee's name, occupation and company details from the
// stored object
Employee.prototype.getName = function() {
return this.name;
};
Employee.prototype.getOccupation = function() {
return this.occupation;
};
Employee.prototype.getCompany = function() {
return [this.companyName, this.companyAddress, this.companyCountry].join(", ");
};
// Create four employee objects - note that two share the same company information, and two
// share the same ssId and name. As more objects are created, the amount of data repeated will
// grow, consuming more memory due to inefficiency
var denOdell = new Employee({
employeeId: 1456,
ssId: "1234-567-8901",
name: "Den Odell",
occupation: "Head of Web Development",
companyName: "AKQA",
companyAddress: "1 St. John's Lane, London",
companyCountry: "GB"
}),
steveBallmer = new Employee({
employeeId: 3,
ssId: "8376-940-1673",
name: "Steve Ballmer",
occupation: "Ex-CEO",
companyName: "Microsoft",
companyAddress: "1 Microsoft Way, Redmond, WA",
companyCountry: "US"
}),
billGates = new Employee({
employeeId: 1,
ssId: "7754-342-7584",
name: "Bill Gates",
occupation: "Founder",
companyName: "Microsoft",
companyAddress: "1 Microsoft Way, Redmond, WA",
companyCountry: "US"
}),
billGatesPhilanthropist = new Employee({
employeeId: 2,
ssId: "7754-342-7584",
name: "Bill Gates",
occupation: "Philanthropist",
companyName: "Gates Foundation",
companyAddress: "500 Fifth Avenue North, Seattle, WA",
companyCountry: "US"
});
flyweight 模式是通过尝试解构一个现有的“类”来应用的,这样可以最小化对象实例之间可能重复的任何数据。这是通过研究重复数据的任何当前对象实例并创建单独的“类”来表示该数据来实现的。然后,单个对象实例可以表示重复的数据,这些数据可以从原始“类”的多个对象实例中引用,从而减少存储的数据,从而减少应用的内存占用。
每个当前对象实例的任何数据核心都称为该“类”的内部数据,任何可以从对象中提取、单独存储和引用的数据都称为其外部数据。在清单 6-7 中,与雇员相关的内在数据——本质上是唯一的——是它的employeeId
和occupation
值。目前复制在多个Employee
对象上的公司数据可以单独提取和存储;每个人的数据也是如此,比如他们的name
和ssId
值。因此,一个雇员可以用四个属性来表示:employeeId
、occupation
、company
、person
。最后两个属性引用其他对象实例。
flyweight 模式分三个阶段应用,如清单 6-8 所示:首先,创建新的“类”来表示外部数据;第二,通过应用工厂模式来确保先前创建的对象不会被重新创建;最后,通过编写代码,以与最初相同的方式创建对象,允许所有 flyweight 的繁重工作在幕后进行。
清单 6-8。轻量级模式
// The first stage of applying the flyweight pattern is to extract intrinsic data from
// extrinsic data in the objects we wish to make more memory-efficient
//
// There are two sets of extrinsic data in an Employee object from Listing 6-7 - people data
// and company data. Let's create two "classes" to represent those types of data
//
// A Person object represents an individual's social security number and their name
function Person(data) {
this.ssId = data.ssId || "";
this.name = data.name || "";
}
// A Company object represents a company's name, address and country details
function Company(data) {
this.name = data.name || "";
this.address = data.address || "";
this.country = data.country || "";
}
// The second stage of the flyweight pattern is to ensure any objects representing unique
// extrinsic data are only created once and stored for use in future. This is achieved by
// harnessing the factory pattern for each of the new extrinsic data "classes" to abstract
// away the creation of the object instance so that if a previously-existing object is found,
// that can be returned instead of creating a new instance
var personFactory = (function() {
// Create a variable to store all instances of the People "class" by their ssId
var people = {},
personCount = 0;
return {
// Provide a method to create an instance of the People "class" if one does not
// already exist by the given ssId provided in the data input. If one exists,
// return that object rather than creating a new one
createPerson: function(data) {
var person = people[data.ssId],
newPerson;
// If the person by the given ssId exists in our local data store, return their
// object instance, otherwise create a new one using the provided data
if (person) {
return person;
} else {
newPerson = new Person(data);
people[newPerson.ssId] = newPerson;
personCount++;
return newPerson;
}
},
// Provide a method to let us know how many Person objects have been created
getPersonCount: function() {
return personCount;
}
};
}()),
// Create a similar factory for Company objects, storing company data by name
companyFactory = (function() {
var companies = {},
companyCount = 0;
return {
createCompany: function(data) {
var company = companies[data.name],
newCompany;
if (company) {
return company;
} else {
newCompany = new Company(data);
companies[newCompany.name] = newCompany;
companyCount++;
return newCompany;
}
},
getCompanyCount: function() {
return companyCount;
}
};
}()),
// The third stage of the flyweight pattern is to allow the creation of objects in a
// simliar way to that in Listing 6-7, providing all the handling of data storage in the
// most efficient way in a transparent way to the end user
//
// Create an object with methods to store employee data and to return data from each
// object by their employeeId. This simplifies the end user's code as they do not need to
// access methods on underlying objects directly, they only need interface with this handler
employee = (function() {
// Create a data store for all employee objects created
var employees = {},
employeeCount = 0;
return {
// Provide a method to add employees to the data store, passing the provided data
// to the Person and Company factories and storing the resulting object, consisting
// of the enployeeId, occupation, person object reference, and company object
// reference in the local data store
add: function(data) {
// Create or locate Person or Company objects that correspond to the provided
// data, as appropriate
var person = personFactory.createPerson({
ssId: data.ssId,
name: data.name
}),
company = companyFactory.createCompany({
name: data.companyName,
address: data.companyAddress,
country: data.companyCountry
});
// Store a new object in the local data store, containing the employeeId,
// their occupation, and references to the company they work for and their
// unique personal data, including their name and social security number
employees[data.employeeId] = {
employeeId: data.employeeId,
occupation: data.occupation,
person: person,
company: company
};
employeeCount++;
},
// Provide a method to return the name of an employee by their employeeId - the
// data is looked up from the associated Person object
getName: function(employeeId) {
return employees[employeeId].person.name;
},
// Provide a method to return the occupation of an employee by their employeeId
getOccupation: function(employeeId) {
return employees[employeeId].occupation;
},
// Provide a method to return the address of the company an employee works for -
// the data is looked up from the associated Company object
getCountry: function(employeeId) {
var company = employees[employeeId].company;
return [company.name, company.address, company.country].join(", ");
},
// Provide a utlility method to tell us how many employees have been created
getTotalCount: function() {
return employeeCount;
}
};
}());
清单 6-8 中的 flyweight 代码可以如清单 6-9 所示使用,它复制了清单 6-7 的行为。应用 flyweight 模式的原始内存消耗对象中的重复数据越多,共享的对象就越多,因此减少了应用的内存占用,证明了这种设计模式的有用性。
清单 6-9。正在使用的轻量级模式
// Create four employee objects - note that two share the same company information, and two
// share the same ssId and name. Behind the scenes, the flyweight pattern from Listing 6-8
// ensures that repeated person and company data is stored in the most efficient way possible.
var denOdell = employee.add({
employeeId: 1456,
ssId: "1234-567-8901",
name: "Den Odell",
occupation: "Head of Web Development",
companyName: "AKQA",
companyAddress: "1 St. John's Lane, London",
companyCountry: "GB"
}),
steveBallmer = employee.add({
employeeId: 3,
ssId: "8376-940-1673",
name: "Steve Ballmer",
occupation: "Ex-CEO",
companyName: "Microsoft",
companyAddress: "1 Microsoft Way, Redmond, WA",
companyCountry: "US"
}),
billGates = employee.add({
employeeId: 1,
ssId: "7754-342-7584",
name: "Bill Gates",
occupation: "Founder",
companyName: "Microsoft",
companyAddress: "1 Microsoft Way, Redmond, WA",
companyCountry: "US"
}),
billGatesPhilanthropist = employee.add({
employeeId: 2,
ssId: "7754-342-7584",
name: "Bill Gates",
occupation: "Philanthropist",
companyName: "Gates Foundation",
companyAddress: "500 Fifth Avenue North, Seattle, WA",
companyCountry: "US"
});
// We've created three objects representing people by ssId and name - Den Odell, Steve Ballmer
// and Bill Gates
alert(personFactory.getPersonCount()); // 3
// We've created three objects representing companies by name, address and country - AKQA,
// Microsoft and the Gates Foundation
alert(companyFactory.getCompanyCount()); // 3
// We've created four objects representing employees, with two unique properties and two
// properties linking to existing person and company objects. The more employee objects we
// create with shared person and company data, the less data we're storing in our application
// and the more effective the flyweight pattern becomes
alert(employee.getTotalCount()); // 4
当您有大量具有相似共享属性名称-值对的对象时,最好使用 flyweight 模式,这些对象可以被分成更小的对象,这些对象之间通过引用共享数据,以便减少代码的内存占用,提高代码的效率。要在线关于 flyweight 模式的内容,请查阅以下资源:
- Addy Osmani 在《MSDN》杂志上发表的“用 Flyweight 模式管理应用资源”(via
http://bit.ly/flyweight_pattern
) - Gurpreet Singh 的“轻量级模式”(via
http://bit.ly/flyweight_js
)
混合模式
mixin 模式通过快速方便地将一组方法和属性从一个对象直接应用到另一个对象,或者直接应用到“类”的原型,使得所有对象实例都可以访问这些属性和方法,从而避免了对大量子类化和继承链的需求。尽管这听起来像是“黑客”,特别是对于那些从传统的面向对象背景开始接触 JavaScript 的开发人员来说,这种模式直接利用了 JavaScript 语言的优势及其对原型的使用,而不是其他语言所应用的严格的经典继承,并且如果小心使用,可以简化开发和代码维护。清单 6-10 中的代码展示了如何使用 mixin 模式简单快速地将一组通用方法应用到多个对象上。
清单 6-10。混合模式
// Define a mixin which enables debug logging, to be applied to any object or "class"
var loggingMixin = {
// Define a storage array for logs
logs: [],
// Define a method to store a message in the log
log: function(message) {
this.logs.push(message);
},
// Define a method to read out the stored logs
readLog: function() {
return this.logs.join("\n");
}
},
element,
header,
textField,
emailField;
// Function to apply methods and properties from one object to another, which we'll use to apply
// the mixin to other objects
function extendObj(obj1, obj2) {
var obj2Key;
for (obj2Key in obj2) {
if (obj2.hasOwnProperty(obj2Key)) {
obj1[obj2Key] = obj2[obj2Key];
}
}
return obj1;
}
// Define a singleton to which we will apply the mixin, though will function fine without it
element = {
allElements: [],
create: function(type) {
var elem = document.createElement(type);
this.allElements.push(elem);
// Use the mixin method log(), ensuring it exists first before calling it. If the mixin
// is not applied, then the method will still function fine
if (typeof this.log === "function") {
this.log("Created an element of type: " + type);
}
return elem;
},
getAllElements: function() {
return this.allElements;
}
};
// Define a simple "class" to which we will apply the mixin
function Field(type, displayText) {
this.type = type || "";
this.displayText = displayText || "";
// Ensure the mixin method log() exists before executing
if (typeof this.log === "function") {
this.log("Created an instance of Field");
}
}
Field.prototype = {
getElement: function() {
var field = document.createElement("input");
field.setAttribute("type", this.type);
field.setAttribute("placeholder", this.displayText);
if (typeof this.log === "function") {
this.log("Created a DOM element with placeholder text: " + this.displayText);
}
return field;
}
};
// Apply the mixin directly to the 'element' object by essentially copying over methods and
// properties from the mixin to the singleton
element = extendObj(element, loggingMixin);
// Apply the mixin to the Field "class" prototype, making its methods available to each object
// instance created from it
Field.prototype = extendObj(Field.prototype, loggingMixin);
// Create a new DOM element using the element.create() method
header = element.create("header");
// Create two object instances, both of which receive the getElement method from the prototype
textField = new Field("text", "Enter the first line of your address");
emailField = new Field("email", "Enter your email address");
// Add the elements stored in these objects to the current page
document.body.appendChild(textField.getElement());
document.body.appendChild(emailField.getElement());
// Output the logs stored via the mixin
alert(loggingMixin.readLog());
// Outputs the following - note how all the logs from each usage of the mixin are
// stored together:
/*
Created an element of type: header
Created an instance of Field
Created an instance of Field
Created a DOM element with placeholder text: Enter the first line of your address
Created a DOM element with placeholder text: Enter your email address
*/
如果您研究清单 6-10 中的代码,您可能会注意到一些意想不到的事情:尽管将 mixin 独立地应用于 singleton 和“class”,但是所有记录的数据都存储在一起。对任何包含该方法的对象调用readLog()
方法都会输出相同的结果。发生这种情况是因为当extendObj()
函数将 objectlike 属性从一个对象复制到另一个对象时,比如本例中的logs
数组(记住数组是 JavaScript 中的一种对象类型),这些是通过引用复制的,而不是实际的数据副本。每次从任何对象访问该属性时,都使用相同的属性,最初来自loggingMixin
对象。在这个例子中,我们希望看到所有的日志,所以这是有用的;然而,在您自己的代码中使用这种模式时,这可能不是您需要的结果。如果你想为复制的属性创建单独的副本,更新extendObj()
函数,如清单 6-11 所示。
清单 6-11。更新了 extendObj()函数以复制属性,而不是通过引用复制
// Update extendObj() to duplicate object-based properties rather than point to them
// by reference
function extendObj(obj1, obj2) {
var obj2Key,
value;
for (obj2Key in obj2) {
if (obj2.hasOwnProperty(obj2Key)) {
value = obj2[obj2Key];
// If the value being copied is an array, then copy a duplicate of that array using
// the slice() method
if (Object.prototype.toString.apply(value) === "[object Array]") {
obj1[obj2Key] = value.slice();
// Otherwise, if the value being copied in an object, and not an array, then copy
// across a duplicate of that object using a recursive call to this function
} else if (typeof obj2[obj2Key] === "object") {
obj1[obj2Key] = extendObj({}, value);
// Otherwise, copy across the value as usual
} else {
obj1[obj2Key] = value;
}
}
}
return obj1;
}
当您希望快速地将一组属性和方法直接从一个对象应用到另一个对象,或者应用到一个“类”以供其所有对象实例使用时,mixin 模式是最好的选择,而不需要求助于复杂的子类化和继承。要在线关于 mixin 模式的内容,请参考以下资源:
- 安格斯·克罗尔的《JavaScript Mixins 的新观点》(via
http://bit.ly/mixin_pattern
) - 《JavaScript Mixins:超越简单对象扩展》作者吴镇男·贝利(via
http://bit.ly/mixins_beyond
)
模块模式
模块模式可能是专业 JavaScript 开发人员最常用的模式。事实上,我们已经在前面的章节中两次讨论了模式的基础:第一次是在第一章中讨论公共、私有和受保护变量时,第二次是在第四章中讨论改进 JavaScript 压缩的方法时。这一切都基于自执行函数闭包,它允许我们创建一个沙盒代码区域,可以访问全局变量和函数,但不会将其中声明的变量或函数暴露给周围的作用域,除非使用return
语句显式声明。自执行函数的最简单示例如下所示:
(function() {
// Any variables or functions declared within this function aren't accessible outside it
}());
我们可以使用这种模式将我们的代码库划分成更小的、相关的代码块,我们称之为模块,这就是该模式的名字。这些模块中的每一个都应该清楚地说明它们对代码的其他部分的依赖性,如果有的话,这些部分应该作为参数传递给函数,如下所示:
(function($) {
// We very clearly define jQuery as a dependency for this 'module', making it available
// internally through the $ variable
}(jQuery));
Tip
在函数内访问 JavaScript 参数比在函数外访问全局变量更快,因为语言解释器不必执行离开当前函数范围来搜索变量的额外步骤。
模块模式的基本形式是通过使用函数闭包内的return
语句来传递回任何可能对其他模块或主应用本身有用的声明代码来完成的。清单 6-12 显示了模块模式的完整形式,基于上一章的清单 5-10。
清单 6-12。模块模式
// The module pattern is distinctive as it uses a combination of a self-executing anonymous
// function closure, with any dependencies passed in as parameters, and an optional return
// statement which allows code created within the closure to be made available externally
// Our only dependency is the 'document' object which contains the browser's cookie data. As an
// added security measure, we can include a final listed parameter named 'undefined' to which we
// never pass a value. This ensures that the variable named 'undefined' always contains an
// undefined value provided we always ensure we never pass in a value to this parameter.
// Otherwise it might be possible for other code, whether through malicious reasons or
// otherwise, to overwrite this value as it is not a reserved word in the language causing all
// kinds of havoc to the way our code behaves.
var cookie = (function(document, undefined) {
var allCookies = document.cookie.split(";"),
cookies = {},
cookiesIndex = 0,
cookiesLength = allCookies.length,
cookie;
for (; cookiesIndex < cookiesLength; cookiesIndex++) {
cookie = allCookies[cookiesIndex].split("=");
cookies[unescape(cookie[0])] = unescape(cookie[1]);
}
// Return any methods, properties or values that you wish to make available to the rest of
// your code base. In this case, the following two methods will be exposed through the
// 'cookie' variable, creating a singleton
return {
get: function(name) {
return cookies[name] || "";
},
set: function(name, value) {
cookies[name] = value;
document.cookie = escape(name) + "=" + escape(value);
}
};
// Pass in any dependencies at the point of function execution
}(document));
在通过单例对象结构利用命名空间的大型代码库中,模块模式的使用方式与我们看到的略有不同;在这种情况下,我们传入一个依赖项,然后在函数闭包结束时返回,使用该模块用新的属性和方法来增加单例。清单 6-13 显示了模块模式应用于名称空间的扩充,这是它最常见的用途之一。
清单 6-13。使用模块模式扩充名称空间
// Define a namespace which we will populate with code modules
var myData = {};
// Ajax module, added to the myData namespace through augmentation
// The namespace is passed in as a parameter and, once it has been augmented with new method, is
// finally returned back, overwriting the original namespace with the new, augmented one
myData = (function(myNamespace, undefined) {
// Add an 'ajax' object property to the namespace and populate it with related methods
myNamespace.ajax = {
get: function(url, callback) {
var xhr = new XMLHttpRequest(),
LOADED_STATE = 4,
OK_STATUS = 200;
xhr.onreadystatechange = function() {
if (xhr.readyState !== LOADED_STATE) {
return;
}
if (xhr.status === OK_STATUS) {
callback(xhr.responseText);
}
};
xhr.open("GET", url);
xhr.send();
}
};
// Return the new, augmented namespace back to the myData variable
return myNamespace;
// We can use the following defence mecahnism, which reverts to an empty object if the myData
// namespace object does not yet exist. This is useful when you have modules split over several
// files in a large namespace and you're unsure if the namespace passed in has been initialized
// elsewhere before
}(myData || {}));
// Cookies module, added to the myData namespace through augmentation
// As before, the namespace is passed in, augmented, and then returned, overwriting the original
// namespace object. At this point, the myData namespace contains the Ajax module code
myData = (function(myNamespace, undefined) {
// Add a 'cookies' object property to the namespace and populate it with related methods
myNamespace.cookies = {
get: function(name) {
var output = "",
escapedName = escape(name),
start = document.cookie.indexOf(escapedName + "="),
end = document.cookie.indexOf(";", start);
end = end === -1 ? (document.cookie.length - 1) : end;
if (start >=0) {
output = document.cookie.substring(start + escapedName.length + 1, end);
}
return unescape(output);
},
set: function(name, value) {
document.cookie = escape(name) + "=" + escape(value);
}
};
return myNamespace;
}(myData || {}));
// Execute methods directly through the myData namespace object, which now contains both Ajax
// and Cookies modules
myData.ajax.get("/user/12345", function(response) {
alert("HTTP GET response received. User data: " + response);
});
myData.cookies.set("company", "AKQA");
myData.cookies.set("name", "Den Odell");
alert(myData.cookies.get("company")); // AKQA
alert(myData.cookies.get("name")); // Den Odell
当您希望将大型代码库分解成更小的、可管理的、自包含的部分时,最好使用模块模式,每个部分都有一组清晰的依赖项和定义明确的目的。由于它们的沙箱特性,它们的自执行功能块也是通过混淆和编译创建较小文件的主要领域,我们在第四章中讨论过这些主题。在第九章中,我们将会看到一种使用异步模块定义(AMD) API 来定义模块并将模块加载到 JavaScript 代码中的替代方法,但是现在如果你想在线关于模块模式的内容,请查阅以下资源:
- 本·切瑞的《JavaScript 模块模式:深入研究》(via
http://bit.ly/module_pattern
) - 雷蒙德·卡姆登的《JavaScript 设计模式——揭示模块模式》(via
http://bit.ly/revealing_module
)
代理模式
代理模式是一种定义代理或替代对象或方法的模式,用于替换或增强现有的对象或方法,以提高其性能或添加额外的功能,而不会影响已经使用该对象或方法的代码的其他部分。我和许多其他专业 JavaScript 开发人员使用这种模式的最常见方式是在不改变方法或函数名的情况下包装现有的方法或函数,如清单 6-14 所示。
清单 6-14。代理模式
// To proxy the myData.cookies.get() method from Listing 6-13, we begin by storing the current
// method in a variable
var proxiedGet = myData.cookies.get;
// Override the get() method with a new function which proxies the original and augments its
// behavior
myData.cookies.get = function() {
// Call the proxied (original) method to get the value it would have produced
var value = proxiedGet.apply(this, arguments);
// Do something with the value returned from the proxied method
value = value.toUpperCase();
// Return the manipulated value with the same type as the proxied method, so that the use of
// this new method does not break any existing calls to it
return value;
};
代理模式的一个变种叫做虚拟代理,它可以通过延迟对象实例化,从而延迟构造函数的执行,直到来自对象实例的方法被实际调用,来提高性能和内存使用,如清单 6-15 所示。
清单 6-15。虚拟代理模式
// Define a "class" for constructing an object representing a simple form field
function FormField(type, displayText){
this.type = type || "text";
this.displayText = displayText || "";
// Create and initialize a form field DOM element
this.element = document.createElement("input");
this.element.setAttribute("type", this.type);
this.element.setAttribute("placeholder", this.displayText);
}
// Define two methods for object instances to inherit
FormField.prototype = {
getElement: function() {
return this.element;
},
isValid: function() {
return this.element.value !== "";
}
};
// Now replace the FormField "class" with a proxy that implements the same methods, yet delays
// calling the original constructor function until those methods are actually called, saving on
// memory resources and improving performance
// Optionally, use the module pattern to localise the scope of the proxy "class", passing in the
// original FormField "class" and returning the proxied version of it
FormField = (function(FormField) {
// Define a proxy constructor, similar to the original FormField "class"
function FormFieldProxy(type, displayText) {
this.type = type;
this.displayText = displayText;
}
FormFieldProxy.prototype = {
// Define a property to store the reference to the object instance of the original
// "class" once instantiated
formField: null,
// Define a new 'initialize' method whose task it is to create the object instance of
// FormField if it does not already exist and execute the constructor function from the
// original "class"
initialize: function() {
if (!this.formField) {
this.formField = new FormField(this.type, this.displayText);
}
},
// Proxy the original methods with new ones that call the intialize() method to
// instantiate the FormField "class" only when one of these methods are called
getElement: function() {
this.initialize();
return this.formField.getElement();
},
isValid: function() {
this.initialize();
return this.formField.isValid();
}
};
// Return the proxied "class" to replace the original with
return FormFieldProxy;
}(FormField));
// Create two object instances, both of which will actually be calling the proxy rather than the
// original "class", meaning the DOM elements will not be created at this stage, saving memory
// and improving performance
var textField = new FormField("text", "Enter the first line of your address"),
emailField = new FormField("email", "Enter your email address");
// Add the elements stored in these objects to the current page when loaded - at this point the
// getElement() method is called, which in turn calls initialize(), creating an instance of the
// original "class" and executing its constructor function which performs the actual DOM element
// creation. This ensures the memory used to store the DOM element is only taken up at the exact
// point it is required
window.addEventListener("load", function() {
document.body.appendChild(textField.getElement());
document.body.appendChild(emailField.getElement());
}, false);
// Execute another method from the proxy, this time the object instance of the original "class"
// won't be recreated and the stored instance will be used instead
alert(emailField.isValid()); // false
对于可能同时进行多个调用的对象,可以通过延迟或分组调用(如 Ajax 请求或其他与网络相关的调用)来进一步扩展代理模式,以提高性能和减少内存。
当您需要覆盖一个对象或“类”上的特定方法的行为时,最好使用代理模式,或者应用代理模式来提高现有“类”的性能,以便在调用它的一个方法之前,它不会被实际实例化。要在线阅读有关代理模式的更多信息,请查看以下资源:
- 关于使用 jQuery 的“JavaScript 中的代理模式”(通过
http://bit.ly/proxy_pattern
) - “JavaScript 设计模式:代理”,作者 Joseph Zimmerman,Adobe Developer Connection(通过
http://bit.ly/proxy_js
)
摘要
在这一章中,我们研究了结构化设计模式,你可以在适当的时候使用这些模式来帮助你构建大型的 JavaScript 应用并提高它们的性能。这些是 JavaScript 开发的瑞士军刀中的工具,但是像所有工具一样,您需要知道何时何地最好地使用它们。记住那句古老的格言:“当你有一把锤子时,一切看起来都像钉子。”熟悉本章中的模式及其用例,并确保在代码中认识到需要使用设计模式之前,不要使用它。
在下一章中,我们将会看到一些行为设计模式,它们可以用来简化 JavaScript 应用代码库中不同对象之间的通信。
七、设计模式:行为型
在这一章中,我们将继续关注设计模式,重点是行为设计模式。我们在第五章中看到的创造性设计模式侧重于对象创建,而我们在前一章中看到的结构性设计模式侧重于对象结构,行为设计模式侧重于帮助代码库中多个对象之间的通信。这里的要点是让你更容易理解你的代码是如何作为一个整体运作的,而不是仅仅关注单个对象的构造和结构。让我们一起来看看八种行为设计模式,你可能会发现它们在你的代码中很有用,还有一些例子。
责任链模式
当基于同一个“类”的多个对象中的任何一个可以处理一个请求或方法调用时,就使用责任链模式。请求被发送给一个对象,如果它不是处理请求的最合适的对象,它就把请求传递给另一个对象来处理。所以它会一直继续下去,直到一个对象处理了请求,并通过对象链将操作的结果传递回原始请求或方法调用。链中的每个对象都知道另一个对象,如果链中的下一个对象不能完成请求,那么它可以处理请求。这种模式最适用于共同形成某种层次结构的对象,如清单 7-1 所示,你不希望将的实现暴露给代码的其他部分。
清单 7-1。责任链模式
// Define an object listing different levels of logging in a system - info, warn, and error –
// each indicating something more severe than the last
var LogLevel = {
INFO: 'INFO',
WARN: 'WARN',
ERROR: 'ERROR'
},
log;
// Define a "class" to create appropriately formatted log messages for different logging levels
function LogFormatter(logLevel) {
this.logLevel = logLevel;
}
LogFormatter.prototype = {
// Define a property to store the successor to this object instance in the chain
// of responsibility
nextInChain: null,
// Define a method to set the successor in the chain of responsibility
setNextInChain: function(next) {
this.nextInChain = next;
},
// Define a method to create an appropriately formatted log message based on the current
// logging level
createLogMessage: function(message, logLevel) {
var returnValue;
// If the logging level assigned to the current object instance is the same as that
// passed in, then format the log message
if (this.logLevel === logLevel) {
// Format the log message as appropriate according to the logging level
if (logLevel === LogLevel.ERROR) {
returnValue = logLevel + ": " + message.toUpperCase();
} else if (logLevel === LogLevel.WARN) {
returnValue = logLevel + ": " + message;
} else {
returnValue = message;
}
// If the logging level assigned to the current object instance does not match that
// passed in, then pass the message onto the next object instance in the chain
// of responsibility
} else if (this.nextInChain) {
returnValue = this.nextInChain.createLogMessage(message, logLevel);
}
return returnValue;
}
};
// Define a singleton we can use for storing and outputting logs in a system
log = (function() {
// Define a storage array for log messages
var logs = [],
// Create object instances representing the three levels of logging - info, warn,
// and error
infoLogger = new LogFormatter(LogLevel.INFO),
warnLogger = new LogFormatter(LogLevel.WARN),
errorLogger = new LogFormatter(LogLevel.ERROR),
// Set the 'error' logging level to be the first and highest level in our chain of
// responsibility, which we'll store in the 'logger' variable
logger = errorLogger;
// Set the chain of responsibility hierarchy using the setNextInChain() method on each
// object instance - we're assuming that the 'error' logging level is the most important and
// is first in the chain
// The next in the logging hierarchy after 'error' should be 'warn' as this is
// less important
errorLogger.setNextInChain(warnLogger);
// The next in the chain after the 'warn' logging level should be 'info' as this is the
// least important level
warnLogger.setNextInChain(infoLogger);
return {
// Define a method for reading out the stored log messages
getLogs: function() {
return logs.join("\n");
},
// Define a method for formatting a log message appropriately according to its
// logging level
message: function(message, logLevel) {
// We call the createLogMessage() method on the first object instance in our
// hierarchy only, which in turn calls those further down the chain if it does not
// handle the specified logging level itself. The message passes further down the
// chain of responsibility until it reaches an object instance who can handle the
// specific logging level
var logMessage = logger.createLogMessage(message, logLevel);
// Add the formatted log message to the storage array
logs.push(logMessage);
}
};
}());
// Execute the message() method of the 'log' singleton, passing in a message and the logging
// level. The first object in the chain of responsibility handles the 'error' logging level, so
// the message is not passed down the chain of responsibility and is returned by the
// errorLogger object
log.message("Something vary bad happened", LogLevel.ERROR);
// This message is passed through the errorLogger object to the warnLogger object through the
// chain of responsibility since the errorLogger object is only told to handle messages with the
// 'error' logging level
log.message("Something bad happened", LogLevel.WARN);
// This message is passed through the errorLogger object to the warnLogger object, and onto the
// infoLogger object which is the one handling 'info' type log messages
log.message("Something happened", LogLevel.INFO);
// Output the stored logs
alert(log.getLogs());
// Outputs the following:
/*
ERROR: SOMETHING VERY BAD HAPPENED
WARN: Something bad happened
Something happened
*/
当您有一个对象层次结构,并且希望在整个代码中访问它而不暴露这个结构时,最好使用责任链模式。要了解有关责任链模式的更多信息,请查看以下在线资源:
- 约瑟夫·齐默曼的《JavaScript 设计模式:责任链》(via
http://bit.ly/chain_pattern
) - 维基百科上的“责任链模式”(via
http://bit.ly/chain_wiki
)
命令模式
command 模式用于在调用代码和对象的特定方法之间提供一个抽象层,确保所有调用都是通过该对象上的一个公共方法进行的,通常命名为run()
或execute()
。使用这种模式提供了在不影响调用代码的情况下更改底层代码和 API 的能力。清单 7-2 中的例子显示了一个简单的命令模式的例子,它把要执行的方法名和参数传递给一个单独的execute()
方法。
清单 7-2。命令模式
var cookie = (function() {
var allCookies = document.cookie.split(";"),
cookies = {},
cookiesIndex = 0,
cookiesLength = allCookies.length,
cookie;
for (; cookiesIndex < cookiesLength; cookiesIndex++) {
cookie = allCookies[cookiesIndex].split("=");
cookies[unescape(cookie[0])] = unescape(cookie[1]);
}
return {
get: function(name) {
return cookies[name] || "";
},
set: function(name, value) {
cookies[name] = value;
document.cookie = escape(name) + "=" + escape(value);
},
remove: function(name) {
// Remove the cookie by removing its entry from the cookies object and setting its
// expiry date in the past
delete cookies[name];
document.cookie = escape(name) + "=; expires=Thu, 01 Jan 1970 00:00:01 GMT;";
},
// Supply an execute() method, which is used to abstract calls to other methods so that
// other method names can be changed as needs be in future without affecting the API
// available to the rest of the code - provided this execute() method continues to exist
execute: function(command, params) {
// The command parameter contains the method name to execute, so check that the
// method exists and is a function
if (this.hasOwnProperty(command) && typeof this[command] === "function") {
// If the method exists and can be executed, then execute it, passing across the
// supplied params
return this[command].apply(this, params);
}
}
};
}());
// Set a cookie using the execute() method to indirectly call the set() method of the cookie
// singleton and supplying parameters to pass onto that method
cookie.execute("set", ["name", "Den Odell"]);
// Check that the cookie was set correctly using execute() with the "get" method
alert(cookie.execute("get", ["name"])); // Den Odell
命令模式也可以在需要“撤消”功能的应用的上下文中使用,其中执行的语句可能需要在将来的某个时间点被撤销,例如,在文字处理 web 应用的上下文中。在这种情况下,命令是通过一个命令执行对象来传递的,该对象使用这个抽象来存储适当的函数,以反转传递给它的方法调用,如清单 7-3 所示,它显示了一个简单的命令执行对象和一个基于清单 7-2 中的代码使用 cookies 的例子。
清单 7-3。支持 web 应用中多级撤消的命令执行对象
// Create a singleton for allowing execution of other methods and providing the ability to
// 'undo' the actions of those methods
var command = (function() {
// Create an array to store the 'undo' commands in order, also known as a 'stack'
var undoStack = [];
return {
// Define a method to execute a supplied function parameter, storing a second function
// parameter for later execution to 'undo' the action of the first function
execute: function(command, undoCommand) {
if (command && typeof command === "function") {
// If the first parameter is a function, execute it, and add the second
// parameter to the stack in case the command needs to be reversed at some point
// in future
command();
undoStack.push(undoCommand);
}
},
// Define a method to reverse the execution of the last command executed, using the
// stack of 'undo' commands
undo: function() {
// Remove and store the last command from the stack, which will be the one most
// recently added to it. This will remove that command from the stack, reducing the
// size of the array
var undoCommand = undoStack.pop();
if (undoCommand && typeof undoCommand === "function") {
// Check the command is a valid function and then execute it to effectively
// 'undo' the last command
undoCommand();
}
}
};
}());
// Wrap each piece of functionality that can be 'undone' in a call to the command.execute()
// method, passing the command to execute immediately as the first parameter, and the function
// to execute to reverse that command as the second parameter which will be stored until such
// point as it is needed
command.execute(function() {
// Using the code from Listing 7-2, set a cookie - this will be executed immediately
cookie.execute("set", ["name", "Den Odell"]);
}, function() {
// The reverse operation of setting a cookie is removing that cookie - this operation will
// be stored for later execution if the command.undo() method is called
cookie.execute("remove", ["name"]);
});
// Execute a second piece of functionality, setting a second cookie
command.execute(function() {
cookie.execute("set", ["company", "AKQA"]);
}, function() {
cookie.execute("remove", ["company"]);
});
// Check the value of the two cookies
alert(cookie.get("name")); // Den Odell
alert(cookie.get("company")); // AKQA
// Reverse the previous operation, removing the 'company' cookie
command.undo();
// Check the value of the two cookies
alert(cookie.get("name")); // Den Odell
alert(cookie.get("company")); // "" (an empty string), since the cookie has now been removed
// Reverse the first operation, removing the 'name' cookie
command.undo();
// Check the value of the two cookies
alert(cookie.get("name")); // "", since the cookie has now been removed
alert(cookie.get("company")); // ""
当您需要从代码的其余部分中抽象出特定的方法名时,最好使用 command 模式。通过按名称引用方法,就像存储在字符串中一样,底层代码可以随时更改,而不会影响代码的其余部分。要在线阅读有关命令模式的更多信息,请查阅以下资源:
- 《JavaScript 设计模式:命令》,作者 Joseph Zimmerman,Adobe Developer Connection(通过
http://bit.ly/command_pattern
) - 彼得·米肖的《JavaScript 中的命令模式》(经由
http://bit.ly/command_js
)
迭代器模式
顾名思义,交互模式允许应用中的代码在一组数据上迭代或循环,而不需要知道数据是如何在内部存储或构造的。迭代器通常提供一组标准方法,用于移动到集合中的下一项,并检查当前项是集合中的第一项还是最后一项。
清单 7-4 显示了一个通用“类”的例子,它可以迭代Array
类型和Object
类型的数据。这个迭代器的实例可以使用提供的方法rewind()
、current()
、next()
、hasNext()
和first()
手动操作和查询,或者可以使用其each()
方法提供自动自迭代,其中函数回调参数为数据集中的每一项执行一次,提供了一个有用的for
循环的等效形式。
清单 7-4。迭代器模式
// Define a generic iterator "class" for iterating/looping over arrays or object-like data
// structures
function Iterator(data) {
var key;
// Store the supplied data in the 'data' property
this.data = data || {};
this.index = 0;
this.keys = [];
// Store an indicator to show whether the supplied data is an array or an object
this.isArray = Object.prototype.toString.call(data) === "[object Array]";
if (this.isArray) {
// If the supplied data is an array, store its length for fast access
this.length = data.length;
} else {
// If object data is supplied, store each property name in an array
for (key in data) {
if (data.hasOwnProperty(key)) {
this.keys.push(key);
}
}
// The length of the property name array is the length of the data to iterate over,
// so store this
this.length = this.keys.length;
}
}
// Define a method to reset the index, effectively rewinding the iterator back to the start of
// the data
Iterator.prototype.rewind = function() {
this.index = 0;
};
// Define a method to return the value stored at the current index position of the iterator
Iterator.prototype.current = function() {
return this.isArray ? this.data[this.index] : this.data[this.keys[this.index]];
};
// Define a method to return the value stored at the current index position of the iterator,
// and then advance the index pointer to the next item of data
Iterator.prototype.next = function() {
var value = this.current();
this.index = this.index + 1;
return value;
};
// Define a method to indicate whether the index position is at the end of the data
Iterator.prototype.hasNext = function() {
return this.index < this.length;
};
// Define a method to reset the index of the iterator to the start of the data and return
// the first item of data
Iterator.prototype.first = function() {
this.rewind();
return this.current();
};
// Define a method to iterate, or loop, over each item of data, executing a callback
// function each time, passing in the current data item as the first parameter to
// that function
Iterator.prototype.each = function(callback) {
callback = typeof callback === "function" ? callback : function() {};
// Iterate using a for loop, starting at the beginning of the data (achieved using the
// rewind() method) and looping until there is no more data to iterate over (indicated
// by the hasNext() method)
for (this.rewind(); this.hasNext();) {
// Execute the callback function each time through the loop, passing in the current
// data item value and incrementing the loop using the next() method
callback(this.next());
}
};
清单 7-4 中的代码可以像清单 7-5 中所示的那样使用,它展示了使用通用迭代器“class”对存储的数据进行迭代和循环的不同方式
清单 7-5。正在使用的迭代器模式
// Define an object and an array which we can use to iterate over
var user = {
name: "Den Odell",
occupation: "Head of Web Development",
company: "AKQA"
},
daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
// Create instances of the Iterator "class" using these two different types of data
userIterator = new Iterator(user),
daysOfWeekIterator = new Iterator(daysOfWeek),
// Create three arrays for storing outputs of interations to be displayed later
output1 = [],
output2 = [],
output3 = [];
// The userIterator is ready for use, so let's use a for loop to iterate over the stored data –
// note how we don't need to supply the first argument to the for loop as the data is already
// reset and initialized in its start position, and we don't require the last argument since the
// next() method call within the for loop body performs the advancement of the index position
// for us
for (; userIterator.hasNext();) {
output1.push(userIterator.next());
}
// Since we iterated over an object, the resulting data consists of the values stored in each of
// the object's properties
alert(output1.join(", ")); // Den Odell, Head of Web Development, AKQA
// Before iterating over the same data again, its index must be rewound to the start
userIterator.rewind();
// Iterate over the object properties using a while loop, which continues to execute until the
// iterator has no further data items
while (userIterator.hasNext()) {
output2.push(userIterator.next());
}
alert(output2.join(", ")); // Den Odell, Head of Web Development, AKQA
// Iterate over the array data using the Iterator's built-in each() method - using this
// approach requires no manual work to manipulate the position of the index, simply pass a
// callback function
daysOfWeekIterator.each(function(item) {
output3.push(item);
});
alert(output3.join(", ")); // Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
当您需要为代码的其余部分提供一种标准方式来遍历复杂的数据结构,而又不暴露数据最终是如何存储或表示的时候,最好使用迭代器模式。要在线了解迭代器模式的更多信息,请查阅以下在线资源:
- DoFactory 上的“JavaScript 迭代器设计模式”(通过
http://bit.ly/iterator_pattern
) - 维基百科上的“迭代器模式”(via
http://bit.ly/iterator_wiki
)
观察者模式
观察者模式用于由许多独立的代码模块组成的大型代码库中,这些代码模块相互依赖或者必须相互通信。在这样的代码库中,从一个模块到其他模块的硬编码引用提供了所谓的紧耦合,即需要明确了解系统中的每个其他模块,以便整个代码能够一起正确运行。然而,理想情况下,大型代码库中的模块应该是松散耦合的。没有明确地引用其他模块;相反,整个代码库都会触发和监听系统范围的事件,就像标准 DOM 事件处理的定制版本一样。
例如,如果一个模块负责通过 Ajax 进行的所有客户端-服务器通信,而另一个模块负责在将表单传输到服务器之前对其进行渲染和验证,那么当用户成功提交和验证表单时,代码库可以触发一个全局的“表单已提交”事件以及表单中的数据,通信模块将监听这些数据。然后,通信模块将执行其任务,向服务器发送数据,并在自身触发“接收到响应”事件之前接收其响应,表单模块将监听该事件。收到该事件后,表单模块可以显示一条消息,表明表单已成功提交,所有这些都不需要任何一个模块相互了解——每个模块唯一知道的是一组全局配置的事件名,系统中的任何模块都可以触发或响应这些事件名。
实现观察者模式的系统必须有三个对系统代码库可用的全局方法:publish()
,它通过名称触发事件,传递任何可选数据;subscribe()
,它允许模块分配一个函数,当特定的命名事件被触发时执行;和unsubscribe()
,它取消了函数的设计,这样当指定的事件被触发时,它将不再被执行。清单 7-6 中的代码演示了一个简单的对象,它可以在您的应用中全局使用,以实现 observer 模式中的这些方法。
清单 7-6。观察者模式
// Define an object containing global publish(), subscribe(), and unsubscribe() methods to
// implement the observer pattern
var observer = (function() {
// Create an object for storing registered events in by name along with the associatedw
// callback functions for any part of the full code base that subscribes to those
// event names
var events = {};
return {
// Define the subscribe() method, which stores a function along with its associated
// event name to be called at some later point when the specific event by that name
// is triggered
subscribe: function(eventName, callback) {
// If an event by the supplied name has not already been subscribed to, create an
// array property named after the event name within the events object to store
// functions to be called at a later time when the event by that name is triggered
if (!events.hasOwnProperty(eventName)) {
events[eventName] = [];
}
// Add the supplied callback function to the list associated to the specific
// event name
events[eventName].push(callback);
},
// Define the unsubscribe() method, which removes a given function from the list of
// functions to be executed when the event by the supplied name is triggered
unsubscribe: function(eventName, callback) {
var index = 0,
length = 0;
if (events.hasOwnProperty(eventName)) {
length = events[eventName].length;
// Cycle through the stored functions for the given event name and remove the
// function matching that supplied from the list
for (; index < length; index++) {
if (events[eventName][index] === callback) {
events[eventName].splice(index, 1);
break;
}
}
}
},
// Define the publish() method, which executes all functions associated with the given
// event name in turn, passing to each the same optional data passed as arguments to
// the method
publish: function(eventName) {
// Store all parameters but the first passed to this function as an array
var data = Array.prototype.slice.call(arguments, 1),
index = 0,
length = 0;
if (events.hasOwnProperty(eventName)) {
length = events[eventName].length;
// Cycle through all of the functions associated with the given event name and
// execute them each in turn, passing along any supplied parameters
for (; index < length; index++) {
events[eventName][index].apply(this, data);
}
}
}
};
}());
清单 7-7 中的代码演示了如何使用清单 7-6 中给出的观察者模式的publish()
、subscribe()
和unsubscribe()
方法。假设它运行在一个 HTML 页面的上下文中,该页面包含一个具有有效action
属性的<form id=
"my-form"
>
标签,并且包含几个表示表单字段的<input type=
"text"
>
标签。
清单 7-7。正在使用的观察者模式
// Define a module for Ajax communication, with a dependency on the observer object
// from Listing 7-6
(function(observer) {
// Define a function for performing an Ajax POST based on a supplied URL, form-encoded data
// string, and a callback function to execute once a response has been received from
// the server
function ajaxPost(url, data, callback) {
var xhr = new XMLHttpRequest(),
STATE_LOADED = 4,
STATUS_OK = 200;
xhr.onreadystatechange = function() {
if (xhr.readyState !== STATE_LOADED) {
return;
}
if (xhr.status === STATUS_OK) {
// Execute the supplied callback function once a successful response has been
// received from the server
callback(xhr.responseText);
}
};
xhr.open("POST", url);
// Inform the server that we will be sending form-encoded data, where names and values
// are separated by the equals sign (=) character, and name/value pairs are separated by
// the ampersand (&) character
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
// POST the data to the server
xhr.send(data);
}
// Subscribe to the global, custom "form-submit" event and, when this event is triggered by
// another module in the code base, make a Ajax POST request to the server using the
// supplied URL and data. Trigger the "ajax-response" event when complete, passing in the
// server's response from the Ajax call
observer.subscribe("form-submit", function(url, formData) {
ajaxPost(url, formData, function(response) {
// Trigger the global "ajax-response" event, passing along the data returned from
// the server during the Ajax POST
observer.publish("ajax-response", response);
});
});
}(observer));
// Define a module for handling submission of a simple form on the page containing text fields
// only with an ID of "my-form". Note that neither of the modules in this code listing reference
// each other, they only reference the observer object which handles all communication between
// modules in the system. Each module is said to be "loosely-coupled" as it has no hardcoded
// dependency on any other module
(function(observer) {
// Get a reference to a form on the current HTML page with ID "my-form"
var form = document.getElementById("my-form"),
// Get the "action" attribute value from the form, which will be the URL we perform an
// Ajax POST to
action = form.action,
data = [],
// Get a reference to all <input> fields within the form
fields = form.getElementsByTagName("input"),
index = 0,
length = fields.length,
field,
// Create a HTML <p> tag for use as a thank you message after form submission has
// taken place
thankYouMessage = document.createElement("p");
// Define a function to execute on submission of the form which uses the observer pattern to
// submit the form field data over Ajax
function onFormSubmit(e) {
// Prevent the default behavior of the submit event, meaning a normal in-page HTML form
// submission will not occur
e.preventDefault();
// Loop through all <input> tags on the page, creating an array of name/value pairs of
// the data entered into the form
for (; index < length; index++) {
field = fields[index];
data.push(escape(field.name) + "=" + escape(field.value));
}
// Trigger the global "form-submit" event on the observer object, passing it the URL to
// use for the Ajax POST and the form data to be sent. The Ajax communication module is
// listening for this event and will handle everything pertaining to the submission of
// that data to the server.
observer.publish("form-submit", action, data.join("&"));
}
// Wire up the onFormSubmit() function to the "submit" event of the form
form.addEventListener("submit", onFormSubmit, false);
// Subscribe to the global, custom "ajax-response" event, and use the server's response data
// sent along with the event to populate a Thank You message to display on the page beside
// the form
observer.subscribe("ajax-response", function(response) {
thankYouMessage.innerHTML = "Thank you for your form submission.<br>The server responded with: " + response;
form.parentNode.appendChild(thankYouMessage);
});
}(observer));
observer 模式允许您删除代码中模块之间的硬编码引用,以维护自定义的系统范围的事件列表。随着代码库的增长和模块数量的增加,可以考虑使用这种模式来简化代码,并将模块彼此分离。请注意,如果在您的模块之一中发生错误,并且没有触发应该触发的事件,错误的来源可能不会立即显现出来,并且可能需要额外的调试。我建议在开发期间将您自己的调试日志记录添加到您的 observer 对象中,以允许您更容易地跟踪代码中的事件。
当您希望将模块松散地耦合在一起以减少杂乱无章的代码时,最好使用观察者模式。要在线阅读有关这种流行模式的更多信息,请查看以下资源:
- “JavaScript 设计模式:观察者”,作者 Joseph Zimmerman,Adobe Developer Connection(通过
http://bit.ly/observer_pattern
) - 罗布·多德森的《JavaScript 设计模式:观察者》(经由
http://bit.ly/observer_js
)
中介模式
中介模式是观察者模式的一种变体,在一个关键方面有所不同。观察者模式定义了一个全局对象,用于在整个系统中发布和订阅事件,而中介模式定义了用于特定目的的本地化对象,每个对象都有相同的publish()
、subscribe()
和unsubscribe()
方法。随着您的代码库变得越来越大,observer 模式被证明会产生大量难以管理的事件,因此可以使用 mediator 模式将这个较大的事件列表分成较小的组。观察者模式是通过一个全局单例对象实现的,而中介者模式是通过使用一个“类”来实现的,因此可以根据需要创建尽可能多的对象实例来支持代码的特性。清单 7-8 显示了用来在你的代码中实现中介模式的“类”。注意与清单 7-6 中为实现观察者模式而创建的对象的相似之处。
清单 7-8。中介模式
// Define a "class" containing publish(), subscribe(), and unsubscribe() methods to implement
// the mediator pattern. Note the similarilty to the observer pattern, the only difference is
// that we are creating a "class" here for creating object instances from later, and that we
// initialize the events array afresh for each object instance to avoid all instances sharing
// the same array in memory.
function Mediator() {
this.events = {};
}
Mediator.prototype.subscribe = function(eventName, callback) {
if (!this.events.hasOwnProperty(eventName)) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
};
Mediator.prototype.unsubscribe = function(eventName, callback) {
var index = 0,
length = 0;
if (this.events.hasOwnProperty(eventName)) {
length = this.events[eventName].length;
for (; index < length; index++) {
if (this.events[eventName][index] === callback) {
this.events[eventName].splice(index, 1);
break;
}
}
}
};
Mediator.prototype.publish = function(eventName)
{
var data = Array.prototype.slice.call(arguments, 1),
index = 0,
length = 0;
if (this.events.hasOwnProperty(eventName)) {
length = this.events[eventName].length;
for (; index < length; index++) {
this.events[eventName][index].apply(this, data);
}
}
};
清单 7-8 中的中介模式可以如清单 7-9 所示实现,创建中介对象来表示代码中的特定特性,并允许代码库中有模块。假设它运行在包含一个<form id=
"my-form"
>
标签的 HTML 页面的上下文中,该标签包含几个表示表单字段的<input type=
"text"
>
标签。
清单 7-9。正在使用的中介模式
// Define two mediators for our code base, one pertaining to code for a forms feature, and
// another to enable a message logging feature.
// The formsMediator will feature two events: "form-submit", and "ajax-response", whereas
// the loggingMediator will feature three events, "log", "retrieve-log", and "log-retrieved".
// Note how we're able to separate events for different features in our code using the
// mediator pattern
var formsMediator = new Mediator(),
loggingMediator = new Mediator();
// Define a module for Ajax communication which POSTs some supplied data to the server when a
// "form-submit" event is triggered within the formsMediator
(function(formsMediator) {
function ajaxPost(url, data, callback) {
var xhr = new XMLHttpRequest(),
STATE_LOADED = 4,
STATUS_OK = 200;
xhr.onreadystatechange = function() {
if (xhr.readyState !== STATE_LOADED) {
return;
}
if (xhr.status === STATUS_OK) {
callback(xhr.responseText);
}
};
xhr.open("POST", url);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send(data);
}
formsMediator.subscribe("form-submit", function(url, formData) {
ajaxPost(url, formData, function(response) {
formsMediator.publish("ajax-response", response);
});
});
}(formsMediator));
// Define a module for handling submission of a simple form on the page containing text fields
// only with an ID of "my-form". When the form is submitted, the "form-submit" event is
// triggered within the formsMediator
(function(formsMediator) {
var form = document.getElementById("my-form"),
action = form.action,
data = [],
fields = form.getElementsByTagName("input"),
index = 0,
length = fields.length,
field,
thankYouMessage = document.createElement("p");
function onFormSubmit(e) {
e.preventDefault();
for (; index < length; index++) {
field = fields[index];
data.push(escape(field.name) + "=" + escape(field.value));
}
formsMediator.publish("form-submit", action, data.join("&"));
}
form.addEventListener("submit", onFormSubmit, false);
formsMediator.subscribe("ajax-response", function(response) {
thankYouMessage.innerHTML = "Thank you for your form submission.<br>The server responded with: " + response;
form.parentNode.appendChild(thankYouMessage);
});
}(formsMediator));
// Define a module for logging messages within the system to aid with debugging of issues that
// might occur. Uses the loggingMediator to separate the logging feature of the code base
// separate from that handling the form submission with the formsMediator
(function(loggingMediator) {
// Create an array to store the logs
var logs = [];
// When the "log" event is triggered on the loggingMediator, add an object to the logs
// containing a supplied message and the date / time that the message was received at
loggingMediator.subscribe("log", function(message) {
logs.push({
message: message,
date: new Date()
});
});
// When the "retrieve-log" event is triggered on the loggingMediator, trigger the
// "log-retrieved" event, passing along the current state of the stored logs
loggingMediator.subscribe("retrieve-log", function() {
loggingMediator.publish("log-retrieved", logs);
});
}(loggingMediator));
// Define a module which allows the stored logs in the loggingMediator to be displayed on screen
(function(loggingMediator) {
// Create a button which, when clicked, will display the current state of the log
var button = document.createElement("button");
button.innerHTML = "Show logs";
button.addEventListener("click", function() {
// Trigger the "retrieve-log" event within the loggingMediator. This triggers the
// "log-retrieved" event, passing along the current state of the logs
loggingMediator.publish("retrieve-log");
}, false);
// When the "log-retrieved" event occurs, display the logs on screen
loggingMediator.subscribe("log-retrieved", function(logs) {
var index = 0,
length = logs.length,
ulTag = document.createElement("ul"),
liTag = document.createElement("li"),
listItem;
// Loop through each log in the list of logs, rendering the date / time and message
// stored within a <li> tag
for (; index < length; index++) {
listItem = liTag.cloneNode(false);
listItem.innerHTML = logs[index].date.toUTCString() + ": " + logs[index].message;
ulTag.appendChild(listItem);
}
// Add the <ul> tag containing all the <li> tags representing the log data to the bottom
// of the page
document.body.appendChild(ulTag);
});
// Add the button to the bottom of the current page
document.body.appendChild(button);
}(loggingMediator));
// Define a module which logs events that occur within the formsMediator. This is the only
// module in this example to use more than one mediator
(function(formsMediator, loggingMediator) {
// Use the loggingMediator's "log" events to log the URL the form is submitted to when the
// "form-submit" event is triggered within the formsMediator
formsMediator.subscribe("form-submit", function(url) {
loggingMediator.publish("log", "Form submitted to " + url);
});
// Log the response from the server that is supplied when the "ajax-response" event is
// triggered within the formsMediator
formsMediator.subscribe("ajax-response", function(response) {
loggingMediator.publish("log", "The server responded to an Ajax call with: " + response);
});
}(formsMediator, loggingMediator));
随着代码库的增长,您可能会发现从观察者模式转移到中介者模式是有意义的,这样可以将系统的事件组织成更易于管理的特性。
当您希望在一个非常大的代码库中将模块松散地耦合在一起时,最好使用 mediator 模式,如果使用 observer 模式,要处理的事件数量将会非常多。要了解有关中介模式的更多信息,请查看以下在线资源:
- HB Stone 的《JavaScript 设计模式:中介者》(via
bit . ly/Mediator _ pattern
- Addy Osmani 的《大规模 JavaScript 应用架构的模式:中介者》(via
http://bit.ly/mediator_js
)
纪念品图案
memento 模式定义了对象数据在存储器中以静态形式的存储,使得它可以在代码执行过程中的稍后时间被恢复;这就好像您可以在任何时候拍摄一个对象的快照,然后您可以恢复它。清单 7-10 显示了一个简单的“类”,通过将对象的快照存储为 JSON 格式的字符串表示,并提供存储和恢复原始 JavaScript 对象的方法,可以用来实现这种模式。
清单 7-10。纪念品图案
// Define a simple "class" to be used to implement the memento pattern. It can be used to
// provide the ability to save and restore a snapshot of an object in memory.
// Certain older browsers (e.g. Internet Explorer 7) do not support the JSON.stringify() and
// JSON.parse() methods natively. For these, you should include Doug Crockford's json2.js
// library found at
https://github.com/douglascrockford/JSON-js
function Memento() {
// Define an object in memory to store snapshots of other objects under a specified key
this.storage = {};
}
// Define a method to save the state of any object under a specified key
Memento.prototype.saveState = function(key, obj) {
// Convert the supplied object to a string representation in JSON format
this.storage[key] = JSON.stringify(obj);
};
// Define a method to restore and return the state of any object stored under a specified key
Memento.prototype.restoreState = function(key) {
var output = {};
// If the supplied key exists, locate the object stored there
if (this.storage.hasOwnProperty(key)) {
output = this.storage[key];
// Convert the stored value from a JSON string to a proper object
output = JSON.parse(output)
;
}
return output;
};
清单 7-11 展示了清单 7-10 中 memento“class”的应用。
清单 7-11。正在使用的纪念品图案
// Define an instance of a memento to allow us to save and restore the state of objects
var memento = new Memento(),
// Define an object whose state we wish to be able to save and restore
user = {
name: "Den Odell",
age: 35
};
// Save the current state of the user object using the memento
memento.saveState("user", user);
// Prove that the state of the object is save in JSON format by reading from the storage object
// of the memento directly
alert(memento.storage["user"]); // {"name":"Den Odell","age":35}
// Now change the values in the user object as you wish
user.name = "John Smith";
user.age = 21;
// Output the current state of the user object
alert(JSON.stringify(user)); // {"name":"John Smith","age":21}
// Whenever you wish to restore the last saved state of the user object, simply call the restoreState() method of the memento
user = memento.restoreState("user");
// Output the new value of the user object, which has been restored to its last saved state
alert(JSON.stringify(user)); // {"name":"Den Odell","age":35}
当您需要在应用执行的特定时刻存储和恢复应用中对象的快照时,最好使用 memento 模式。要在线阅读有关 memento 模式的更多信息,请查看以下资源:
- DoFactory 上的“JavaScript Memento 设计模式”(通过
http://bit.ly/memento_pattern
) - 维基百科上的“纪念模式”(通过
bit . ly/Memento _ wiki
)
承诺模式
当处理异步函数时,通常会将回调函数传递给这样的函数。该函数在完成工作后,将代表我们执行回调函数。这正如我们所希望的那样工作;唯一的问题是,它可能会创建更难阅读、更模糊的代码——您必须知道调用的函数是异步函数,传递给它的函数被用作回调函数。如果您希望在执行回调之前等待几个异步函数的结果完成,这会使结果代码更加模糊,难以理解。进入 promises 模式,这是一个创建于 20 世纪 70 年代的设计模式,但在 CommonJS 小组( http://bit.ly/common_js
)的工作中针对 JavaScript 进行了更新。它定义了一种从异步调用返回承诺的方法,然后可以将该方法与对另一个函数的调用链接起来,该函数只有在承诺完成时才会执行,这发生在异步调用完成时。这样做的好处是确保回调与异步函数的调用充分分离,从而提高代码的清晰度,使代码更具可读性,从而更易于理解和维护。
承诺在 JavaScript 中表现为包含一个then()
方法的对象实例,一旦相关的异步函数完成执行,就会执行该方法。考虑一个简单的 Ajax 调用,它需要一个回调函数作为第二个参数,执行如下:
ajaxGet("/my-url", function(response) {
// Do something with response
}).
使用 promises 模式,对同一个 Ajax 调用生成的 JavaScript 如下所示:
ajaxGet("/my-url").then(function(response) {
// Do something with response
});
您可能认为这两者之间没有什么区别,但事实上后者更加清晰:它清楚地告诉我们,一旦第一个函数完成,第二个函数将会执行,而这仅仅是暗示了前者的情况。一旦与多个异步调用一起使用,promises 模式比使用回调的等价代码更加清晰。例如,考虑下面的代码,它对一个 URL 进行 Ajax 调用,然后对另一个 URL 进行第二次 Ajax 调用:
ajaxGet("/my-url", function() {
ajaxGet("/my-other-url", function() {
// Do something
});
});
使用 promises 模式,这段代码被简化为更容易理解的代码,并且避免了嵌套代码的层次,这种层次在链中的异步调用越多,就会变得越极端:
ajaxGet("/my-url").then(ajaxGet("/my-other-url")).then(function() {
// Do something
});
当在标准 JavaScript 中发生了大量同步异步调用之后,试图执行单个回调时,事情会变得更加复杂。使用 promises 模式,您可以简单地将一个承诺数组传递给它的all()
方法,它将同时执行每个承诺,当数组中的每个方法都实现了它自己的承诺时,返回一个承诺,如下所示:
Promise.all([ajaxGet("/my-url"), ajaxGet("/my-other-url")]).then(function() {
// Do something with the data returned from both calls
});
清单 7-12 显示了 JavaScript 中表示承诺的“类”。我在 http://bit.ly/js_promises
的 GitHub 项目中管理这个代码的独立版本,你可以在你的项目中自由使用。
清单 7-12。承诺模式
// Define a "class" representing a promise, allowing readable and understandable code to be
// written to support asynchronous methods and their callbacks. Instances created from this
// "class" adhere to the Promises/A+ specification detailed at
http://promisesaplus.com
// pass all the official unit tests found at
https://github.com/promises-aplus/promises-tests
// which prove compliance of this specification.
var Promise = (function() {
// Define the three possible states a promise can take - "pending" - the default value
// meaning it has not resolved yet, "fulfilled" - meaning the promise has resolved
// successfully, and "rejected" - meaning the promise has failed and an error has occurred
var state = {
PENDING: "pending",
FULFILLED: "fulfilled",
REJECTED: "rejected"
};
// Define the "class" to represent a promise. If an asynchronous function is passed in at
// the point of instantiation, it will be executed immediately
function Promise(asyncFunction) {
var that = this;
// Define a property to represent the current state of the promise, set to "pending" by
// default
this.state = state.PENDING;
// Define a property to be used to store a list of callback functions to call once the
// asynchronous method has completed execution
this.callbacks = [];
// Define a property to store the value returned by the asynchronous method represented
// by this promise
this.value = null;
// Define a property to store the details of any error that occurs as a result of
// executing the asynchronous method
this.error = null;
// Define two functions which will be passed to the asynchronous function
// represented by this promise. The first will be executed if the asynchronous
// function executed successfully, the second will be executed if the execution
// failed in some way
function success(value) {
// Executes the resolve() method of this promise, which will ensure that any
// functions linked to this promise to be executed once its asynchronous method
// has executed successfully is executed at this point
that.resolve(value);
}
function failure(reason) {
// Executes the reject() method of this promise, which will execute any
// linked callback functions for displaying or handling errors. Any furthe
r
// associated promises chained to this one will not be executed.
that.reject(reason);
}
// If an asynchronous function is passed to this promise at instantiation, it is
// executed immediately, and the success() and failure() functions defined above
// are passed in as function parameters. The asynchronous function must ensure it
// executes the most appropriate of these two functions depending on the outcome
// of the behaviour it is attempting to perform
if (typeof asyncFunction === "function") {
asyncFunction(success, failure);
}
}
// Define a then() method, the crux of the Promises/A+ spec, which allows callbacks to
// be associated to the result of the asynchronous function's execution depending on
// whether that function completed its task successfully or not. It allows chaining of
// promises to each other to allow further asynchronous functions to be executed at
// the point at which the current one is completed successfully
Promise.prototype.then = function(onFulfilled, onRejected) {
// Create a new promise (and return it at the end of this method) to allow for
// chaining of calls to then()
var promise = new Promise(),
// Define a callback object to be stored in this promise and associate the new
// promise instance to it to act as the context of any callback methods
callback = {
promise: promise
};
// If a function was provided to be executed on successful completion of the
// asynchronous function's action, store that function in the callback object
// together with its newly created promise as context
if (typeof onFulfilled === "function") {
callback.fulfill = onFulfilled;
}
// If a function was provided to be executed on unsuccessful completion of the
// asynchronous function's action, store that function in the callback object
// together with the new context promise
if (typeof onRejected === "function") {
callback.reject = onRejected;
}
// Add the callback object to the list of callbacks
this.callbacks.push(callback);
// Attempt to execute the stored callbacks (will only do this if the asynchronous
// function has completed execution by this point - if not, it will be called at
// such time as it has by other code in the "class")
this.executeCallbacks();
// Return the newly created promise, to allow for chaining of other asynchronous
// functions through repeated calls to the then() method
return promise;
};
// Define a method to execute any callbacks associated with this promise if the
// associated asynchronous function has completed execution
Promise.prototype.executeCallbacks = function() {
var that = this,
value,
callback;
// Define two functions to use as defaults to execute if an equivalent function has
// not been stored in the list of callbacks tied to this promise
function fulfill(value) {
return value;
}
function reject(reason) {
throw reason;
}
// Only execute the callbacks if the promise is not in its pending state, i.e. that
// the asynchronous function has completed execution
if (this.state !== state.PENDING) {
// Point 2.2.4 of the Promises/A+ spec dictates that callback functions should
// be executed asynchronously, outside of the flow of any other calls to then()
// which might take place. This ensures the whole chain of promises is in place
// before calls to the callbacks take place. Using a setTimeout with a delay of
// 0 milliseconds gives the JavaScript engine a split second to complete the
// process of going through the promise chain before any callbacks are run.
// Browsers have a minimum delay value possible for a setTimeout call so in
// reality the callbacks will be executed after, typically, 4 milliseconds
setTimeout(function() {
// Loop through all the callbacks associated with this promise and execute
// them each in turn, selecting the callback's fulfill method if the promise
// was fulfilled (by the asynchronous function completing execution
// successfully), or its reject method if the function returned an error
// during execution
while(that.callbacks.length) {
callback = that.callbacks.shift();
// Wrap the execution of the callback in a try/catch block, in case it
// throws an error. We don't want the promise chain to stop executing if
// an error is thrown, rather we want to reject the promise, allowing
// the calling code to handle the error itself
try {
// Execute the appropriate callback method based on the state of
// the promise. If no callback method has been associated, fall
// back to the default fulfill() and reject() functions defined at
// the top of the executeCallbacks() method, above
if (that.state === state.FULFILLED) {
value = (callback.fulfill || fulfill)(that.value);
} else {
value = (callback.reject || reject)(that.error);
}
// Pass the result of executing the callback function to the
// resolve() method, which will either mark the promise as fulfilled
// or continue to further execute chained calls to the then() method
callback.promise.resolve(value);
} catch (reason) {
// If an error is thrown by the callback
callback.promise.reject(reason);
}
}
}, 0);
}
};
// The fulfill() method will mark this promise as fulfilled provided it has not already
// been fulfilled or rejected before. Any associated callbacks will be executed at
// this point
Promise.prototype.fulfill = function(value) {
// Only transition the promise to the fulfilled state if it is still in the pending
// state, and a value is passed to this method when it is executed
if (this.state === state.PENDING && arguments.length) {
this.state = state.FULFILLED;
this.value = value;
this.executeCallbacks();
}
};
// The reject() method will mark this promise as rejected provided it has not already
// been fulfilled or rejected before. Any associated callbacks will be executed at
// this point
Promise.prototype.reject = function(reason) {
// Only transition the promise to the rejected state if it is still in the pending
// state, and a value is passed to this method when it is executed
if (this.state === state.PENDING && arguments.length) {
this.state = state.REJECTED;
this.error = reason;
this.executeCallbacks();
}
};
// The resolve() method takes the return value from a successfull call to a promise's
// fulfill() callback and uses it to fulfill the promise if it is the last promise in
// a chain of then() method calls. If it is not the last promise, it continues down
// the promise chain, recursively fulfilling and rejecting the linked promises as
// appropriate
Promise.prototype.resolve = function(value) {
var promise = this,
// Detect the type of the value returned from the fulfill() callback method. If
// this is the last promise in a chain, this should be the result of executing
// the asynchronous function itself. If this promise has other chained promises
// then the value passed to this method will contain another promise which will
// call the resolve() method again, recursively
valueIsThisPromise = promise === value,
valueIsAPromise = value && value.constructor === Promise,
// The term "thenable" refers to an object that looks like a promise in that it
// contains a then() method of its own, yet isn't an instance of this Promise
// "class" - useful for connecting promises created by other implementations of
// the Promises/A+ spec together
valueIsThenable = value && (typeof value === "object" || typeof value === "function"),
isExecuted = false,
then;
// Reject this promise if the value passed to this method represents the same
// promise represented here - otherwise we could potentially get stuck in a loop
if (valueIsThisPromise) {
// The Promises/A+ spec dictates that should this promise be the same as the
// one passed to this method, then a TypeError should be passed to the reject()
// method, effectively stopping execution of further promises in the chain
promise.reject(new TypeError());
// If the value passed to the resolve() method is another instance of this Promise
// "class", then either fulfill or reject the current promise based on the state of
// the provided promise
} else if (valueIsAPromise) {
// If the promise passed into this method has already been fulfilled or
// rejected, pass on the value or error contained within it to this promise
if (value.state === state.FULFILLED) {
promise.fulfill(value.value);
} else if (value.state === state.REJECTED) {
promise.reject(value.error);
// If the promise passed into this method hasn't yet been fulfilled or rejected,
// execute its then() method to ensure the current promise will get resolved
// or rejected along with that promise once it has completed execution of its
// asynchronous function
} else {
value.then(function(value) {
promise.resolve(value);
}, function(reason) {
promise.reject(reason);
});
}
// If the value passed to the resolve() method is not an instance of this Promise
// "class" but resembles a promise in that it is an object containing its own
// then() method, then execute its then() method, fulfilling or rejecting the
// current promise based on the state of this promise. This comes in useful when
// attempting to connect promises created with other implementations of the same
// spec together with this one
} else if (valueIsThenable) {
// Wrap execution in a try/catch block in case an error is thrown in the
// underlying code of the other promise implementation
try {
then = value.then;
// If the object stored in the value variable contains a then() method,
// execute it to ensure the current promise gets fulfilled or rejected when
// that promise does
if (typeof then === "function") {
then.call(value, function(successValue) {
if (!isExecuted) {
isExecuted = true;
promise.resolve(successValue);
}
}, function(reason) {
if (!isExecuted) {
isExecuted = true;
promise.reject(reason);
}
});
} else {
promise.fulfill(value);
}
} catch (reason) {
if (!isExecuted) {
isExecuted = true;
promise.reject(reason);
}
}
// If the value passed to the resolve() method is not a promise, then fulfill the
// current promise using its value. Any associated callbacks will then be executed
} else {
promise.fulfill(value);
}
};
// Add a bonus method, Promise.all(), which isn't part of the Promises/A+ spec, but is part
// of the spec for ECMAScript 6 Promises, which bring the benefits of promises straight into
// the JavaScript language itself.
//
// The method accepts an array of promises, each representing an asynchronous function,
// which are executed simultaneously, and returns a single promise, allowing a single
// then() method to be executed at such point all the supplied promsies are fulfilled. The
// value passed on fulfillment contains an array of all the returned values of the
// individual promises, in the same order as the promises in the original array passed to
// this method
Promise.all = function(promises) {
var index = 0,
promiseCount = promises.length;
// Return a single promise representing all the promises supplied to this method. It
// will be fulfilled as soon as every one of the supplied promises have been fulfilled.
return new Promise(function(fulfill, reject) {
var promise,
results = [],
resultsCount = 0;
// Execute an onSuccess() function each time one of the supplied promises is
// fulfilled, adding its resulting value to an array in the same index position as
// the promise was in the original array
function onSuccess(result, index) {
results[index] = result;
resultsCount++;
// If we have collected the results for all of the promises, then fulfill the
// current single promise, passing across the array of fulfilled values from
// the individual promises
if (resultsCount === promiseCount) {
fulfill(results);
}
}
// If any of the supplied promises are rejected, then reject the current promise
function onError(error) {
reject(error);
}
// Resolve a given promise, executing onSuccess() if fulfilled, or onError() if not
function resolvePromise(index, promise) {
promise.then(function(value) {
onSuccess(value, index);
}, onError);
}
// Loop through all the promises supplied to this method, resolving each in turn
for (; index < promiseCount; index++) {
promise = promises[index];
resolvePromise(index, promise);
}
});
};
return Promise;
}());
看一下清单 7-13,它展示了如何利用清单 7-12 中的Promise
“class ”,在你的代码中创建和使用承诺的例子。
清单 7-13。正在使用的承诺模式
// Define a variable to use as a counter further down in this code
var millisecondCount = 0;
// Define a method to get the data returned by a GET request to a given URL. Returns a promise
// to which callback functions can be hooked into using its then() method.
function ajaxGet(url) {
// Return a new promise, initializing it with the asynchronous function to perform the Ajax
// request. When the promise executes the function, it will pass in two function parameters,
// the first should be called by our code if and when the asynchronous request succeeds, and
// the second should be called if and when an error occurs in the execution of the
// asynchronous request.
return new Promise(function(fulfill, reject) {
var xhr = new XMLHttpRequest(),
STATE_LOADED = 4,
STATUS_OK = 200;
xhr.onreadystatechange = function() {
if (xhr.readyState !== STATE_LOADED) {
return;
}
// If the Ajax GET request returns data successfully, execute the fulfill method
if (xhr.status === STATUS_OK) {
fulfill(xhr.responseText);
// If the Ajax request does not return data successfully, execute the reject method
} else {
reject("For the URL '" + url + "', the server responded with: " + xhr.status);
}
};
// Perform the Ajax GET request
xhr.open("GET", url);
xhr.send();
});
}
// Define a method which waits a given number of milliseconds before continuing. Returns
// a promise.
function wait(milliseconds) {
return new Promise(function(fulfill, reject) {
// If the value provided for milliseconds is a number greater than 0, call the
// setTimeout method to wait that number of milliseconds before executing the fulfill
// method
if (milliseconds && typeof milliseconds === "number" && milliseconds > 0) {
setTimeout(function() {
fulfill(milliseconds);
}, milliseconds);
// If the value provided for milliseconds is not a number or is less than or equal to
// 0, then reject the promise immediately
} else {
reject("Not an acceptable value provided for milliseconds: " + milliseconds);
}
});
}
// Define two functions for use if a particular promise is fulfilled or rejected, respectively
function onSuccess(milliseconds) {
alert(milliseconds + "ms passed");
}
function onError(error) {
alert(error);
}
// EXAMPLE 1: Success
// Execute the wait() function with a value we know will cause it to succeed, and show that
// the first of the two supplied functions to the then() method is executed
wait(500).then(onSuccess, onError); // After 0.5 seconds, outputs: "500ms passed"
// EXAMPLE 2: Error
// Execute the wait() function with a value we know will cause it to error. Because this
// rejects immediately, this will alert the user before the result of example 1 is known
wait(0).then(onSuccess, onError); // "Not an acceptable value provided for milliseconds: 0"
// EXAMPLE 3: Chaining
// Multiple promises can be chained together using the then() method which allows operations to
// be executed in order once the result of the execution of the previous asynchronous function
// is known. This considerably simplifies the nesting of callbacks which would be necessary
// without the use of promises.
wait(1000)
.then(function(milliseconds) {
// After a delay of 1 second, increment the counter by the number of milliseconds
// passed into the function parameter (in this case, 1000)
millisecondCount += milliseconds;
// Returning a promise in this function means that the operation indicated by that
// promise will be executed once the previous operation is complete
return wait(1600);
})
.then(function(milliseconds) {
// By this point, 2600 milliseconds have passed, and this is stored in our counter
// variable
millisecondCount += milliseconds;
// Return another promise, indicating that a delay of 400 milliseconds should now
// take place before the function specified in the following then() statement is
// executed
return wait(400);
})
.then(function(milliseconds) {
// Increment the counter by the 400 milliseconds just passed, making its total 3000
millisecondCount += milliseconds;
// Finally, output the combined value of the counter, which indicates the number of
// milliseconds passed since the first operation in this chain began
alert(millisecondCount + "ms passed"); // After 3 seconds, outputs: "3000ms passed"
});
// EXAMPLE 4: Multiple Promises
// Different promises can be chained together, since as in this example, which gets a page by
// the URL /page1.html (assuming it exists on the server), then waits 3 seconds before getting
// another page by the URL /page2.html (again, assuming it exists).
ajaxGet("/page1.html")
.then(function() {
return wait(3000);
})
.then(function() {
return ajaxGet("/page2.html");
})
.then(function() {
// This alert will fire only if both /page1.html and /page2.html exist and can
// be accessed
alert("/page1.html and /page2.html received, with a 3s gap between requests");
});
// EXAMPLE 5: Simultaneous Promises
// The Promise.all() method accepts an array of promises which will be resolved simultaneously,
// passing the results as an array to the success function passed to its then() method. Get
// both /page1.html and /page2.html simultaneously, and when they are both complete, execute
// the success callback function with the contents of both files in the array parameter passed
// into this function, in the same order as in the array of promises. If any of the supplied
// promises fails, the error callback function will be executed, with the detail of the first
// error that occurred passed into this function parameter.
Promise.all([ajaxGet("/page1.html"), ajaxGet("/page2.html")])
.then(function(files) {
alert("/page1.html = " + files[0].length + " bytes. /page2.html = " + files[1].length + " bytes.");
}, function(error) {
alert(error);
});
当代码中出现许多异步操作,导致嵌套回调函数混乱时,最好使用 promises 模式。它允许将回调函数链接到异步调用,使代码更容易理解,因此更容易开发和维护。要在线阅读有关承诺模式的更多信息,请查看以下资源:
- “Promises/A+ Specification”由 Promises/A+组织(通过
http://bit.ly/promises_aplus
)提供 - 里斯·布雷特-鲍恩的《承诺模式》(via
http://bit.ly/promises_js
)
战略模式
策略模式适用于这样的情况:您有一个包含大型条件语句(if
… else
或switch
)的“类”,其中每个选项都会导致该“类”的特定行为以不同的方式改变。与其管理一个大的条件语句,不如将每个行为拆分成单独的对象,每个对象称为一个策略。在任何时候,只有其中一个应用于原始对象,称为客户端。拥有多个策略对象也有助于提高代码的质量,因为策略对象可以彼此独立地进行单元测试。
清单 7-14 显示了一个应用策略模式的“类”的例子——它包含了许多条件语句,这些语句改变了从它创建的对象的一个非常特殊的行为。
清单 7-14。将策略模式应用于的代码已经成熟
// Define a "class" representing a form field in an HTML page
function FormField(type, displayText){
this.type = type || "text";
this.displayText = displayText || "";
// Create a new <input> tag, setting its field type to the value supplied upon instantiation
this.element = document.createElement("input");
this.element.setAttribute("type", this.type);
// Create a new <label> tag, setting its text to the value supplied upon instantiation
this.label = document.createElement("label");
this.label.innerHTML = this.displayText;
// Add the <label> and <input> tags to the current page
document.body.appendChild(this.label);
document.body.appendChild(this.element);
}
// Give each form field object instance three methods
FormField.prototype = {
// Return the current value stored in the form field
getValue: function() {
return this.element.value;
},
// Set a new value for the form field
setValue: function(value) {
this.element.value = value;
},
// Return a true / false value depending on whether the value in the form field is valid
isValid: function() {
var isValid = false,
value;
// If this is a <input type="text"> field, it is considered valid if its value is not
// an empty string
if (this.type === "text") {
isValid = this.getValue() !== "";
// If this is a <input type="email"> field, it is considered valid if its value is not
// an empty string, contains the "@" character and contains the "." character after "@"
} else if (this.type === "email") {
value = this.getValue();
isValid = value !== "" && value.indexOf("@") > 0 && value.indexOf(".", value.indexOf("@")) > 0;
// If this is a <input type="number"> field, it is considered valid if its value is
// a number
} else if (this.type === "number") {
value = this.getValue();
isValid = !isNaN(parseInt(value, 10));
// This could go on a while as there are 24 possible <input> types in HTML5\. We need a
// way to simplify this to make it easier to understand and extend in future - this is
// where the strategy pattern comes into play, as shown in Listing 7-14
} else {
// etc.
}
return isValid;
}
};
清单 7-15 中的代码展示了我们如何通过应用策略模式将清单 7-14 中的代码重构为一个更有效、更易于管理的结构。
清单 7-15。战略模式
// Define a "class" representing a form field in an HTML page. Note a new object is passed into
// the third parameter at instantiation, containing a strategy object. This object contains a
// specific implementation of the isValid() method pertaining to the specific type of form field
// we are creating - for example, a "text" field would require an isValid() method that checks
// to see if the stored value is not an empty string, so we create an object containing this
// method and pass it in through the strategy object at instantiation time
function FormField(type, displayText, strategy){
this.type = type || "text";
this.displayText = displayText || "";
this.element = document.createElement("input");
this.element.setAttribute("type", this.type);
this.label = document.createElement("label");
this.label.innerHTML = this.displayText;
// Check to see if the strategy object passed in contains the isValid() method to use and,
// if so, store the stragety object for use when the isValid() method of this object is
// executed. If no strategy object is supplied, use a default
if (strategy && typeof strategy.isValid === "function") {
this.strategy = strategy;
} else {
this.strategy = {
isValid: function() {
return false;
}
};
}
document.body.appendChild(this.label);
document.body.appendChild(this.element);
}
FormField.prototype = {
getValue: function() {
return this.element.value;
},
setValue: function(value) {
this.element.value = value;
},
// Replace the previous isValid() method with one that simply calls the isValid() method
// provided by the stored strategy object - no more extensive if..else statements, making
// the code for this "class" much smaller and easier to manage
isValid: function() {
return this.strategy.isValid.call(this);
}
};
// Define three strategy objects for three different types of form field to be used with the
// FormField "class" when it is instantiated. Here we provide specific implementations for the
// isValid() method, but we could have extended these to include more methods and/or properties
// to meet our needs. In cases like this, we would have created a strategy "class" and created
// these objects as instances of that "class". Here we have simple objects so it is smarter to
// keep the code short and to the point
var textFieldStrategy = {
// Specific functionality for validation of a <input type="text"> field
isValid: function() {
return this.getValue() !== "";
}
},
emailFieldStrategy = {
// Specific functionality for validation of a <input type="email"> field
isValid: function() {
var value = this.getValue();
return value !== "" && value.indexOf("@") > 0 && value.indexOf(".", value.indexOf("@")) > 0;
}
},
numberFieldStrategy = {
// Specific functionality for validation of a <input type="number"> field
isValid: function() {
var value = this.getValue();
return !isNaN(parseInt(value, 10));
}
};
清单 7-15 中的代码可以如清单 7-16 所示使用。
清单 7-16。正在使用的策略模式
// Create three form fields for our HTML page, each with different types. We pass in the type,
// the text for the associated <label> tag, and the strategy object associated with this field
// type to provide the required behavior for field value validation
var textField = new FormField("text", "First Name", textFieldStrategy),
emailField = new FormField("email", "Email", emailFieldStrategy),
numberField = new FormField("number", "Age", numberFieldStrategy);
// Set values for each form field we know will validate
textField.setValue("Den Odell");
emailField.setValue("denodell@me.com");
numberField.setValue(35);
// Check to see if the values in the fields validate correctly
alert(textField.isValid()); // true
alert(emailField.isValid()); // true
alert(numberField.isValid()); // true
// Change the values in the fields to ones we know will fail validation
textField.setValue("");
emailField.setValue("denodell");
numberField.setValue("Den Odell");
// Check to ensure the isValid() method is working correctly, reflecting the new field values
alert(textField.isValid()); // false
alert(emailField.isValid()); // false
alert(numberField.isValid()); // false
当您需要管理大量条件逻辑来实现“类”中方法的行为时,最好使用策略模式要了解有关策略模式的更多信息,请查看以下在线资源:
- 迈克尔·索科尔的《JavaScript 中的策略设计模式》(经由
http://bit.ly/strategy_pattern
) - 迈克·彭尼斯的《JavaScript 中的策略模式》(经由
http://bit.ly/strategy_js
)
摘要
在这一章中,我们已经看到了行为设计模式,你可以在你自己的 JavaScript 应用中使用这些模式来简化不同对象之间的通信。这些是 JavaScript 开发的瑞士军刀中的工具,但是像所有工具一样,您需要知道何时何地最好地使用它们。熟悉本章中的模式及其用例,并确保在代码中认识到需要使用设计模式之前,不要使用它。
在下一章中,我们将着眼于架构设计模式,它实际上是我们已经讨论过的现有设计模式的组合,用来解决大型 JavaScript 代码库中的特定问题。