众妙之门——JavaScript与jQuery技术精粹

978-7-115-31811-4
作者: 【德】Smashing Magazine
译者: 吴达茄芮鹏飞
编辑: 赵轩

图书目录:

详情

本书通过精彩的实例详细讲解了jQuery的相关技术,重点在于简化JavaScript的开发步骤,注重实例之间的对比与递进,充分展示jQuery所带来的革新。学习初期需要知道的关于JavaScript的七件事 对Javascript代码的复查的启示;利用七步测试法找到正确的Javascript解决方案;关于Javascript的10个古怪和秘密;Javascript的“七宗罪”

图书摘要

众妙之门JavaScript与jQuery技术精粹
[德] Smashing Magazine 著

吴达茄 芮鹏飞 译

人民邮电出版社

北京

前言

对于网站开发设计人员而言,在面对选择解决方案时做出正确的决定并不容易。不论是在建立复杂的网站应用还是在改进网站的过程中,都会有很多前期解决方案可供选择,有时选择最合适的一款方案至关重要。本书着重讲述了在选择相应解决方案时务必要注意的事项,即是否稳定并易于定制、是否有实用性并易于理解、是否具有可维护性、兼容性,以及功能的可拓展性。本书重点阐述了检验代码的重要性以及在执行JavaScript程序时需要避免的问题。所选择的解决方案应能符合较高的编码标准并能够剔除常见错误。本书将会告诉你如何获得此类解决方案。其中一部分内容介绍了利用专家系统审核代码并检查是否有其他方法解决当前问题。

在本书中,你将会熟悉JavaScript基本动画操作的黄金法则,了解JSON作为一种数据格式与JavaScript函数(数学、数组、字符串函数)共同存在,了解一些快捷符号等。当然,我们还会提到触发网站应用的JS事件、匿名函数的执行、模块模式、配置信息,以及与后台的交互及代码库的使用说明。对AJAX感兴趣的读者可以获得与动态可搜索内容相关的知识。在本书的后半部分,着重介绍了与jQuery有关的重要内容,能够帮助读者对jQuery的应用有更加透彻的认识。

——Andrew Rogerson,Smashing Magazine 编辑

版权声明

JAVASCRIPT-ESSENTIALS

Copyright © 2012 by Smashing Media GmbH

All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by any means, electronic, mechanical, by photocopying, recording or otherwise, without the prior permission in writing from Smashing Media GmbH.

CHINESE SIMPLIFIED language edition published by POSTS & TELECOM-MUNICATIONS PRESS, Copyright ©2013.

本书中文简体版由德国Smashing Media 公司授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。版权所有,侵权必究。

第一部分 JavaScript基础篇

 

第1章 初学JavaScript需知的七件事

Christian Heilmann

我很早以前就开始编写JavaScript代码,很高兴看到这种语言在今天所取得的成功,能成为这个成功故事中的一部分我很开心。关于JavaScript,我写过许多文章、章节以及一整本书,直到今天我仍在寻找新的东西。下文是一些我工作学习过程中激动时刻的记录,大家与其守株待兔,不如自己尝试去体会这种感受。

 

1.1 缩略标记

在创建对象和数组过程中可以使用缩略标记是我喜欢JavaScript的重要原因之一。过去,当我们需要创建一个对象时,我们会这样写:

var car = new Object();

car.colour = 'red';

car.wheels = 4;

car.hubcaps = 'spinning';

car.age = 4;

现在也可以写成

var car = {

colour:'red',

wheels:4,

hubcaps:'spinning',

age:4

}

这样写更加简洁,并且不用重复写对象名。现在,car运行良好,但是如果使用了invalidUserInSession会怎样呢?这种标记法中主要的缩略标记是IE,在第二个大括号前千万不要写逗号,否则你将会遇到麻烦。

另一个使用缩略标记的地方是定义数组。老的定义方法是这样的:

var moviesThatNeedBetterWriters = new Array(

'Transformers','Transformers2','Avatar','Indiana Jones 4'

);

更简洁的版本是这样的:

var moviesThatNeedBetterWriters = [

'Transformers','Transformers2','Avatar','Indiana Jones 4'

];

关于数组,另一个要注意的是没有所谓的关联数组。你会在很多代码中看到这样定义car:

var car = new Array();

car['colour'] = 'red';

car['wheels'] = 4;

car['hubcaps'] = 'spinning';

car['age'] = 4;

这不是Sparta,这是一种疯狂的行为——但不要为此而困扰。“关联数组”是一种令人困惑的对象命名方式。

另一种非常有意思的缩略标记方法叫做三重标记法。如下语句:

var direction;

if(x < 200){

direction = 1;

} else {

direction = -­1;

}

用三重标记法可以写成:

var direction = x < 200 ? 1 : -­1;

该条件为true时执行问号后的内容,否则执行冒号后的内容。

 

1.2 JSON数据格式

在我发现使用JSON存储数据之前,我试过使用各种JavaScript自带的格式来存储内容:带有控制字符进行分隔的数组、字符串等。Douglas Crockford所发明的JSON彻底改变了这一切。运用JSON,你可以使用Jav Script自带的格式存储各种复杂的数据并且不需要进行额外的转换。

JSON 是JavaScript Object Notation 的缩写,使用了我们前面介绍的两种缩略标记。

例如,想要描述一个乐队的话,可以写成:

