javascript--24最佳实践

可维护性

什么是可维护的代码
可维护的代码有一些特征。
可理解性——其他人可以接手代码并理解它的意图和一般途径,而无需原开发人员的完整解释。 直观性——代码中的东西一看就能明白,不管其操作过程多么复杂。
可适应性——代码以一种数据上的变化不要求完全重写的方法撰写。
可扩展性——在代码架构上已考虑到在未来允许对核心功能进行扩展。
可调试性——当有地方出错时,代码可以给予你足够的信息来尽可能直接地确定问题所在。
代码约定:
可读性
格式化:建议缩进大小为4个空格
注释:函数和方法、大段代码、复杂的算法、hack
变量和函数命名
变量名为名词
函数名为动词开始
变量和函数使用合符逻辑的名字,不要担心长度。
变量类型透明:表示变量类型的三种方式
初始化: var found = false; //布尔型
使用匈牙利标记法来指定变量类型:o代表对象,s代表字符串,i代表整数,f代表浮点数,b代表布尔型
使用类型注释: var found / :Boolen / = false;
松散耦合

解耦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。
解耦应用逻辑/事件处理程序
应用逻辑和事件处理程序相分离,一个事件处理程序应该从事件对象中获取相关信息,并将这些信息传送到处理应用程序的某个方法中。好处是可以更容易更改触发特定过程的事件,其次可以在不附加到事件的情况下测试代码,使其更易创建单元测试或者是自动化应用流程。
应用和业务逻辑之间松散耦合的几条原则:
勿将event对象传给其他方法;只传来自event对象中所需的数据;
任何在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行。
任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑。
编程实践:

尊重对象所有权
如果你不负责创建和维护某个对象、它的对象或者它的方法,那么你就不能对它们进行修改。不要为实例或者原型添加属性;不要为实例或者原型添加方法;不要重定义已存在的方法。
避免全局变量
最多创建一个全局量,让其他对象和函数存在其中。
避免与null进行比较
如果值应为一个引用类型,使用instanceof操作符检查其构造函数。
如果值应为一个基本类型,使用typeof检查其类型。
如果是希望对象包含某个特定的方法名,则使用typeof操作符确保指定名称的方法存在于对象上。
使用常量
关键在于将数据和使用它的逻辑进行分离。
重复值:任何在多处用到的值都应抽取为一个常量,也包含css类名,这就限制了当一个值变了而另一个没变的时候会造成的错误。
用户界面字符串:方便国际化。
URLs:在web应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL 任意可能会更改的值。
性能

注意作用域
避免全局查找:使用全局变量和函数肯定要比局部的开销更大,因为涉及作用域链上的查找。
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!";
}

