本篇内容
  • 可维护性
  • 性能
  • 部署

1 可维护性

在早期的网站中,JavaScript主要是用于小特效或者是表单验证。而今天的Web应用则会有成千上万行JavaScript代码,执行各种复杂的过程。这种演化让开发者必须得考虑到可维护性。除了秉承较传统理念的软件工程师外,还要雇佣JavaScript开发人员为公司创造价值,而他们并非仅仅按时交付产品,同时还要开发智力成果在之后不断地增加价值。
编写可维护的代码很重要,因为大部分开发人员都花费大量时间维护他人代码。很难从头开始开发新代码的,很多情况下是以他人的工作成果为基础的。确保自己代码的可维护性,以便其他开发人员在 此基础上更好的开展工作。

1.1 什么是可维护的代码

可维护的代码有一些特征。一般来说,如果说代码是可维护的,它需要遵循-以下特点。

  • 可理解性:其他人可以接手代码并理解它的意图和一般途径,而无需原开发人员的完整解释。
  • 直观性:代码中的东西一看就能明白,不管其操作过程多么复杂。
  • 可适应性:代码以一种数据上的变化不要求完全重写的方法撰写。
  • 可扩展性:在代码架构上已考虑到在未来允许对核心功能进行扩展。
  • 可调试性:当有地方出错时,代码可以给予你足够的信息来尽可能直接地确定问题所在。

1.2 代码约定

一种让代码变得可维护的简单途径是形成一套JavaScript代码的书写约定。绝大多数语言都开发出了各自的代码约定,只要在网上一搜就能找到大量相关文档。专业的组织为开发人员制定了详尽的代码约定试图让代码对任何人都可维护。杰出的开放源代码项目有着严格的代码约定要求,这让社区中的任何人都可以轻松地理解代码是如何组织的。
由于JavaScript的可适应性,代码约定对它也很重要。由于和大多数面向对象语言不同,JavaScript并不强制开发人员将所有东西都定义为对象。语言可以支持各种编程风格,从传统面向对象式到声明式到函数式。只要快速浏览一下一些开源JavaScript库,就能发现好几种创建对象、定义方法和管理环境的途径。

以下小节将讨论代码约定的概论。对这些主题的解说非常重要,虽然可能的解说方式会有区别,这取决于个人需求。

1.可读性

要让代码可维护,首先它必须可读。可读性与代码作为文本文件的格式化方式有关。可读性的大部分内容都是和代码的缩进相关的。当所有人都使用一样的缩进方式时,整个项目中的代码都会更加易于阅读。通常会使用若干空中备而非制表符来进行缩进,这是因为制表符在不同的文本编辑器中显示效果不同。一种不错的、很常见的缩进大小为4个空格,当然你也可以使用其他数量。
可读性的另一方面是注释。在大多数编程语言中,对每个方法的注释都视为一个可行的实践。因为JavaScript可以在代码的任何地方创建函数,所以这点常常被忽略了。然而正因如此,在JavaScript中为每个函数编写文档就更加重要了。一般而言有如下一些地方需要进行注释。

  • 函数和方法:每个函数或方法都应该包含一个注释,描述其目的和用于完成任务所可能使用的算法。陈述事先的假设也非常重要.参数代表什么,函数是否有返回值(因为这不能从函数定义中推断出来)。
  • 大段代码:用于完成单个任务的多行代码应该在前面放-个描述任务的注释。
  • 复杂的算法:如果使用了一种独特的方式解决某个问题,则要在注释中解释你是如何做的。这不仅仅可以帮助其他浏览你代码的人,也能在下次你自己查阅代码的时候帮助理解。
  • Hack:因为存在浏览器差异,JavaScript代码一般会包含一些hack。不要假设其他人在看代码的时候能够理解back所要应付的浏览器问题。如果因为某种浏览器无法使用普通的方法,所以你需要用一些不同的方法,那么诸将这些信息放在注释中。这样可以减少出现这种情况的可能性:有人偶然看到你的hack,然后”修正“了它,最后重新引入了你本来修正了的错误。

缩进和注释可以带来更可读的代码、在未来则更容易维护。

2.变量和函数命名

适当纷变量和函数起名字对于增加代码可理解性和可维护性是非常重要的。 由于很多JavaScript开发人员最初都只是业余爱好者,所以有一种使用无意义名字的倾向,诸如给变量起”foo“、”bar“等名字,给函数器”doSomething“这样的名字。专业JavaScript开发人员必须克服这些恶习以创建可维护的代码。命名的一般规则如下所示。

  • 变量名应为名词,如car或person。
  • 函数名应该以动词开始,如getName()。返回布尔类型值的函数一般以is开头,如
    isEnable()。
  • 变量和函数都应使用合乎逻辑的名字,不要担心长度。长度问题可以通过后处理和压缩来缓解。

必须避免出现无法表示所包含的数据类型的无用变量名。有了合适的命名,代码阅读起来就像讲述故事一样,更容易理解。

3.变量类型透明

由于在JavaScript中变量是松散类型的,很容易就忘记变量所应包含的数据类型。合适的命名方式可以一定程度上缓解这个问题,但敢到所有的情况下看,还不够。有三种表示变量数据类型的方式。
第一种方式是初始化。当定义了一个变量后,它应该被初始化为一个值,来暗示它将来应该如何应用。例如,将来保存布尔类型值的变量应该初始化为true或者false,将来保存数字的变量就应该初 始化为一个数字,如以下例子所示:

1
2
3
4
var found = false;	// Boolean
var count = -1; // Number
var name = ''; // String
var person = null; // Object

初始化为一个特定的数据类型可以很好的指明变量的类型。但缺点是它无法用于函数声明中的函数参数。
第二种方法是使用匈牙利标记法来指定变量类型。匈牙利标记法在变量名之前加上一个或多个字符来表示数据类型。这个标记法在脚本语言中很流行,曾经很长时间也是JavaScript所推崇的方式。JavaScript中最传统的匈牙利标记法是用单个字符表示基本类型:”o“代表对象,”s“代表字符串,”i“代表整数、”f“代表浮点数,”b“代表布尔型。如

1
2
3
4
var bFound;	// Boolean
var iCount; // Number(Int)
var sName; // String
var oPerson; // Object

JavaScript中用匈牙利标记法的好处是函数参数一样可以使用。但它的缺点是让代码某种程度上难以阅读,阻碍了没有用它时代码的直观性和句子式的特质。因此,匈牙利标记法失去了一些开发者的宠爱。

最后一种指定变量类型的方式是使用类型注释。类型注释放在变量名右边,,但是在初始化前面。这种方式是在变量旁边放-段指定类型的注释,如下所示:

1
2
3
4
var found /* :Boolean */ = false;
var count /* :Int */ = 10;
var name /* :String */ = 'Micheal';
var person /* :Object */ = null;

类型注得维持了代码的整体可读性,同时注入f类型信息。类型注释的缺点是你不能用多行注释-次注释大块的代码,因为类型注释也是多行注释,两者会冲突,如下例所示所示:

1
2
3
4
5
6
7
// error
/*
var found /* :Boolean */ = false;
var count /* :Int */ = 10;
var name /* :String */ = 'Micheal';
var person /* :Object */ = null;
*/