var band = {

"name":"The Red Hot Chili Peppers",

"members":[

{

"name":"Anthony Kiedis",

"role":"lead vocals"

},

{

"name":"Michael 'Flea' Balzary",

"role":"bass guitar, trumpet, backing vocals"

},

{

"name":"Chad Smith",

"role":"drums,percussion"

},

{

"name":"John Frusciante",

"role":"Lead Guitar"

}

],

"year":"2009"

}

可以在JavaScript中直接使用JSON,并且封装在函数调用中时可作为API的返回值。这称为JSON-P格式,被很多API函数支持。可以使用数据端点在脚本语句中直接返回JSON-P格式。

<div id="delicious"></div><script>

function delicious(o){

var out = '<ul>';

for(var i=0;i<o.length;i++){

out += '<li><a href="' + o[i].u + '">' +

o[i].d + '</a></li>';

}

out += '</ul>';

document.getElementById('delicious').innerHTML = out;

}

</script>

<script src="http://feeds.delicious.com/v2/json/codepo8/javascript?

count=15&callback=delicious "></script>

这里调用了Delicious Web 服务来获得最新的J vaScript 书签(JSON 格式),然后将其显示为无序列表。

其实,JSON可能是在浏览器运行中描述复杂数据最轻松的方式了,甚至可以在PHP中调用json_decode()函数。

 

1.3 JavaScript自带函数(数学、数组以及字符串函数)

通读了JavaScript的数学、数组和字符串函数后,我意识到它们会让编程变得非常方便,使用它们可避免使用许多循环和条件。例如,当需要找到一组数中的最大数时,需要写这样一个循环:

var numbers = [3,342,23,22,124];

var max = 0;

for(var i=0;i<numbers.length;i++){

if(numbers[i] > max){

max = numbers[i];

}

}

alert(max);

可以不通过循环而这样实现:

var numbers = [3,342,23,22,124];

numbers.sort(function(a,b){return b -­ a});

alert(numbers[0]);

需要注意的是,不能对一个数值数组使用sort()函数,因为它会按照词法排序。

另一个有趣的方法是利用Math.max()函数,返回一列参数中的最大值:

Math.max(12,123,3,2,433,4); // returns 433

因为这个函数可以测试数据并返回最大值,因此可以用它来测试浏览器支持的默认属性:

var scrollTop= Math.max(

doc.documentElement.scrollTop,

doc.body.scrollTop

);

这解决了一个IE问题。我们可以读出当前文件的特性,但是对于该文件不同的文档类型,两个属性中其中之一将被赋予该值。而使用Math. max()则可以获得正确的值,因为只有一个属性有返回值,另一个将是未定义。

其余操作字符串的常用函数是split()和join()。最经典的例子可能就是利用一个函数将CSS的类添加到元素中。

现在的问题是,当需要在DOM元素中添加一个类时,要么是将它作为第一个类添加,要么是将它和一个空格键一起加在已经存在的类前面。当删除该类时,也需要删除相应的空格(这在过去更为重要,因为有些浏览器会因为多余的空格报错)。

因此,原始方程应该写成这样:

function addclass(elm,newclass){

var c = elm.className;

elm.className = (c === '') ? newclass : c+' '+newclass;

}

可以运用split()和join()函数来自动实现:

function addclass(elm,newclass){

var classes = elm.className.split(' ');

classes.push(newclass);

elm.className = classes.join(' ');

}

这样操作可以保证类与空格自动分离且结果被附加在最后。

 

1.4 事件代理

事件使得网络应用可以工作,我最爱事件,尤其是定制事件。它的存在,使得用户不需要接触核心代码就可以使产品具有更好的可拓展性。但主要的问题(其实也是它的优势)在于,事件会被HTML删除:对元素添加了事件监视器后它将被激活,但在HTML中无法表示这种情况。可以这样抽象地来考虑(这对初学者可能有困难):诸如IE6之类的浏览器内存问题较多,事件处理量大,因此不要使用太多的事件处理是明智的选择。

这里就是事件代理的来源。当某一特定的元素或者其上DOM层的所有元素发生某一事件时,可以通过单一的处理程序对父元素进行处理来简化事件处理过程,而不是使用大量的程序。

我的意思是什么?比如说想要获得一个链接列表,而且想要通过函数的调用而不是通过加载来获得,其HTML实现方法如下:

<h2>Great Web resources</h2>

<ul id="resources">

<li><a href="http://opera.com/wsc ">Opera Web Standards Curriculum</a></

li>

<li><a href="http://sitepoint.com ">Sitepoint</a></li>

<li><a href="http://alistapart.com ">A List Apart</a></li>

<li><a href="http://yuiblog.com ">YUI Blog</a></li>

<li><a href="http://blameitonthevoices.com ">Blame it on the voices</a></

li>

<li><a href="http://oddlyspecific.com ">Oddly specific</a></li>

</ul>

通常事件处理程序是在整个链接中使用循环:

// Classic event handling example

(function(){

var resources = document.getElementById('resources');

var links = resources.getElementsByTagName('a');

var all = links.length;

for(var i=0;i<all;i++){

// Attach a listener to each link

links[i].addEventListener('click',handler,false);

};

function handler(e){

var x = e.target; // Get the link that was clicked

alert(x);

e.preventDefault();

};

})();

也可通过一个事件处理程序来实现:

