JS最佳实践
2016/10/25 create by wdd 一直会更新 git:https://github.com/wangduanduan/JS-Best-Practices
[TOC]
很多时候,我们不是从零开始,开发新代码。而是去维护别人的代码,以他人的工作成果为基础。确保自己的代码可维护,是赠人玫瑰,手留余香的好事。一方面让别人看的舒服,另一方面也防止自己长时间没看过自己的代码,自己都难以理解。
可维护的代码的一些特征
可理解
易于理解代码的用途可适应
数据的变化,不需要完全重写代码可扩展
要考虑未来对核心功能的扩展可调试
给出足够的信息,让调试的时候,确定问题所在不可分割
函数的功能要单一,功能粒度不可分割,可复用性增强
- 统一的缩进方式
- 注释
- 空白行
- 一般使用4个空格
- 不用制表符的原因是它在不同编辑器里显示效果不同
- 函数和方法
- 大段代码
- 复杂的算法
- hack
- 方法之间
- 方法里的局部变量和第一个语句之间
- 单行或者多行注释
- 方法内衣个逻辑单元之间
// Good
if (wl && wl.length) {
for (i = 0, l = wl.length; i < l; ++i) {
p = wl[i];
type = Y.Lang.type(r[p]);
if (s.hasOwnProperty(p)) {
if (merge && type == 'object') {
Y.mix(r[p], s[p]);
} else if (ov || !(p in r)) {
r[p] = s[p];
}
}
}
}
There are only two hard problem in Computer Science cache invalidation and naming things.---Phil Karlton
- 驼峰式命名
- 变量名以名词开头
- 方法名以动词开头
- 常量全部大写
- 构造函数以大写字母开头
- jQuery对象以"$"符号开头
- 自定义事件处理函数以“on”开头
// Good
var count = 10;
var myName = "wdd";
var found = true;
// Bad: Easily confused with functions
var getCount = 10;
var isFound = true;
// Good
function getName() {
return myName;
}
// Bad: Easily confused with variable
function theName() {
return myName;
}
// Bad:
var btnOfSubmit = $('#submit');
// Good:
var $btnOfSubmit = $('#submit');
// Bad:给App添加一个处理聊天事件的函数,一般都是和websocket服务端推送消息相关
App.addMethod('createChat',function(res){
App.log(res);
});
// Bad: 此处调用,这里很容易误以为这个函数是处理创建聊天的逻辑函数
App.createChat();
// Good:
App.addMethod('onCreateChat',function(res){
App.log(res);
});
// Good:此处调用
App.onCreateChat();
变量命名不仅仅是一种科学,更是一种艺术。总之,要短小精悍,见名知意。有些名词可以反应出变量的类型。
名词 | 数据类型含义 |
---|---|
count, length,size | 数值 |
name, title,message | 字符串 |
i, j, k | 用来循环 |
car,person,student,user | 对象 |
success,fail | 布尔值 |
payload | post数据的请求体 |
method | 请求方式 |
动词 | 含义 |
---|---|
can | Function returns a boolean |
has | Function returns a boolean |
is | Function returns a boolean |
get | Function returns a nonboolean |
set | Function is used to save a value |
动词 | 用法 |
---|---|
send | 发送 |
resend | 重发 |
validate | 验证 |
query | 查询 |
create | 创建 |
add | 添加 |
delete | 删除 |
remove | 移除 |
insert | 插入 |
update | 更新,编辑 |
copy | 复制 |
render | 渲染 |
close | 关闭 |
open | 开启 |
clear | 清除 |
edit | 编辑 |
query | 查询 |
on | 当事件发生 |
list | 渲染一个列表,如用户列表renderUsersList() |
content | 渲染内容,如用户详情的页面 renderUserContent() |
对于http请求的最常用的四种方法,get,post,put,delete,有一些常用的名词与其对应
含义 | 请求方法 | 词语 | 栗子 |
---|---|---|---|
增加 | post | create | createUser,createCall |
删除 | delete | delete | deleteUser |
修改 | put | update | updateUser,updateProfile |
查询 | get | get,query | getUser,queryUser(无条件查询使用get,有条件查询使用query) |
函数名 | 含义 |
---|---|
getUser() | 获取一个用户,一般是通过唯一的id来获取 |
getUsers() | 获取一组用户,一般是通过一些条件来获取 |
createUser() | 创建一个用户 |
createUsers() | 创建一组用户 |
var MAX_COUNT = 10;
var URL = "http://www.nczonline.net/";
// Good
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
alert(this.name);
};
var me = new Person("wdd");
- 建议使用“_”开头,例如App._getUsers();而对于接口函数的封装,例如App.getUsers(),内部逻辑调用App._getUsers();
- 全部使用小写字母
- 单词之间的间隔使用“-”
eg:
app-main.js
app-event.js
app-user-manger.js
自己写的js文件最好和引用的一些第三方js分别放置在不同的文件夹下。
alert的缺点
- 如果你用alert来显示提醒消息,那么用户除了点击alert上的的确定按钮外,就只能点击上面的关闭,或者选择禁止再选择对话框,除此以外什么都不能操作。
- 有些浏览器如果禁止了alert的选项,那么你的alert是不会显示的
- 如果你在try catch语句里使用alert,那么console里将不会输出错误信息,你都没办法查看错误的详细原因,以及储出错的位置。
更优雅的提醒方式
- console.log() 普通提示消息
- console.error() 错误提示消息
- console.info() 信息提示消息
- console.warn() 警告提示消息
- html文件中尽可能避免写js语句
- 尽量避免在js更改某个css类的属性,而使用更改类的方法
- 不要在css中写js的表达式
- 解耦应用逻辑和事件处理程序
//一般事件订阅的写法,以jQuery的写法为栗子
$(document).on('click','#btn-get-users',function(event){
event.stopPropagation();
//下面的省略号表示执行获取所有用于并显示在页面上的逻辑
// Bad
...
...
...
//
});
如果增加了需求,当点击另外一个按钮的时候,也要执行获取所有用户并显示在页面上,那么上面省略的代码又要复制一份。如果接口有改动,那么需要在两个不同的地方都要修改。 所以,应该这样。
$(document).on('click','#btn-get-users',function(event){
event.stopPropagation();
//将应用逻辑分离在其他个函数中
// Good
App.getUsers();
App.renderUsers();
});
- 不要将event对象传给其他方法,只传递来自event对象中的某些数据
- 任何事件处理程序都应该只处理事件,然后把处理转交给应用逻辑。
// Bad
ReqApi.tenant.queryUsers({},function(res){
if(!res.success){
console.error(res);
return;
}
//对数据的处理
...
...
...
});
上面代码对数据的处理直接写死在异步请求里面,如果换了一个请求,但是数据处理方式是一样的,那么又要复制一遍数据处理的代码。最好的方式是将数据处理模块化成为一个函数。
// Good
ReqApi.tenant.queryUsers({},function(res){
if(!res.success){
console.error(res);
return;
}
//对数据的处理
App.renderUsers(res.data);
});
异步请求只处理请求,不处理数据。函数的功能要专一,功能粒度不可分割。
javascript动态性质是的几乎任何东西在任何时间都能更改,这样就很容易覆写了一些默认的方法。导致一些灾难性的后果。如果你不负责或者维护某个对象,那么你就不能对它进行修改。
- 不要为实例或原型添加属性
- 不要为实例或者原型添加方法
- 不要重定义存已存在的方法
// Bad 两个全局变量
var name = "wdd";
funtion getName(){
console.log(name);
}
// Good 一个全局变量
var App = {
name:"wdd",
sayName:funtion(){
console.log(this.name);//如果这个函数当做回调数使用,这个this可能指向window,
}
};
单一的全局变量便是命名空间的概念,例如雅虎的YUI,jQuery的$等。
funtion sortArray(values){
// 避免
if(values != null){
values.sort(comparator);
}
}
function sortArray(values){
// 推荐
if(values instanceof Array){
values.sort(compartor);
}
}
- 如果值是一个应用类型,使用instanceof操作符,检查其构造函数
- 如果值是基本类型,使用typeof检查其类型
- 如果是希望对象包含某个特定的方法名,则只用typeof操作符确保指定名字的方法存在于对象上。
代码中与null比较越少,就越容易确定代码的目的,消除不必要的错误。
配置数据是一些硬代码(hardcoded),看下面的栗子
function validate(value){
if(!value){
alert('Invalid value');
location.href = '/errors/invalid.php';
}
}
上面代码里有两个配置数据,一个是UI字符串('Invalid value'),另一个是一个Url('/error/invalid.php')。如果你把他们写死在代码里,那么如果当你需要修改这些地方的时候,那么你必须一处一处的检查并修改,而且还可能会遗漏。
- 显示在UI元素中的字符串
- URL
- 一些重复的唯一值
- 一些设置变量
- 任何可能改变的值
var Config = {
"MSG_INVALID_VALUE":"Invalid value",
"URL_INVALID":"/errors/invalid.php"
}
在开发过程中,可能随处留下几个console.log,或者alert语句,这些语句在开发过程中是很有价值的。但是项目一旦进入生产环境,过多的console.log可能影响到浏览器的运行效率,过多的alert会降低程序的用户体验。而我们最好不要在进入生产环境前,一处一处像扫雷一样删除或者注释掉这些调试语句。
最好的方式是设置一个开关。
//全局命令空间
var App = {
debug:true,
log:function(msg){
if(debug){
console.log(msg);
}
},
alert:function(msg){
if(debug){
alert(msg);
}
}
};
//使用
App.log('获取用户信息成功');
App.alert('密码不匹配');
//关闭日志输出与alert
App.debug = false;
没使用promise之前的回调函数写法
// bad:没使用promise之前的回调函数写法
function sendRequest(req,successCallback,errorCallback){
var inputData = req.data || {};
inputData = JSON.stringify(inputData);
$.ajax({
url:req.base+req.destination,
type:req.type || "get",
headers:{
sessionId:session.id
},
data:inputData,
dataType:"json",
contentType : 'application/json; charset=UTF-8',
success:function(data){
successCallback(data);
},
error:function(data){
console.error(data);
errorCallback(data);
}
});
}
//调用
sendRequest(req,function(res){
...
},function(res){
...
});
使用promise之后
function sendRequest(req){
var dfd = $.Deferred();
var inputData = req.data || {};
inputData = JSON.stringify(inputData);
$.ajax({
url:req.base+req.destination,
type:req.type || "get",
headers:{
sessionId:session.id
},
data:inputData,
dataType:"json",
contentType : 'application/json; charset=UTF-8',
success:function(data){
dfd.resolve(data);
},
error:function(data){
dfd.reject(data);
}
});
return dfd.promise();
}
//调用
sendRequest(req)
.done(function(){
//请求成功
...
})
.fail(function(){
//请求失败
...
});
假如前端要去接口获取用户信息并显示出来,如果你的请求格式是正确的,但是接口返回400以上的错误,你必须通过提醒来告知测试,这个错误是接口的返回错误,而不是前端的逻辑错误。
对资源的操作包括获取、创建、修改和删除资源,这些操作正好对应HTTP协议提供的GET、POST、PUT和DELETE方法。
对应方式
请求类型 | 接口前缀 |
---|---|
GET | .get, |
POST | .create 或者 .get |
PUT | .update |
DELETE | .delete |
说明
- 有些接口虽然是获取某一个资源,但是它使用的却是POST请求,所以建议使用.get比较好
示例:
// 与用户相关的接口
App.api.user = {};
// 获取一个用户: 一般来说是一个指定的Id,例如userId
App.api.user.getUser = function(){
...
};
// 获取一组用户: 一般来说是一些条件,获取条件下的用户,筛选符合条件的用户
App.api.user.getUsers = function(){
...
};
// 创建一个用户
App.api.user.createUser = function(){
};
// 创建一组用户
App.api.user.createUsers = function(){
};
// 更新一个用户
App.api.user.updateUser = function(){
};
// 更新一组用户
App.api.user.updateUsers = function(){
};
// 更新一个用户
App.api.user.updateUser = function(){
};
// 更新一组用户
App.api.user.updateUsers = function(){
};
// 删除一个用户
App.api.user.deleteUser = function(){
};
// 删除一组用户
App.api.user.deleteUsers = function(){
};
- 避免全局查找
- 避免with语句
- 优化循环
减值迭代
:从最大值开始,在循环中不断减值的迭代器更加高效简化终止条件
:由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。也就是避免其他属性查找简化循环体
:由于循环体是执行最多的,所以要确保其最大限度地优化。
- 展开循环
- 避免双重解释:
// **Bad** 某些代码求值
eval("alert('hello')");
// **Bad** 创建新函数
var sayHi = new Function("alert('hello')");
// **Bad** 设置超时
setTimeout("alert('hello')");
- 性能的其他注意事项
- 原生方法较快
- switch语句较快:可以适当的替换ifelse语句
case 的分支不要超过128条
- 位运算符较快
// Bad
var count = 5;
var name = 'wdd';
var sex = 'male';
var age = 10;
// Good
var count = 5,
name = 'wdd',
sex = 'male',
age = 10;
// Good
var name = values[i++];
// Good
var values = ['a','b','c'];
var person = {
name:'wdd',
age:10
};
只要有可能,尽量使用数组和对象字面量的表达式来消除不必要的语句
在JavaScript各个方面中,DOM无疑是最慢的一部分。DOM操作与交互要消耗大量的时间。因为他们往往需要重新渲染整个页面或者某一部分。进一步说,看似细微的操作也可能花很久来执行。因为DOM要处理非常多的信息。理解如何优化与DOM的交互可以极大的提高脚本完成的速度。
- 使用dom缓存技术
- 最小化现场更新
- 使用innerHTML插入大段html
- 使用事件代理
调用频率非常高的dom查找,可以将DOM缓存在于一个变量中
// 最简单的dom缓存
var domCache = {};
function myGetElement(tag){
return domCache[tag] = domCache[tag] || $(tag);
}
// 先看下面的极端情况
app.user.mother.parent.home.name = 'wdd'
app.user.mother.parent.home.adderess = '上海'
app.user.mother.parent.home.weather = '晴天'
// 更优雅的方式
var home = app.user.mother.parent.home;
home.name = 'wdd';
home.address = '上海',
home.weather = '晴天'
注意
使用上面的方式是有前提的,必须保证app.user.mather.parent.home是一个对象,因为对象是传递的引用。如果他的类型是一个基本类型,例如:number,string,boolean,那么复制操作仅仅是值传递,新定义的home的改变,并不会影响到app.user.mather.parent.home的改变。
- JavaScript高级程序设计(第3版) 【美】尼古拉斯·泽卡斯
- Maintainable JavaScript (英文版) Nicholas C. Zakas(其实和上边那本书应该是同一个人)
- JavaScript忍者秘籍 John Resig / Bear Bibeault (John Resig 大名鼎鼎jQuery的创造者)