[TOC]

可维护性

在早期网站中,JavaScript 主要用于实现一些小型动效或表单验证。

今天的 Web 应用程序则动辄成千上万行 JavaScript 代码,用于完成各种各样的复杂处理。这些变化要求开发者把可维护能力放到重要位置上。

正如更传统意义上的软件工程师一样,JavaScript 开发者受雇是要为公司创造价值的。

他们不仅要保证产品如期上线,而且要随着时间推移为公司不断积累知识资产。

编写可维护的代码十分重要,因为大多数开发者会花大量时间去维护别人写的代码。

实际开发中,从第一行代码开始写起的情况非常少,通常是要在别人的代码之上构建自己的工作。

让自己的代码容易维护,可以保证其他开发者更好地完成自己的工作。

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

1、可读性

代码缩进

代码缩进是保证可读性的重要基础。

如果所有人都使用相同的缩进,整个项目的代码就会更容易让人看懂。

缩进通常要使用空格数而不是 Tab(制表符)来定义,因为后者在不同文本编辑器中的显示不同

注释

广泛接受的做法是为每个方法都编写注释。

 函数和方法。
每个函数和方法都应该有注释来描述其用途,以及完成任务所用的算法。
同时,也写清使用这个函数或方法的前提(假设)、每个参数的含义,以及函数是否返回值(因为通过函数定义看不出来)。

 大型代码块。
多行代码但用于完成单一任务的,应该在前面给出注释,把要完成的任务写清楚。

 复杂的算法。
如果使用了独特的方法解决问题,要通过注释解释明白。这样不仅可以帮助别人查看代码,也可以帮助自己今后查看代码。

 使用黑科技。
由于浏览器之间的差异,JavaScript 代码中通常包含一些黑科技。
不要假设其他人一看就能明白某个黑科技是为了解决某个浏览器的什么问题。如果某个浏览器不能使用正常方式达到目的,那要在注释里把黑科技的用途写出来。这样可以避免别人误以为黑科技没有用而把它“修复”掉,结果你已解决的问题又会出现。

2、变量和函数命名

 变量名应该是名词
例如 car 或 person。

 函数名应该以动词开始
例如 getName()。返回布尔值的函数通常以 is 开头,比如 isEnabled()。

 对变量和函数都使用符合逻辑的名称,不用担心长度。
长名字的问题可以通过后处理和压缩解决。

 变量、函数和方法应该以小写字母开头,使用驼峰大小写(camelCase)形式如 getName()和isPerson。

 类名应该首字母大写,如 Person、RequestFactory。

 常量值应该全部大写并以下划线相接,比如 REQUEST_TIMEOUT。

 名称要尽量用描述性和直观的词汇,但不要过于冗长。
getName()一看就知道会返回名称,而PersonFactory 一看就知道会产生某个 Person 对象或实体。

 要完全避免没有用的变量名,如不能表示所包含数据的类型的变量名。

3、变量类型透明化

因为 JavaScript 是松散类型的语言,所以很容易忘记变量包含的数据类型。

当命名可以在某种程度上解决这个问题,但还不够。

有三种方式可以标明变量的数据类型。

1、标明变量类型的方式是通过初始化。
定义变量时,应该立即将其初始化为一个将来要使用的类型值。
ES6 之后,可以在函数声明中为参数指定默认值来标明参数类型。

2、标明变量类型的方式是使用匈牙利表示法。
匈牙利表示法指的是在变量名前面前缀一个或多个字符表示数据类型。

// 使用匈牙利表示法标明数据类型
let bFound; // 布尔值
let iCount; // 整数
let sName; // 字符串
let oPerson; // 对象

匈牙利表示法也可以很好地应用于函数参数。
它的缺点是使代码可读性下降、不够直观,并破坏了类似句子的自然阅读流畅性。因此,匈牙利表示法在开发者中失宠了。

3、标明变量类型的方式是使用类型注释。
类型注释放在变量名后面、初始化表达式的前面。基本思路是在变量旁边使用注释说明类型