选择正确方法
避免不必要的属性查找
常数值O(1):指代字面值和存储在变量中的值,访问数组元素
对数值O(log n):总的执行时间和值的数量相关,但是要完成算法并不一定要获取每个值。例如:二分查找。
线性O(n):访问对象,对象上的任何属性查找都要比访问变量或者数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索,属性查找越多,执行时间久越长。一旦多次用到对象属性,应该将其存储在局部变量中。
平方O(n²):总执行时间和值的数量有关,每个值至少要获取n次。例如:插入排序。
一般来讲,只要能减少算法的复杂度,就要尽可能减少。尽可能多地使用局部变量将属性查找替换 为值查找。进一步讲,如果即可以用数字化的数组位置进行访问,也可以使用命名属性(诸如 NodeList 对象),那么使用数字位置。
优化循环
基本步骤如下
减值迭代:在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效。
简化终止条件:由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。也就是说避免属性查找或者其他O(n)的操作。
简化循环体:循环体是执行最多的,所以要确保其被最大限度的优化。确保没有某些可以被很容易移除循环的密集计算。
使用后测试循环:最常用的for循环和while循环都是前测试循环,而如do-while这种后测试循环,可以避免最初终止条件的计算,因此运行更快。
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 / ;
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 重写一个。原生方法是用诸如 C/C++之类的编译型语言写出来的,所以要比 JavaScript 的快很多很多。JavaScript 中最容易被忘记的就是可以在 Math 对象中找到的复杂的数学运算;这些方法要比任何用JavaScript写 的同样方法如正弦、余弦快的多。
Switch 语句较快 —— 如果有一系列复杂的if-else语句,可以转换成单个switch语句则可以得到更快的代码。还可以通过将case语句按照最可能的到最不可能的顺序进行组织,来进一步优化switch语句。
位运算符较快 —— 当进行数学运算的时候,位运算操作要比任何布尔运算或者算数运算快。选择性地用位运算替换算数运算可以极大提升复杂计算的性能。诸如取模,逻辑与和逻辑或都可以考虑用位运算来替换。
最小化语句数
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 部分是已经显示的页面的一部分,那么你就是在进行一个现场更新。之所 以叫现场更新,是因为需要立即(现场)对页面对用户的显示进行更新。每一个更改,不管是插入单个字符,还是移除整个片段,都有一个性能惩罚,因为浏览器要重新计算无数尺寸以进行更新。现场更新进行得越多,代码完成执行所花的时间就越长;完成一个操作所需的现场更新越少,代码就越快。
使用 innerHTML
有两种在页面上创建DOM节点的方法:使用诸如createElement()和appendChild()之类的DOM方法,以及使用innerHTML。对于小的DOM更改而言,两种方法效率都差不多。然而,对于大的DOM更改,使用innerHTML要比使用标准DOM方法创建同样的DOM结构快得多。
使用事件代理
事件代理,用到了事件冒泡。任何可以冒泡的事件都不仅仅可以在事件目标上进行处理,目标的任何祖先节点上也能处理。使用这个知识,就可以将事件处理程序附加到更高层的地方负责多个目标的事件处理。如果可能,在文档级别附加事件处理程序,这样可以处理整个页面的事件。
注意 HTMLCollection
任何时候要访问HTMLCollection,不管它是一个属性还是一个方法,都是在文档上进 行一个查询,这个查询开销很昂贵。最小化访问HTMLCollection的次数可以极大地改进脚本的性能。
编写JavaScript的时候,一定要知道何时返回HTMLCollection对象,这样你就可以最小化对他们的访问。发生以下情况时会返回HTMLCollection对象:
进行了对 getElementsByTagName() 的调用;
获取了元素的 childNodes 属性;
获取了元素的 attributes 属性;
访问了特殊的集合,如 document.forms、document.images等。
要了解当使用HTMLCollection对象时,合理使用会极大提升代码执行速度。
部署

