runfast󰀦lynda

JavaScript高级程序设计学习笔记:最佳实践

可维护性

什么是可维护的代码

可维护的代码有一些特征。

代码约定:

可读性

变量和函数命名

变量类型透明:表示变量类型的三种方式

松散耦合

解耦HTML/JavaScript

HTML中包含JavaScript,示例:

<script type="text/javascript">document.write("hello world!")</script>; // script标签紧密耦合
<input type="button" value="Click me " onclick="doSomething();"/> //事件属性值紧密耦合

理想情况:HTML和JavaScript应该完全分离,并通过外部文件和使用DOM附加行为来包含JavaScript。

问题:出现JavaScript错误就要判断是在HTML中还是在JavaScript中,且在doSomething()可用之前就按下button,也会引发JavaScript错误。JavaScript中包含HTML,JavaScript生成HTML,这个应该避免,保持层次的分离有助于很容易的确定错误来源。

理想情况:JavaScript用于插入数据时,尽量不直接插入标记,可以控制标记的显示和隐藏,而非生成它。另一种方法是进行Ajax请求并获取更多要显示的HTML,这个方法可以让同样的渲染层(PHP、JSP、Ruby)来输出标记。

解耦CSS/JavaScript

利用JavaScript修改样式时,应该通过动态修改样式类而非特定样式来实现。显示问题的唯一来源应该是CSS,行为问题的唯一来源应该是JavaScript。

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

应用逻辑和事件处理程序相分离,一个事件处理程序应该从事件对象中获取相关信息,并将这些信息传送到处理应用程序的某个方法中。好处是可以更容易更改触发特定过程的事件,其次可以在不附加到事件的情况下测试代码,使其更易创建单元测试或者是自动化应用流程。

应用和业务逻辑之间松散耦合的几条原则:

编程实践:

尊重对象所有权

如果你不负责创建和维护某个对象、它的对象或者它的方法,那么你就不能对它们进行修改。不要为实例或者原型添加属性;不要为实例或者原型添加方法;不要重定义已存在的方法。

避免全局变量

最多创建一个全局量,让其他对象和函数存在其中。

避免与null进行比较
使用常量

关键在于将数据和使用它的逻辑进行分离。

性能

注意作用域

避免全局查找:使用全局变量和函数肯定要比局部的开销更大,因为涉及作用域链上的查找。

