最佳实践
可维护性
协作开发中的代码量很惊人,大多数开发者会花大量时间去维护别人写的代码;让自己的代码更容易维护,可以保证其它开发者更好地完成自己的工作
什么是可维护代码
容易理解:无需求助原始开发者,任何人一看代码就知道干什么,以及怎么实现
符合常识:代码中一切都显得顺理成章
容易适配:即使数据发生变化也不用完全重写
容易扩展:代码架构经过认真设计,支持未来的功能
容易调试:出问题时代码可以给出明确信息
编码规范
编码规范对 JavaScript 而言非常重要,因为这门语言实在太灵活了
1、可读性
想要代码容易维护,首先就是必须使其可读;代码缩进是保证可读性的重要基础,缩进通常使用空格数而不是 Tab(因为 Tab 在不同编辑器中可能不一样);另一方面就是函数注释,一般在以下地方应该写注释:函数和方法、大型代码块、复杂的算法、使用黑科技
2、变量和函数命名
有一些规范也是我们最好需要遵守的:
变量应该是名词
函数名应该以动词开始;返回布尔值函数通常 is 开头
对变量和函数都使用符合逻辑的名称,不用担心长度
变量、函数、方法应该以小写字母开头,使用驼峰大小写形式;类名字母应该大写;常量值应该全部大写并以下划线拼接
名称要用描述性和直观的词汇,但不要过于冗长
3、变量类型透明化
因为 js 是松散类型的语言,所以很容易忘记变量包含的数据类型;有三种方式可以标明变量的数据类型:
第一种是通过初始化,在变量定义之初就将其初始化为一个将来要用的类型值
第二是使用匈牙利表示法,在变量名前加一个前缀:o 表示对象、s 表示字符串、i 表示整数、f 表示浮点数、b 表示布尔值
第三种是使用类型注释,如let found /*:Boolean*/ = false;
以上三种方法有优点也有缺点
松散耦合
只要应用程序某个部分对另一部分依赖得过于紧密,代码就会紧密耦合,而难以维护
在开发中应该注意养成意识,不要让代码紧密耦合
1、解耦 HTML/JavaScript
开发中最常见的耦合是 HTML/JavaScript 耦合
使用嵌入代码的 script 元素和使用 HTML 属性添加事件处理程序都会造成紧密耦合
理想情况下,HTML 和 JavaScript 应该完全分开,外部文件导入 js,使用 DOM 添加行为
通过 innerHTML 将 HTML 插入到页面中也会造成紧密耦合
解耦 HTML 和 JavaScript 可以节省排错时间、更容易定位错误来源;也有助于保证可维护性
2、解耦 CSS/JavaScript
CSS 和 JavaScript 也可能产生紧密耦合,通常在使用 JavaScript 修改样式的时候
但是我们不可能完全解耦 CSS 和 JavaScript,但是可以让这种耦合边得松散:可以通过改变类名的方式而不是改变样式来实现
3、解耦应用程序逻辑/事件处理程序
尽量让应用程序逻辑和事件处理程序分离;事件处理程序处理点击事件,再调用应用程序逻辑函数处理逻辑
这样有两个好处:用最少的工作量修改某些触发事件;可以在不用添加事件的情况下测试代码
在解耦这两者时应该注意几点:
不要把 event 对象传给其他方法,只是传递必要数据
应用程序中每个可能的操作都应该无需事件处理程序就能执行
事件处理程序应该处理事件,而把后续处理交给应用程序逻辑
编码惯例
1、尊重对象所有权
不要给实例或原型添加属性
不要给实例或原型添加方法
不要重定义已有方法
不要修改不属于你的对象,如果你想要为对象添加新功能:创建包含想要功能的对象;创建自定义类型继承本来想要修改的类型
2、不声明全局变量
尊重对象所有权密切相关的是尽可能不声明全局变量和函数;最多创建一个全局变量,作为其它对象对象和函数的命名空间
3、不要比较 null
JavaScript 不会做任何类型检查,因此开发者需要担当这个重任
要注意一点是不要单纯比较 null,而是真正的检查类型,并且还能略过无效值
可以使用以下几种方法:
如果值是引用类型,则使用 instanceof 操作符检查其构造函数
如果值是原始类型,则用 typeof 检查其类型
如果希望值是特定方法名方法的对象,则使用 typeof 操作符确保对象上存在给定名字的方法
4、使用常量
依赖常量的目标是从应用程序逻辑中分离数据,以便修改数据时不会引发错误
满足下列标准的值应该提取出来:
重复出现的值
用户界面的字符串
URL
任何可能变化的值
性能
JavaScript 一开始就是一门解释型语言,因此执行速度比编译型语言要慢一些;Chrome 是第一个引入优化引擎将 JavaScript 编译为原生代码的浏览器
即使到了编译 JavaScript 时代,仍然能写出运行慢的代码;不过遵循一些基本模式,就能保证写出执行速度很快的代码
作用域意识
1、避免全局查找
要提防全局查询;局部函数总是比全局函数更快
可以通过在局部引用 document 来提升某个函数中访问 document 代码的速度
一个经验规则是:只要函数中有引用超过两次全局对象,就因该保存它为一个局部变量
2、不使用 with 语句
在性能很重要的代码中,应避免使用 with 语句
选择正确的方法
通常很多能在其他编程语言中提升性能的技术和方法同样也适用于 JavaScript
1、避免不必要的属性查找
注意算法的事件复杂度
重复查找的对象应该单独提取出来创建一个新对象,然后使用这个新对象
2、优化循环
循环在 JavaScript 中很常用,所以有必要优化循环,一般步骤如下:
简化终止条件
简化循环体
使用后测试循环(do-while)
3、展开循环
如果循环的次数是有限的,通常抛弃循环直接多次调用会更快
如果不能提前预知次数,可以使用达夫设备的技术
相关代码查看红宝书 p855
4、避免重复解释
该问题是存在于 JavaScript 代码尝试解释 JavaScript 代码的情形;在使用 eval 函数、Function 构造函数、给 setTimeout 传入字符串参数时会出现
eval("console.log('hello world!')");
let test = new Function("console.log('hello world!')");
setTimeout("cosnole.log('hello world')", 500);
这意味着 JavaScript 在运行时,必须启动新解析器实例来解析这些字符串中的代码;实例化新解析器比较费时间,因此直接包含原生代码更快
三种方法都可以直接将函数提出来,eval 尽量不使用、Function 构造函数重写为常规函数、setTimeout 直接将函数提出来
5、其它性能优化注意事项
有些地方需要注意,尽管不是主要问题,但是页要注意以下几点:
原生方法很快
switch 语句很快
位操作很快
语句最少化
JavaScript 代码中语句的数量影响操作执行的速度;优化的目标是寻找可以合并的语句,减少整个脚本的执行时间
1、多个变量声明
尽量用一个语句声明多个变量,这会比多条语句执行更快
2、插入迭代性值
任何时候只要使用迭代值,都最好使用组合型语句,例如:
let name = val[i];
++i;
let name = val[i++];
3、使用数组和对象字面量
使用字面量后语句明显变少
但是一味的追求语句最少化,可能导致一条与举报逻辑过多最终难以理解
优化 DOM 交互
在所有 JavaScript 代码中,涉及 DOM 的部分无疑是非常慢的
1、实时更新最小化
访问 DOM 时,之要访问的部分是显示页面的一部分,就是在执行实时更新操作;因为修改这种 DOM,页面将会立即更新;而每次这样的更新都会损耗浏览器性能,因为浏览器要计算数千项指标之后才能执行更新
所以尽量减少实时更新的次数
可以使用 fragment 等方法
2、使用 innerHTML
对于大量 DOM 更新,使用 innerHTML 要比使用标准 DOM 方法创建同样的结构快得多
在给 innerHTML 赋值时,后台会创建 HTML 解析器,然后会使用原生 DOM 调用而不是 JavaScript 的 DOM 方法来创建 DOM 结构;原生 DOM 方法要快得多,因为它是执行编译代码而不是解释代码
但是尽量也一次性将 DOM 填充到 innerHTML 中,多次调用同样会损耗性能
使用 innerHTML 可以提升性能,但是会暴露巨大的 XSS 攻击面;无论何时使用它都需要注意
3、使用事件委托
为了减少对页面的影响,应该尽可能使用事件委托
4、注意 HTMLCollection
HTMLCollection 的缺点:只要访问它,无论是访问其属性还是方法,都会触发查询文档,这个查询相当耗时,减少 HTMLCollection 的访问可以极大的提升脚本性能
例如访问 HTMLCollection 的 length 时,可以先将其值保存在一个变量中,然后再使用那个变量
以下情形会返回 HTMLCollection:
调用 getElementsByTagName()
读取元素的 childNodes 属性
读取元素的 attribute 属性
访问特殊集合,如 document.from、document.image 等
部署
在代码发布之前我们需要解决一些问题
构建流程
准备发布代码时最重要的一环是准备构建流程
你写的代码不应该不做任何处理就直接交给浏览器,因为:知识产权问题、文件大小、代码组织
为此需要为 JavaScript 文件建立构建流程
1、文件结构
源代码控制时应该不要把所有 JavaScript 代码放在一个文件中;相反要遵循面向对象编程语言的典型模式,把对象和自定义类型保存到自己独立的文件中
把代码分散到各个文件是从可维护性出发的而不是从部署角度出发;对于部署,应该把所有源文件合并为一个或多个汇总文件;Web 应用程序使用的 JavaScript 文件越少越好,因为 HTTP 请求对于某些 Web 应用程序来说是主要性能瓶颈
要以符合逻辑的方式把 JavaScript 代码组织到部署文件中
2、任务运行器
要把大量文件组合成一个应用程序,很可能需要任务运行器自动完成一些任务;任务运行器可以完成代码检查、打包、转义、启动本地服务器、部署,以及其它可以脚本化的任务
可以参考一下 Grunt 和 Gulp 这两个主流的任务运行器
3、摇树优化
摇树优化是非常常见且极为有效的减少冗余代码的策略
实现摇树优化策略的构建工具能够分析出选择性导入的代码,其余模块文件中的代码可以在最终打包得到的文件中完全省略
4、模块打包器
以模块形式编写的代码,并不意味着必须以模块形式交付代码
模块打包器的工作是识别应用程序中涉及的 JavaScript 依赖关系,将他们组合成一个大文件,完成对模块的组织和拼接,然后最终生成提供给浏览器的输出文件
常用的工具有很多:Webpack、Rollupt、Browserify 等;可以将模块代码转换为普遍兼容的网页脚本
验证
有一些工具可以帮我们发现 JavaScript 代码中潜在的问题,最流行的是 JSLint 和 ESLint;他们可以发现代码中的语法错误和常见的编码错误,下面是它们会报告的一些问题:使用 eval、使用未声明的变量、遗漏了分号、不适当的换行、不正确的使用逗号、遗漏了包含语句的括号、遗漏了 switch 分支中的 break、重复声明变量、使用了 with、错误的使用等号、执行不到的代码
压缩
JavaScript 文件压缩实际上是两件事:代码大小、传输负载
代码大小是指浏览器需要解析的字节数、传输负载是服务器实际上给浏览器发送的字节数
1、代码压缩
JavaScript 不是编译成字节码,而是作为源代码传输的,所以源代码文件通常包含对浏览器的 JavaScript 解释器没有用的额外信息和格式;JavaScript 压缩工具可以把这些信息删除,并保证在逻辑不变的情况下缩小文件大小
压缩工具能够执行以下操作:删除空格(包括换行);删除注释;缩短变量名、函数名和其他标识符
所有 JavaScript 文件都应该在部署到线上环境前进行压缩
压缩与最小化的区别在于前者得到的文件不再包含语法正确的代码;压缩以后的文件必须通过解压缩才能恢复为可读代码;压缩通常能得到比最小化更小的文件,压缩算法不用考虑保留语法结构,因此自由度更高;最小化是将文件大小减少到比原始大小还要小,但结果文件仍然是语法正确的代码
2、JavaScript 编译
类似于最小化,JavaScript 代码编译通常指的是把源代码转换为一种逻辑相同但字节更少的形式
编译可能会执行如下操作:删除未使用的代码、将某些代码转换为更简洁的语法、全局函数调用以及常量和变量行内化
3、JavaScript 转译
JavaScript 转译可以在开发时使用最新的语法特性而不用担心浏览器的兼容性有问题,转译还可以将现代的代码转换成更早的 es 版本,具体取决于你的需求;这样可以确保代码可以跨浏览器兼容
4、HTTP 压缩
当前主流浏览器都支持客户端解压缩收到的资源;服务器可以根据浏览器通过请求头部(Accept-Encoding)标明自己支持的格式,选择用一种来压缩 JavaScript 文件
传输压缩后的文件时,服务器响应头部会有字段(Content-Encoding)标明使用了哪种压缩格式;浏览器看到这个头部字段后,就会根据这个压缩格式进行解压缩