HTML5 程序员参考(六)
十、附录 A:JavaScript 技巧和技术
JavaScript 是事实上的 Web 编程语言,也是您用来与本书中涉及的 HTML5 特性进行交互的主要工具。在这一章中,我将介绍一些为你的应用组织 JavaScript 的技巧,以及一些可以用来简化你的脚本的更强大的技术。这简短的一章并不意味着是完整的 JavaScript 参考——为此,你可以参考其他出版社的书籍,比如 JavaScript 程序员参考。
代码格式
总的来说,我避免关于括号和缩进等代码格式样式的圣战。通常个别的选择并不重要,因为更重要的是整个代码的一致性。您应该始终使用相同的括号样式、注释样式、缩进选择等等,因为这将有助于保持代码的可读性。即使你是唯一一个会看你的代码的人,它仍然很重要。
然而,对于 JavaScript,这些选择很重要。例如,在 JavaScript 中,括号的位置在某些情况下很重要。考虑以下代码片段:
function test1tbs() {
return {
objectLiteral: ’value’,
isExpected: true
};
}
这个例子遵循所谓的一个真正的大括号风格(有时缩写为 1TBS),其中函数定义的左括号(和对象文字)和它的声明在同一行。这是我在整本书的例子中使用的风格。然而,同样的例子也可以用奥尔曼式的括号来表示:
function testAllman()
{
return
{
objectLiteral: ’value’,
isExpected: false
};
}
这两个函数会有非常不同的结果。test1tbs函数将返回内联定义为 return 语句一部分的对象文字,而包含testAllman函数的代码甚至不会运行(JavaScript 引擎将抛出一个错误)。在其他语言中,这两个函数是相同的。
此外,管理 JavaScript 行为的 ECMAScript 标准还定义了如何解释丢失的分号的规则。这被称为自动分号插入(ASI ) ,是该语言的一个特性,但它偶尔会导致令人惊讶的结果。这就是为什么 JavaScript 代码中事实上的括号风格是 1TBS 风格,而不是更广泛的奥尔曼风格。
JavaScript 奖励冗长
当您阅读了本书中的示例后,您可能已经注意到代码风格相当冗长。有大量的注释,变量名和函数名往往很长并且是描述性的,等等。这部分是因为代码样本被设计为易于阅读和理解,但总体来说,这种冗长程度并不比我每天编写的代码高多少。
JavaScript 有几个方便的特性,比如 ASI、类型强制(当您比较两个不同类型的变量时会用到)等等。有时这些特性会导致令人惊讶的结果。详细代码通过提醒您变量和函数应该做什么,以及提供逻辑流和预期行为的文档,有助于避免这些意外。这也使得调试更容易,如果你和其他人合作,这将有助于他们更快地学习你的代码。
注释〔??〕
此外,在本书的例子中,我一直使用特定的注释格式来注释代码。如果您熟悉 JSDoc 或 JavaDoc,这些注释看起来会很熟悉,因为格式是从 JSDoc 派生的。如果您不熟悉 JSDoc,它是 JavaScript 代码中注释的标准,不仅提供了解释代码的好方法,还允许您使用解析工具从注释中生成实际的文档。在示例中,我没有使用 JSDoc 的全部功能——我只是专注于提供类型注释。如果您熟悉为 Closure JavaScript 编译器注释代码,这些注释将会非常熟悉。
提示要了解更多关于为闭包编译器注释代码以及在项目中使用闭包编译器的信息,请参见
https://developers.google.com/closure/compiler/docs/js-for-compiler。你可以在www.usejsdoc.org/了解更多关于 JSDoc 和自动文档生成器的知识。
对于每个函数,注释指定了以下内容:
- 对该函数应该做什么的描述。
- 函数的每个参数的预期数据类型(如果有)。
- 返回值的数据类型(如果有)。
- 函数是否意味着是私有的(只适用于类的成员)。
例如,考虑第四章中的函数:
/**
* Returns a random integer between the specified minimum and maximum values.
* @param {number} min The lower boundary for the random number.
* @param {number} max The upper boundary for the random number.
* @return {number}
*/
function getRandomIntegerBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
您可能想知道在动态类型语言(如 JavaScript)中定义参数的预期类型有什么价值。显然,您可以传入任何想要的值,JavaScript 引擎会尽可能地强制这些值,这可能会导致函数返回意外的结果。通过在函数定义中定义预期的数据类型,您不仅可以指出函数需要什么以避免意外的结果,还可以使您在忘记细节后更容易使用该函数。此外,如果你和一个团队合作,你会让他们更容易使用这个功能。在这本书的特殊情况下,我的意图是类型注释将有助于使例子更容易理解。
JavaScript 不直接支持公共或私有属性或方法的概念。在属性或方法上指定@private标签有助于以更传统的方式定义类结构,并且可以使 JavaScript 代码更容易被习惯于具有更严格封装特性的语言的人接受。我还发现记住我打算将哪些接口作为公共或私有接口是有帮助的,因为如果我发现自己需要更改这些决定,这通常表明底层的类结构需要修改。
在本书中,我使用了这些标签:
@private:表示属性或方法被认为是其上下文私有的。通常情况下,这不会以任何方式强制执行,只会有助于澄清意图。@constructor:表示该函数是一个 JavaScript 构造函数,当与new关键字结合使用时,将实例化并返回指定的对象类型。@param:表示函数或方法的参数。一个@param定义将包括一个括号中的类型定义,在整个函数中使用的参数名,以及一个可选的参数描述。可选参数用可选运算符表示(详见下文)。@return:表示函数返回值。一个@return标签将在括号中包含一个类型定义,指示返回值的数据类型。@type:表示定义时变量或属性的类型。
类型定义 是注释的重要组成部分。所有类型定义都用花括号括起来。因为 JavaScript 是动态类型化的,所以类型注释可以指定多种数据类型,每种数据类型用竖线分隔。例如:
{boolean}
指定布尔类型,而
{boolean|number}
指定类型可以是布尔值或数字。
复合类型使用尖括号指定。例如:
{Array<boolean>}
指定该类型是布尔值数组,而
{Object<string, number>}
指定对象的键是字符串,关联的值是数字。
类型定义中还使用了一些运算符:
- 可空的:
?操作符表示类型可以是指定的数据类型或空值。因此{?Object}相当于{Object|null}。我没有在本书的例子中使用可空操作符;相反,我假设所有类型在默认情况下都可以为空,除非使用不可为空的操作符指定了其他类型。 - 不可为空的:
!操作符表示类型不能为空。例如,{!Array<!string>}指定类型必须是字符串数组。不允许空数组或其他类型的数组。 - 可选:在
@param类型定义中使用的=运算符表示该参数是可选的。我通过在所有可选参数的名称前添加前缀opt_来扩展这一点。例如,@param {boolean=} opt_isActive指定参数opt_isActive是可选的,但是如果它存在,它必须是一个布尔值(或 null)。
使用对象作为事件处理程序
我最喜欢的 DOM 鲜为人知的特性之一是EventListener接口。我们都知道如何使用Element.addEventListener方法将事件监听器附加到 DOM 元素,该方法有三个参数:
eventType:表示事件类型的字符串handler:事件发生时执行的功能bubble:是否在气泡阶段执行该功能
鲜为人知的是,DOM 指定你可以使用任何对象作为处理程序,只要它实现了EventListener接口。根据 DOM 级标准:
EventListener 接口是处理事件的主要方法。用户实现 EventListener 接口,并使用 AddEventListener 方法在 EventTarget 上注册他们的侦听器。用户在使用完侦听器后,还应该从 EventTarget 中删除他们的 EventListener。
一个EventListener接口被定义为任何对象上的一个名为handleEvent的方法:
interface EventListener {
void handleEvent(in Event evt);
};
这意味着任何实现了handleEvent方法的对象都可以被用作事件处理器,如清单 A-1 中的所示。
清单 。使用对象作为事件处理程序
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<p id="targetElement">Click me!</p>
<script>
var targetElement = document.getElementById(’targetElement’);
var eventObject = {
handleEvent: function(event) {
console.log(event.type);
}
};
targetElement.addEventListener(’click’, eventObject, true);
</script>
</body>
</html>
在这个例子中,您已经创建了一个简单的eventObject,它以一个叫做handleEvent的方法的形式实现了EventListener接口。然后将它绑定到目标元素的 click 事件,当您单击“click me”文本时,您将看到“click”出现在控制台中。
这种技术对于将事件处理程序封装在对象和类中很有用,而不是将它们作为单独的函数。您甚至可以创建一个具有多个事件处理程序的事件处理程序对象,并根据需要使用 EventListener 接口来委托活动。例如,回想一下第三章中的 WebSockets 示例(清单 3-7 ),它有单独的函数用于处理error、close、open和message事件,都绑定到 WebSocket 接口。您可以轻松地将所有这些事件处理程序创建为单个对象上的方法,如清单 A-2 中的所示。
清单 A-2 。重写清单 3-7 以使用通用 EventListener 接口
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Web Sockets Demonstration</h1>
<script>
// Create a new web socket connection to the chat service.
var chatUrl = ’ws://www.fgjkjk4994sdjk.com/chat’;
var validProtocols = [’chat’, ’json’];
var chatSocket = new WebSocket(chatUrl, validProtocols);
/**
* Creates an error handling class that implements the EventListener interface.
* @constructor
* @returns {Object}
*/
function CreateWebSocketEventObject() {
/**
* Handles an error event on the chat socket object.
* @private
*/
this.handleError_ = function() {
console.log(’An error occurred on the chat connection.’);
};
/**
* Handles a close event on the chat socket object.
* @param {CloseEvent} event The close event object.
* @private
*/
this.handleClose_ = function(event) {
console.log(’The chat connection was closed because ’, event.reason);
};
/**
* Handles an open event on the chat socket object.
* @param {OpenEvent} event The open event object.
* @private
*/
this.handleOpen_ = function(event) {
console.log(’The chat connection is open.’);
};
/**
* Handles a message event on the chat socket object.
* @param {MessageEvent} event The message event object.
* @private
*/
this.handleMessage_ = function(event) {
console.log(’A message event has been sent.’);
// The event object contains the data that was transmitted from the server.
// That data is encoded either using the chat protocol or the json protocol,
// so we need to deterine which protocol is being used.
if (chatSocket.protocol === validProtocols[0]) {
console.log(’The chat protocol is active.’);
console.log(’The data the server transmitted is: ’, event.data);
// etc...
} else {
console.log(’The json protocol is active.’);
console.log(’The data the server transmitted is: ’, event.data);
// etc...
}
};
/**
* Implements the EventListener interface for the object and invokes the
* correct handler based on the event type.
* @param {SocketEvent} event
*/
this.handleEvent = function(event) {
switch (event.type) {
case ’error’:
this.handleError_();
break;
case ’close’:
this.handleClose_(event);
break;
case ’open’:
this.handleOpen_(event);
break;
case ’message’:
this.handleMessage_(event);
break;
default:
console.warn(’Unknown event of type ’, event.type);
}
};
}
// Create a new event object using the constructor.
var eventHandlerObject = new CreateWebSocketEventObject();
// Bind the event object to the chat socket.
chatSocket.addEventListener(’error’, eventHandlerObject);
chatSocket.addEventListener(’close’, eventHandlerObject);
chatSocket.addEventListener(’open’, eventHandlerObject);
chatSocket.addEventListener(’message’, eventHandlerObject);
</script>
</body>
</html>
在这个版本的示例中,您构建了一个构造函数,它返回一个实现了EventListener接口的对象。在该接口方法中,它检查传入事件的类型属性,并调用正确的处理程序方法。这为您提供了更好的事件处理程序封装,并提供了轻松打开多个 Web 套接字并使用同一个构造函数为所有这些套接字构建事件处理程序的可能性。
承诺
异步活动在 JavaScript 应用中很常见,处理它们的标准方式是使用回调函数。举个例子,考虑你在第六章中做的动态脚本加载。清单 A-13 有一个函数,它动态加载一个指定的脚本,并根据结果执行成功或错误回调函数:
/**
* Dynamically loads a script and invokes an optional callback.
* @param {string} srcUrl The URL of the script file to load.
* @param {function=} opt_onLoadCallback An optional function to call when the
* script is loaded.
* @param {function=} opt_onErrorCallback An optional function to call if the
* script fails to load.
*/
function loadScript(srcUrl, opt_onLoadCallback, opt_onErrorCallback) {
// Create a script tag.
var newScript = document.createElement(’script’);
// Apply the load callback, if one was provided.
if (opt_onLoadCallback) {
if (newScript.readyState) {
// Internet explorer.
newScript.onreadystatechange = function() {
if (newScript.readyState == ’loaded’ ||
newScript.readyState == ’complete’) {
newScript.onreadystatechange = null;
opt_onLoadCallback.call();
}
};
} else {
// Every other browser in the universe.
newScript.onload = opt_onLoadCallback;
}
}
// Apply the error callback, if one was provided.
if (opt_onErrorCallback) {
newScript.onerror = opt_onErrorCallback;
}
newScript.src = srcUrl;
document.querySelector(’head’).appendChild(newScript);
}
这个函数有三个参数:它需要加载的脚本的 URL,以及成功和错误回调函数。
使用回调的问题是它们会导致复杂的代码。如果有嵌套的回调函数,例如,如果成功回调函数还执行另一个异步任务,那么回调函数可能会变得难以管理,代码也难以阅读。
承诺提供了一种不同的方式来处理 JavaScript 代码中的异步操作。Promise 是一个表示异步操作结果的对象。实际结果(成功或失败)不需要在承诺产生时就知道;相反,异步动作将像任何其他同步动作一样返回一个承诺对象。这允许您简化异步代码,减少甚至消除对嵌套回调的需要。
许诺对象处于四种状态之一:
- 已完成:承诺表示的异步操作已经完成并且成功。
- 拒绝:承诺表示的异步操作已经完成,但导致了错误。
- 待定:这是创建承诺时的初始状态。处于待定状态的承诺既不履行也不拒绝。
- 已解决:承诺不再待定,并且已经履行或拒绝。
一旦承诺进入履行或拒绝状态,它就不能改变,所以履行的承诺永远不会被拒绝,反之亦然。
使用承诺构造函数创建承诺:
var myPromise = new Promise(executor)
executor是带有两个参数的函数:resolve和reject。当您创建新的承诺时,这些resolve和reject参数将成为您稍后将指定的函数的占位符。通常,它们是异步操作成功或失败时要调用的函数。
Promise 对象公开了一个 API,用于访问异步操作的状态以及它可能返回的任何内容。
Promise.then(resolve, reject):then方法使您能够指定当承诺完成时将被调用的resolve和reject函数。Promise.catch(reject):catch方法允许您指定当承诺被拒绝时调用的reject函数。
为了演示如何创建一个基本承诺,然后分配 resolve 和 reject 处理程序,清单 A-3 展示了重新编写的清单 A-13,以使用一个承诺。
清单 A-3 。使用承诺来表示动态加载脚本
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<script src="../js-lib/detect-support.js"></script>
<script>
/**
* Dynamically loads a script and invokes an optional callback.
* @param {string} srcUrl The URL of the script file to load.
* @return {Promise<null>}
*/
function loadScript(srcUrl) {
var myPromise = new Promise(function(resolve, reject) {
var newScript = document.createElement(’script’);
if (newScript.readyState) {
// Internet explorer.
newScript.onreadystatechange = function() {
if (newScript.readyState == ’loaded’ ||
newScript.readyState == ’complete’) {
newScript.onreadystatechange = null;
resolve();
}
};
} else {
// Every other browser in the universe.
newScript.onload = resolve;
}
newScript.onError = reject;
newScript.src = srcUrl;
document.querySelector(’head’).appendChild(newScript);
});
return myPromise;
}
// Test for supported features.
var supportedFeatures = new DetectHTML5Support();
if (!supportedFeatures.localStorage) {
// The Web Storage is not supported, so load a shim. The loadScript function
// now returns a Promise.
loadScript(’../js-lib/webstorage-shim.js’).then(function() {
initApplication();
}, function() {
console.log(’Script failed to load.’);
});
} else {
// Web Storage was supported, so continue with the application.
initApplication();
}
/**
* Hypothetical function for initializing the application.
*/
function initApplication() {
console.log(’Application continues...’);
// Etc.
}
</script>
</body>
</html>
您会注意到,loadScript函数现在使用占位符来构造并返回一个承诺,这些占位符用于稍后将指定的resolve和reject函数。当函数被调用时,代码使用Promise.then方法应用resolve和reject函数。
连锁承诺
承诺为处理涉及多个异步操作的情况提供了很大的灵活性。例如,如果您从Promise.then方法返回一个承诺,您可以将承诺链接在一起。为了说明这一点,您可以使用loadScript函数一个接一个地加载三个不同的脚本。由于loadScript函数返回一个承诺,您可以简单地将对Promise.then的调用链接在一起。要做到这一点,你需要以一个永远成功的空头承诺开始这个链条:
var promiseChain = Promise.resolve();
promiseChain.then(function() {
return loadScript(’script1.js’);
}).then(function() {
return loadScript(’script2.js’);
}).then(function() {
return loadScript(’script3.js’);
}).catch(function() {
console.log(’An error occurred when loading the scripts.’);
});
如果你使用第六章的的“处理破损或缺失的 HTML5 实现”一节中提到的特性注册模式,你可以进一步简化为一个简单的for循环。清单 A-4 展示了使用这种技术重写清单 A-14。
清单 。链接承诺按顺序加载多个垫片
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<script src="../js-lib/detect-support.js"></script>
<script>
// Create a registry of HTML features that we need and shims to apply if they
// are not present. The registry will be an array of objects; each object will
// consist of a feature name and a path to a shim to apply if that feature is
// not supported.
var featureRegistry = [
{
’featureName’ : ’sessionStorage’,
’shim’ : ’../js-lib/webstorage-shim.js’
},
{
’featureName’ : ’requestAnimationFrame’,
’shim’ : ’../js-lib/animationframe-shim.js’
}
];
/**
* Dynamically loads a script and invokes an optional callback.
* @param {string} srcUrl The URL of the script file to load.
* @return {Promise<null>}
*/
function loadScript(srcUrl) {
var myPromise = new Promise(function(resolve, reject) {
var newScript = document.createElement(’script’);
if (newScript.readyState) {
// Internet explorer.
newScript.onreadystatechange = function() {
if (newScript.readyState == ’loaded’ ||
newScript.readyState == ’complete’) {
newScript.onreadystatechange = null;
resolve();
}
};
} else {
// Every other browser in the universe.
newScript.onload = resolve;
}
newScript.onError = reject;
newScript.src = srcUrl;
document.querySelector(’head’).appendChild(newScript);
});
return myPromise;
}
// Test for supported features.
var supportedFeatures = new DetectHTML5Support();
// Go through the registry and for each item load a shim if it isn’t supported.
var promiseChain = Promise.resolve();
featureRegistry.forEach(function(currFeature) {
if (!supportedFeatures[currFeature.featureName]) {
promiseChain = promiseChain.then(function() {
return loadScript(currFeature.shim);
});
}
});
promiseChain.then(function() {
initApplication();
}, function() {
console.log(’A shim failed to load.’);
});
/**
* Hypothetical function for initializing the application.
*/
function initApplication() {
console.log(’Application continues...’);
// Etc.
}
</script>
</body>
</html>
关于这个例子,你可能会注意到的第一件事是,它比第六章中的原始例子要紧凑得多。更简单的代码是使用承诺的好处之一。
在本例中,首先创建一个已履行的承诺,作为承诺链中的第一个环节。然后遍历特性注册表并测试每个特性。不支持的功能加载了垫片,它们的承诺添加到了链条上。这时在链条上使用Promise.then方法。如果所有需要的特性都得到支持,那么这个链将只包含最初实现的承诺,因此将立即调用resolve处理程序。如果有不支持的特性,那么这个链将由多个承诺组成,每个承诺将依次执行。
从承诺中返回值
到目前为止,在我的例子中,异步操作没有返回任何值。他们只是成功了或者失败了。许多异步动作将返回一个值,您需要在您的resolve和reject方法中访问该值。为此,您可以为您的resolve和reject方法指定参数。例如,想象这样一种情况,您使用返回承诺的fetchData方法从服务器获取数据:
function fetchData() {
var myPromise = new Promise(function(resolve, reject) {
var client = new XMLHttpRequest();
client.open(’POST’, ’http://www.fakeservice.com/myservice’);
client.send();
client.onload = function () {
if (this.status == 200) {
// Successfully fetched information from the service. Resolve the
// promise with the information.
resolve(this.response);
} else {
// Did not successfully fetch information from the service. Reject the
// promise with the error message.
reject(this.statusText);
}
};
client.onerror = function () {
reject(this.statusText);
};
});
return myPromise;
}
fetchData().then(function(serviceResponse) {
console.log(’The service returned ’, serviceResponse);
}, function(errorMessage) {
console.error(errorMessage);
});
在这个示例函数中,您创建了一个新的XMLHttpRequest对象来从服务器异步获取数据,但是返回一个包装了响应值的承诺。然后,您可以在Promise.then回调中访问响应值。
浏览器支持承诺
承诺是 JavaScript 的一个相对较新的特性。截至本文撰写时,除了 Internet Explorer 之外的所有浏览器都支持承诺,Internet Explorer Edge 发布时将提供全面支持。与此同时,在https://github.com/jakearchibald/es6-promise有一个很好的承诺填补空白。
进一步阅读
这只是对承诺的简单介绍。你可以用它们做更多的事情。本书中的许多例子可以使用 Promises 重写,从而产生更简单的代码。
要了解更多关于承诺的信息,请查看以下资源:
https://promisesaplus.com/处的承诺/A+规格- 在
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise的 Mozilla 开发者网络参考 - HTML5Rocks 的承诺教程,
www.html5rocks.com/en/tutorials/es6/promises/