function updateUI(){
    var imgs = document.getElementsByTagName("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.";
}
//优化后的代码
function updateUI(){
    var doc = document;
    var imgs = doc .getElementsByTagName("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.";
}
避免with语句

和函数类似,with语句会创建自己的作用域,肯定会增加其中执行的代码的作用域链的长度。必须使用with语句的情况很少,它主要用于消除额外的字符。在大多数情况下,可以用局部变量完成相同的事情而不用引入新的作用域。

function updateBody(){
    with(document.body){
        alert(tagName);
        innerHTML = "hello world!";
    }
}
//改进后的代码
function updateBody(){
    var body = document.body;
    alert(body.tagName);
    body.innerHTML = "hello world!";
}
选择正确方法

避免不必要的属性查找

一般来讲,只要能减少算法的复杂度,就要尽可能减少。尽可能多地使用局部变量将属性查找替换 为值查找。进一步讲,如果即可以用数字化的数组位置进行访问,也可以使用命名属性(诸如 NodeList 对象),那么使用数字位置。

优化循环

基本步骤如下

for(var i=0; i < values.length; i++){
    process(value[i]);
}
//减值迭代优化:
for(var i=values.length; i >= 0 ; i--){
    process(value[i]);
}

后测试循环优化:记住使用后测试循环时必须确保要处理的值至少有一个,空数组会导致多余的一次循环而前测试循环则可以避免。

var i = values.length - 1;
if(i > -1){
    do{
        process(values[i]);
    }while(--i > 0);
}
展开循环

当循环的次数是确定的,消除循环并使用多次函数调用往往更快。 如果循环中的迭代次数不能事先确定,可以使用duff装置技术,它以创建者Tom Duff命名,并最早在C语言中使用这项技术。Jeff Greenberg 用JavaScript实现了Duff装置,基本概念是通过计算迭代的次数是否为8的倍数将一个循环展开为一系列语句。Jeff Greenberg的Duff装置技术代码:通过将values数组中元素个数除以8来计算出循环需要进行多次迭代的。

//credit: Jeff Greenberg for JS implementation of Duff’s Device //假设 values.length > 0
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()。展开循环可以提升大数据集的处理速度。

避免双重解释

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

//某些代码求值——避免!! 
eval("alert('Hello world!')");
//创建新函数——避免!!
var sayHi = new Function("alert('Hello world!')");
//设置超时——避免!! 
setTimeout("alert('Hello world!')", 500);

以上代码中都要解析包含了JavaScript代码的字符串,这个操作是不能再初始的解析过程中完成的,因为代码是包含在字符串中的,也就是说在JavaScript代码运行的同时必须新启动一个解析器来解析新的代码。

//已修正
alert('Hello world!');
//创建新函数——已修正
var sayHi = function(){
  alert('Hello world!');
};
//设置一个超时——已修正 
setTimeout(function(){
  alert('Hello world!');
}, 500);
性能的其他注意事项

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

最小化语句数

JavaScript代码中的语句数量也影响所执行的操作的速度。完成多个操作的单个语句要比完成单个操作的多个语句快。

在 JavaScript 中所有的 变量都可以使用单个 var 语句来声明。

//一个语句
var count = 5,
    color = "blue",
    values = [1,2,3],
    now = new Date();

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

var name = values[i];
    i++;
//优化上面的代码
var name = values[i++];

只要有可能,尽量使用数组和对象的字面量表达方式来消除不必要的语句。

优化DOM交互

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

有两种在页面上创建DOM节点的方法:使用诸如createElement()和appendChild()之类的DOM方法,以及使用innerHTML。对于小的DOM更改而言,两种方法效率都差不多。然而,对于大的DOM更改,使用innerHTML要比使用标准DOM方法创建同样的DOM结构快得多。

事件代理,用到了事件冒泡。任何可以冒泡的事件都不仅仅可以在事件目标上进行处理,目标的任何祖先节点上也能处理。使用这个知识,就可以将事件处理程序附加到更高层的地方负责多个目标的事件处理。如果可能,在文档级别附加事件处理程序,这样可以处理整个页面的事件。

任何时候要访问HTMLCollection,不管它是一个属性还是一个方法,都是在文档上进 行一个查询,这个查询开销很昂贵。最小化访问HTMLCollection的次数可以极大地改进脚本的性能。

编写JavaScript的时候,一定要知道何时返回HTMLCollection对象,这样你就可以最小化对他们的访问。发生以下情况时会返回HTMLCollection对象:

要了解当使用HTMLCollection对象时,合理使用会极大提升代码执行速度。

部署

构建过程

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

基于这些原因,最好给JavaScript文件定义一个构建过程。构建过程始于在源控制中定义用于存储文件的逻辑结构。最好避免使用一个文件存放所有的JavaScript,遵循以下面向对象语言中的典型模式:将每个对象或自定义类型分别放入其单独的文件中。这样可以确保每个文件包含最少量的代码,使其在不引入错误的情况下更容易修改。另外,在使用像CVS或Subversion这类并发源控制系统的时候,这样做也减少了在合并操作中产生冲突的风险。

记住将代码分离成多个文件只是为了提高可维护性,并非为了部署。要进行部署的时候,需要将这些源代码合并为一个或几个归并文件。

验证

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

压缩

当谈及JavaScript文件压缩,其实在讨论两个东西:代码长度和配重(Wire weight)。代码长度指的是浏览器所需解析的字节数,配重指的是实际从服务器传送到浏览器的字节数。

因为JavaScript并非编译为字节码,而是按照源代码传送的,代码文件通常包含浏览器执行所不需要的额外的信息和格式。注释,额外的空白,以及长长的变量名和函数名虽然提高了可读性,但却是传送给浏览器时不必要的字节。

压缩器一般进行如下一些步骤:删除额外的空白(包括换行),删除所有注释,缩短变量名。

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