(function(){

var resources = document.getElementById('resources');

resources.addEventListener('click',handler,false);

function handler(e){

var x = e.target; // get the link tha

if(x.nodeName.toLowerCase() === 'a'){

alert('Event delegation:' + x);

e.preventDefault();

}

};

})();

因为单击事件发生在列表中所有的元素之上,所以你所要做的就是将节点Name 与 需要响应事件的元素进行对比。

说明:以上例子在IE6浏览器中会运行失败。对于IE6,需要使用事件模型而不是W3C,这就是我们在这种情况下使用库的原因。

这种方法的好处在于可以使用单独的事件处理程序。例如,想要在列表中动态地进行添加操作,如果使用事件代理,则不需要进行任何改变,只需在事件处理过程中重新分配处理程序并对列表重新进行循环操作就可以了。

 

// [...]

1.5 匿名函数和模块模式

JavaScript最令人烦恼的事情是变量的范围没有定义。任何在函数外定义的变量、函数、数组和对象都是全局的,这意味着相同页中的其他脚本都可以进行调用,因而经常出现参数被覆盖现象。

解决方法就是将变量封装在一个匿名函数中,在定义完函数后立即调用。例如,下例将生成三个全局变量和两个全局函数:

var name = 'Chris';

var age = '34';

var status = 'single';

function createMember(){

// [...]

}

function getMemberDetails(){

// [...]

}

该页中其他的脚本语句如果含有名为status的变量的话就会出问题。如果将它们封装在名为myApplication的匿名函数中,就可以解决这个问题了:

var myApplication = function(){

var name = 'Chris';

var age = '34';

var status = 'single';

function createMember(){

// [...]

}

function getMemberDetails(){

// [...]

}

}();

但是这样定义使得参数在函数外不起作用,如果这正是所需要的,没有问题。另外可以省略定义的名字。

(function(){

var name = 'Chris';

var age = '34';

var status = 'single';

function createMember(){

// [...]

}

function getMemberDetails(){

// [...]

}

})();

但如果需要部分变量或函数可被外部调用,则需要这样改写程序:为了可以调用createMember()或getMemberDetails()函数,将它们作为myApplication的属性返回。

var myApplication = function(){

var name = 'Chris';

var age = '34';

var status = 'single';

return{

createMember:function(){

// [...]

},

getMemberDetails:function(){

// [...]

}

}

}();

// myApplication.createMember() and

// myApplication.getMemberDetails() now works.

这样的用法被称为模块模式或单例模式。Dougl s Crockf rd 多次提到过这个概念,Yahoo用户接口函数库YUI中经常使用它。为了使函数和变量可以被外部调用,需要改变定义的语法,这很令人烦恼。而且,如果要从一个方法中调用另一个方法,还必须在调用时加上myApplication前缀。因此,我更倾向于返回这些我想要其成为全局元素的元素的指针,这样还可以缩短外部调用时的使用长度。

var myApplication = function(){

var name = 'Chris';

var age = '34';

var status = 'single';

function createMember(){

}

function getMemberDetails(){

// [...]

}

return{

create:createMember,

get:getMemberDetails

}

}();

//myApplication.get() and myApplication.create() now work.

我将这种方法称为“揭示模块模式”。

 

1.6 允许配置

每当我写完JavaScript源程序并将之公布于众时,人们总是想修改程序,有时是因为人们想进行功能拓展,但大多数时候是因为我的程序太难于定制。解决方法是在脚本语言中加入配置文件。我在《JavaScript配置对象》一文中进行了详细的讲述,下面是其中的一些要点。

① 在整个脚本文件中添加一个对象作为配置文件。

② 在配置文件中加入使用该脚本程序可能需要改变的所有信息:

• CSS 的 ID 和类名称;

• 生成按钮的字符串(比如说标签);

• 数据:例如“要展示的图片张数”,“地图的尺寸”;

• 地点、区域和语言设置。

③ 将其作为全局属性返回该对象以便人们可以将其重载。

大多数时候这一步放在编程的最后阶段。

其实,配置文件就是为了使代码更易于被其他开发人员使用和更改,这样添加配置文件之后就很少会收到邮件,抱怨你的代码或者询问他人更改过的地方。

 

1.7 与后台交互

这些年使用JavaScript的经验告诉我:JavaScript包含丰富的交互接口,但在进行数据处理和数据库访问时效果不佳。

最初,我用JavaScript代替Perl的原因是厌倦了每次要将代码复制到目录文件夹中才能运行的情况。后来我学会了利用后台程序来处理数据,而不是将所有的功能用JavaScript来实现,这样使得代码在安全性和语言性上都得到了提高。

访问一个Web服务时,可以得到JSON-P格式的返回值并在客户机上进行大量的数据转换。但是为什么在已经有了服务器并有更多的数据转换方法和JSON、HTML格式的返回值的时候,还要在客户机上进行启动缓存呢?

因此,如果想要使用Ajax,试着接触一些HTTP并编写自己的缓存和转换代理程序,这样可以节约大量的时间和精力。

 

1.8 特定于浏览器的代码就是浪费时间,试试库文件

在我进行网络开发之初,利用document.all还是document.layers来访问文件还存在很大的争议。我当时选择了document.layers方式,因为我喜欢将层作为当前层文件的思想(我为此还编写了大量document.write方法)。这两种方式后来都被淘汰了。Netscape 6 问世以后,它仅支持W3C DOM 模型,我非常喜欢这种方式,但是终端用户并不在意这些,他们看到的只是这种浏览器没有正确显示大部分互联网内容(实际上是显示了),我们最早开发的代码变成了错误。为此我们编写了即用型的代码,它支持顶尖的开发环境,其特点是变化丰富多样。