// 使用类型注释表明数据类型
let found /*:Boolean*/ = false; 
let count /*:int*/ = 10; 
let name /*:String*/ = "Nicholas"; 
let person /*:Object*/ = null;

类型注释在保持代码整体可读性的同时向其注释了类型信息。
类型注释的缺点是不能再使用多行注释把大型代码块注释掉了。因为类型注释也是多行注释,所以会造成干扰

4、使用Typescript
https://www.yuque.com/yejielin/mypn47/ah7ky5#HbjdY
代码编辑器有很好的类型提示支持

4、松散耦合

只要应用程序的某个部分对另一个部分依赖得过于紧密,代码就会变成紧密耦合,因而难以维护。

典型的问题是在一个对象中直接引用另一个对象,这样,修改其中一个,可能必须还得修改另一个。

紧密耦合的软件难于维护,肯定需要频繁地重写。

考虑到相关的技术,Web 应用程序在某些情况下可能变得过于紧密耦合。

关键在于有这个意识,随时注意不要让代码产生紧密耦合。

解耦 HTML/JavaScript

Web 开发中最常见的耦合是 HTML/JavaScript 耦合。

在网页中,HTML 和 JavaScript 分别代表不同层面的解决方案。

HTML 是数据,JavaScript 是行为。

这是因为它们之间要交互操作,需要通过不同的方式将这两种技术联系起来。

可惜的是,其中一些方式会导致 HTML 与 JavaScript 紧密耦合。

<!-- 使用<script>造成 HTML/JavaScript 紧密耦合 --> 
<script> 
 document.write("Hello world!"); 
</script> 

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

理想情况下,HTML 和 JavaScript 应该完全分开,通过外部文件引入 JavaScript,然后使用DOM 添加行为。

HTML 与 JavaScript 紧密耦合的情况下,每次分析 JavaScript 的报错都要先确定错误来自 HTML 还 是 JavaScript。这样也会引入代码可用性的新错误。

// HTML 紧密耦合到了 JavaScript 
function insertMessage(msg) { 
 let container = document.getElementById("container"); 
 container.innerHTML = `<div class="msg"> 
 <p> class="post">${msg}</p> 
 <p><em>Latest message above.</em></p> 
 </div>`; 
}

一般来说,应该避免在 JavaScript 中创建大量 HTML。
同样,这主要是为了做到数据层和行为层各司其职,在出错时更容易定位问题所在。

HTML 渲染应该尽可能与 JavaScript 分开。
在使用 JavaScript 插入数据时,应该尽可能不要插入标记。
相应的标记可以包含并隐藏在页面中,在需要的时候 JavaScript 可以直接用它来显示,而不需要动态生成。
另一个办法是通过 Ajax 请求获取要显示的 HTML,这样也可以保证同一个渲染层(PHP、JSP、Ruby 等)负责输出标记,而不是把标记嵌在 JavaScript 中。

解耦 CSS/JavaScript

Web应用程序的另一层是 CSS,主要负责页面显示。

JavaScript和CSS紧密相关,它们都建构在HTML之上,因此也经常一起使用。与 HTML 和 JavaScript 的情况类似,CSS 也可能与 JavaScript 产生紧密耦合。

// CSS 紧耦合到了 JavaScript 
element.style.color = "red"; 
element.style.backgroundColor = "blue";

因为 CSS 负责页面显示,所以任何样式的问题都应该通过 CSS 文件解决。

可是,如果 JavaScript直接修改个别样式(比如颜色),就会增加一个排错时要考虑甚至要修改的因素。

如果将来有一天要修改样式,那么 CSS 和JavaScript 可能都需要修改。这对负责维护的开发者来说是一个噩梦。

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

通过修改元素的 CSS 类名,可以把大部分样式限制在 CSS 文件里。

JavaScript 只负责修改应用样式的类名,而不直接影响元素的样式。只要应用的类名没错,那么显示的问题就只跟 CSS 有关,而跟JavaScript 无关。

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