构建过程
完备JavaScript代码可以用于部署的一件很重要的事情,就是给它开发某些类型的构建过程。软件开发的典型模式是写代码—编译—测试,即首先书写好代码,将其编译通过,然后运行并确保其正常工作。由于JavaScript并非一个编译型语言,模式变成了写代码—测试,这里你写的代码就是你要在浏览器中测试的代码。这个方法的问题在于它不是最优的,你写的代码不应该原封不动地放入浏览器中,理由如下所示。
知识产权问题:如果把带有完整注释的代码放到线上,那别人就更容易知道你的意图,对它 再利用,并且可能找到安全漏洞。
文件大小:书写代码要保证容易阅读,才能更好地维护,但是这对于性能是不利的。浏览器 并不能从额外的空白字符或者是冗长的函数名和变量名中获得什么好处。
代码组织:组织代码要考虑到可维护性并不一定是传送给浏览器的最好方式。
基于这些原因,最好给JavaScript文件定义一个构建过程。构建过程始于在源控制中定义用于存储文件的逻辑结构。最好避免使用一个文件存放所有的JavaScript,遵循以下面向对象语言中的典型模式:将每个对象或自定义类型分别放入其单独的文件中。这样可以确保每个文件包含最少量的代码,使其在不引入错误的情况下更容易修改。另外,在使用像CVS或Subversion这类并发源控制系统的时候,这样做也减少了在合并操作中产生冲突的风险。
记住将代码分离成多个文件只是为了提高可维护性,并非为了部署。要进行部署的时候,需要将这些源代码合并为一个或几个归并文件。
验证
大多数开发人员还是要在浏览器中运行代码以检查其语法。这种方法有一些问题。首先,验证过程难以自动化或者在不同系统间直接移植。其次,除了语法错误外,很多问题只有在执行代码的时候才会遇到,这给错误留下了空间;有些工具可以帮助 确定JavaScript代码中潜在的问题,其中最著名的就是DouglasCrockford的JSLint(www.jslint.com)。
压缩
当谈及JavaScript文件压缩,其实在讨论两个东西:代码长度和配重(Wire weight)。代码长度指的是浏览器所需解析的字节数,配重指的是实际从服务器传送到浏览器的字节数。
文件压缩
因为JavaScript并非编译为字节码,而是按照源代码传送的,代码文件通常包含浏览器执行所不需要的额外的信息和格式。注释,额外的空白,以及长长的变量名和函数名虽然提高了可读性,但却是传送给浏览器时不必要的字节。
压缩器一般进行如下一些步骤:删除额外的空白(包括换行),删除所有注释,缩短变量名。
HTTP 压缩
配重指的是实际从服务器传送到浏览器的字节数。因为现在的服务器和浏览器都有压缩功能,这个字节数不一定和代码长度一样。所有的五大Web浏览器(IE、Firefox、Safari、Chrome和Opera)都支持对所接收的资源进行客户端解压缩。这样服务器端就可以使用服务器端相关功能来压缩JavaScript文件。一个指定了文件使用了给定格式进行了压缩的HTTP头包含在了服务器响应中。接着浏览器会查看该HTTP头确定文件是否已被压缩,然后使用合适的格式进行解压缩。结果是和原来的代码量相比在网络中传递的字节数量大大减少了。
可维护性
1.1 什么是可维护的代码:
□可理解性
□直观性
□可适应性
□可扩展性
□可调试性
1.2 代码约定
①可读性
1)代码缩进,建议使用4空格缩进
2)代码注释
□函数和方法
□大段代码
□复杂的算法
□Hack
②变量和函数名
□变量名应为名词
□函数名应该以动词开头
□变量和函数都应使用合乎逻辑的名字,不要担心长度(进行js压缩时会替换掉)
③变量类型透明
□初始化,当定义一个变量后,应初始化一个值,来暗示它将来应该如何应用。
var found = false;
□使用匈牙利标记法指定变量
var bFound; //布尔值
var iCount; //整数
var sName; //字符串
var oPerson; //对象
□使用类型注释
var found /* Boolean */ = false;
2.松散耦合
①解耦HTML/JavaScript
②解耦CSS/JavaScript
③解耦应用逻辑/事件处理程序
3.编程实践
①尊重对象所有权
1)如果你不负责维护某个对象、它的对象或者它的方法,那么你就不能对他们进行修改。
□不要为实例或原型添加属性。
□不要为实例或原型添加方法。
□不要重定义已存在的方法。
2)最佳的方法是永远不修改不是由你所有的对象,可以通过以下方式为对象创建新的功能:
□创建包含所需功能的新对象,并用它与相关对象进行交互。
□创建自定义类型,继承需要进行修改的类型。然后为自定义类添加额外功能。
②避免全局变量
使用命名空间:
//创建全局对象。
var Wrox = {};
//为Professional JavaScript创建命名空间
Wrox.ProJS = {};
//将书中用到的对象附加上去
Wrox.ProJS.EventUtil = {…};
Wrox.ProJS.CookieUtil = {…};
③避免与null进行比较
□如果值为一个引用类型,使用instanceof操作符检查其构造函数;
□如果值为一个基本类型,使用typeof检查其类型
□如果是希望对象包含某个特定的方法名,则使用typeof操作符确保指定名字的方法存在于对象上。
□代码中的null比较少,就容易确定代码的目的,并消除不必要的错误。
④使用常量
□重复值
□用户界面字符串
□URLs
□任意可能会更改的值
4.性能
4.1 注意作用域
①避免全局查找:在一个函数中若会用到多次的全局对象则存储为局部变量。
②避免with语句
4.2 选择正确方法
①避免不必要的树形查找
□一旦多次用到对象属性,应该将其存储在局部变量中。第一次访问改值为O(n),后续访问为O(1).
②优化循环
□减值迭代
□简化终止条件
□简化循环体
□使用后测试循环
③展开循环
□当循环的次数是确定的,消除循环并使用多次函数调用往往更快。
□Duff装置处理循环。
④避免双重解析
□当JavaScript代码想解析JavaScript的时候就会存在双重解析惩罚。
当使用eval()函数或者是function构造函数以及使用setTimeout()传入一个字符串参数时。
⑤性能的其他注意事项
□原生方法较快
□switch语句较快
□位运算符较快
4.3 最小化语句数
完成多个操作的单个语句要比完成单个操作的多个语句快。
①多个变量声明
②插入迭代值
③使用数组和对象字面量
4.4 优化DOM交互
①最小化现场更新
②使用innerHTML
③使用事件代理
④注意NodeList
发生一下情况会返回NodeList对象
□getElementsByTagName()
□获取元素的childNodes属性
□获取元素的attribute属性
□访问了特殊的集合,document.forms、document.images等
5.部署(略)

