JavaScript Best Practice

JS最佳实践

可维护性

可读性

  • 要让代码可维护,首先它必须可读。多人协作共同开发的情况下,大家要使用同样的缩进方式。
  • 可读性的另外一个方面是注释:
    • 函数和方法 - 每个函数或方法都应该包含一个注释,描述其目的和用于完成任务所可能使用的算法。另外还可以包括前置条件的说明,参数的含义,以及是否有返回值等。
    • 大段代码 - 用于完成单个任务的多行代码应该在前面放一个描述任务的注释。
    • 复杂的算法 - 为了让别人看懂,同时也为了下次自己看代码的时候更容易一些,应该给复杂的算法加一些描述思路的注释。

变量和函数命名

    命名的一般规则:

  • 变量名应为名词,如name,age。
  • 函数名应该以动词开始,如 getName()。返回的布尔类型值的函数一般以 is 开头,如isEnable()。
  • 变量和函数都应该使用合乎逻辑的名字,不要担心长度。

变量类型透明

因为js中的变量是松散类型的,因此很容易忘记变量最初包含的数据类型。可以通过给变量初始化的方式,记住变量的数据类型。

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

松散耦合

    减少应用内部的依赖,使代码更容易维护。

  • 解耦HTML/JavaScript
  • 解耦CSS/JavaScript
  • 解耦应用逻辑/事件处理程序

解耦HTML/JavaScript

HTML是数据,JavaScript是行为。

直接在HTML里面写JS, 或者直接在JS中创建大量的HTML,都是过于紧密的耦合。

// 使用<script>元素的情况      
<script type="text/javascript">
	document.write("Hello World");
</script>

// 直接在HTML中加入js事件处理代码
<input type="button" value="Click Me" onclick="doSomething()" />

// 在JS中创建大量HTML
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

  • HTML 呈现应该尽可能与 JavaScript 保持分离。
  • 当用js插入数据时,尽量不要直接插入标记。一般直接在页面中包含并隐藏标记,然后等页面渲染完成后,使用js显示该标记,而不是生成它。
  • 另外一种是使用Ajax来获取要显示的 HTML。
  • 将标记和行为分开解耦可以在调试过程中节省时间,更加容易确定错误的来源,也可以减轻维护的难度。

解耦CSS/JavaScript

CSS主要负责页面的显示,和JS一样,都是HTML之上的层次。我们经常使用JS来更改样式:

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

可以通过动态更改样式类而非特定样式来实现更松散的耦合:

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

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

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

每个Web application一般都有相当多的事件处理程序,监听着无数的不同的事件。例如:

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";
    }
  }
}
			
  • 这样带来的一个问题是:除了通过事件之外再没有办法执行应用逻辑,调试和测试变得比较困难。
  • 程序执行和预期行为不一致时,不知道是事件处理程序没有被调用,还是应用逻辑出现问题?
  • 同样应用逻辑代码复用的问题。

把应用逻辑和事件处理相分离

事件处理程序应该从事件对象中提取相关信息,并将这些信息传送给处理应用逻辑的某个方法中。

