代码规范

《JavaScript高级程序设计(第3版) 24章》

可维护性

特征

可理解性、直观性、可适应性、可扩展性、可调试性。

可读性

缩进 + 注释

  • 函数和方法——每个函数或方法都应该包含一个注释,描述其目的和用于完成任务所可能使用的算法。
  • 大段代码——用于完成单个任务的多行代码应该在前面放一个描述任务的注释。
  • 复杂的算法——如果使用了一种独特的方式解决某个问题,则要在注释中解释你是如何做的。
  • Hack——因为存在浏览器差异,JavaScript代码一般会包含一些hack。

注释

  • 没有注释的代码不是最优的。
  • 过多注释(比如每行一个)可能是拙劣代码的征兆。
  • 代码应该解释为什么,而非是什么。如果编写的代码特别容易令人迷惑的话,那么注释也可以解释一下实现原理。
变量和函数命名

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

命名规则:

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

命名参考链接:

https://unbug.github.io/codelf/

https://github.com/kettanaito/naming-cheatsheet

变量类型透明

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

// 通过初始化指定变量类型
var found = false; // 布尔型
var count = 1; // 数字
var name = ""; // 字符串
var person = null;
var obj = {}; // 对象
松散耦合
解耦HTML/JavaScript

HTMLJavaScript应该完全分离,并通过外部文件和使用DOM附加行为来包含JavaScript。一般来说,应该避免在JavaScript中创建大量HTMLHTML呈现应该尽可能与JavaScript保持分离。当JavaScript用于插入数据时,尽量不要直接插入标记。一般可以在页面中直接包含并隐藏标记,然后等到整个页面渲染好之后,就可以用JavaScript显示该标记,而非生成它。

解耦CSS/JavaScript

通过只修改某个元素的CSS类,就可以让大部分样式信息严格保留在CSS中。JavaScript可以更改样式类,但并不会直接影响到元素的样式。

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

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

应用与业务逻辑之间松散耦合的原则

  • 勿将event对象传给其他方法;只传来自event对象中所需的数据;
  • 任何可以在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行;
  • 任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑。
编程实践
尊重对象所有权
  • 不要为实例或原型添加属性;
  • 不要为实例或原型添加方法;
  • 不要重定义已存在的方法。
  • 创建包含所需功能的新对象,并用它与相关对象进行交互;
  • 创建自定义类型,继承需要进行修改的类型。然后可以为自定义类型添加额外功能。
避免全局变量

最多创建一个全局变量,让其他对象和函数存在其中。用这种方式将功能组合在一起的对象,叫做命名空间。命名空间很重要的一部分是确定每个人都同意使用的全局对象的名字,并且尽可能唯一,让其他人不太可能也使用这个名字。

命名空间有助于确保代码可以在同一个页面上与其他代码以无害的方式一起工作。

避免与null进行比较

现实中,与null比较很少适合情况而被使用。必须按照所期望的对值进行检查,而非按照不被期望的那些。

替换与null进行比较

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

可以通过将数据抽取出来变成单独定义的常量的方式,将应用逻辑与数据修改隔离开来。

关键在于将数据和使用它的逻辑进行分离。注意事项:

  • 重复值——任何在多处用到的值都应抽取为一个常量。
  • 用户界面字符串——任何用于显示给用户的字符串,都应被抽取出来以方便国际化。
  • **URLs**——在Web应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL。
  • 任意可能会更改的值——每当你在用到字面量值的时候,你都要问一下自己这个值在未来是不是会变化。

性能

注意作用域

访问全局变量总是要比访问局部变量慢,因为需要遍历作用域链。只要能减少花费在作用域链上的时间,就能增加脚本的整体性能。

避免全局查找

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

将在一个函数中会用到多次的全局对象存储为局部变量使用

避免with语句

和函数类似,with语句会创建自己的作用域,因此会增加其中执行的代码的作用域链的长度。由于额外的作用域链查找,在with语句中执行的代码肯定会比外面执行的代码要慢。

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

选择正确方法
避免不必要的属性查找

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

进行一两次属性查找并不会导致显著的性能问题,但是进行成百上千次则肯定会减慢执行速度。

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

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

优化循环

循环的基本优化步骤:

  1. 简化终止条件——由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。也就是说避免属性查找或其他O(n)的操作。
  2. 简化循环体——循环体是执行最多的,所以要确保其被最大限度地优化。确保没有某些可以被很容易移出循环的密集计算。
  3. 使用后测试循环——最常用for循环和while循环都是前测试循环。而如do-while这种后测试循环,可以避免最初终止条件的计算,因此运行更快。如将终止条件和自减操作符组合成了单个语句

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

展开循环

Duff装置

针对大数据集使用展开循环可以节省很多时间,但对于小数据集,额外的开销则可能得不偿失。它是要花更多的代码来完成同样的任务,如果处理的不是大数据集,一般来说并不值得。

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

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

多个变量声明

变量声明只用了一个var语句,之间由逗号隔开。在大多数情况下这种优化都非常容易做,并且要比单个变量分别声明快很多。

插入迭代值

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

使用数组和对象字面量

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

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

优化DOM交互
最小化现场更新

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

使用文档片段(document.createDocumentFragment())来构建DOM结构,接着将其添加到待插入DOM元素中。

一旦需要更新DOM,请考虑使用文档片段来构建DOM结构,然后再将其添加到现存的文档中。

使用innerHTML

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

当把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM调用来创建DOM结构,而非基于JavaScript的DOM调用。由于内部方法是编译好的而非解释执行的,所以执行快得多。

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

使用事件委托

当把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM调用来创建DOM结构,而非基于JavaScript的DOM调用。由于内部方法是编译好的而非解释执行的,所以执行快得多。

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

注意HTMLCollection

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

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

  • 进行了对getElementsByTagName() 的调用;
  • 获取了元素的childNodes属性;
  • 获取了元素的attributes属性;
  • 访问了特殊的集合,如document.formsdocument.images等。
尾递归

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。

如果不是尾调用,就需要保存每一步函数调用的内部数据,但是如果是尾调用就只保存最后一步的调用数据。

尾调用优化Tail call optimization):即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。

尾递归:如果尾调用自身,就称为尾递归。

优质代码写法

风格指南

https://github.com/zh-google-styleguide/zh-google-styleguide

参考链接

垃圾代码和优质代码的区别?