这里,试图通过多行注释注释所有变量。类型注释与其相冲突,因为第一次出现的户(第二行)匹配了第一次出现的叫(第3行),这会造成一个语法错误。如果你想注释掉这些使用类型注释的代码行,最好在每一行上使用单行注释(很多编辑器可以帮你完成)。
这就是最常见的三种指定变量数据类型的方法。每种都有各自的优势和劣势,要自己在使用之前进行评估。最重要的是要确定哪种最适合你的项目;并一致使用。

1.3 松散耦合

只要应用的某个部分过分依赖于另一部分,代码就是耦合过紧,难于维护。典型的问题如:对象直接引用另一个对象,并且当修改其中一个的同时需要修改另外一个。紧密耦合的软件难于维护并且需要经常重写。
因为Web应用所涉及的技术,有多种情况会使它变得耦合过紧。 必须小心这些情况,并尽可能维护弱耦合的代码。

1.解耦HTML/JavaScript

一种最常见的耦合类型是HTML/JavaScript耦合。在Web上,HTML和JavaScript各自代表了解决方案中的不同层次:HTML是数据,JavaScript是行为。 因为它们天生就需要交互,所以有多种不同的方法将这两个技术关联起来。但是,有些方法会将HTML和JavaScript过于紧密地耦合在一起。
直接写在HTML中的JavaScript,使用包含内联代码的<script>元素或者是使用HTML属性来分配事件处理程序,都是过于紧密的耦合。请看以下代码。

1
2
3
4
5
6
7
<!-- 紧密耦合 -->
<script type="text/javascript">
document.write('Hello');
</script>

<!-- 事件处理的紧密耦合 -->
<input type="button" value="click" onclick="doSomething()"/>

虽然这些从技术上来说都是正确的,但是实践中,它们将表示数据的HTML和定义行为的JavaScript紧密耦合在了一起。 理想情况是,HTML和JavaScript应该完全分离,并通过外部文件和使用DOM附加行为来包含JavaScript。
当HTML和JavaScript过于紧密的耦合在一起时,出现JavaScript错误时就要先判断错误是出现在HTML部分还是在JavaScript文件中。它还会引入和代码是否可用的相关新问题。在这个例子中,可能在doSomething()函数可用之前,就已经按下了按钮,引发了一个JavaScript错误。因为任何对按钮行为的更改要同时触及HTML和JavaScript,因此影响了可维护性。而这些更改本该只在JavaScript中进行。
HTML和JavaScript的紧密耦合也可以在相反的关系上成立:JavaScript包含了HTML。这通常会出现在使用innerHTML来插入-段HTML文本到页面上这种情况中,如下面的例子所示:

1
2
3
4
5
// 将HTML紧密耦合到JavaScript
function insertMessage (msg) {
var container = document.getElementById('container');
container.innerHTML = '<div class="msg"><p class="post">' + msg + '</p></div>';
}

一般来说,你应该避免在JavaScript中创建大量HTML。再一次重申要保持层次的分离,这样可以很容易的确定错误来源。当使用上而这个例子的时候,有一个页面布局的问题,可能和动态创建的HTML没有被正确格式化有关。不过,要定位这个错误可能非常困难,因为你可能一般先看页面的源代码来查找那段烦人的HTML,但是却没能找到,因为它是动态生成的。对数据或者布局的更改也会要求更改JavaScript,这也表明了这两个层次过于紧密地耦合了。
HTML呈现应在该尽可能与JavaScript保持分离。当JavaScript用于插入数据时,尽量不要直接插入标记。 一般可以在页面中直接包含并隐藏标记,然后等到整个页面渲染好之后,就可以用JavaScript显示该标记,而非生成它。另一种方法是进行Ajax请求并获取更多主要显示的HTML,这个方法可以让同样的渲染层(PHP、JSP、Ruby等等)来输出标记,而不是直接嵌在JavaScript中。
将HTML和JavaScript解耦可以在调试过程中节省时间,更加容易确定错误的来源,也减轻维护的难度:更改行为只需要在JavaScript文件中进行,而更改标记则只要在渲染文件中。

2.解耦CSS/JavaScript

另一个Web层则是css,它主要负责页面的显示。JavaScript和css也是非常紧密相关的:他们都是HTML之上的层次,因此常常一起使用。但是,和HTML与JavaScript的情况一样,css和JavaScript也可能会过于紧密地耦合在一起。最常见的紧密耦合的例子是使用JavaScript来更改某些样式,如下所示:

1
2
3
// css对JavaScript的紧密耦合
element.style.color = 'red';
element.style.backgroundColor = 'blue';

由于css负责页面的显示,当显示出现任何问题时都应该只是查看css文件来解决。然而,当使用了JavaScript来更改某些样式的时候,比如颜色,就出现了第二个可能已更改和必须检查的地方。结果是JavaScript也在某种程度上负责了页面的显示,并与css紧密耦合了。如果未来需要更改样式表,css和JavaScript文件可能都需要修改。这就给开发人员造成了维护上的噩梦。所以在这两个层次之间 必须有清晰的划分。
现代Web应用常常要使用JavaScript来更改样式,所以虽然不可能完全将css和JavaScript解耦,但是还是能让耦合更松散的。这是通过动态更改样式类而非特定样式来实现的,如下例所示:

1
2
// css对JavaScript的松散耦合
element.className = 'edit';

通过只修改某个元素的css类,就可以让大部分样式信息严格保留在css中。JavaScript可以更改样式类,但并不会直接影响到元素的样式。 只要应用了正确的类,那么任何显示问题都可以直接追溯到css而非JavaScript。
第二类紧密耦合仅会在IE中出现(但运行于标准模式下的IE8不会出现),它可以在css中通过表达式嵌入JavaScript,如下例所示:

1
2
3
4
/* javascript对css的紧密耦合 */
div {
width: expression(document.body.offsetWidth - 10 + 'px');
}

通常要避免使用表达式,因为它们不能跨浏览器兼容,还因为它们所引入的JavaScript和css之间的紧密耦合。如果使用了表达式,那么可能会在css中出现JavaScript错误。由于css表达式而追踪过JavaScript错误的开发人员,会告诉你在他们决定看一下css之前花了多长时间来查找错误。
再次提醒,好的层次划分是非常重要的。显示问题的唯一来源应该是CSS,行为问题的唯一来源应该是JavaScript。在这些层次之间保持松散耦合可以让你的整个应用更加易于维护。

3.解耦应用逻辑/事件处理程序

每个Web应用一般都有相当多的事件处理-程序,监听着无数不同的事件。然而,很少有能仔细得将应用逻辑从事件处理程序中分离的。请看以下例子:

1
2
3
4
5
6
7
8
9
10
11
function handleKeyPress (event) {
event = EventUtil.getEvent(event);

if (event.keyCode == 13) {
var target = EventUtil.getTarget(event);
var value = 5 * parseInt(target.value);
if (value > 10) {
document.getElementById('error-msg').style.dispaly = 'block';
}
}
}

