{T}

最佳实践

2000 年以来,Web 开发一直在以惊人的速度发展。从最初毫无章法可循的“野蛮生长”,到如今已发展出完整的规范体系,各种研究成果和最佳实践层出不穷。随着简单的网站变成复杂的 Web 应用程序,曾经的 Web 开发爱好者也变成了收入不菲的专业人士。Web 开发领域的最新技术和开发工具已经令人目不暇接。其中,JavaScript 尤其成为了研究和关注的焦点。JavaScript 的最佳实践可以分成几类,适用于开发流程的不同阶段

可维护性

编写可维护的代码十分重要,因为大多数开发者会花大量时间去维护别人写的代码。实际开发中,从第一行代码开始写起的情况非常少,通常是要在别人的代码之上构建自己的工作。让自己的代码容易维护,可以保证其他开发者更好地完成自己的工作。

可维护代码

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

  • 容易理解:无须求助原始开发者,任何人一看代码就知道它是干什么的,以及它是怎么实现的
  • 符合常识:代码中的一切都显得顺理成章,无论操作有多么复杂
  • 容易适配:即使数据发生变化也不用完全重写
  • 容易扩展:代码架构经过认真设计,支持未来扩展核心功能
  • 容易调试:出问题时,代码可以给出明确的信息,通过它能直接定位问题

能够写出可维护的 JavaScript 代码是一项重要的专业技能。这就是业余爱好者和专业开发人员之间的区别,前者用一个周末就拼凑出一个网站,而后者真正了解自己的技术

编码规范

编写可维护代码的第一步是认真考虑编码规范。大多数编程语言会涉及编码规范,简单上网一搜,就可以找到成千上万的相关文章。专业组织有为开发者建立的编码规范,旨在让人写出更容易维护的代码。优秀开源项目有严格的编码规范,可以让社区的所有人容易地理解代码是如何组织的

可读性

要让代码可维护,首先它必须可读。

可读性的大部分内容都是和代码的缩进相关的。当所有人都使用一样的缩进方式时,整个项目中的代码都会更加易于阅读。通常会使用若干空格而非制表符来进行缩进,这是因为制表符在不同的文本编辑器中显示效果不同。一种不错的、很常见的缩进大小为 4 个空格,当然你也可以使用其他数量。

可读性的另一方面是注释。在大多数编程语言中,对每个方法的注释都视为一个可行的实践。因为 JavaScript 可以在代码的任何地方创建函数,所以这点常常被忽略了。然而正因如此,在 JavaScript 中为每个函数编写文档就更加重要了。一般而言,有如下一些地方需要进行注释。

  • 函数和方法。每个函数和方法都应该有注释来描述其用途,以及完成任务所用的算法。同时,也写清使用这个函数或方法的前提(假设)、每个参数的含义,以及函数是否返回值(因为通过函数定义看不出来)。
  • 大型代码块。多行代码但用于完成单一任务的,应该在前面给出注释,把要完成的任务写清楚。
  • 复杂的算法。如果使用了独特的方法解决问题,要通过注释解释明白。这样不仅可以帮助别人查看代码,也可以帮助自己今后查看代码。
  • 使用黑科技。由于浏览器之间的差异,JavaScript 代码中通常包含一些黑科技。不要假设其他人一看就能明白某个黑科技是为了解决某个浏览器的什么问题

变量和函数命名

适当给变量和函数起名字对于增加代码可理解性和可维护性是非常重要的

  • 变量名应为名词,如 car 或 person。
  • 函数名应该以动词开始,如 getName()。返回布尔类型值的函数一般以 is 开头,如 isEnable()
  • 变量和函数都应使用合乎逻辑的名字,不要担心长度。长度问题可以通过后处理和压缩来缓解
  • 变量、函数和方法应该以小写字母开头,使用驼峰大小写(camelCase)形式,如 getName() 和 isPerson。类名应该首字母大写,如 Person、RequestFactory。常量值应该全部大写并以下划线相接,比如 REQUEST_TIMEOUT
  • 名称要尽量用描述性和直观的词汇,但不要过于冗长。getName() 一看就知道会返回名称,而 PersonFactory 一看就知道会产生某个 Person 对象或实体。

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