每个 Web 应用程序中都会有大量事件处理程序在监听各种事件。

可是,其中很少能真正做到应用程序逻辑与事件处理程序分离。

function handleKeyPress(event) { // 事件处理程序
 if (event.keyCode == 13) { 
   let target = event.target; 

   // 应用程序逻辑
   let value = 5 * parseInt(target.value); 
   if (value > 10) { 
        document.getElementById("error-msg").style.display = "block"; 
   } 
   // 应用程序逻辑
 } 
}

这个事件处理程序除了处理事件,还包含了应用程序逻辑。这样做的问题是双重的。

首先,除了事件没有办法触发应用程序逻辑,结果造成调试困难。
如果没有产生预期的结果怎么办?
是因为没有调用事件处理程序,还是因为应用程序逻辑有错误?

其次,如果后续事件也会对应相同的应用程序逻辑,则会导致代码重复,或者把它提取到单独的函数中。
无论情况如何,都会导致原本不必要的多余工作。

更好的做法是将应用程序逻辑与事件处理程序分开,各自负责处理各自的事情。
事件处理程序应该专注于 event 对象的相关信息,然后把这些信息传给处理应用程序逻辑的某些方法。

// 应用程序逻辑,只负责怎么干(实现什么功能)
// 不包含任何依赖事件处理程序的代码。这个函数只负责接收一个值,并根据该值执行其他所有操作。
function validateValue(value) { 
 value = 5 * parseInt(value); 
 if (value > 10) { 
   document.getElementById("error-msg").style.display = "block"; 
 } 
} 

// 事件处理程序,只负责这个事件发生,要干什么
function handleKeyPress(event) { 
 if (event.keyCode == 13) { 
   let target = event.target; 
   validateValue(target.value); 
 } 
}

把应用程序逻辑从事件处理程序中分离出来有很多好处。

首先,这可以让我们以最少的工作量轻松地修改触发某些流程的事件。
如果原来是通过鼠标单击触发流程,而现在又想增加键盘操作来触发,那么修改起来也很简单。

其次,可以在不用添加事件的情况下测试代码,这样创建单元测试或自动化应用程序流都会更简单。

以下是在解耦应用程序逻辑和业务逻辑时应该注意的几点。
1、不要把 event 对象传给其他方法,而是只传递 event 对象中必要的数据。
2、应用程序中每个可能的操作都应该无须事件处理程序就可以执行。
3、事件处理程序应该处理事件,而把后续处理交给应用程序逻辑。

做到上述几点能够给任何代码的可维护性带来巨大的提升,同时也能为将来的测试和开发提供很多可能性。

5、编码惯例

编写可维护的 JavaScript 不仅仅涉及代码格式和规范,也涉及代码做什么。

企业开发 Web 应用程序通常需要很多人协同工作。

这时候就需要保证每个人的浏览器环境都有恒定不变的规则。

为此,开发者应该遵守某些编码惯例。

尊重对象所有权

JavaScript 的动态特性意味着几乎可以在任何时候修改任何东西。

在其他语言中,在没有源代码的情况下,对象和类不可修改。

JavaScript 则允许在任何时候修改任何对象,因此就可能导致意外地覆盖默认行为。

在企业开发中,非常重要的编码惯例就是尊重对象所有权,这意味着不要修改不属于你的对象。

简单来讲,如果你不负责创建和维护某个对象及其构造函数或方法,就不应该对其进行任何修改。

更具体一点说,就是如下惯例:
 不要给实例或原型添加属性。
 不要给实例或原型添加方法。
 不要重定义已有的方法。

修改了多个人使用的对象也就意味着会有错误发生。

假设有人希望某个函数叫作 stopEvent(),用于取消某个事件的默认行为。

然后,你把它给改了,除了取消事件的默认行为,又添加了其他事件处理程序。

可想而知,问题肯定会接踵而至。