这个事件处理程序除了包含了应用逻辑,还进行了事件的处理。这种方式的问题有其双重性。首先,除了通过事件之外就再没有方法执行应用逻辑,这让调试变得困难。如果没有发生预想的结果怎么办? 是不是表示事件处理程序没有被调用还是指应用逻辑失败?其次,如果一个后续的事件引发同样的应用逻辑,那就必须复制功能代码或者将代码抽取到一个单独的属数中。无论何种方式,都要作比实际所需更多的改动。
较好的方法是将应用逻辑和事件处理程序相分离,这样两者分别处理各自的东西。一个事件处理程序应该从事件对象中提取相关信息,并将这些信息传送到处理应用逻辑的某个方法中。例如,前面的代码可以被重写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function validateValue (value) {
value = 5 * parseInt(value);
if (value > 10) {
document.getElementById('error-msg').style.dispaly = 'block';
}
}

function handleKeyPress (event) {
event = EventUtil.getEvent(event);

if (event.keyCode == 13) {
var target = EventUtil.getTarget(event);
validateValue(target.value);
}
}

改动过的代码合理将应用逻辑从事件处理程序中分离了出来。handleKeyPress()函数确认是按下了Enter键(event.keyCode为13),取得了事件的目标并将value属性传递给validateValue()函数,这个函数包含了应用逻辑。注意validateValue()中没有任何东西会依赖于任何事件处理程序逻辑,它只是接收一个值,并根据该值进行其他处理。
从事件处理程序中分离应用逻辑有几个好处。首先,可以让你更容易更改触发特定过程的事件。如果最开始由鼠标点击事件触发过程,但现在按键也要进行同样处理,这种更改就很容易。其次,可以在不附加到事件的情况下测试代码,使其更易创建单元测试或者是自动化应用流程。
以下是要牢记的应用和业务逻辑之间松散耦合的几条原则:

  • 勿将event对象传给其他方法;只传来自event对象中所需的数据;
  • 任何可以在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行;
  • 任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑。

牢记这几条可以在任何代码中都获得极大的可维护性的改进,并且为进一步的测试和开发制造了很多可能。

1.4 编程实践

书写可维护的JavaScript并不仅仅是关于如何格式化代码;它还关系到代码做什么的问题。在企业环境中创建的Web应用往往同时由大量人员一同创作。这种情况下的目标是确保每个人所使用的浏览器环境都有一致和不变的规则。因此,最好坚持以下一些编程实践。

#### 1.尊重对象所有权
JavaScript的动态性质使得儿乎任何东西在任何时间都可以修改。有人说在JavaScript没有什么神圣的东西,因为无法将某些东西标记为最终或恒定状态。这种状况在ECMAScript5中通过引人防篡改对象得以改变;不过,默认情况下所有对象都是可以修改的。在其他语言中,当没有实际的源代码的时候,对象和类是不可变的。JavaScript可以在任何时候修改任意对象,这样就可以以不可预计的方式覆写默认的行为。因为这门语言没有强行的限制,所以对于开发者来说,这是很重要的,也是必要的。
也许在企业环境中最重要的编程实践就是尊重对象所有权,它的意思是你不能修改不属于你的对象。简单地说,如果你不负责创建或维护某个对象它的对象或者它的方法,那么你就不能对它们进行修改。更具体地说:

  • 不要为实例或原型添加属性
  • 不要为实例或原型添加方法
  • 不要重定义已存在的方法。

问题在于开发人员会假设浏览器环境按照某个特定方式运行,而对于多个人都用到的对象进行改动就会产生错误。如果某人期望叫做stopEvent()的函数能取消某个事件的默认行为,但是你对其进行了更改.然后它完成了本来的任务,后来还追加了另外的事件处理程序,那肯定会出现问题了。其他开发人员会认为函数还是按照原来的方式执行,所以他们的用法会出错并有可能造成危害,因为他们并不知道有副作用。

这些规则不仅适用于自定义类型和对象,对于诸如Object、String、document、window等原生类型和对象也适用。此处潜在的问题可能更加危险,因为浏览器提供者可能会在不做宣布或者是不可预期的情况下更改这些对象。
著名的Prototype JavaScript库就出现过这种例子:它为document对象实现了getElementsByClassName()方法,返回一个Array的实例并增加了一个each()方法。JohnResig在他的博客上叙述了产生这个问题的一系列事件。他在帖子中说,他发现当浏览器开始内部实现getElementsByClassName()的时候就出现问题了,这个方法并不返回一个Array而是返回一个并不包含each()方法的NodeList。使用Prototype库的开发人员习惯于写这样的代码:

1
document.getElementsByClassName('selected').each(Element.hide);

虽然在没有原生实现getElementsByClassName()的浏览器中可以正常运行,但对于支持的了浏览器就会产生错误,因为返回的值不同。你不能预测浏览器提供者在未来会怎样更改原生对象,所以不管用任何方式修改他们,都可能会导致将来你的实现和他们的实现之间的冲突。
所以,最佳的方法便是永远不修改不是由你所有的对象。所谓拥有对象.就是说这个对象是你创建的,比如你自己创建的自定义类型或对象字面量。而Array、document这些显然不是你的,它们在你的代码执行前就存在了。你依然可以通过以下方式为对象创建新的功能:

  • 创建包含所需功能的新对象,并用它与相关对象进行交互;
  • 创建自定义类型,继承需要进行修改的类型。然后可以为自定义类型添加额外功能。
    现在很多JavaScript库都赞同并遵守这条开发原理,这样即使浏览器频繁更改,库本身也能继续成长和适应。

2. 避免全局量

与尊重对象所有权密切相关的是尽可能避免全局变量和函数。这也关系到创建一个脚本执行的一致的和可维护的环境。最多创建一个全局变量,让其他对象和函数存在其中。请看以下例子:

1
2
3
4
5
// 避免
var name = 'Micheal';
function sayName () {
alert(name);
}

这段代码包含了两个全局量:变量name和函数sayName()。其实可以创建一个包含两者的对象,如下例所示:

1
2
3
4
5
6
var test = {
name: 'Micheal',
sayName: function () {
alert(this.name);
}
}

这段重写的代码引入了一个单一的全局对象MyApplication,name和sayName()都附加到其上。这样做消除了一些存在于前一段代码中的一些问题。首先,变量name覆盖了window.name属性,可能会与其他功能产生冲突;其次,它有助消除功能作用域之间的混淆。调用MyApplication.sayName()在逻辑上暗示了代码的任何问题都可以通过检查定义MyApplication的代码来确定。
单一的全局量的延伸便是命名空间的概念,由YUI(Yahoo! User Interface)库普及。命名空间包括创建-个用于放置功能的对象。在YUI的2.x版本中,有若干用于追加功能的命名空间。比如:

  • YAfiOO.util.Dom 一一处理 DOM 的方法;
  • YAHOO.util.Event 一一与事件交互的方法;
  • YAHOO.lang 一一用于底层语言特性的方法。

对于YUI,单-的全局对象YAHOO作为一个容器,其中定义了其他对象。用这种方式将功能组合在一起的对象,叫做命名空间。整个YUI库便是构建在这个概念上的,让它能够在同一个页面上与其他的JavaScript库共存。
命名空间很重要的一部分是确定每个人都同意使用的全局对象的名字,并且尽可能唯一,让其他人不太可能也使用这个名字。在大多数情况下,可以是开发代码的公司的名字,例如YAHOO或者Wrox。你可以如下例所示开始创建命名率间来组合功能。