变量类型透明化

由于在 JavaScript 中变量是松散类型的,很容易就忘记变量所应包含的数据类型。合适的命名方式可以一定程度上缓解这个问题,有三种方式可以标明变量的数据类型

  1. 初始化

当定义了一个变量后,它应该被初始化为一个值,来暗示它将来应该如何应用。例如,将来保存布尔类型值的变量应该初始化为 true 或者 false,将来保存数字的变量就应该初始化为一个数字。

javascript
//通过初始化指定变量类型
var found = false; //布尔型
var count = -1; //数字
var name = ""; //字符串
var person = null; //对象

初始化为一个特定的数据类型可以很好的指明变量的类型。但缺点是它无法用于函数声明中的函数参数。

  1. 匈牙利标记

第二种方法是使用匈牙利标记法来指定变量类型。匈牙利标记法在变量名之前加上一个或多个字符来表示数据类型。这个标记法在脚本语言中很流行,曾经很长时间也是 JavaScript 所推崇的方式。

JavaScript 中最传统的匈牙利标记法是用单个字符表示基本类型:"o"代表对象,"s"代表字符串,"i"代表整数,"f"代表浮点数,"b"代表布尔型。

javascript
//用于指定数据类型的匈牙利标记法
var bFound; //布尔型

var iCount; //整数

var sName; //字符串

var oPerson; //对象

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

  1. 类型注释

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

javascript
//用于指定类型的类型注释

var found /*:Boolean*/ = false; 

var count /*:int*/ = 10; 

var name /*:String*/ = "Nicholas"; 

var person /*:Object*/ = null;

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

javascript
//以下代码不能正确运行
/*
var found /*:Boolean*/ = false;

var count /*:int*/ = 10;

var name /*:String*/ = "Nicholas";

var person /*:Object*/ = null;
*/

这就是最常见的三种指定变量数据类型的方法。每种都有各自的优势和劣势,要自己在使用之前进行评估。最重要的是要确定哪种最适合你的项目并一致使用。

松散耦合

只要应用的某个部分过分依赖于另一部分,代码就是耦合过紧,难于维护。典型的问题如:对象直接引用另一个对象,并且当修改其中一个的同时需要修改另外一个。紧密耦合的软件难于维护并且需要经常重写。

因为 Web 应用所涉及的技术,有多种情况会使它变得耦合过紧。必须小心这些情况,并尽可能维护弱耦合的代码。

解耦 HTML/JavaScript

有一些方法会将 HTML 和 JavaScript 过于紧密地耦合在一起。直接写在 HTML 中的 JavaScript,使用包含内联代码的<script>元素或者是使用 HTML 属性来分配事件处理程序,都是过于紧密的耦合。

html
<!-- 使用了 <script> 的紧密耦合的 HTML/JavaScript --> 
<script type="text/javascript"> 
  document.write("Hello world!"); 
</script>

<!-- 使用事件处理程序属性值的紧密耦合的 HTML/JavaScript --> 
<input type="button" value="Click Me" onclick="doSomething()" />

理想情况是,HTML 和 JavaScript 应该完全分离,并通过外部文件和使用 DOM 附加行为来包含 JavaScript。HTML 和 JavaScript 的紧密耦合也可以在相反的关系上成立:JavaScript 包含了 HTML

javascript
//将 HTML 紧密耦合到 JavaScript 

function insertMessage(msg){ 
 var container = document.getElementById("container"); 
 container.innerHTML = "<div class=\"msg\"><p class=\"post\">" + msg + "</p>" + 
 "<p><em>Latest message above.</em></p></div>"; 
}

HTML 呈现应该尽可能与 JavaScript 保持分离。当 JavaScript 用于插入数据时,尽量不要直接插入标记。一般可以在页面中直接包含并隐藏标记,然后等到整个页面渲染好之后,就可以用 JavaScript 显示该标记,而非生成它。

另一种方法是进行 Ajax 请求并获取更多要显示的 HTML,这个方法可以让同样的渲染层(PHP、JSP、Ruby 等等)来输出标记,而不是直接嵌在 JavaScript 中

解耦 CSS/JavaScript