别人还认为这个函数只做最开始的那点事,但由于对它后来添加的副作用并不知情,因此很可能就会用错或者造成损失。

以上规则不仅适用于自定义类型和对象,而且适用于原生类型和对象,比如 Object、String、document、window,等等。

考虑到浏览器厂商也有可能会在不公开的情况下以非预期方式修改这些对象,潜在的风险就更大了。

只有你自己创建的才是你的对象,包括自定义类型和对象字面量。

Array、document 等对象都不是你的,因为在你的代码执行之前它们已经存在了。

可以按如下这样为对象添加新功能。
 创建包含想要功能的新对象,通过它与别人的对象交互。
 创建新自定义类型继承本来想要修改的类型,可以给自定义类型添加新功能。

不声明全局变量

同样,这也关系到创建一致和可维护的脚本运行环境。

最多可以创建一个全局变量,作为其他对象和函数的命名空间。

// 两个全局变量:name 和 sayName()。不要!
var name = "Nicholas"; 
function sayName() { 
 console.log(name); 
}

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

首先,变量 name 会覆盖 window.name 属性,而这可能会影响其他功能。
其次,有助于分清功能都集中在哪里。调用 MyApplication.sayName()从逻辑上会暗示,出现任何问题都可以在 MyApplication 的代码中找原因。

这样一个全局对象可以扩展为命名空间的概念。命名空间涉及创建一个对象,然后通过这个对象来暴露能力。
比如,Google Closure 库就利用了这样的命名空间来组织其代码。
 goog.string:用于操作字符串的方法。
 goog.html.utils:与 HTML 相关的方法。
 goog.i18n:与国际化(i18n)相关的方法。

对象 goog 就相当于一个容器,其他对象包含在这里面。只要使用对象以这种方式来组织功能,就可以称该对象为命名空间。

关于命名空间,最重要的确定一个所有人都同意的全局对象名称。这个名称要足够独特,不可能与其他人的冲突。

不要比较 null

JavaScript 不会自动做任何类型检查,因此就需要开发者担起这个责任。

结果,很多 JavaScript 代码不会做类型检查。最常见的类型检查是看值是不是 null。

然而,与 null 进行比较的代码太多了,其中很多因为类型检查不够而频繁引发错误。

function sortArray(values) { 
  // 这个函数的目的是使用给定的比较函数对数组进行排序。
  // 为保证函数正常执行,values 参数必须是数组。
 if (values != null) { // 不要这样比较!
   values.sort(comparator); 
 } 
}

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

如果看到比较 null 的代码,可以使用下列某种技术替换它。
1、如果值应该是引用类型,则使用 instanceof 操作符检查其构造函数。
2、如果值应该是原始类型,则使用 typeof 检查其类型。
3、如果希望值是有特定方法名的对象,则使用 typeof 操作符确保对象上存在给定名字的方法。

代码中比较 null 的地方越少,就越容易明确类型检查的目的,从而消除不必要的错误。

使用常量

依赖常量的目标是从应用程序逻辑中分离数据,以便修改数据时不会引发错误。

显示在用户界面上的字符串就应该以这种方式提取出来,可以方便实现国际化。

URL 也应该这样提取出来,因为随着应用程序越来越复杂,URL 极有可能变化。

基本上,像这种地方将来因为某种原因而需要修改时,可能就要找到某个函数并修改其中的代码。

每次像这样修改应用程序逻辑,都可能引入新错误。

为此,可以把这些可能会修改的数据提取出来,放在单独定义的常量中,以实现数据与逻辑分离。

关键在于把数据从使用它们的逻辑中分离出来。
可以使用以下标准检查哪些数据需要提取。

1、重复出现的值:任何使用超过一次的值都应该提取到常量中,这样可以消除一个值改了而另一个值没改造成的错误。这里也包括 CSS 的类名。
2、用户界面字符串:任何会显示给用户的字符串都应该提取出来,以方便实现国际化。
3、URL:Web 应用程序中资源的地址经常会发生变化,因此建议把所有 URL 集中放在一个地方管理。
4、任何可能变化的值:任何时候,只要在代码中使用字面值,就问问自己这个值将来是否可能会变。