1
2
3
4
var Wrox = {};
Wrox.ProJS = {};
Wrox.ProJS.EventUtil = {};
Wrox.ProJS.CookieUtil = {};

在这个例子中,Wrox是全局量,其他命名空间在此之上创建。如果本书所有代码都放在Wrox.ProJS命名空间,那么其他作者也应把自己的代码添加到Wrox对象中。只要所有人都遵循这个规则,那么就不用担心其他人也创建叫做EventUtil或者CookieUtil的对象,因为它会存在于不同的命名空间中。请看以下例子:

1
2
3
4
5
6
Wrox.ProAjax = {};
Wrox.ProAjax.EventUtil = {};
Wrox.ProAjax.CookieUtil = {};

Wrox.ProJS.EventUtil.addHandler(/*...*/);
Wrox.ProAjax.EventUtil.addHandler(/*...*/);

虽然命名空间会需要多写一些代码,但是对于可维护的目的而言是值得的。命名空间有助于确保代码可以在同一个页面上与其他代码以无害的方式一起工作。

3.避免与null进行比较

由于JavaScript不做任何自动的类型检查,所有它就成了开发人员的责任。因此,在JavaScript代码中其实很少进行类型检测。最常见的类型检测就是查看某个值是否为null。但是,直接将值与null比较是使用过度的,并且常常由于不充分的类型检查导致错误。看以下例子:

1
2
3
4
5
function sortArray (values) {
if (values != null) { // 避免
values.sort(comparator);
}
}

该函数的目的是根据给定的比较子对一个数组进行排序。 为了函数能正确执行,values参数必需是数组,但这里的if语句仅仅检查该values是否为null。还有其他的值可以通过if语句,包括字符串、数字,它们会导致函数抛出错误。
现实中,与null比较很少适合情况而被使用。必须按照所期望的对值进行检查,而非按照不被期望的那些。例如,在前面的范例中,values参数应该是一个数组,那么就要检查它是不是一个数组,而不是检查它是否非null。函数按照下面的方式修改会更加合适:

1
2
3
4
5
function sortArray (values) {
if (values instanceof Array) {
values.sort(comparator);
}
}

这种验证数组的技术在多框架的网页中不一定正确工作,因为每个框架都有其自己的全局对象,因此,也有自己的Array构造函数。如果你是从一个框架将数组传送到另一个框架,那么就要另外检查是否存在sort()方法。

如果看到了与null比较的代码,尝试使用以下技术替换:

  • 如果值应为一个引用类型,使用instanceof操作符检查其构造函数;
  • 如果值应为一个基本类型,使用typeof检查其类型;
  • 如果是希望对象包含某个特定的方法名,则使用typeof操作符确保指定名字的方法存在于对象上。

代码中的null比较越少,就越容易确定代码的目的,并消除不必要的错误。

4.使用常量

尽管JavaScript没有常量的正式概念,但它还是很有用的。这种将数据从应用逻辑分离出来的思想,可以在不冒引入错误的风险的同时,就改变数据。请看以下例子:

1
2
3
4
5
6
function validate (value) {
if (!value) {
alert('Invalid value!');
location.href = '/error.php'
}
}

在这个函数中有两段数据:要显示给用户的信息以及URL。显示在用户界面上的字符串应该以允许进行语言国际化的方式抽取出来。URL也应被抽取出来,因为它们有随着应用成长而改变的倾向。基本上,有着可能由于这样那样原因会变化的这些数据,那么都会需要找到通数并在其中修改代码。而每次修改应用逻辑的代码,都可能会引入错误。可以通过将数据抽取出来变成单独定义的常最的方式,将应用逻镜与数据修改隔离开来。请看以下例子:

1
2
3
4
5
6
7
8
9
10
11
var Constants = {
INVALID_VALUE_MSG: 'Invalid value!',
INVALID_VALUE_URL: '/error.php'
};

function validate (value) {
if (!value) {
alert(Constants.INVALID_VALUE_MSG);
location.href = Constants.INVALID_VALUE_URL;
}
}

在这段重写过的代码中,消息和URL都被定义于Constants对象中,然后雨数引用这些值。这些设置允许数据在无须接触使用它的函数的情况下进行变更。Constants对象甚至可以完全在单独的文件中进行定义,同时该文件可以由包含正确值的其他过程根据国际化设置来生成。
关键在于将数据和使用它的逻辑进行分离。要注意的值的类型如下所示。

  • 重复值:任何在多处用到的值都应抽取为一个常量。这就限制了当一个值变了而另一个没变的时候会造成的错误。这也包含了css类名。
  • 用户界面字符串:任何用于显示给用户的字符事.都应被抽取出来以方便国际化。
  • URLs:在Web应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL。
  • 任意可能会更改的值:每当你在用到字面量值的时候,你都要问一下自己这个值在未来是不是会变化。如果答案是“是”,那么这个值就应该被提取出来作为一个常量。

对于企业级的JavaScript开发而言,使用常量是非常重要的技巧,因为它能让代码更容易维护,并且在数据更改的同时保护代码。

2 性能

自从JavaScript诞生以来,用这门语言编写网页的开发人员有了极大的增长。与此同时,JavaScript代码的执行效率也越来越受到关注。因为JavaScript最初是一个解释型语言,执行速度要比编译型语言慢得多。Chrome是第一款内置优化引擎,将JavaScript编译成本地代码的浏览器。此后,主流浏览器纷纷效仿,陆续实现了JavaScript的编译执行。
即使到了编译执行JavaScript的新阶段,仍然会存在低效率的代码。不过,还是有一些方式可以改进代码的整体性能的。

2.1 注意作用域

随着作用域链中的作用域数量的增加,访问当前作用域以外的变量的时间也在增加。访问全局变量总是要比访问局部变量慢,因为需要遍历作用域链。只要能减少花费在作用域链上的时间,就能增加脚本的整体性能。

1.避免全局查找

可能优化脚本性能最重要的就是注意全局查找。使用全局变量和函数肯定要比局部的开销更大,因为要涉及作用域链上的查找。请看以下函数:

1
2
3
4
5
6
7
8
9
function updateUI () {
var imgs = document.getElementById('img');
for (var i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = document.title + ' image ' + i;
}

var msg = document.getElementById('msg');
msg.innerHTML = 'update complete';
}

该函数可能看上去完全正常,但是它包含了三个对于全局document对象的引用。如果在页面上有多个图片,那么for循环中的document引用就会被执行多次甚至上百次,每次都会要进行作用域链查找。通过创建一个指向document对象的局部变量,就可以通过限制一次全局查找来改进这个函数的性能:

1
2
3
4
5
6
7
8
9
10
function updateUI () {
var doc = document;
var imgs = doc.getElementById('img');
for (var i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = doc.title + ' image ' + i;
}

var msg = doc.getElementById('msg');
msg.innerHTML = 'update complete';
}

这里,首先将document对象存在本地的doc变量中;然后在余下的代码中替换原来的document。与原来的的版本相比,现在的函数只有一次全局查找,肯定更快。
将在一个函数中会用到多次的全局对象存储为局部变量总是没错的