我在学习浏览器的复杂细节并解决与之相关的问题上花费过大量时间。当时这样做使我可以有一份非常棒的工作,但是现在的学习者不用再经历这样的过程了。

YUI、jQuery和Dojo这些库文件可以帮助我们。它们可以解决浏览器操作性与稳定性差,以及漏洞多的问题,使得我们可以忽略这些琐事。除非你是个发烧者,想测试某款特定的浏览器,不然的话,不要用JavaScript去修复浏览器的漏洞,因为你无法一直对修复代码进行更新,你要做的就是添加网络上已经存在的大量代码。

也就是说,单纯的依靠库文件来提升核心能力的做法是目光短浅的。要多读读JavaScript代码,看一些好的视频和帮助文档来帮助你理解这门语言(闭合性是JavaScript自有的优势)。库文件可以帮你快速地建立应用程序,但是如果因此添加了过多的事件和应用,而且还需要为文件中每个HTML元素添加类的话,那就不对了。

 

第2章 复查JavaScript代码的启示

Addy Osmani

在开始之前,我想问一个问题:你最近一次复查代码是什么时候?代码复查应该是提高整体解决问题能力的最好方式,如果没有利用好它,将会错过发现漏洞和聆听建议的机会,而这些正是使你的代码更加完美所需的。

没有人能写出100%没有漏洞的代码,所以不要为寻求帮助感到羞愧。我们行业中一些非常有经验的开发者,包括架构师和浏览器开发师都会经常要求别人来复查他们的代码,询问别人是否有地方可以改进以避免发生尴尬。代码复查应该被当成一项和其他技术方式解决问题同等重要的方法。

现在我们来谈一谈在哪里可以使代码得到复查,怎样构造复查请求以及哪些是需要复查的内容。我最近被邀请复查一项JavaScript应用程序的代码,所以考虑和大家分享一下成果,因为它大致上覆盖了关于JavaScript代码复查必须熟记于心的全部相关知识。

 

2.1 简介

代码复查与维护严格的编码标准紧密相关,也就是说,标准并不是为了防止逻辑错误或者对一些编程语言特殊语法的理解错误,无论是JavaScript、Ruby、Objective-C还是其他语言都适用于此规则。即使是最有经验的开发人员也有可能犯这样的错误,复查代码可以很好地帮助他们发现这些错误。

我们对于批评的第一反应都是保护自己(或者自己的代码),还有就是反击回去。诚然,批评确实会让人感觉低落,但是可以试着把它看成一种可以激励我们做得更好,并且能促进我们能力提升的学习经验。因为大多数情况下,当我们冷静下来时,事实也是如此。

没有人有义务为你的工作提供反馈,如果建议真的具有建设性的话,要感激别人对你的付出。

复查使我们可以学习别人的经验并从别人的观点中获益。当一天的工作结束后,这会增加我们写出更好代码的机会。是否接受这种机会完全在于你。

 

2.2 在哪里可以使代码得到复查?

一般最具挑战性的部分在于找到一个值得信任的有经验的开发者来帮我们复查。以下是一些可以请求别人复查代码的地方(有时是别国语言)。

• JSMentors

JSMentors是一个讨论JavaScript相关内容的邮件列表,其复查面板中有一大批有经验的开发者(包括JD Dalton、Angus Croll 和Nicholas Zakas)在复查人员名单上。这些老师不一定一直在线,但是对于提交的代码他们都会尽全力提供有用的、建设性的反馈意见。如果希望获得的是基于某种特殊JavaScript框架的代码帮助,绝大多数框架和库都有相关的邮件列表和论坛,可以提供相应水平的帮助。

• freenode IRC

有许多聊天室致力于讨论JavaScript语言并提供相关的帮助和代码复查。那些最出名的聊天室命名都很明显,#javascript主要讨论一般性的JavaScript问题请求,#jquery和#dojo很适合讨论与特定的库和框架相关的问题和请求。

• Code Review (beta)

可以理解将StackOverflow和代码复查弄混淆这件事,但是它实际上是获得同行复查的一个非常有用的、广谱的和主观的工具。在StackOverflow上你可能会问“为什么我的代码运行不了?”,而代码复查更像是“为什么我的代码这么丑?”这样一个问题。如果对于其提供的服务还有什么疑问,我强烈建议你去FAQ上看看。

• Twitter

这听起来可能很奇怪,但是我的至少一半以上的代码是通过社交网络来请求复查的。如果你的代码是开源的,社交网络是最好的选择,做这样的选择你并没什么损失。我唯一的建议是,确保与你交流的是一个有经验的开发人员,让一个没有什么经验的开发者来复查你的代码可能会比不复查更加糟糕,所以小心一点!

• GitHub+reviewth.is