如果答案是“是”,那么就应该把它提取到常量中。

===================

性能

相比 JavaScript 刚问世时,目前每个网页中 JavaScript 代码的数量已有极大的增长。代码量的增长也带来了运行时执行 JavaScript 的性能问题。

JavaScript 一开始就是一门解释型语言,因此执行速度比编译型语言要慢一些。

Chrome 是第一个引入优化引擎将 JavaScript 编译为原生代码的浏览器。

即使到了编译 JavaScript 时代,仍可能写出运行慢的代码。

不过,如果遵循一些基本模式,就能保证写出执行速度很快的代码。

1、作用域意识

随着作用域链中作用域数量的增加,访问当前作用域外部变量所需的时间也会增加。

访问全局变量始终比访问局部变量慢,因为必须遍历作用域链。

任何可以缩短遍历作用域链时间的举措都能提升代码性能。

避免全局查找

改进代码性能非常重要的一件事,可能就是要提防全局查询。

全局变量和函数相比于局部值始终是最费时间的,因为需要经历作用域链查找。

function updateUI() { 
  // 引用了全局 document 对象
 let imgs = document.getElementsByTagName("img"); 

 for (let i = 0, len = imgs.length; i < len; i++) { 
   // 引用了全局 document 对象
   imgs[i].title = '${document.title} image ${i}'; 
 } 

  // 引用了全局 document 对象
 let msg = document.getElementById("msg"); 
 msg.innerHTML = "Update complete."; 
}

如果页面的图片非常多,那么 for 循环中就需要引用 document 几十甚至上百次,每次都要遍历一次作用域链。

通过在局部作用域中保存 document 对象的引用,能够明显提升这个函数的性能,因为只需要作用域链查找。

function updateUI() { 
 // 这里先把 document 对象保存在局部变量 doc 中。
 let doc = document; 

  // 然后用 doc 替代了代码中所有的 document。
  // 这样调用这个函数只会查找一次作用域链,相对上一个版本,肯定会快很多。
 let imgs = doc.getElementsByTagName("img"); 
 for (let i = 0, len = imgs.length; i < len; i++) { 
   imgs[i].title = '${doc.title} image ${i}'; 
 } 
 let msg = doc.getElementById("msg"); 
 msg.innerHTML = "Update complete."; 
}

因此,一个经验规则就是,只要函数中有引用超过两次的全局对象,就应该把这个对象保存为一个局部变量。

不使用 with 语句

在性能很重要的代码中,应避免使用 with 语句。

与函数类似,with 语句会创建自己的作用域,因此也会加长其中代码的作用域链。

在 with 语句中执行的代码一定比在它外部执行的代码慢,因为作用域链查找时多一步。

实际编码时很少有需要使用 with 语句的情况,因为它的主要用途是节省一点代码。

大多数情况下,使用局部变量可以实现同样的效果,无须增加新作用域。

// with 语句让使用 document.body 更简单了
function updateBody() { 
 with(document.body) { 
   console.log(tagName); 
   innerHTML = "Hello world!"; 
 } 
}

// 使用局部变量也可以实现同样的效果
function updateBody() { 
  // 还通过把 document.body 保存在局部变量中来省去全局查找。
 let body = document.body; 
   console.log(body.tagName); 
   body.innerHTML = "Hello world!"; 
}

2、选择正确的方法

与其他语言一样,影响性能的因素通常涉及算法或解决问题的方法。

经验丰富的开发者知道用什么方法性能更佳。

通常很多能在其他编程语言中提升性能的技术和方法同样也适用于 JavaScript。

算法

在计算机科学中,算法复杂度使用大 O 表示法来表示。

最简单同时也最快的算法可以表示为常量值或 O(1)。

然后,稍微复杂一些的算法同时执行时间也更长一些。
image.png

避免不必要的属性查找