2.避免with语旬

在性能非常重要的地方必须避免使用with语句。和函数类似,with语句会创建自己的作用域,因此会增加其中执府的代码的作用域链的长度。由于额外的作用域链查找,在with语句中执行的代码 肯定会比外面执行的代码要慢。
必须使用with语句的情况很少,因为它主要用于消除额外的字符。在大多数情况下,可以用局部变量完成相同的事情而不引入新的作用域。下面是一个例子:

1
2
3
4
5
6
function updateBody () {
with (document.body) {
alert(tagName);
innerHTML = 'Hello world';
}
}

这段代码中的with语句让document.body变得更容易使用。其实可以使用局部变量达到相同的效果,如下所示:

1
2
3
4
5
function updateBody () {
var body = document.body;
alert(body.tagName);
body.innerHTML = 'Hello world';
}

虽然代码稍微长了点,但是阅读起来比with语句版本更好,它确保让你知道tagName和innerHTML是属于哪个对象的。同时,这段代码通过将document.body存储在局部变量中省去了额外的全局查找。

2.2 选择正确方法

和其他语言一样,性能问题的一部分是利用于解决问题的算法或者方法有关的。老练的开发人员根据经验可以得知哪种方法可能获得更好的性能。 很多应用在其他编程语言中的技术和方法也可以在 JavaScript中使用。

1.避免不必要的属性查找

在计算机科学中,算法的复杂度是使用O符号来表示的。最简单、最快捷的算法是常数值即O(1)。之后,算法变得越来越复杂并花更低时间执行。 下面的表格列出了JavaScript中常见的算法类型。

标记 名称 描述
O(1) 常数 不管有多少值,执行的时间都是恒定的。一般表示简单值和存储在变量中的值
O(log n) 对数 总的执行时间和值的数量相关.但是要完成算法并不一定要获取每个值。例如:二分查找
O(n) 线性 总执行时间和值的数量正比相关。例如:遍历某个数组中的所有元索
O(n^2) 平方 总执行时间和值的数量有关,每个值至少要获取n次。例如:插入排序

常数值,即O(1),指代字面值和存储在变量中的值。符号O(1)表示无论有多少个值,需要获取常量值的时间都一样。获取常量值是非常高效的过程。请看下面代码:

1
2
3
var value = 5;
var sum = 10 + value;
alert(sum);

该代码进行了四次常量值查找:数字5,变量value,数字10和变量sum。这段代码的整体复杂度被认为是0(1)。
在JavaScript中访问数组元素也是一个0(1)操作,和简单的变量查找效率一样。所以以下代码和前面的例子效率一样:

1
2
3
var values = [5, 10];
var sum = values[0] + values[1];
alert(sum);

使用变量和数组要比访问对象上的属性更有效率,后者是一个O(n)操作。对象上的任何属性查找都要比访问变盘或者数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索。简而言之,属性查找越多,执行时间就越长。请看以下内容:

1
2
3
4
5
6
var values = {
first: 5,
second: 10
};
var sum = values.first + values.second;
alert(sum);

这段代码使用两次属性查找来计算:sum的值。进行一两次属性查找并不会导致显著的性能问题,但是进行成百上千次则肯定会减慢执行速度。
注意获取单个值的多重属性查找。例如,请看以下代码:

1
var query = window.location.href.substring(window.location.href.indexOf('?'));

在这段代码中,有6次属性查找:window.location.href.substring()有3次,window.location.href.indexOf()又有3次。只要数一数代码中的点的数量,就可以确定属性查找的次数了。这段代码由于两次用到了window.location.href,同样的查找进行了两次,因此效率特别不好。
一旦多次用到对象属性,应该将其存储在局部变量中。第一次访问该值会是O(n),然而后续的访问都会是O(1),就会节省很多。例如,之前的代码可以如下重写:

1
2
var url = window.location.href;
var query = url.substring(url.indexOf('?'));

这个版本的代码只有4次属性查找,相对于原始版本节省了33%。在更大的脚本中进行这种优化,倾向于获得更多改进。
一般来讲,只要能减少算法的复杂度,就要尽可能减少。尽可能多地使用局部变量将属性查找替换为值资找。进一步讲,如果即可以用数字化的数组位置进行访问,也可以使用命名属性(诸如NodeList对象),那么使用数字位置。

2.优化循环

循环是编程中最常见的结构,在JavaScript程序中同样随处可见。优化循环是性能优化过程中很重要的一个部分,由于它们会反复运行同一段代码,从而自动地增加执行时间。在其他语言中对于循环优化有大量研究,这些技术也可以应用于JavaScript。一个循环的基本优化步骤如下所示。

  • (1)减值选代:大多数循环使用一个从0开始、增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效。
  • (2)简化终止条件:由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。也就是说避免属性查找或其他O(n)的操作。
  • (3)简化循环体:循环体是执行最多的,所以要确保其被最大限度地优化。确保没有某些可以被很容易移出循环的密集计算。
  • (4)使用后测试循环:且常用for循环和while循环都是前测试循环。而如do-while这种后测试循环,可以避免最初终止条件的计算,因此运行更快。

用一个例子来描述这种改动。以下是-个基本的for循环:

1
2
3
for (var i = 0; i < values.length; i++) {
process(values[i]);
}

这段代码中变量i从0递增到values数组中的元素总数。假设值的处理顺序无关紧要,那么循环可以改为i减值,如下所示:

1
2
3
for (var i = values.length - 1; i >= 0; i--) {
process(values[i]);
}

这里,变量i每次循环之后都会减1。在这个过程中,将终止条件从value.length的O(n)调用简化成了0的O(1)调用。由于循环体只有一个语句,无法进一步优化。不过循环还能改成后测试循环,如下:

1
2
3
4
5
6
var i = values.length - 1;
if (i > -1) {
do {
process(values[i]);
} while (--i >= 0);
}

此处主要的优化是将终止条件和自减操作符组合成了单个语句。这时,任何进一步的优化只能process()函数中进行了,因为循环部分已经优化完全了。
记住使用“后测试”循环时必须确保要处理的值至少有一个。空数组会导致多余的一次循环而 “前测试” 循环则可以避免。

3.展开循环

当循环的次数是确定的,消除循环并使用多次函数调用往往更快。请看一下前面的例子。如果数组的长度总是一样的,对每个元素都调用process()可能更优,如以下代码所示:

1
2
3
process(values[0]);
process(values[1]);
process(values[2]);

这个例子假设values数组里面只有3个元素,直接对每个元素调用process()。这样展开循环可以消除建立循环和处理终止条件的额外开销,使代码运行得更快。
如果循坏中的选代次数不能事先确定,那可以考虑使用一种叫做Duff装置的技术。这个技术是以其创建者Tom Duff命名的,他最早在C语言中使用这项技术。 正是Jeff Greenberg用JavaScript实现了Duff装置。Duff装置的基本概念是通过计算迭代的次数是否为8的倍数将一个循环展开为一系列语句。请看以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var iterations = Math.ceil(values.length / 8);
var startAt = values.length % 8;
var i = 0;