我们都知道GitHub可以提供一个复查代码非常完美的结构体系。它包含提交文件、行注释和更改说明等功能,可以非常方便地跟踪各种叉形指令,唯一缺少的是实际的复查初始化。一个叫做reviewth.is的工具可以通过提供一个后提交的挂钩点来自动实现这个过程,这样提交的修改都会有一个清晰的#reviewthis散列标签,可以标记任何你想要求为你复查的用户。如果碰巧你的同事和你使用相同的编程语言,那么这项设置可以让你的代码复查在家门口进行。一个好的工作流程应该像这样进行(如果你在一个项目组或者课题组工作):将你的代码在智囊团的某个主题栏目中展示,然后为该栏目方面的所有请求发送代码;复查人员可以检查更改和提交情况,并按行或按文件作出注释;你(开发人员)可以获取这些反馈并在该主题栏目中进行修改和再请求,重复这种循环直到所有融合在一起的修改可接受为止。

 

2.3 该怎样构造复查请求?

以下是一些让你的复查请求更可能被接受的指导(基于经验)。如果复查人员属于你们的团队,不必拘泥于此,但是如果复查者是外部人员,这些会节省你一些时间。

• 隔离出你想要复查的代码,确保它们是易于运行的、叉状的和带注释的,标出你觉得可以改进的地方,除此之外,还要保持耐心。

• 使复查者尽可能容易地查看、演示和更改你的代码。

• 不要提交整个网站或工程的压缩文件,很少有人有时间来看全部代码,除非你的代码必须进行本地调试。

• 相反,在jsFiddle、jsbin 和GitHub 上你应该隔离和减少想要被复查的地方。这样可以让复查者更容易地分叉出你提交的代码并将更改和注释显示出来。如果想要区分出提交代码和别人修改的代码,可以试试PasteBin。

• 同样,不要只提交一个链接然后让别人来自己显示代码并找到要被改进的地方。网站上一般有很多脚本语句,所以这会降低复查者同意提供帮助的可能。因为没人想要花时间去为你找需要改进的地方。

• 明确地标示出你个人觉得可以改进的地方,这可以使复查者更快地找到你最想要被复查的部分以节省他们的时间。许多复查者也会因此看看你提交的别的部分的代码,至少也会优先考虑帮助你。

• 将你为改进代码做过的调查显示出来,如果复查者知道你做了这些调查,他们就不会建议你去了解这些相同的资源,而是提供另外的建议(这是你想要的东西)。

• 如果英语不是你的母语,告诉别人。因为当别的开发人员告知我这一点的时候,我就知道该使用技术性的还是通俗的复查建议了。

• 耐心一点。很多复查需要好几天才能得到反馈,这并没有什么问题。其他开发人员经常会忙于别的开发项目,那些答应安排看一下你代码的人是令人感激的。耐心一点,不要急于提醒他们,理解他们推迟的原因。这样做对你有好处,因为这样复查者才会有更充足的时间来给出详细的反馈意见。

 

2.4 进行代码复查的人员需要提供的信息

Google 前开发人员Jonathan Betz 曾经提到过对别人进行代码复查时应该提供的六样东西:

1. 正确性

代码能实现所有它声明的功能吗?

2.复杂性

代码是否直接完成了其功能?

3.一致性

它是否与目标一致?

4.可维护性

团队内其他人员付出一定合理水平的努力时是否可以较容易地拓展代码?

5.可扩缩性

代码是否是按照对100个或者10000个用户同样工作的原则来书写的?它是最优的吗?

6.风格

代码是否按某一特定风格编写的(最好是按照团队统一的风格)?

我赞同以上六点,并将它扩展成复查者在实际操作中可以遵循的行动手册。所以,复查者应该做到以下几点:

• 提供清晰的评论、依据并保持良好的沟通。

• 提出可实现的不足之处(不要批评过度)。

• 指出为什么某种方法不推荐,如果可能的话,给出博客、帖子、要点、说明、MDN页和jsPerf测试来支持你的观点。

• 给出替代解决方案,或是用一个单独的可运行格式,或是通过fork 整合在代码中,方便开发者清晰地看到它们错误的地方。

• 首先关注解决方案,其次看编程风格。对于编程风格的建议可以放在复查的后面,但是在关注这个之前首先要找出根本的问题。

• 复查要求外的部分,这完全由复查者自己决定,但是如果我发现开发者其他方面的问题,我一般会建议他们如何改进。到目前为止我还没收到过关于这方面的抱怨,所以我认为这并不是一件坏事。

 

2.5 协作代码复查

尽管单独的开发者可以工作得很好,但将更多的开发人员带入这个流程也是不错的选择。这样有几个明显的优点:减轻单独复查人员的负担,得到更多人的改进意见,并可以使某一位复查者的评论得到展示和修改以防发生错误。为了更好地帮助复查团队,你需要一个可以允许同时检查和评论的工具。幸运的是,这里有一些不错的选择:

• Review Board

这个基于网络的工具拥有MIT许可即可免费使用,它集成了Gits、CVS、Mercurial 以及其他源代码控制系统。Review Board 可以在运行 Apache 或lighttpd的服务器(基于个人或商业用途)上免费使用。

• Crucible

这款由澳大利亚软件公司Atlassian开发的工具也是基于网络的,它的服务对象是公司,特别适合分布式团队。Crucible简化了复查和注释功能,与Review Board 一样,集成了大量控制源码工具,如Git、Subversion。

• Rietveld

和前面两种工具一样,Rietveld也支持合作复查功能。它是由Python的创始人Guido van Rossum 开发的,得益于Guido Mondrian 的开发经验,被设计用来运行谷歌的云服务,谷歌利用这项专利来复查内部代码。

• 其他工具