常量
整体代码的复杂度可以认为是 O(1)。

// 查询了 4 次常量值:数值 5、变量 value、数值 10 和变量 sum。
let value = 5; 
let sum = 10 + value; 
console.log(sum);




// 使用变量和数组相比访问对象属性效率更高,访问对象属性的算法复杂度是 O(n)。
// 访问对象的每个属性都比访问变量或数组花费的时间长,因为查找属性名要搜索原型链。
// 简单来说,查找的属性越多,执行时间就越长。
let values = { first: 5, second: 10 }; 
let sum = values.first + values.second; 
console.log(sum);

数组
JavaScript 中访问数组元素也是 O(1)操作,与简单的变量查找一样。

//  
let values = [5, 10]; 
let sum = values[0] + values[1]; 
console.log(sum);

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

访问对象的每个属性都比访问变量或数组花费的时间长,因为查找属性名要搜索原型链。

简单来说,查找的属性越多,执行时间就越长。

let values = { first: 5, second: 10 }; 
let sum = values.first + values.second; 
console.log(sum);

这个例子使用两次属性查找来计算 sum 的值。一两次属性查找可能不会有明显的性能问题,但几百上千次则绝对会拖慢执行速度。

特别要注意避免通过多次查找获取一个值。

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

这里有 6 次属性查找:3 次是为查找 window.location.href.substring(),3 次是为查找window.location.href.indexOf()。

通过数代码中出现的点号数量,就可以知道有几次属性查找。以上代码效率特别低,这是因为使用了两次 window.location.href,即同样的查找执行了两遍。

只要使用某个 object 属性超过一次,就应该将其保存在局部变量中。第一次仍然要用 O(n)的复杂度去访问这个属性,但后续每次访问就都是 O(1),这样就是质的提升了。

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

这个版本的代码只有 4 次属性查找,比之前节省了约 33%。

在大型脚本中如果能这样优化,可能就会明显改进性能。

通常,只要能够降低算法复杂度,就应该尽量通过在局部变量中保存值来替代属性查找。

另外,如果实现某个需求既可以使用数组的数值索引,又可以使用命名属性(比如 NodeList 对象),那就都应该使用数值索引。

优化循环

循环是编程中常用的语法构造,因此在 JavaScript 中也十分常见。

优化这些循环是性能优化的重要内容,因为循环会重复多次运行相同的代码,所以运行时间会自动增加。

(1) 简化终止条件。
因为每次循环都会计算终止条件,所以它应该尽可能地快。这意味着要避免属性查找或其他 O(n)操作。

(2) 简化循环体。
循环体是最花时间的部分,因此要尽可能优化。要确保其中不包含可以轻松转移到循环外部的密集计算。

(3) 使用后测试循环。
最常见的循环就是 for 和 while 循环,这两种循环都属于先测试循环。
do-while就是后测试循环,避免了对终止条件初始评估 ,因此应该会更快。

// 这个循环会将变量 i 从 0 递增至数组 values 的长度。
// 每次循环,都要读取values.length的值进行判断
for (let i = 0; i < values.length; i++) { 
 process(values[i]); 
}

// 假设处理这些值的顺序不重要,那么可以将循环变量改为递减的形式
// 每次循环,不需要每次都读values.length,终止条件的计算复杂度也从查找 values.length的 O(n)变成了访问 0 的 O(1)。
for (let i = values.length - 1; i >= 0; i--) { 
 process(values[i]); 
}

// 后测试循环,这里主要的优化是将终止条件和递减操作符合并成了一条语句。
let i = values.length-1; 
if (i > -1) { 
 do { 
   process(values[i]); 
 }while(--i >= 0); 
}

使用后测试循环时要注意,一定是至少有一个值需要处理一次。如果这里的数组是空的,那么会浪费一次循环,而先测试循环就可以避免这种情况。

展开循环

如果循环的次数是有限的,那么通常抛弃循环而直接多次调用函数会更快。

// 抛弃循环
process(values[0]); 
process(values[1]); 
process(values[2]);