do {
switch (startAt) {
case 0: process(values[i++]);
case 7: process(values[i++]);
case 6: process(values[i++]);
case 5: process(values[i++]);
case 4: process(values[i++]);
case 3: process(values[i++]);
case 2: process(values[i++]);
case 1: process(values[i++]);
}

startAt = 0;
} while (--iterations > 0);

Duff装置的实现是通过将values数组中元素个数除以8来计算出循环需要进行多少次选代的。然后使用取整的上限函数确保结果是整数。如果完全根据除8来进行迭代,可能会有一些不能被处理到的元素,这个数量保存在startAt变量中。首次执行该循环时,会检查StartAt变量着有需要多少额外调用。例如,如果数组中有10个值,startAt则等于2,那么最开始的时候process()则只会被调用2次。在接下来的循环中,startAt被重置为0,这样之后的每次循环都会调用8次process()。展开循环可以提升大数据集的处理速度。
由Andrew B. King所著的Seed Up Your Site (New Riders,2003)提出了一个更快的Duff装置技术,将do-while循环分成2个单独的循环。以下是例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var iterations = Math.ceil(values.length / 8);
var startAt = values.length % 8;
var i = 0;

if (leftover > 0) {
do {
process(values[i++]);
} while (--leftover > 0);
}
do {
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
} while (--iterations > 0);

在这个实现中,剩余的计算部分不会在实际循环中处理,而是在一个初始化循环中进行除以8的操作。当处理掉了额外的元素.继续执行每次调用8次process()的主循环。这个方法几乎比原始的Duff 装置实现快上40%。
针对大数据集使用展开循环可以节省很多时间,但对于小数据集,额外的开销则可能得不偿失。它是要花更多的代码来完成同样的任务,如果处理的不是大数据集,一般来说并不值得。

4.避免双重解释

当JavaScript代码想解析JavaScript的时候就会存在双重解释惩罚。当使用eval()函数或者是Function构造函数以及使用setTimeout()传一个字符串参数时都会发生这种情况。下面有一些例子:

1
2
3
4
5
6
7
8
// 某些代码求值——避免
eval('alert("Hello")');

// 创建新函数——避免
var sayHi = new Function('alert("Hello")');

// 设置超时——避免
setTimeout('alert("Hello")', 500);

在以上这些例子中,都要解析包含了JavaScript代码的字符串。这个操作是不能在初始的解析过程中完成的,因为代码是包含在字符串中的,也就是说在JavaScript代码运行的同时必须新启动一个解析器来解析新的代码。 实例化一个新的解析器有不容忽视的开销,所以这种代码要比直接解析慢得多。 对于这几个例子都有另外的办法。只有极少的情况下eval()是绝对必须的,所以尽可能避免使用。 在这个例子中,代码其实可以直接内嵌在原代码中。对于Function构造函数,完全可以直接写成一般的函数,调用setTimeout()可以传人函数作为第一个参数。 以下是一些例子:

1
2
3
4
5
6
7
8
9
alert('Hello');

var sayHi = function () {
alert('Hello');
};

setTimeout(function () {
alert('Hello');
}, 500);

如果要提高代码性能,尽可能避免出现需要按照JavaScript解释的字符串。

5.性能的其他注意事项

当评估脚本性能的时候,还有其他一些可以考虑的东西。下面并非主要的问题, 不过如果使用得当也会有相当大的提升。

  • 原生方法较快:只要有可能,使用原生方法而不是自己用JavaScript重写一个。原生方法是用诸如C/C++之类的编译型语言写出来的,所以要比JavaScript的快很多很多。JavaScript中最容易被忘记的就是可以在Math弃战中找到的复杂的数学运算;这些方法要比任何用JavaScript写的同样方法如正弦、余弦快的多。
  • Switch语句较快:如果有一系列复杂的if-else语句,可以转换成单个switch语句则可以得到更快的代码。还可以通过将case语句按照最可能的到最不可能的顺序进行组织,来进一步优化switch语句。
  • 位运算符较快:当进行数学运算的时候,位运算操作要比任何布尔运算或者算数运算快。选择性地用位运算替换算数运算可以极大提升复杂计算的性能。诸如取模,逻辑与和逻辑或都可以考虑用位运算来替换。

2.3 最小化语句数

JavaScript代码中的语句数量也影响所执行的操作的速度。完成多个操作的单个语句要比完成单个操作的多个语句快。所以,就要找出可以组合在一起的语句,以减少脚本整体的执行时间。这里有几个可以参考的模式。

1.多个变量声明

有个地方很多开发人员都容易创建很多语句,那就是多个变量的声明。很容易看到代码中由多个var语句来声明多个变量,如下所示:

1
2
3
4
5
// 浪费
var count = 5;
var color = 'blue';
var values = [1, 2, 3];
var now = new Date();

在强类型语言中,不同的数据类型的变量必须在不同的语句中声明。然而,在JavaScript中所有的变量都可以使用单个var语句来声明。前面的代码可以如下重写:

1
2
3
4
5
// nice
var count = 5,
color = 'blue',
values = [1, 2, 3],
now = new Date();

此处,变量声明只用了一个var语句,之间由逗号隔开。 在大多数情况下这种优化都非常容易做,并且要比单个变量分别声明快很多。

2.插入选代值

当使用迭代值(也就是在不同的位置进行增加或减少的值)的时候,尽可能合并语句。请看以下代码:

1
2
var name = values[i]; 
i++;

前面这2句话句各只有一个目的:第一个从values数组中获取值,然后存储在name中;第二个给变量i增加1。这两句可以通过迭代值插入第一个语句组合成一个语句,如下所示

1
var name = values[i++];

这一个语句可以完成和前面两个语句一样的事情。因为自增操作符是后缀操作符,i的值只有在语句其他部分结束之后才会增加。一旦出现类似情况,都要尝试将送代值插入到最后使用它的语句中去。

3.使用数组和对象字面量

本书中,你可能看过两种创建数组和对象的方法:使用构造函数或者是使用字面量。使用构造函数总是要用到更多的话句来插入元素或者定义属性,而字面量可以将这些操作在一个语句中完成。请看以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
var values = [];
values[0] = 0;
values[1] = 1;
values[2] = 2;
values[3] = 3;

// bad
var person = {};
person.name = 'Micheal';
person.age = 25;
person.sayName = function () {
alert(this.name);
};

这段代码中,只创建和初始化了一个数组和一个对象。各用了4个语句:一个调用构造函数,其他3个分配数据。其实可以很容易地转换成使用字面量的形式。如下:

1
2
3
4
5
6
7
8
var values = [0, 1, 2, 3];
var person = {
name: 'Micheal',
age: 25,
sayName: function () {
alert(this.name);
}
};

重写后的代码只包含两条语句,一条创建和初始化数组,另一条创建和初始化对象。之前用了八条语句的东西现在只用了两条,减少了75%的语句量。在包含成千上万行JavaScript的代码库中,这些优化的价值更大。
只要有可能,尽量使用数组和对象的字面量表达方式来消除不必要的语句。

*IE6及之前使用字面量有微小的性能惩罚

2.4 优化 DOM 交互