CSS 和 JavaScript 也可能会过于紧密地耦合在一起。最常见的紧密耦合的例子是使用 JavaScript 来更改某些样式,如下所示:

javascript
//CSS 对 JavaScript 的紧密耦合
element.style.color = "red"; 
element.style.backgroundColor = "blue";

由于 CSS 负责页面的显示,当显示出现任何问题时都应该只是查看 CSS 文件来解决。然而,当使用了 JavaScript 来更改某些样式的时候,比如颜色,就出现了第二个可能已更改和必须检查的地方。

现代 Web 应用常常要使用 JavaScript 来更改样式,所以虽然不可能完全将 CSS 和 JavaScript 解耦,但是还是能让耦合更松散的。这是通过动态更改样式类而非特定样式来实现的。

javascript
//CSS 对 JavaScript 的松散耦合
element.className = "edit";

通过只修改某个元素的 CSS 类,就可以让大部分样式信息严格保留在 CSS 中

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

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

javascript
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.display = "block"; 
    } 
  } 
}

较好的方法是将应用逻辑和事件处理程序相分离,这样两者分别处理各自的东西。一个事件处理程序应该从事件对象中提取相关信息,并将这些信息传送到处理应用逻辑的某个方法中

javascript
function validateValue(value){ 
 	value = 5 * parseInt(value); 
 	if (value > 10){ 
 		document.getElementById("error-msg").style.display = "block"; 
 	} 
} 

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

从事件处理程序中分离应用逻辑有几个好处。

首先,可以让你更容易更改触发特定过程的事件。如果最开始由鼠标点击事件触发过程,但现在按键也要进行同样处理,这种更改就很容易。

其次,可以在不附加到事件的情况下测试代码,使其更易创建单元测试或者是自动化应用流程。

以下是要牢记的应用和业务逻辑之间松散耦合的几条原则:

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

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

编码管理

编写可维护的 JavaScript 不仅仅涉及代码格式和规范,也涉及代码做什么。企业开发 Web 应用程序通常需要很多人协同工作。这时候就需要保证每个人的浏览器环境都有恒定不变的规则。为此,开发者应该遵守某些编码惯例

尊重对象所有权

在企业开发中,非常重要的编码惯例就是尊重对象所有权,这意味着不要修改不属于你的对象。简单来讲,如果你不负责创建和维护某个对象及其构造函数或方法,就不应该对其进行任何修改。更具体一点说,就是如下惯例

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

这些规则不仅仅适用于自定义类型和对象,对于诸如 Object、String、document、window 等原生类型和对象也适用。此处潜在的问题可能更加危险,因为浏览器提供者可能会在不做宣布或者是不可预期的情况下更改这些对象。

最佳的方法便是永远不修改不是由你所有的对象

所谓拥有对象,就是说这个对象是你创建的,比如你自己创建的自定义类型或对象字面量。而 Array、document 这些显然不是你的,它们在你的代码执行前就存在了。你依然可以通过以下方式为对象创建新的功能:

  • 创建包含所需功能的新对象,并用它与相关对象进行交互;
  • 创建自定义类型,继承需要进行修改的类型。然后可以为自定义类型添加额外功能

现在很多 JavaScript 库都赞同并遵守这条开发原理,这样即使浏览器频繁更改,库本身也能继续成长和适应

避免全局量

尽可能避免全局变量和函数。这也关系到创建一个脚本执行的一致的和可维护的环境,最多创建一个全局变量,让其他对象和函数存在其中

javascript
// 两个全局变量:不要!
var name = "Nicholas"; 
function sayName(){ 
  alert(name); 
}

其实可以创建一个包含两者的对象

javascript
// 一个全局量——推荐
var MyApplication = { 
  name: "Nicholas", 
  sayName: function(){ 
    alert(this.name); 
  } 
};

它有助消除功能作用域之间的混淆。调用 MyApplication.sayName() 在逻辑上暗示了代码的任何问题都可以通过检查定义 MyApplication 的代码来确定。

这样一个全局对象可以扩展为命名空间的概念。命名空间涉及创建一个对象,然后通过这个对象来暴露能力。比如,Google Closure 库就利用了这样的命名空间来组织其代码。下面是几个例子

  • goog.string:用于操作字符串的方法
  • goog.html.utils:与 HTML 相关的方法
  • goog.i18n:与国际化(i18n)相关的方法