function validateValue(value) {
  var 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对象中所需要的数据。
  • 任何可以在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行。
  • 任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑。

避免全局变量

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

// 两个全局变量 - 避免!!
var name = "Tom";    
function sayName() {
	alert(name);
}
    

这段代码包含了两个全局变量:变量name和函数sayName()。可以创建一个包含两者的对象:

// 一个全局变量 - 推荐
var myApplication = {
  name: "Tom",
  sayName: function() {
    alert(this.name);
  }
}    
    
  • 避免了和其他功能产生冲突
  • 作用域更加清晰

避免与null进行比较

要按照所期望的对值进行检查,而非按照不被期望的那些(如null)。

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

在这个例子中,values参数应该是一个数组,我们应该检查它是不是一个数组,而不是检查它是否非null。我们可以按照下面的方式进行修改:

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

避免与null进行比较

    如果我们看到了和null进行比较的代码,尝试使用以下技术替换:

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

有关typeof,参见:JavaScript Reference > Operatores > typeof

使用常量

使用常量,我们可以把数据从应用逻辑中分离出来。看一个例子:

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

    在这个例子中有两段数据:要显示给用户的信息以及URL。

  • 显示在用户界面上的字符串应该以允许国际化的方式提取出来;
  • URL也应该被提取出来,随着应用的演进可能会发生变化;
  • 上述两种情况都可能会导致后面在应用逻辑的函数中直接修改代码。

隔离应用逻辑和数据修改

通过将数据提取出来作为单独定义的常量的方式,可以将应用逻辑与数据修改隔离开来:

      
var Constants = {
  INVALID_VALUE_MSG: "Invalid value!",
  INVALID_VALUE_URL: "/errors/invalid.php"
};
      
function validate(value) {
  if (!values) {
    alert(Constants.INVALID_VALUE_MSG);
    location.href = Constants.INVALID_VALUE_URL;
  }
}
      

常量使用指南

要注意的值的类型如下所示:

  • 重复值 - 任何在多个地方都用到的值都应该提取为一个常量。(这样就避免了当一个值变了而另一个地方的值没变的情况,也包含CSS类名的情况)
  • 用户界面字符串 - 任何用于显示给用户的字符串,都应该被提取出来以方便国际化。
  • URLs - 在Web application中,资源地址很容易变更,建议把所有的URL都存到一个公共的地方。
  • 任意可能会更改的值 - 在使用字面量时,要考虑到这个值在未来是不是会变化。如果是的话,最好都提取成常量。

性能

注意作用域

随着作用域中的作用链的数量的增加,访问当前作用域以外的变量的时间也在增加。

避免全局查找

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

function updateUI() {
  var imgs = document.getElementByTagName("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 = "更新完成!";
}
      

上述代码包含了三个对全局变量document对象的引用,特别是for循环中的document 引用每次都要进行作用域链查找。

避免全局查找

改进

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

function updateUI() {
  var doc = document;
  var imgs = doc.getElementByTagName("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 = "更新完成!";
}
      

现在的函数只有一次全局查找。将在一个函数中会多次用到的全局对象存储为局部变量总是没错的。

避免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";
}
      

在上面的代码中,通过将 document.body 存储在局部变量中省去了额外的全局查找。在性能非常重要的地方要避免使用with语句。

避免不必要的属性查找

字面量和存储在常量中的值,代码复杂度是O(1)。

var value = 5;
var sum = 10 + value;
alert(sum);
      

在JS中访问数组元素也是一个O(1)复杂度。

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

访问对象上的属性复杂度是O(n)。对象上的任何属性查找都要比访问变量或者数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索。

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

避免不必要的属性查找(续)

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

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

共有6次属性查找。可以把对象属性存储到局部变量。第一次访问该值会是O(n), 然后连续的访问都会是O(1)。

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

这个版本的代码只有4次属性查找。

优化循环

一个基本的 for 循环

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

变量 i 从 0 递增到values数组中的元素总数。如果不考虑值的处理顺序,那么循环可以由 i++ 改成 i--, 如下所示:

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

修改以后,每次循环过程中对终止条件的判断的复杂度由O(n)简化成了O(1)调用。

优化循环

基本步骤

优化循环的步骤在很多语言中都是一样的:

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

最小化语句数

JavaScript代码中语句的数量也会影响到所执行操作的速度。将多个单条语句组合在一起,可以减少脚本的整体的执行时间。

// 4 条语句的变量声明
var count = 5;
var color = "blue";
var values = [1,2,3];
var now = new Date();
      

由于JavaScript中所有的变量都可以使用var来声明,因此前面的代码可以重写为一个语句:

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

最小化语句数

插入迭代值

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

可以合并成一条语句:

var name = values[i++];
      

在类似的情况下,都可以把迭代值插入到最后使用它的语句中。

最小化语句数

使用数组字面量

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

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

var values = [123, 456, 789];
      

最小化语句数

使用对象字面量

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

对象同样可以很容易地转换成使用字面量的形式:

var person = {
  name: "Tom",
  age: 32,
  sayName: function() {
    alert(this.name);
  }
}      
      

部署

  • 使用JSLint 进行验证(在线命令行下)
  • 文件压缩
    • 删除额外的空白(包括换行)
    • 删除所有的注释
    • 缩短变量名
  • HTTP 压缩

扩展阅读

</ Thank you!>