在JavaScript各个方面中,DOM毫无疑问是是慢的一部分。DOM操作与交互要消费大量时间,因为它们往往需要重新渲染整个页面或者某一部分。进一步说,看似细微的操作也可能要花很久来执行,因为DOM要处理非常多的信息。理解如何优化与DOM的交互可以极大得提高脚本完成的速度。

1.最小化现场更新

一旦你需要访问的DOM部分是已经显示的页面的一部分,那么你就是在进行-个现场更新。之所以叫现场更新,是因为需要立即(现场)对页面对用户的显示进行更新。每一个更改,不管是插入单个字符,还是移除整个片段,都有一个性能惩罚,因为浏览器要重新计算无数尺寸以进行更新。现场更新进行得越多,代码完成执行所花的时间就越长;完成一个操作所需的现场更新越少,代码就越快。请看以下例子:

1
2
3
4
5
6
7
8
9
var list = document.getElementById('myList'),
item,
i;

for (i = 0; i < 10; i++) {
item = document.createElement('li');
list.appendChild(item);
item.appendChild(document.createTextNode('Item ' + i));
}

这段代码为列表添加了10个项目。添加每个项目时,都有2个现场更新:一个添加<li>元素,另个给它添加文本节点。这样添加10个项目,这个操作总共要完成20个现场更新。
要修正这个性能瓶颈,需要减少现场更新的数量。一般有2种方法。第一种是将列表从页面上移除,最后进行更新,最后再将列表插回到同样的位置。这个方法不是非常理想,因为在每次页面更新的时候它会不必要的闪烁。第二个方法是使用文档碎片来构建DOM结构,接着将其添加到List元素中。这个方式避免了现场更新和页面闪烁问题。请看下面内容:

1
2
3
4
5
6
7
8
9
10
11
12
var list = document.getElementById('myList'),
fragment = document.createDocumentFragment(),
item,
i;

for (i = 0; i < 10; i++) {
item = document.createElement('li');
fragment.appendChild(item);
item.appendChild(document.createTextNode('Item ' + i));
}

list.appendChild(fragment);

在这个例子中只有一次现场更新,它发生在所有项目都创建好之后。文档碎片用作一个临时的占位符,放置新创建的项目。然后使用appendChild()将所有项目添加到列表用。记住,当给appendChild()传入文档碎片时,只有碎片中的子节点被添加到目标,碎片本身不会被添加的。
一旦需要更新DOM,请考虑使用文档碎片来构建DOM结构,然后再将其添加到现存的文档中。

2.使用innerHTML

有两种在页面上创建DOM节点的方法:使用诸如createElement()和appendChild()之类的DOM方法,以及使用innerHTML。对于小的DOM更改而言,两种方法效率都差不多。然而,对于大的DOM更改,使用innerHTML要比使用标准DOM方法创建同样的DOM结构快得多。
当把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM调用来创建DOM结构,而非基于JavaScript的DOM调用。由于内部方法是编译好的而非解释执行的,所以执行快得多。前面的例子还可以用innerHTML改写如下:

1
2
3
4
5
6
7
8
9
var list = document.getElementById('myList'),
html = '',
i;

for (i = 0; i < 10; i++) {
html += '<li>Item ' + i + '</li>';
}

list.innerHTML = html;

这段代码构建了一个HTML字符串,然后将其指定到list.innerHTML,便创建了需要的DOM结构。虽然字符串连接上总是有点性能损失,但这种方式还是要比进行多个DOM操作更快。
使用innerHTML的关键在于(和其他DOM操作一样)最小化调用它的次数。例如,下面的代码在这个操作中用到innerHTML的次数太多了:

1
2
3
4
5
6
7
var list = document.getElementById('myList'),
html = '',
i;

for (i = 0; i < 10; i++) {
list.innerHTML += '<li>Item ' + i + '</li>';
}

这段代码的问题在于每次循环都要调用innerHTML,这是极其低效的。调用innerHTML实际上就是一次现场更新,所以也要如此对待。构建好一个字符串然后一次性调用innerHTML,要比调用innerHTML多次快得多。

3.使用事件代理

大多数Web应用在用户交互上大量用到事件处理程序。页面上的事件处理程序的数量和页面响应用户交互的速度之间有个负相关。为了减轻这种惩罚,最好使用事件代理。
任何可以冒泡的事件都不仅仅可以在事件目标上进行处理,目标的任何祖先节点上也能处理。使用这个知识,就可以将事件处理程序附加到更高层的地方负责多个目标的事件处现。如果可能,在文档级别附加事件处理程序,这样可以处理整个页面的事件。

4.注意HTMLCollection

HTMLCollection对象的陷阱已经在本书中讨论过了,因为它们对于Web应用的性能而言是巨大的损害。记住,任何时候要访问HTMLCollection,不管它是-个属性还是一个方法,都是在文档上进行一个查询,这个查询开销很昂贵。最小化访问HTMLCollection的次数可以极大地改进脚本的性能。
也许优化HTMLCollection访问最重要的地方就是循环了。前面提到过将长度计算移入for循环的初始化部分。现在看一下这个例子:

1
2
3
4
5
6
7
var images = document.getElementById('img'),
i,
len;

for (i = 0, len = images.length; i < len; i++) {
// deal
}

这里的关键在于长度length存入了len变量,而不是每次都去访问HTMLCollection的length属性。当在循环中使用HTMLCollection的时候,下一步应该是获取要使用的项目的引用,如下所示.以便避免在循环体内多次调用HTMLCollection。

1
2
3
4
5
6
7
8
9
var images = document.getElementById('img'),
image,
i,
len;

for (i = 0, len = images.length; i < len; i++) {
image = images[i];
// deal
}

这段代码添加了Image变量,保存了当前的图像。这之后,在循环内就没有理由再访问images的HTMLCollection了。
编写JavaScript的时候,一定要知道何时返回HTMLCollection对象,这样你就可以最小化对他们的访问。发生以下情况时会返回HTMLCollection对象:

  • 进行了对getElementsByTagName()的调用;
  • 获取了元素的childNodes属性;
  • 获取了元素的attributes属性;
  • 访问了特殊的集合,如document.forms、document.images等。

3 部署

也许所有JavaScript解决方案最重要的部分,便是最后部署到运营中的网站或者是Web应用的过程。在这之前可能你已经做了相当多的工作,为普通的使用进行架构并优化一个解决方案。现在是时候从开发环境中走出来并进入Web阶段了。在此将会和真正的用户交互。然而,在这之前还有一系列需要解决的问题。

3.1 构建过程

完备JavaScript代码可以用于部署的-件很重要的事情,就是给它开发某些类型的构建过程。软件开发的典型模式是写代码-编译-测试,即首先书写好代码,将其编译通过,然后运行并确保其正常工作。 由于JavaScript并非一个编译型语言,模式变成了写代码-测试,这里你写的代码就是你要在浏览器中测试的代码。这个方法的问题在于它不是最优的,你写的代码不应该原封不动地放入浏览器中,理由如下所示。

  • 知识产权问题一一如果把带有完整注释的代码放到线上,那别人就更容易知道你的意图,对它再利用,并且可能找到安全漏洞。
  • 文件大小一一书写代码要保证容易阅读,才能更好地维护,但是这对于性能是不利的。 浏览器并不能从额外的空白字符或者是冗长的函数名和变量名中获得什么好处。
  • 代码组织一一组织代码要考虑到可维护性并不一定是传送给浏览器的最好方式。