其他大量支持复查代码功能的软件并不是基于这个目的开发的。这些包括CollabEdit(基于网络的免费工具),还有我的最爱EtherPad(也是基于网络的免费工具)。

 

2.6 JavaScript代码复查实例

最近一位开发人员让我对他的代码进行复查并提供改进建议。虽然我并不是代码复查专家(不要被我上面所说的忽悠),我在这里还是给出我提出的问题和解决方案。

问题1

问题:函数和对象没经过任何类型校验就作为参数传递给其他函数。

回复:类型校验是保证输入类型的必要步骤,如果没有进行检查,可能就有输入类型(字符串、日期、数组等)不确定的风险,这些可以轻易地毁掉你未经防御处理的应用程序。对于函数,至少应该进行以下处理:

1. 测试以确保传递的变量真实存在;

2.进行typeof检查以阻止执行的输入为非有效函数。

if (callback && typeof callback === "function"){

/* rest of your logic */

}else{

/* not a valid function */

}

不幸的是,简单的typeof检查是不够的,正如Angus Croll在“Fixing the typeofoperator”中指出,在对包括函数在内的许多内容进行typeof检查时需要注意大量细节。

例如,对空返回对象进行typedef检查在技术上是错误的。实际上,对于除了函数之外的任何对象类型进行typedef检查时,都会返回对象而不区分它们是数组、日期、RegEx还是什么。

可以利用Object.prototype.toString来调用JavaScript内部对象的属性,即[Class],也就是对象的类属性。不幸的是,内置对象通常会覆盖Object.pro-totype.toString,但是可以对它们加上通用的toString函数:

Object.prototype.toString.call([1,2,3]); // "[object Array] "

你可能也会发现下面Angus的函数是比typeof更适合的选择,对对象、数组以及其他类型调用betterTypeOf()函数来看看会发生什么。

function betterTypeOf( input ){

return Object.prototype.toString.call(input).match(/^\[object\s(.*)\]$/)

[1];

}

这里,par eInt()函数被盲目地用来解析用户输入的整数值却没有指定基,这样会引起麻烦。

在"JavaScript :the Good Parts "中,Douglas Crockford 指出parseInt() 函数的调用是非常危险的。尽管你知道输入字符串变量会返回整数,也应该指定一个基作为第二个变量,否则会返回意想不到的输出,考虑下面的例子:

parseInt('20'); // returns what you expect, however…

parseInt('020'); // returns 16

parseInt('000020'); // returns 16

parseInt('020', 10); // returns 20 as we've specified the base to use

你会对多少开发人员忽略第二个参数感到吃惊,但实际上这经常发生。记住使用者(如果允许自由输入数值)并不一定会根据标准的数值惯例来输入(因为他们太疯狂了!)。我见过020、尽可能为各种方式的输入值进行解析,下列使用parseInt()函数的方式偶尔会更好:

Math.floor("020"); // returns 20

Math.floor("0020"); //returns 20

Number("020"); //returns 20

Number("0020"); //returns 20

+"020"; //returns 20

问题2

问题:在整个代码库上重复检查是否满足特定于浏览器的条件(例如:特性监测,检查支持的ES5特性等)。

回复:理想情况下,应保持代码库尽可能的“干燥”,有一些好的解决方案可以解决这个问题。例如,可以从加载时间配置模式(也称为加载时间和初始化时间分支)中获益。基本思想是仅测试条件一次(加载应用时)然后在后续检查中来调用这个结果。这种模式在JavaScript库文件中很常见,这些JavaScript库文件在加载时会自我配置,以针对具体浏览器进行优化。

这种模式可以这样实现:

var tools = {

addMethod: null,

removeMethod: null

};

 

if(/* condition for native support */){

tools.addMethod = function(/* params */){

/* method logic */

}

}else{

/* fallback -­ eg. for IE */

tools.addMethod = function(/* */){

/* method logic */

}

}

下面的例子演示了如何规范化得到XMLHttpRequest对象。

var utils = {

getXHR: null

};

if(window.XMLHttpRequest){

utils.getXHR = function(){

return new XMLHttpRequest;

}

}else if(window.ActiveXObject){

utils.getXHR = function(){

/* this has been simplified for example sakes */

return new ActiveXObject(’Microsoft.XMLHTTP’);

}

}

有一个很著名的例子,Stoyan Stefanov 运用这个来添加和删除跨浏览器的事件监听器,在他的《JavaScript Patterns》一书中有介绍。

var ut ls = {

addListener: null,

removeListener: null

};

// the implementation