一、可维护性:可理解性、直观性、可适应性、可扩展性、可调试性
代码约定:
可读性
格式化:建议缩进大小为4个空格
注释:函数和方法、大段代码、复杂的算法、hack
变量和函数命名
变量名为名词
函数名为动词开始
变量和函数使用合符逻辑的名字,不要担心长度。
变量类型透明:表示变量类型的三种方式
初始化: var found = false; //布尔型
使用匈牙利标记法来指定变量类型:o代表对象,s代表字符串,i代表整数,f代表浮点数,b代表布尔型
使用类型注释: var found /*:Boolen*/ = false;
松散耦合:
解耦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。
解耦应用逻辑/事件处理程序
应用逻辑和事件处理程序相分离,一个事件处理程序应该从事件对象中获取相关信息,并将这些信息传送到处理应用程序的某个方法中。
好处:可以更容易更改触发特定过程的事件;其次可以在不附加到事件的情况下测试代码,使其更易创建单元测试或者是自动化应用流程。
应用和业务逻辑之间松散耦合的几条原则:
勿将event对象传给其他方法;只传来自event对象中所需的数据;
任何在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行。
任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑。
编程实践:
尊重对象所有权:如果你不负责创建和维护某个对象、它的对象或者它的方法,那么你就不能对它们进行修改。
不要为实例或者原型添加属性;
不要为实例或者原型添加方法;
不要重定义已存在的方法。
避免全局变量:最多创建一个全局量,让其他对象和函数存在其中。
避免与null进行比较
如果值应为一个引用类型,使用instanceof操作符检查其构造函数。
如果值应为一个基本类型,使用typeof检查其类型。
如果是希望对象包含某个特定的方法名,则使用typeof操作符确保指定名称的方法存在于对象上。
使用常量
关键在于将数据和使用它的逻辑进行分离
重复值:任何在多处用到的值都应抽取为一个常量,也包含css类名,这就限制了当一个值变了而另一个没变的时候会造成的错误。
用户界面字符串:方便国际化
URLs:在web应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL
任意可能会更改的值

二、保证代码性能
注意作用域:
避免全局查找:使用全局变量和函数肯定要比局部的开销更大,因为涉及作用域链上的查找。
示例代码:
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语句会创建自己的作用域,肯定会增加其中执行的代码的作用域链的长度。
必须使用with语句的情况很少,它主要用于消除额外的字符。在大多数情况下,可以用局部变量完成相同的事情而不用引入新的作用域。
实例代码:
function updateBody(){
    with(document.body){
        alert(tagName);
        innerHTML = "hello world!";
    }
}
改进后的代码:

function updateBody(){
    var body = document.body;
    alert(body.tagName);
    body.innerHTML = "hello world!";
}

选择正确方法
避免不必要的属性查找
常数值O(1):指代字面值和存储在变量中的值,访问数组元素
对数值O(log n):
线性O(n):访问对象,对象上的任何属性查找都要比访问变量或者数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索,属性查找越多,执行时间久越长。
一旦多次用到对象属性,应该将其存储在局部变量中。
平方O(n²):
优化循环:基本步骤如下:
减值迭代:在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效。
简化终止条件:由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。也就是说避免属性查找或者其他O(n)的操作。
简化循环体:循环体是执行最多的,所以要确保其被最大限度的优化。确保没有某些可以被很容易移除循环的密集计算。
使用后测试循环:最常用的for循环和while循环都是前测试循环,而如do-while这种后测试循环,可以避免最初终止条件的计算,因此运行更快。
示例代码:
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 / ;
var startAt = values.length % 8;
var i = 0;

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