对象 goog 就相当于一个容器,其他对象包含在这里面。只要使用对象以这种方式来组织功能,就可以称该对象为命名空间。整个 Google Closure 库都构建在这个概念之上,能够在同一个页面上与其他 JavaScript 库共存

关于命名空间,最重要的确定一个所有人都同意的全局对象名称。这个名称要足够独特,不可能与其他人的冲突。大多数情况下,可以使用开发者所在的公司名,例如 goog 或 Wrox

javascript
//创建全局对象
var Wrox = {}; 

//为 Professional JavaScript 创建命名空间
Wrox.ProJS = {}; 

//将书中用到的对象附加上去
Wrox.ProJS.EventUtil = { ... }; 
Wrox.ProJS.CookieUtil = { ... };

在这个例子中,Wrox是全局量,其他命名空间在此之上创建。如果本书所有代码都放在Wrox.ProJS命名空间,那么其他作者也应把自己的代码添加到 Wrox 对象中。

javascript
//为 Professional Ajax 创建命名空间
Wrox.ProAjax = {}; 

//附加该书中所使用的其他对象
Wrox.ProAjax.EventUtil = { ... }; 
Wrox.ProAjax.CookieUtil = { ... }; 

//ProJS 还可以继续分别访问
Wrox.ProJS.EventUtil.addHandler( ... ); 

//以及 ProAjax 
Wrox.ProAjax.EventUtil.addHandler( ... );

避免与 null 进行比较

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

javascript
function sortArray(values){ 
  if (values != null){ //避免!
    values.sort(comparator); 
  } 
}

现实中,与 null 比较很少适合情况而被使用。必须按照所期望的对值进行检查,而非按照不被期望的那些。例如,在前面的范例中,values 参数应该是一个数组,那么就要检查它是不是一个数组,而不是检查它是否非 null。函数按照下面的方式修改会更加合适:

javascript
function sortArray(values){ 
  if (values instanceof Array){ //推荐
    values.sort(comparator); 
  } 
}

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

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

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

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

使用常量

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

javascript
function validate(value){ 
  if (!value){ 
    alert("Invalid value!"); 
    location.href = "/errors/invalid.php"; 
  } 
}

而每次修改应用逻辑的代码,都可能会引入错误。可以通过将数据抽取出来变成单独定义的常量的方式,将应用逻辑与数据修改隔离开来

javascript
var Constants = { 
  INVALID_VALUE_MSG: "Invalid value!", 
  INVALID_VALUE_URL: "/errors/invalid.php" 
}; 

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

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

重复值——任何在多处用到的值都应抽取为一个常量。这就限制了当一个值变了而另一个没变的时候会造成的错误。这也包含了 CSS 类名对于企业级的 JavaScript 开发而言,使用常量是非常重要的技巧,因为它能让代码更容易维护,并且在数据更改的同时保护代码

性能优化

相比 JavaScript 刚问世时,目前每个网页中 JavaScript 代码的数量已有极大的增长。代码量的增长也带来了运行时执行 JavaScript 的性能问题。JavaScript 一开始就是一门解释型语言,因此执行速度比编译型语言要慢一些。Chrome 是第一个引入优化引擎将 JavaScript 编译为原生代码的浏览器。随后,其他主流浏览器也紧随其后,实现了 JavaScript 编译

即使到了编译 JavaScript 时代,仍可能写出运行慢的代码。不过,如果遵循一些基本模式,就能保证写出执行速度很快的代码

注意作用域

随着作用域链中的作用域数量的增加,访问当前作用域以外的变量的时间也在增加。访问全局变量总是要比访问局部变量慢,因为需要遍历作用域链

避免全局查找

改进代码性能非常重要的一件事,可能就是要提防全局查询。全局变量和函数相比于局部值始终是最费时间的,因为需要经历作用域链查找

javascript
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.";
}

该函数可能看上去完全正常,但是它包含了三个对于全局 document 对象的引用。如果在页面上有多个图片,那么 for 循环中的 document 引用就会被执行多次甚至上百次,每次都会要进行作用域链查找