这个例子假设 values 数组始终只有 3 个值,然后分别针对每个元素调用一次 process()。

像这样展开循环可以节省创建循环、计算终止条件的消耗,从而让代码运行更快。

如果不能提前预知循环的次数,那么或许可以使用一种叫作达夫设备(Duff’s Device)的技术。

// 来源:Speed Up Your Site(New Riders,2003)
let iterations = Math.floor(values.length / 8); 
let leftover = values.length % 8; 
let 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)

展开循环对于大型数据集可以节省很多时间,但对于小型数据集来说,则可能不值得。

因为实现同样的任务需要多写很多代码,所以如果处理的数据量不大,那么显然没有必要。

避免重复解释

重复解释的问题存在于 JavaScript 代码尝试解释 JavaScript 代码的情形。

在使用 eval()函数或Function 构造函数,或者给 setTimeout()传入字符串参数时会出现这种情况。

// 对代码求值:不要
eval("console.log('Hello world!')"); 

// 创建新函数:不要
let sayHi = new Function("console.log('Hello world!')"); 

// 设置超时函数:不要
setTimeout("console.log('Hello world!')", 500);

在上面所列的每种情况下,都需要重复解释包含 JavaScript 代码的字符串。

这些字符串在初始解析阶段不会被解释,因为代码包含在字符串里。

这意味着在 JavaScript 运行时,必须启动新解析器实例来解析这些字符串中的代码。

实例化新解析器比较费时间,因此这样会比直接包含原生代码慢。

// 直接写出来
console.log('Hello world!'); 

// 创建新函数:直接写出来
let sayHi = function() { 
 console.log('Hello world!'); 
}; 

// 设置超时函数:直接写出来
setTimeout(function() { 
 console.log('Hello world!'); 
}, 500);

为了提升代码性能,应该尽量避免使用要当作 JavaScript 代码解释的字符串。

原生方法很快

应该尽可能使用原生方法,而不是使用 JavaScript 写的方法。

原生方法是使用 C 或 C++等编译型语言写的,因此比 JavaScript 写的方法要快得多。

JavaScript 中经常被忽视的是Math 对象上那些执行复杂数学运算的方法。

这些方法总是比执行相同任务的 JavaScript 函数快得多,比如求正弦、余弦等。

switch 语句很快

如果代码中有复杂的 if-else 语句,将其转换成 switch 语句可以变得更快。

然后,通过重新组织分支,把最可能的放前面,不太可能的放后面,可以进一步提升性能。

switch(true){
    case A === null:
        console.log("A为null");
        break;
    case typeof(A) === "number":
        console.log("A不为null,且A为数值");
        break;
    case typeof(A) === "string":
        console.log("A不为null,且A为文本");
        break;
   default:
       console.log("A不为null,不是数值,也不是文本");
}

位操作很快

在执行数学运算操作时,位操作一定比任何布尔值或数值计算更快。

选择性地将某些数学操作替换成位操作,可以极大提升复杂计算的效率。

像求模、逻辑 AND 与和逻辑 OR或都很适合替代成位操作。

3、语句最少化

多个变量声明

这种优化很容易做到,且比使用多条语句执行速度更快。

// 有四条语句:浪费
let count = 5; 
let color = "blue"; 
let values = [1,2,3]; 
let now = new Date();

// 一条语句更好
let count = 5, 
  color = "blue", 
  values = [1,2,3], 
  now = new Date();

插入迭代性值

任何时候只要使用迭代性值(即会递增或递减的值),都要尽可能使用组合语句。

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

// 优化
let name = values[i++];

使用数组和对象字面量

// 创建和初始化数组用了四条语句:浪费
let values = new Array(); 
values[0] = 123; 
values[1] = 456; 
values[2] = 789; 

// 创建和初始化对象用了四条语句:浪费
let person = new Object(); 
person.name = "Nicholas"; 
person.age = 29; 
person.sayName = function() { 
 console.log(this.name); 
};