由Andrew B.King 所著的Speed Up your Site(New Riders,2003),提出了一个更快的Duff装置技术,将do-while循环分成2个单独的循环。一下是例子:
//credit: Speed Up your Site(New Riders,2003)
var iterations = Math.floor(values.length / ;
var leftover = values.length % 8;

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%。
针对大数据集使用展开循环可以节省很多时间,不过对于小数据集,额外的开销则可能得不偿失。

避免双重解释:当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);
性能的其他注意事项:
原生方法较快:原生方法是用诸如C/C++之类的编译型语言写出来的,所以要比JavaScript快的很多很多。JavaScript最容易被忘记的就是可以在Math对象中找到的复杂的数学运算,这些方法要比任何用JavaScript的同样方法如正弦、余弦快的多。
Switch语句较快
位运算符较快:取模、逻辑与和逻辑或

最小化语句数:JavaScript代码中的语句数量也影响所执行的操作的速度。完成多个操作的单个语句要比完成单个操作的多个语句快。
多个变量声明
插入迭代值
使用素组和对象字面量
优化DOM交互
最小化现场更新
现场更新:需要立即(现场)对页面对用户的显示进行更新。每一个更改,不管是插入单个字符,还是移除整个片段,都有一个性能惩罚,因为浏览器要重新计算无数尺寸以进行更新。
实例代码:
var list = document.getElementById("myList"),
                item,
                i;
for(i = 0; i < 10;i ++){
     item = document.createElement("li");
     list = appendChild(item);
     item.append(document.createTextNode("Item " + i));
}
分析:该代码添加每个项目时都有2个现场更新:一个添加<li>元素,另一个给它添加文本节点。总共需要20个现场更新。两种优化方法:第一种将列表从页面上移除,最后进行更新,最后再将列表插回到同样的位置,这个方法不理想,因为每次页面更新的时候会不必要的闪烁。第二个方法是使用文档片段来构建DOM结构,接着将其添加到List元素中,这种方法避免了现场更新和页面闪烁问题。
优化后的代码:
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);

使用innerHTML:
页面中创建DOM节点的方法有:使用诸如createElement()和appendChild()之类的DOM方法,以及使用innerHTML。对于小的DOM更改而言,两种方法效率都差不多。然而对于大的DOM更改,使用innerHTML要比使用标准DOM方法创建同样的DOM结构快得多。
因为当把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM调用来创建DOM结构,而非基于JavaScript的DOM调用。由于内部方法是编译好的而非解释执行的,所以执行快的多。
调用innerHTML(和其他DOM操作一样)关键在于最小化调用它的次数。
使用事件代理
页面上的事件处理程序的数量和页面响应用户交互的速度之间有个负相关,为了减轻这种惩罚,最好使用事件代理。
事件代理用到了事件冒泡,任何可以冒泡的事件都不仅仅可以在事件目标上进行处理,目标的任何祖先节点上也能处理,因此可以将事件处理程序附加到更高层的地方负责多个目标的事件处理,如果在文档级别附加事件处理程序,就可以处理整个页面的事件。
注意HTMLCollection
访问HTMLCollection,不管它是一个属性还是一个方法,都是在文档上进行一个查询,而且这个查询开销很昂贵,最小化访问HTMLColletcion的次数可以极大地改进脚本性能。
优化HTMLCollection访问最重要的地方在于循环
实例代码:
var images = document.getElementsByTagName("img"),image,i,len;

for(i=0,len=images.length;i < len;i++){
     image = images[i];
    //处理
}
何时会返回HTMLCollection对象:
进行了对getElementsByTagName()的调用
获取了元素的childNodes属性
获取了元素的attributes属性
访问了特殊的集合,如document.forms、document.images等。

可维护性

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

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

可读性

格式化:建议缩进大小为4个空格
注释:函数和方法、大段代码、复杂的算法、hack
变量和函数命名

变量名为名词
函数名为动词开始
变量和函数使用合符逻辑的名字,不要担心长度。
变量类型透明:表示变量类型的三种方式

初始化: var found = false; //布尔型
使用匈牙利标记法来指定变量类型:o代表对象,s代表字符串,i代表整数,f代表浮点数,b代表布尔型
使用类型注释: var found /:Boolen/ = false;
松散耦合

解耦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。

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

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

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

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

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

避免与null进行比较
如果值应为一个引用类型,使用instanceof操作符检查其构造函数。
如果值应为一个基本类型,使用typeof检查其类型。
如果是希望对象包含某个特定的方法名,则使用typeof操作符确保指定名称的方法存在于对象上。
使用常量
关键在于将数据和使用它的逻辑进行分离。