通过创建一个指向 document 对象的局部变量,就可以通过限制一次全局查找来改进这个函数的性能:

javascript
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 语句中执行的代码肯定会比外面执行的代码要慢

必须使用 with 语句的情况很少,因为它主要用于消除额外的字符。大多数情况下,用局部变量完成相同的事情而不引入新的作用域

javascript
function updateBody(){ 
  with(document.body){ 
    alert(tagName); 
    innerHTML = "Hello world!"; 
  } 
}

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

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

虽然代码稍微长了点,但是阅读起来比 with 语句版本更好,它确保让你知道 tagName 和 innerHTML 是属于哪个对象的

选择正确方法

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

避免不必要的属性查找

在计算机科学中,算法的复杂度是使用 O 符号来表示的。最简单、最快捷的算法是常数值即 O(1)

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

使用变量和数组要比访问对象上的属性更有效率,访问对象属性的算法复杂度是 O(n)操作。

对象上的任何属性查找都要比访问变量或者数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索。简而言之,属性查找越多,执行时间就越长

javascript
var values = { first: 5, second: 10}; 
var sum = values.first + values.second; 
alert(sum);

这段代码使用两次属性查找来计算 sum 的值。进行一两次属性查找并不会导致显著的性能问题,但是进行成百上千次则肯定会减慢执行速度。

注意获取单个值的多重属性查找

javascript
var query = window.location.href.substring(window.location.href.indexOf("?"));

这段代码由于两次用到了 window.location.href,同样的查找进行了两次,因此效率特别不好。一旦多次用到对象属性,应该将其存储在局部变量中。第一次访问该值会是 O(n),然而后续的访问都会是 O(1),就会节省很多

javascript
var url = window.location.href; 
var query = url.substring(url.indexOf("?"));

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

优化循环

优化循环是性能优化过程中很重要的一个部分,由于它们会反复运行同一段代码,从而自动地增加执行时间。

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

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

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

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

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

此处主要的优化是将终止条件和自减操作符组合成了单个语句。这时,任何进一步的优化只能在 process() 函数中进行了,因为循环部分已经优化完全了。

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

避免双重解释

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

javascript
// 某些代码求值——避免!! 
eval("alert('Hello world!')"); 

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

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

在以上这些例子中,都要解析包含了 JavaScript 代码的字符串。这个操作是不能在初始的解析过程中完成的,因为代码是包含在字符串中的,也就是说在 JavaScript 代码运行的同时必须新启动一个解析器来解析新的代码。实例化一个新的解析器有不容忽视的开销,所以这种代码要比直接解析慢得多

对于这几个例子都有另外的办法。只有极少的情况下 eval() 是绝对必须的,所以尽可能避免使用

在这个例子中,代码其实可以直接内嵌在原代码中。对于 Function 构造函数,完全可以直接写成一般的函数,调用 setTimeout()可以传入函数作为第一个参数。

javascript
//已修正
alert('Hello world!'); 

//创建新函数——已修正
var sayHi = function(){ 
  alert('Hello world!'); 
}; 

//设置一个超时——已修正
setTimeout(function(){ 
  alert('Hello world!'); 
}, 500);

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

性能的其他注意事项

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

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

最小化语句数

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

多个变量声明

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

javascript
//4 个语句——很浪费
var count = 5; 
var color = "blue"; 
var values = [1,2,3]; 
var now = new Date();

在强类型语言中,不同的数据类型的变量必须在不同的语句中声明。然而,在 JavaScript 中所有的变量都可以使用单个 var 语句来声明

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

插入迭代值

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

javascript
var name = values[i]; 
i++;

第一个从 values 数组中获取值,然后存储在 name 中;第二个给变量 i 增加 1。这两句可以通过迭代值插入第一个语句组合成一个语句

javascript
var name = values[i++];

使用数组和对象字面量

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

javascript
//用 4 个语句创建和初始化数组——浪费
var values = new Array(); 
values[0] = 123; 
values[1] = 456; 
values[2] = 789; 

//用 4 个语句创建和初始化对象——浪费
var person = new Object(); 
person.name = "Nicholas"; 
person.age = 29; 
person.sayName = function(){ 
 	alert(this.name); 
};