if (typeof w ndow.addEve tL stener === ’function’) {

uti s.a dLis ener = function ( el, type, fn ) {

el.addEventListener(type, fn, false);

};

uti s.removeLis ener = function ( el, type, fn ) {

el.removeEventListener(type, fn, false);

};

} else if (typeof docume t.attachEven === ’function’) { // IE

uti s.addList er = func ion ( el, type, fn ) {

el.attachEvent(’on’ + type, fn);

};

问题3

问题:定期扩展本机Object.prototype。

回复:扩展本机类型经常会出问题,很少有(如果有的话)著名的代码库敢于扩展Object.prototype类型。事实是并没有一定要扩展它的情况存在。除非是要破坏JavaScript代码中的对象散列表及增加命名冲突可能性,这种扩展的操作一般被认为是糟糕的,这种操作应该是最后选择项(这同扩展自定义对象属性大有不同)。

如果因为某种原因你需要结束扩展对象原型,确保该方法已经不存在并拟出文件使小组中其他成员知道为什么需要这样做,你可以使用以下代码作为指导:

if(typeof Object.prototype.myMethod != ’function’){

Object.prototype.myMethod = function(){

//implem

};

}

Juriy Zaytsev 有一篇关于“扩展本机和主机对象”的非常著名的帖子,可能你会感兴趣。

问题4

问题:有些代码严重阻塞页面,因为它在进行任何进一步操作之前都要等待进程完成或数据加载。

回复:页面阻塞导致用户使用体验差,有很多不损坏应用的解决方法。

一个解决方法是使用“延迟执行”(通过“许诺”和“将来”的概念)。“许诺”的基本思想是与其让某些调用占用资源,不如直接返回一个“将来”会实现的“许诺”。这样将允许编写可异步运行的非阻塞逻辑。常见的做法是在方程中引入一个调用,当请求完成时执行。

我曾经和Julian Aubourg 写过一篇全面介绍这种方法的帖子,如果你对通过jQuery实现它感兴趣可以看看这篇帖子。当然也可以利用JavaScript实现。微框架Q提供了一个一般性的JS-兼容的“许诺”、“将来”实现方案,它相对而言比较全面,具体如下:

/* define a promise-­only delay function that resolves when a timeout

completes */

function delay(ms) {

var deferred = Q.defer();

setTimeout(deferred.resolve, ms);

return deferred.promise;

}

/* usage of Q with the 'when' pattern to execute a callback once delay

fulfils the promise */

Q.when(delay(500), function () {

});

/* do stuff in the callback */

如果你想找一些更基础的可通读程序,这里是Douglas Crockford 关于“许诺”的实现方法:

function make_promise() {

var status = ’unresolved’,

outcome,

waiting = [],

dreading = [];

function vouch( deed, func ) {

switch (status) {

case ’unresolved’:

(deed === ’fulfilled’ ? waiting : dreading).push(func);

break;

case deed:

func(outcome);

break;

}

};

function resolve( deed, value ) {

if (status !== ’unresolved’) {

throw new Error(’The promise has already been resolved:’ + status);

}

status = deed;

outcome = value;

(deed == ’fulfilled’ ? waiting : dreading).forEach(function (func) {

try {

func(outcome);

} catch (ignore) {}

});

waiting = null;

dreading = null;

};

return {

when: function ( func ) {

vouch(’fulfilled’, func);

},

fail: function ( func ) {

vouch(’smashed’, func);

},

fulfill: function ( value ) {

resolve(’fulfilled’, value);

},

smash: function ( string ) {

resolve(’smashed’, string);

},

status: function () {

return status;

}

};

};

问题5

问题:通常使用“= =”操作符测试某一属性的显式数值等式,但应该使用的是“= = =”操作符。

回复:正如你可能知道也可能不知道的,“= =”操作符在JavaScript 中的使用非常自由,即使两个量的值是完全不同的类型也会认为它们相等。这是因为该操作符会优先进行强制类型转换而不是比较,“= = =”却是在两个类型不一样的情况下不会进行强制类型转换,因而会报错。

我之所以在特定类型比较(本例)时更多地推荐使用“= = =”操作符,是因为“= =”操作符有许多陷阱并被许多开发人员认为是不可靠的。

你可能想知道在抽象化的语言(如CoffeeScript)中,由于其不可靠性,“= =”操作符的使用率相对“= = =”完全处于下风。

与其听我片面之言,不如看看下面运用“= =”进行布尔相等性检查的例子,该例子运行会产生无法预期的结果。

3 == "3" // true

3 == "03" // true

3 == "0003" // true

3 == "+3" //true

3 == [3] //true

3 == (true+2) //true

\t\r\n ’ == 0 //true

"\t\r\n" == 0 //true

"\t" == 0 // true

"\t\n" == 0 // true

"\t\r" == 0 // true

" " == 0 // true

" \t" == 0 // true

" \ " == 0 // true

" \r\n\ " == 0 //true

上面列表中许多结果等于true,因为JavaScript是一种弱类型化的语言:它尽量多地使用强制类型转换。如果你对上述表达式等于true的原因感兴趣,可以参阅《Annotated ES5 指导》,其中的解释更为精彩。

回到复查上面来,如果100%确信进行比较的量不会被用户干扰,可以谨慎地使用“= =”操作符。一定记住,如果有非预期的输入,使用“= = =”操作符会更好。

问题6

问题:非缓存的数组长度被用于所有的for循环中是非常糟糕的,因为你在利用它遍历整个元素集合。

这里有个例子:

for( var i=0; i<myArray.length;i++ ){

/* do stuff */

}

回复:这种方法(我依然看到许多开发人员在使用)的问题在于该数组长度在每个循环的迭代中被不必要的重复访问。这会导致程序运行非常慢,尤其是用在 HTMLCollection 上时(在这种情况下,正如 Nicholas C. Zakas 在 《 High-Perform nce JavaScri t》一书中提到的,对长度进行缓存可以比反复访问它快上190倍)。以下是对数组长度进行缓存的一些方法。

/* cached outside loop */

var len = myArray.length;

for ( var i = 0; i < len; i++ ) {

}

/* cached inside loop */

for ( var i = 0, len = myArray.length; i < len; i++ ) {

}

/* cached outside loop using while */

var len = myArray.length;

while (len-­-­) {

}

如果你想研究哪种方法表现最佳的话,使用jsPerf对循环内外的数组捕捉、前缀增量使用、倒计时等进行测试以比较其性能优劣也是可行的。

问题7

问题:jQuery的$.each()函数用于遍历对象和数组,然而在某些情况下则使用for。

回复:在j uery中,有两种方法可以无缝地遍历对象和数组。通用的$.each可以遍历这两种类型,$.fn.each()函数专门用于遍历jQuery对象(其中标准对象利用$()函数封装,你应该更倾向于使用后者)。低级别的$.each()函数执行效果比$.fn.each() 函数好,标准的JavaScript for 和while 循环比这两个都要好,这是经jsPerf测试验证的。以下是一些运行情况也不错的循环:

/* jQuery $.each */

$.each(a, function() {

e = $(this);

});

/* classic for loop */

var len = a.length;

for ( var i = 0; i < len; i++ ) {

//if this must be a jQuery object do..

e = $(a[i]);

//otherwise just e = a[i] should suffice

};

/* reverse for loop */

for ( var i = a.length; i-­-­ ) {

e = $(a[i]);

}

/* classic while loop */

var i = a.length;

while (i-­-­) {

e = $(a[i]);

}

/* alternative while loop */

var i = a.length -­ 1;

while ( e = a[i-­-­] ) {

$(e)

};

你可能会发现,Angus Croll 的帖子"Rethinking JavaScript for Loops "是对这些建议的一个有趣的延伸。

考虑一个以数据为中心的应用程序,每一个对象或数组都包含大量数据,你应该考虑进行重构来使用以上方法。从拓展性角度说,你应该尽可能地剔除浪费的毫秒数,因为当页面上有数以千计的元素时,时间会累积到很大。

问题8

问题:JSON字符串在内存中以字符串级联的方式建立。

回复:可以通过更优的方式来实现。例如,为什么不使用可以接收JavaScript对象并返回与JSON格式等效的JSON.stringify()函数呢?对象可以按照需要尽可能的复杂或者深度嵌套,这样将会产生更加简单、有效的解决方法。

var myData = {};

myData.dataA = [’a’, ’b’, ’c’, ’d’];

myData.dataB = {

animal’: ’cat’,

’color’: ’brown’

};

myData.dataC = {

vehicles’: [{

’type’: ’ford’,

’tint’: ’silver’,

’year’: ’2015’

}, {

type’: ’honda’,

’tint’: ’black’,

’year’: ’2012’

}]

};

myData.dataD = {

buildings’: [{

’houses’: [{

’streetName’: ’sycamore close’,

’number’: ’252’

}, {

streetName’: ’slimdon close’,

’number’: ’101’

}]

}]

};

console.log(myData); //object

var jsonData = JSON.stringify(myData);

console.log(jsonData);

/*

{"dataA":["a","b","c","d"],"dataB":

{"animal":"cat","color":"brown"},"dataC":{"vehicles":

[{"type":"ford","tint":"silver","year":"2015"},

{"type":"honda","tint":"black","year":"2012"}]},"dataD":

{"buildings":[{"houses":[{"streetName":"sycamore

close","number":"252"},{"streetName":"slimdon

close","number":"101"}]}]}}

*/

额外的调试小技巧,如果你想要使得终端控制台显示的JSON更为美观可读,可以使用stringify()函数的以下额外参数实现:

JSON.stringify({ foo: "hello", bar: "world" }, null, 4);

问题9

问题:使用的命名空间模式在技术上是无效的。

回复:应用程序中其他部分使用的命名空间是正确的,而对其存在性的检查是无效的,现有:

if ( !MyNamespace ) {

MyNamespace = { };

}

问题在于 !MyNamespace 会报错:ReferenceError。因为 MyNamespace 变量之前未经声明。较好的模式是利用内部变量声明布尔类型的强制转换,如下:

if ( !MyNamespace ) {

var MyNamespace = { };

}

//or

var myNamespace = myNamespace || {};

// Although a more efficient way of doing this is:

// myNamespace || ( myNamespace = {} );

// jsPerf test: http://jsperf.com/conditional-­assignment

//or

if ( typeof MyNamespace == ’undefined’ ) {

var MyNamespace = { };

}

当然,可以通过其他许多方法来实现。如果你想阅读更多关于命名空间模式的内容(以及一些命名空间拓展的思路),可以参阅我最近写的"Essential JavaScript Namespacing Patterns "一文,Juriy Zaytsev 也写过一篇关于命名空间模式非常全面的文章。

 

2.7 总结

代码复查是增强和保持编码质量、标准化、正确性和稳定性的一项非常有效的方法。我强烈建议开发人员可以在日常项目中尝试一下,因为无论对开发者还是复查者,这都是一件很棒的学习工具。下次,希望你可以复查你的代码,并祝你项目进展顺利!

相关图书

深入浅出Spring Boot 3.x
深入浅出Spring Boot 3.x
JavaScript核心原理:规范、逻辑与设计
JavaScript核心原理:规范、逻辑与设计
JavaScript入门经典(第7版)
JavaScript入门经典(第7版)
JavaScript函数式编程指南
JavaScript函数式编程指南
PHP、MySQL和JavaScript入门经典(第6版)
PHP、MySQL和JavaScript入门经典(第6版)
JavaScript学习指南(第3版)
JavaScript学习指南(第3版)

相关文章

相关课程