重复值:任何在多处用到的值都应抽取为一个常量,也包含css类名,这就限制了当一个值变了而另一个没变的时候会造成的错误。
用户界面字符串:方便国际化。
URLs:在web应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL 任意可能会更改的值。
性能

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

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!";
}
选择正确方法
避免不必要的属性查找

常数值O(1):指代字面值和存储在变量中的值,访问数组元素
对数值O(log n):总的执行时间和值的数量相关,但是要完成算法并不一定要获取每个值。例如:二分查找。
线性O(n):访问对象,对象上的任何属性查找都要比访问变量或者数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索,属性查找越多,执行时间久越长。一旦多次用到对象属性,应该将其存储在局部变量中。
平方O(n²):总执行时间和值的数量有关,每个值至少要获取n次。例如:插入排序。
一般来讲,只要能减少算法的复杂度,就要尽可能减少。尽可能多地使用局部变量将属性查找替换 为值查找。进一步讲,如果即可以用数字化的数组位置进行访问,也可以使用命名属性(诸如 NodeList 对象),那么使用数字位置。

优化循环
基本步骤如下

减值迭代:在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效。
简化终止条件:由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。也就是说避免属性查找或者其他O(n)的操作。
简化循环体:循环体是执行最多的,所以要确保其被最大限度的优化。确保没有某些可以被很容易移除循环的密集计算。
使用后测试循环:最常用的for循环和while循环都是前测试循环,而如do-while这种后测试循环,可以避免最初终止条件的计算,因此运行更快。
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 / ;
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 重写一个。原生方法是用诸如 C/C++之类的编译型语言写出来的,所以要比 JavaScript 的快很多很多。JavaScript 中最容易被忘记的就是可以在 Math 对象中找到的复杂的数学运算;这些方法要比任何用JavaScript写 的同样方法如正弦、余弦快的多。
Switch 语句较快 —— 如果有一系列复杂的if-else语句,可以转换成单个switch语句则可以得到更快的代码。还可以通过将case语句按照最可能的到最不可能的顺序进行组织,来进一步优化switch语句。
位运算符较快 —— 当进行数学运算的时候,位运算操作要比任何布尔运算或者算数运算快。选择性地用位运算替换算数运算可以极大提升复杂计算的性能。诸如取模,逻辑与和逻辑或都可以考虑用位运算来替换。
最小化语句数
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 部分是已经显示的页面的一部分,那么你就是在进行一个现场更新。之所 以叫现场更新,是因为需要立即(现场)对页面对用户的显示进行更新。每一个更改,不管是插入单个字符,还是移除整个片段,都有一个性能惩罚,因为浏览器要重新计算无数尺寸以进行更新。现场更新进行得越多,代码完成执行所花的时间就越长;完成一个操作所需的现场更新越少,代码就越快。

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

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

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

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

进行了对 getElementsByTagName() 的调用;
获取了元素的 childNodes 属性;
获取了元素的 attributes 属性;
访问了特殊的集合,如 document.forms、document.images等。
要了解当使用HTMLCollection对象时,合理使用会极大提升代码执行速度。

部署

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

知识产权问题:如果把带有完整注释的代码放到线上,那别人就更容易知道你的意图,对它 再利用,并且可能找到安全漏洞。
文件大小:书写代码要保证容易阅读,才能更好地维护,但是这对于性能是不利的。浏览器 并不能从额外的空白字符或者是冗长的函数名和变量名中获得什么好处。
代码组织:组织代码要考虑到可维护性并不一定是传送给浏览器的最好方式。
基于这些原因,最好给JavaScript文件定义一个构建过程。构建过程始于在源控制中定义用于存储文件的逻辑结构。最好避免使用一个文件存放所有的JavaScript,遵循以下面向对象语言中的典型模式:将每个对象或自定义类型分别放入其单独的文件中。这样可以确保每个文件包含最少量的代码,使其在不引入错误的情况下更容易修改。另外,在使用像CVS或Subversion这类并发源控制系统的时候,这样做也减少了在合并操作中产生冲突的风险。

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

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

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

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

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

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






猜你喜欢

转载自zhyp29.iteye.com/blog/2304876