基于这些原因,最好给JavaScript文件定义一个构建过程。

构建过程始于在源控制中定义用于存储文件的逻辑结构。最好避免使用-个文件存放所有的JavaScript,遵循以下面向对象语言中的典型模式:将每个对象或自定义类型分别放入其单独的文件中。 这样可以确保每个文件包含最少量的代码,使其在不引入错误的情况下更容易修改。另外,在使用像CVS或Subversion这类并发源控制系统的时候,这样做也减少了在合并操作中产生冲突的风险。
记住将代码分离成多个文件只是为了提高可维护性,并非为了部署。要进行部署的时候,需要将这些源代码合并为一个或几个归并文件。推荐Web应用中尽可能使用最少的JavaScript文件,是因为HTTP请求是Web中的主要性能瓶颈之一。 记住通过<script>标记引用JavaScript文件是一个阻塞操作,当代码下载并运行的时候会停止其他所有的下载。因此,尽量从逻辑上将JavaScript代码分组成部署文件。
-旦组织好文件和目录结构,并确定哪些要出现在部署文件中,就可以创建构建系统了。Ant构建工具(http://antapache.org)是为了自动化Java构建过程而诞生的,不过因为其易用性和应用广泛,而在Web应用开发人员中也颇流行,诸如Julien Lecomte的软件工程师,已经写了教程指导如何使用Ant进行JavaScript和css的构建自动化(Lecomte的文章在www.julienlecomte.net/blog/2007/09/16/)。
Ant由于其简便的文件处理能力而非常适合JavaScript编译系统。其使用方法在此不做介绍。

~~~webpack

3.2 验证

尽管现在出现了一些可以理解并支持JavaScript的IDE,大多数开发人员还是要在浏览器中运行代码以检查其语法。这种方法有一些问题。首先,验证过程难以自动化或者在不同系统间直接移植。其次,除了语法错误外,很多问题只有在执行代码的时候才会遇到,这给错误留下了空间;有些工具可以帮助确定JavaScript代码中潜在的问题,其中最著名的就是Douglas Crockford的JSLint(www.jslint.com)。
JSLint可以查找JavaScript代码中的语法错误以及常见的编码错误。它可以发掘的一些潜在问题如下:

  • eval()的使用;
  • 未声明变量的使用;
  • 遗漏的分号;
  • 不恰当的换行;
  • 错误的逗号使用;
  • 语句周围遗漏的括号:
  • switch分支语句中遗漏的break;
  • 重复声明的变量;
  • with的使用;
  • 错误使用的等号(替代了双等号或三等号);
  • 无法到达的代码。

为了方便访问,它有一个在线版本。不过它也可以使用基于Java的Rhino JavaScript引擎(www.mozilla.org/rhino/)运行于命令行模式下。要在命令行中运行JSLint,首先要下载Rhino,并从www.jslint.com/下载Rhino版本的JSLint。-旦安装完成,便可以使用下面的语法从命令行运行JSLint了:

1
java -jar rhino-1.6R7.jar jslint.js a.js b.js c.js

如果给定文件中有任何语法问题或者是潜在的错误,则会输出有关错误和警告的报告。如果没有问题,代码会直接结束而不显示任何信息。

给开发周期添加代码验证这个环节有助于避免将来可能出现的-些错误。建议开发人员给构建过程加入某种类型的代码验证作为确定潜在问题的一个方法,防患于未然。

jslint、eslint

3.3 压缩

当谈及JavaScript文件压缩,其实在讨论两个东西:代码长度和配重(Wire weight)。代码长度指的是浏览器所需解析的字节数,配重指的是实际从服务器传送到浏览器的字节数。在Web开发的早期,这两个数字几乎是一样的,因为从服务器端到客户端原封不动地传递了源文件。而在今天的Web上,这两者很少相等,实际上也不应相等。

1.文件压缩

因为JavaScript并非编译为字节码,而是按照源代码传送的,代码文件通常包含浏览器执行所不需要的额外的信息和格式。注释,额外的空白,以及长长的变量名和函数名虽然提高了可读性,但却是传送给浏览器时不必要的字节。不过,我们可以使用压缩工具减少文件的大小。
压缩器一般进行如下一些步骤:

  • 删除额外的空白(包括换行);
  • 删除所有注释;
  • 缩短变量名。

JavaScript有不少压缩工具可用(附录D中有一个完整列表),其中最优秀的(有争议的)是YUI压缩器,不过多介绍

2.HTTP压缩

配置指的是实际从服务器传送到浏览器的字节数。因为现在的服务器和浏览器都有压缩功能,这个字节数不一定和代码长度一样。所有的五大Web浏览器(IE、Firefox、Safari、Chrome和Opera)都支持对所接收的资源进行客户端解压缩。这样服务器端就可以使用服务器端相关功能来压缩JavaScript文件。 一个指定了文件使用了给定格式进行了压缩的HTTP头包含在了服务器响应中。接着浏览器会查看该HTTP头确定文件是否已被压缩,然后使用合适的格式进行解压缩。结果是和原来的代码量相比在网络中传递的字节数量大大减少了。
对于ApacheWeb服务器,有两个模块可以进行HTTP压缩:mod_gzip(Apache 1.3.x)和mod_deflate(Apache 2.0.x)。对于mod_gzip,可以给httpd.conf文件或者是.htaccess文件添加以下代码启用对JavaScript的自动压缩:

1
2
#告诉mod_zip要包含任何以.js结尾的文件
mod_gzip_item_include file \.js$

该行代码告诉mod_zip要包含来自浏览器请求的任何以.js结尾的文件。假设你所有的JavaScript文件都以.js结尾,就可以压缩所有请求并应用合适的HTTP头以表示内容已被压缩。

对于mod_deflate,可以类似添加一行代码以保证JavaScript文件在被发送之前已被压缩。将以下这一行代码添加到httpd.conf文件,或者是.htaccess文件中:

1
2
# 告诉mod_deflate要包含所有的JavaScript文件
AddOutputFilterByType DEFLATE application/x-javascript

注意这-行代码用到了响应的MIME类型来确定是否对其进行压缩。记住虽然<script>的type属性用的是text/javascript,但是JavaScript文件一般还是用application/x-javascript作为其服务的MIME类型。
mod_gzip和mod_deflate都可以节省大约70%的JavaScript文件大小。 这很大程度上是因为JavaScript都是文本文件,因此可以非常有效地进行压缩。减少文件的配重可以减少需要传输到浏览器的时间。记住有一点点细微的代价,因为服务都必须花时间对每个请求压缩文件,当浏览器接收到这些文件后也需要花一些时间解压缩。不过,一般来说,这个代价还是值得的。

大部分Web服务器,开源的或是商业的,都有一些HTTP压缩功能。请查看服务器的文档说明以确定如何合适地配置压缩。

温习:

  • 可维护的代码的特征,变量、解耦;
  • js性能优化,查找、执行、DOM;
  • 部署,构建、验证、压缩;

(完)