其实可以很容易地转换成使用字面量的形式

javascript
//只用一条语句创建和初始化数组
var values = [123, 456, 789]; 

//只用一条语句创建和初始化对象
var person = { 
  name : "Nicholas", 
  age : 29, 
  sayName : function(){ 
    alert(this.name); 
  } 
};

重写后的代码只包含两条语句,一条创建和初始化数组,另一条创建和初始化对象。之前用了八条语句的东西现在只用了两条,减少了 75%的语句量。在包含成千上万行 JavaScript 的代码库中,这些优化的价值更大

优化DOM交互

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

最小化现场更新

一旦你需要访问的 DOM 部分是已经显示的页面的一部分,那么你就是在进行一个现场更新。之所以叫现场更新,是因为需要立即(现场)对页面对用户的显示进行更新

每一个更改,不管是插入单个字符,还是移除整个片段,都有一个性能惩罚,因为浏览器要重新计算无数尺寸以进行更新。现场更新进行得越多,代码完成执行所花的时间就越长;完成一个操作所需的现场更新越少,代码就越快

javascript
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 元素中。这个方式避免了现场更新和页面闪烁问题

javascript
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 调用。由于内部方法是编译好的而非解释执行的,所以执行快得多

javascript
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 的次数太多了:

javascript
var list = document.getElementById("myList"), 
    i; 

for (i=0; i < 10; i++) { 
  list.innerHTML += "<li>Item " + i + "</li>"; //避免!!! 
}

使用事件代理

大多数 Web 应用在用户交互上大量用到事件处理程序。页面上的事件处理程序的数量和页面响应用户交互的速度之间有个负相关。为了减轻这种惩罚,最好使用事件代理

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

注意 HTMLCollection

HTMLCollection 对象的陷阱已经在本书中讨论过了,因为它们对于 Web 应用的性能而言是巨大的损害。记住,任何时候要访问 HTMLCollection,不管它是一个属性还是一个方法,都是在文档上进行一个查询,这个查询开销很昂贵。最小化访问 HTMLCollection 的次数可以极大地改进脚本的性能

也许优化 HTMLCollection 访问最重要的地方就是循环了。前面提到过将长度计算移入 for 循环的初始化部分。现在看一下这个例子:

javascript
var images = document.getElementsByTagName("img"), 
    i, len; 

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

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

javascript
var images = document.getElementsByTagName("img"), 
    image, 
    i, len; 

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

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

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

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

部署

任何 JavaScript 解决方案最重要的部分可能就是把网站或 Web 应用程序部署到线上环境了。在此之前我们已完成了很多工作,包括架构方面和优化方面的。现在到了把代码移出开发环境,发布到网上,让用户去使用它的时候了。不过,在发布之前,还需要解决一些问题

构建流程

开发软件的典型模式是编码、编译和测试。换句话说,首先要写代码,然后编译,之后运行并确保它能够正常工作。但因为 JavaScript 不是编译型语言,所以这个流程经常会变成编码、测试。你写的代码跟在浏览器中测试的代码一样。这种方式的问题在于代码并不是最优的。你写的代码不应该不做任何处理就直接交给浏览器。

  • 知识产权问题:如果把满是注释的代码放到网上,其他人就很容易了解你在做什么,重用它,并可能发现安全漏洞。
  • 文件大小:你写的代码可读性很好,容易维护,但性能不好。浏览器不会因为代码中多余的空格、缩进、冗余的函数和变量名而受益。
  • 代码组织:为保证可维护性而组织的代码不一定适合直接交付给浏览器。为此,需要为 JavaScript 文件建立构建流程

以模块形式编写代码,并不意味着必须以模块形式交付代码。通常,由大量模块组成的 JavaScript 代码在构建时需要打包到一起,然后只交付一个或少数几个 JavaScript 文件。

模块打包器的工作是识别应用程序中涉及的 JavaScript 依赖关系,将它们组合成一个大文件,完成对模块的串行组织和拼接,然后生成最终提供给浏览器的输出文件。能够实现模块打包的工具非常多。Webpack、Rollupt 和 Browserify 只是其中的几个,可以将基于模块的代码转换为普遍兼容的网页脚本