// 优化

// 一条语句创建并初始化数组
let values = [123, 456, 789]; 

// 一条语句创建并初始化对象
let person = { 
 name: "Nicholas", 
 age: 29, 
 sayName() { 
   console.log(this.name); 
 } 
};

相对于前面使用了 8 条语句,这里使用两条语句,减少了 75%的语句量。

对于数千行的 JavaScript 代码,这样的优化效果可能更明显。

减少代码中的语句量是很不错的目标,但不是绝对的法则。一味追求语句最少化,可能导致一条语句容纳过多逻辑,最终难以理解。

4、优化 DOM 交互

在所有 JavaScript 代码中,涉及 DOM 的部分无疑是非常慢的。

DOM 操作和交互需要占用大量时间,因为经常需要重新渲染整个或部分页面。

此外,看起来简单的操作也可能花费很长时间,因为 DOM 中携带着大量信息。

理解如何优化 DOM 交互可以极大地提升脚本的执行速度。

实时更新最小化

访问 DOM 时,只要访问的部分是显示页面的一部分,就是在执行实时更新操作。

之所以称其为实时更新,是因为涉及立即(实时)更新页面的显示,让用户看到。

每次这样的更新,无论是插入一个字符还是删除页面上的一节内容,都会导致性能损失。

这是因为浏览器需要为此重新计算数千项指标,之后才能执行更新。

实时更新的次数越多,执行代码所需的时间也越长。
反之,实时更新的次数越少,代码执行就越快。

let list = document.getElementById("myList"), item; 
// 码向列表中添加了 10 项
for (let i = 0; i < 10; i++) { 
 item = document.createElement("li"); 
  // 每添加 1 项,就会有两次实时更新:一次添加<li>元素,一次为它添加文本节点。
 list.appendChild(item); 
 item.appendChild(document.createTextNode('Item ${i}'); 
  // 所以整个操作总共要执行 20 次实时更新。
}

// 优化:减少更新次数

let list = document.getElementById("myList"), 
 fragment = document.createDocumentFragment(), 
 item; 

for (let i = 0; i < 10; i++) { 
 item = document.createElement("li"); 
  // 1、使用文档片段构建 DOM 结构
 fragment.appendChild(item); 
 item.appendChild(document.createTextNode("Item " + i)); 
} 
// 2、然后一次性将它添加到 list 元素
list.appendChild(fragment);

完成同样的操作只会触发一次实时更新。

只要是必须更新 DOM,就尽量考虑使用文档片段来预先构建 DOM 结构,然后再把构建好的 DOM结构实时更新到文档中。

使用 innerHTML

在页面中创建新 DOM节点的方式有两种:
1、使用 DOM方法如 createElement()和 appendChild(),
2、使用 innerHTML。

对于少量 DOM 更新,这两种技术区别不大,
但对于大量 DOM 更新,使用innerHTML 要比使用标准 DOM 方法创建同样的结构快很多。

在给 innerHTML 赋值时,后台会创建 HTML 解析器,然后会使用原生 DOM 调用而不是 JavaScript的 DOM 方法来创建 DOM 结构。

原生 DOM 方法速度更快,因为该方法是执行编译代码而非解释代码。

// 上面 实时更新最小化 的例子,还可以优化:
let list = document.getElementById("myList"), 
 html = ""; 


for (let i = 0; i < 10; i++) { 
  // 1、构造了一个HTML字符串
  // 虽然拼接字符串也会有一些性能损耗,但这个技术仍然比执行多次 DOM 操作速度更快
 html += '<li>Item ${i}</li>'; 

 // list.innerHTML += '<li>Item ${i}</li>'; // 不要,效率极低
} 


// 2、然后将它赋值给list.innerHTML,显示DOM,最小化调用次数,同样是一次性添加
list.innerHTML = html;

使用 innerHTML 可以提升性能,但也会暴露巨大的 XSS 攻击隐患

比如 += 后面的是用户输入的内容,用户就可以输入