最佳实践
前言
《Javascript高级程序设计》最佳实践
1 可维护性
- 可理解性
- 直观性
- 可适应性
- 可扩展性
- 可调试性
总结:
要做到可维护性、可读性强的代码,至少完成以下几点
-
函数注释(功能、参数、返回值)
/** * 功能描述 * * @param {参数类型}参数名 参数说明 * @return {返回值类型} 返回值说明 */ function fn(p1, p2) { // 参数类型:fn、Number、String、Boolean、Array、Object }
-
完成独立功能的大段代码,写功能注释
-
有意义的变量名、函数名,驼峰命名
2 降低耦合
耦合:两个模块之间的输入与输出的联系紧密,相互影响过多
代码耦合过紧会导致模块难以维护,修改一处的同时另一处也需要修改,降低了我们的开发效率。
2.1 将css从js中抽离
// bad
element.style.color = '#FFF';
element.style.width = '30px';
element.sytle.display = 'block';
// good
.show { // css
color: #FFF;
width: 30px;
display: block;
}
element.className += 'show';
2.2 模板文本写注释
需要JS动态生成的HTML内容,在父元素下写上模板的注释,易于后期维护
<ul id="mylist">
<!-- <li><a href="1">First item</a></li> -->
</ul>
2.3 应用逻辑 / 事件处理程序分离
2.3.1 概念
什么是事件处理程序:
事件就是用户或浏览器自身执行的某种动作。比如说 click,mouseover,都是事件的名字。而相应某个事件的函数就叫事件处理程序
什么是应用逻辑:
被事件处理函数触发的一些动作,比如对元素的颜色、宽度等属性进行操作
为什么要分离:
- 方便更改触发事件的方式,比如从鼠标点击事件切换成按键事件
- 可以直接测试应用逻辑代码,无需通过特定事件
2.3.2 Demo
按下Enter键,div移动
document.onkeydown = function handle1(event) {
if (event.keyCode === 13) {
div.style.left = 100 + 'px';
div.style.top = 100 + 'px';
}
}
一般这样写并没有什么问题
加功能:点击document对象,div移动到鼠标位置
document.onclick = function handle2(event) {
div.style.left = event.clientX + 'px';
div.style.top = event.clientY + 'px';
}
这样里面的代码就重复了
分析一下代码
// handle1是事件处理函数,判断是否是Enter键按下
document.onkeydown = function handle1(event) {
if (event.keyCode === 13) {
// 下面是应用逻辑的代码,控制div移动
div.style.left = 100 + 'px';
div.style.top = 100 + 'px';
}
}
把应用逻辑的代码单独封装成一个函数
// 两个事件处理函数
document.onkeydown = function(event) {
if (event.keyCode === 13) {
moveFn(100, 100);
}
};
document.onclick = function(event) {
moveFn(event.clientX, event.clientY);
}
// 应用逻辑
function moveFn(x, y) {
div.style.left = x + 'px';
div.style.top = y + 'px';
}
注意:事件处理函数在传值给应用函数时,应该event中所需要的数据,不要传整个event对象。这样做的好处是表明了应用逻辑所需要的具体参数,也便于测试人员理解函数的功能。
2.4 松散耦合原则
- 函数自身的event对象,不能传递给其他函数
- 应用逻辑中的动作,应该可以在不执行任何事件处理的情况下进行
- 任何事件处理程序都应该处理事件,任何将处理交给应用逻辑
3 编程实践
3.1 不轻易修改对象
除了你自己创建 / 维护的对象,原生对象、别人创建的对象一律不能修改
- 不能修改是指:
- 不为实例 / 原型添加属性
- 不为实例 / 原型添加方法
- 不重复定义已存在的方法
- 为什么不能修改别人的对象:
- 例如对原生的Array对象添加一个aaa方法,当将来某一天Array原生支持了aaa方法,这样你以前测试完善的代码就会出错
- 如果需要修改别人的对象:
- 继承该对象,对继承了的对象进行修改
- 重新创建一个
3.2 避免全局量
let name = "yh";
function sayName() {
console.log(name);
}
两个全局量,name & sayName()方法,name还覆盖了window.name的原生属性
let mySpace = {
name: 'yh',
sayname: function() {
console.log(this.name);
}
}
一个全局量,而且属性和方法有单独命名空间,不用担心和别人冲突
3.3 避免与null比较
如果看到与null比较的代码,用以下方法替换:
- 如果值为引用类型,instanceof检查其构造函数
- 如果值为基本类型,typeof检查类型
- 如果想确定对象是否有某个方法,typeof 对象.方法名
3.4 使用常量
4 性能
4.1 注意作用域
- 避免全局查找
function updateUI() {
let imgs = document.getElementsByTagName('img');
for (let i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = document.title + 'image' + i;
}
let msg = document.getElementById('msg');
msg.innerHTML = 'Update complete.';
}
这段代码里至少有三个document的查询,用一个变量将document对象存起来,就可以减少全局查找的次数,提高性能
function updateUI() {
let 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语句
4.2 避免不必要的属性查找
算法复杂度
标记 | 名称 | 操作 |
---|---|---|
O(1) | 常数 | 访问变量、访问数组的元素 |
O(log n) | 对数 | 二分查找 |
O(n) | 线性 | 遍历数组所有元素、访问对象上的属性 |
O(n2) | 平方 |
对象的多重属性查找
let total = ele.style.width + ele.style.height;
查找了 2 + 2 次,效率低
let eleStyle = ele.style;
let total = eleStyle.width + eleStyle.height;
查找了 1 + 1 + 1 次,节省25%
在大的程序中进行这种改进,优化比较明显
数字化的数组位置 vs 命名属性(NodeList对象等),优先选择数组位置
4.3 优化循环
1. 减值迭代
大多数循环从0开始,增加到特定值结束。如果从特定值开始,减小到0结束,效率会更高
2. 简化终止条件
每次循环都会计算终止条件,终止条件越简单越好
// 增值迭代
for (let i = 0; i < arr.length; i++) {
process(arr[i]);
}
// 减值迭代
for (let i = arr.length - 1; i >= 0; i--) {
process(arr[i]);
}
// 每次循环时,少进行了一次 arr.length 的查找,提高了性能
3. 简化循环体
循环体执行次数是最多的,确保性能最优
4. 展开循环
Duff装置
5. 避免双重解释
eval()、function构造函数、setTimeout的第一个参数是字符串时会发生双重解释
eval("alert('Hello world!')");
let sayHi = new Function("alert(''Hello world!')");
setTimeout("alert('Hello world!')", 500);
一般都不会这么写
4.4 最小化语句数
1. 多个变量同时声明
// 不好
let num = 0;
let arr = [1, 2, 3];
let str = 'haha';
let obj = new Date();
// 好
let num = 0,
arr = [1, 2, 3],
str = 'haha',
obj = new Date();
2. 插入迭代值
例如:从arr中获取第i个值以后,让 i+1
// 不好
let name = arr[i];
i++;
// 好
let name = arr[i++];
3. 使用数组和对象字面量
// 不好
let values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
let person = new Object();
person.name = 'yh';
person.age = 18;
person.sayName = function() {
console.log(this.name);
};
// 好
let values = [123, 456, 789];
let person = {
name: 'yh',
age: 18,
sayName: function() {
console.log(this.name);
}
}
4.5 优化DOM交互
1. 最小化DOM更新(createDocumentFragment 或 innerHTML)
let list = document.getElementById('myList'),
item,
i;
for (i = 9; i >= 0; i--) {
item = document.createElement('li');
list.appendChild(item);
item.appendChild(document.createTextNode('Item' + i));
}
上面这段代码添加了十个 li ,每次添加都有两个DOM更新(appendChild, createTextNode)
一共触发了20次DOM更新,非常消耗性能
// 优化
let list = document.getElementById('myList'),
fragment = document.createDocumentFragment(),
item,
i;
for (i = 9; i >= 0; i--) {
item = document.createElement('li');
fragment.appendChild(item);
item.appendChild(document.createTextNode('Item' + i));
}
createDocumentFragment是DOM节点,但并不在DOM树中,向它添加元素不会引起页面重绘
2. 使用事件代理
尽可能将时间委托给祖先节点,减少页面上的事件处理程序
3. 减少HTMLCollection
什么情况下会得到HTMLCollection:
- getElementsByTagName()
- 获取元素的childNodes
- 获取元素的attributes
- 获取特定的元素集合时,如
document.forms, document.images
如果要多次访问,可以用变量保存,减少访问次数
4.6 其他方法
1. 原生方法
原生方法是C/C++编写,比js快的多
多看看JS的原生方法,例如Math对象中的复杂数学运算
2. Switch语句
switch
语句比if-else
更快,
按照 最可能执行到的 → 最不可能执行到的 顺序进行排列
3. 位运算符较快
尽可能用位运算符替代,例如 取模、与、或
等运算
每次循环都会