前端干货之JS最佳实践

持续更新地址 https://wdd.js.org/js-best-pr...

1. 风格

一千个读者有一千个哈姆雷特,每一个人都有本身的code style。我也曾为了要不要加分号给同事闹个脸红脖子粗,实际上有必要吗? 其实JavaScript已经有了比较流行的几个风格javascript

我本身使用的是JavaScript Standard Style, 我之因此使用这个,是由于它有一些工具。可让你写完代码后,一旦保存,就自动帮你把你的风格的代码修正成标准分割,而不是死记硬背应该怎么写。看完这个页面,你就应该立马爱上JavaScript Standard Style , 若是你用vscode, 刚好你有写vue, 你想在.vue文件中使用standard风格,那么你须要看看这篇文章php

2. 可维护性

不少时候,咱们不是从零开始,开发新代码。而是去维护别人的代码,以他人的工做成果为基础。确保本身的代码可维护,是赠人玫瑰,手留余香的好事。一方面让别人看的舒服,另外一方面也防止本身长时间没看过本身的代码,本身都难以理解。

2.1. 什么是可维护代码

可维护的代码的一些特征css

  • 可理解易于理解代码的用途
  • 可适应数据的变化,不须要彻底重写代码
  • 可扩展要考虑将来对核心功能的扩展
  • 可调试给出足够的信息,让调试的时候,肯定问题所在
  • 不可分割函数的功能要单一,功能粒度不可分割,可复用性加强

2.2. 代码约定

2.2.1. 可读性

  • 统一的缩进方式
  • 注释
  • 空白行

2.2.1.1. 缩进:

  • 通常使用4个空格
  • 不用制表符的缘由是它在不一样编辑器里显示效果不一样

2.2.1.2. 注释:哪些地方须要注释?

  • 函数和方法
  • 大段代码
  • 复杂的算法
  • hack

2.2.1.3. 空白行:哪些地方须要空白行?

  • 方法之间
  • 方法里的局部变量和第一个语句之间
  • 单行或者多行注释
  • 方法内衣个逻辑单元之间
// 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];
            }
        }
    }
}

2.2.2. 变量名和函数名

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();
变量命名不只仅是一种科学,更是一种艺术。总之,要短小精悍,见名知意。有些名词能够反应出变量的类型。

2.2.2.1. 变量名

名词 数据类型含义
count, length,size 数值
name, title,message 字符串
i, j, k 用来循环
car,person,student,user 对象
success,fail 布尔值
payload post数据的请求体
method 请求方式

2.2.2.2. 函数名

动词 含义
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

2.2.2.3. 一些与函数名搭配的经常使用动词

动词 用法
send 发送
resend 重发
validate 验证
query 查询
create 建立
add 添加
delete 删除
remove 移除
insert 插入
update 更新,编辑
copy 复制
render 渲染
close 关闭
open 开启
clear 清除
edit 编辑
query 查询
on 当事件发生
list 渲染一个列表,如用户列表renderUsersList()
content 渲染内容,如用户详情的页面 renderUserContent()

2.2.2.4. 接口经常使用的动词

对于http请求的最经常使用的四种方法,get,post,put,delete,有一些经常使用的名词与其对应html

含义 请求方法 词语 栗子
增长 post create createUser,createCall
删除 delete delete deleteUser
修改 put update updateUser,updateProfile
查询 get get,query getUser,queryUser(无条件查询使用get,有条件查询使用query)

2.2.2.5. 学会使用单复数命名函数

函数名 含义
getUser() 获取一个用户,通常是经过惟一的id来获取
getUsers() 获取一组用户,通常是经过一些条件来获取
createUser() 建立一个用户
createUsers() 建立一组用户

2.2.2.6. 常量

var MAX_COUNT = 10;
var URL = "http://www.nczonline.net/";

2.2.2.7. 构造函数

// Good
function Person(name) {
    this.name = name;
}
Person.prototype.sayName = function() {
    alert(this.name);
};
var me = new Person("wdd");

2.2.2.8. 底层http请求接口函数

  • 建议使用“_”开头,例如App._getUsers();而对于接口函数的封装,例如App.getUsers(),内部逻辑调用App._getUsers();

2.2.3. 文件名

  • 所有使用小写字母
  • 单词之间的间隔使用“-”

eg:前端

app-main.js
app-event.js
app-user-manger.js

2.2.4. 文件归类

本身写的js文件最好和引用的一些第三方js分别放置在不一样的文件夹下。vue

2.2.5. 千万别用alert

alert的缺点java

  • 若是你用alert来显示提醒消息,那么用户除了点击alert上的的肯定按钮外,就只能点击上面的关闭,或者选择禁止再选择对话框,除此之外什么都不能操做。
  • 有些浏览器若是禁止了alert的选项,那么你的alert是不会显示的
  • 若是你在try catch语句里使用alert,那么console里将不会输出错误信息,你都没办法查看错误的详细缘由,以及储出错的位置。

更优雅的提醒方式webpack

  • console.log() 普通提示消息
  • console.error() 错误提示消息
  • console.info() 信息提示消息
  • console.warn() 警告提示消息

2.3. 松散耦合

  • html文件中尽量避免写js语句
  • 尽可能避免在js更改某个css类的属性,而使用更改类的方法
  • 不要在css中写js的表达式
  • 解耦应用逻辑和事件处理程序

2.3.1. 将应用逻辑和事件处理程序的解耦

//通常事件订阅的写法,以jQuery的写法为栗子
$(document).on('click','#btn-get-users',function(event){
    event.stopPropagation();
    
    //下面的省略号表示执行获取全部用于并显示在页面上的逻辑
    // Bad
    ...
    ...
    ...
    //
});

若是增长了需求,当点击另一个按钮的时候,也要执行获取全部用户并显示在页面上,那么上面省略的代码又要复制一份。若是接口有改动,那么须要在两个不一样的地方都要修改。
因此,应该这样。git

$(document).on('click','#btn-get-users',function(event){
    event.stopPropagation();
    
    //将应用逻辑分离在其余个函数中
    // Good
    App.getUsers();
    App.renderUsers();
});

2.3.2. 松散解耦规则

  • 不要将event对象传给其余方法,只传递来自event对象中的某些数据
  • 任何事件处理程序都应该只处理事件,而后把处理转交给应用逻辑。

2.3.3. 将异步请求和数据处理解耦

// Bad
ReqApi.tenant.queryUsers({},function(res){
    if(!res.success){
        console.error(res);
        return;
    }
    
    //对数据的处理
    ...
    ...
    ...
});

上面代码对数据的处理直接写死在异步请求里面,若是换了一个请求,可是数据处理方式是同样的,那么又要复制一遍数据处理的代码。最好的方式是将数据处理模块化成为一个函数。github

// Good
ReqApi.tenant.queryUsers({},function(res){
    if(!res.success){
        console.error(res);
        return;
    }
    
    //对数据的处理
    App.renderUsers(res.data);
});

异步请求只处理请求,不处理数据。函数的功能要专注,功能粒度不可分割。

2.3.4. 不要将某个变量写死在函数中,尽可能使用参数传递进来

若是你须要一个函数去验证输入框是不是空,以下。这种方式就会绑定死了这个只能验证id为test的输入框,换成其余的就不行

// bad
function checkInputIsEmpty(){
    var value = $('#test').val();
    if(value){
        return true;
    }
    else{
        return false;
    }
}

// good 
function isEmptyInput(id){
    var value = $('#'+id).val();
    if(value){
        return true;
    }
    else{
        return false;
    }
}

2.4. 编程实践

2.4.1. 尊总对象全部权

javascript动态性质是的几乎任何东西在任什么时候间都能更改,这样就很容易覆写了一些默认的方法。致使一些灾难性的后果。若是你不负责或者维护某个对象,那么你就不能对它进行修改。

  • 不要为实例或原型添加属性
  • 不要为实例或者原型添加方法
  • 不要重定义存已存在的方法

2.4.2. 避免全局变量

// Bad 两个全局变量
var name = "wdd";
funtion getName(){
    console.log(name);
}

// Good 一个全局变量
var App = {
    name:"wdd",
    sayName:funtion(){
        console.log(this.name);//若是这个函数当作回调数使用,这个this可能指向window,
    }
};

单一的全局变量即是命名空间的概念,例如雅虎的YUI,jQuery的$等。

2.4.3. 避免与null进行比较

funtion sortArray(values){
    // 避免
    if(values != null){
        values.sort(comparator);
    }
}
function sortArray(values){
    // 推荐
    if(values instanceof Array){
        values.sort(compartor);
    }
}

2.4.3.1. 与null进行比较的代码,能够用如下技术进行替换

  • 若是值是一个应用类型,使用instanceof操做符,检查其构造函数
  • 若是值是基本类型,使用typeof检查其类型
  • 若是是但愿对象包含某个特定的方法名,则只用typeof操做符确保指定名字的方法存在于对象上。

代码中与null比较越少,就越容易肯定代码的目的,消除没必要要的错误。

2.4.4. 从代码中分离配置文件

配置数据是一些硬代码(hardcoded),看下面的栗子

function validate(value){
    if(!value){
        alert('Invalid value');
        location.href = '/errors/invalid.php';
    }
}

上面代码里有两个配置数据,一个是UI字符串('Invalid value'),另外一个是一个Url('/error/invalid.php')。若是你把他们写死在代码里,那么若是当你须要修改这些地方的时候,那么你必须一处一处的检查并修改,并且还可能会遗漏。

2.4.4.1. 因此第一步是要区分,哪些代码应该写成配置文件的形式?

  • 显示在UI元素中的字符串
  • URL
  • 一些重复的惟一值
  • 一些设置变量
  • 任何可能改变的值

2.4.4.2. 一些例子

var Config = {
    "MSG_INVALID_VALUE":"Invalid value",
    "URL_INVALID":"/errors/invalid.php"
}

2.4.5. 调试信息开关

在开发过程当中,可能随处留下几个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;

2.4.6. 使用jQuery Promise

没使用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(){
    //请求失败
    ...
});

2.4.7. 显示错误提醒,不要给后端接口背锅

假如前端要去接口获取用户信息并显示出来,若是你的请求格式是正确的,可是接口返回400以上的错误,你必须经过提醒来告知测试,这个错误是接口的返回错误,而不是前端的逻辑错误。

2.4.8. REST化接口请求

对资源的操做包括获取、建立、修改和删除资源,这些操做正好对应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(){
    
};

3. 性能

3.1. 注意做用域

  • 避免全局查找
  • 避免with语句

3.2. 选择正确的方法

  • 优化循环

    • 减值迭代:从最大值开始,在循环中不断减值的迭代器更加高效
    • 简化终止条件:因为每次循环过程都会计算终止条件,因此必须保证它尽量快。也就是避免其余属性查找
    • 简化循环体:因为循环体是执行最多的,因此要确保其最大限度地优化。
  • 展开循环
  • 避免双重解释:
// **Bad** 某些代码求值
eval("alert('hello')");

// **Bad** 建立新函数
var sayHi = new Function("alert('hello')");

// **Bad** 设置超时
setTimeout("alert('hello')");
  • 性能的其余注意事项

    • 原生方法较快
    • switch语句较快:能够适当的替换ifelse语句case 的分支不要超过128条
    • 位运算符较快

3.3. 最小化语句数

3.3.1. 多个变量声明(废弃)

// 方式1:Bad
var count = 5;
var name = 'wdd';
var sex = 'male';
var age = 10;

// 方式2:Good
var count = 5,
    name = 'wdd',
    sex = 'male',
    age = 10;

2017-03-07 理论上方式2可能要比方式1性能高一点。可是我在实际使用中,这个快一点几乎是没什么感觉的。就像你没法感觉到小草的生长同样。反而可读性更为重要。因此,每行最好只定义一个变量,而且每行都有一个var,并用分号结尾。

3.3.2. 插入迭代值

// Good
var name = values[i++];

3.3.3. 使用数组和对象字面量

// Good
var values = ['a','b','c'];

var person = {
    name:'wdd',
    age:10
};

只要有可能,尽可能使用数组和对象字面量的表达式来消除没必要要的语句

3.4. 优化DOM交互

在JavaScript各个方面中,DOM无疑是最慢的一部分。DOM操做与交互要消耗大量的时间。由于他们每每须要从新渲染整个页面或者某一部分。进一步说,看似细微的操做也可能花好久来执行。由于DOM要处理很是多的信息。理解如何优化与DOM的交互能够极大的提升脚本完成的速度。
  • 使用dom缓存技术
  • 最小化现场更新
  • 使用innerHTML插入大段html
  • 使用事件代理

3.4.1. Dom缓存技术

调用频率很是高的dom查找,能够将DOM缓存在于一个变量中

// 最简单的dom缓存

var domCache = {};

function myGetElement(tag){
    return domCache[tag] = domCache[tag] || $(tag);
}

3.5. 避免过长的属性查找,设置一个快捷方式

// 先看下面的极端状况
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的改变。

4. 快捷方式

4.1. 字符串转数字

+'4.1' === 4.1

4.2. 数字转字符

4.1+'' === '4.1'

4.3. 字符串取整

'4.99' | 0 === 4

5. 通用编码原则

建议读者自行扩展

  • DRY(dont't repeat yoursele: 不要重复你本身)
  • 高内聚低耦合
  • 开放闭合
  • 最小意外
  • 单一职责(single responsibility)

6. 高级技巧

6.1. 安全类型检测

  • javascript内置类型检测并不可靠
  • safari某些版本(<4)typeof正则表达式返回为function

建议使用Object.prototype.toString.call()方法检测数据类型

function isArray(value){
    return Object.prototype.toString.call(value) === "[object Array]";
}

function isFunction(value){
    return Object.prototype.toString.call(value) === "[object Function]";
}

function isRegExp(value){
    return Object.prototype.toString.call(value) === "[object RegExp]";
}

function isNativeJSON(){
    return window.JSON && Object.prototype.toString.call(JSON) === "[object JSON]";
}

对于ie中一COM对象形式实现的任何函数,isFunction都返回false,由于他们并不是原生的javascript函数。

在web开发中,可以区分原生与非原生的对象很是重要。只有这样才能确切知道某个对象是否有哪些功能

以上全部的正确性的前提是:Object.prototype.toString没有被修改过

6.2. 做用域安全的构造函数

function Person(name){
    this.name = name;
}

//使用new来建立一个对象
var one = new Person('wdd');

//直接调用构造函数
Person();

因为this是运行时分配的,若是你使用new来操做,this指向的就是one。若是直接调用构造函数,那么this会指向全局对象window,而后你的代码就会覆盖window的原生name。若是有其余地方使用过window.name, 那么你的函数将会埋下一个深藏的bug。

那么,如何才能建立一个做用域安全的构造函数?

function Person(name){
    if(this instanceof Person){
        this.name = name;
    }
    else{
        return new Person(name);
    }
}

6.3. 惰性载入函数

假设有一个方法X,在A类浏览器里叫A,在b类浏览器里叫B,有些浏览器并无这个方法,你想实现一个跨浏览器的方法。

惰性载入函数的思想是:在函数内部改变函数自身的执行逻辑

function X(){
    if(A){
        return new A();
    }
    else{
        if(B){
            return new B();
        }
        else{
            throw new Error('no A or B');
        }
    }
}

换一种写法

function X(){
    if(A){
        X = function(){
            return new A();
        };
    }
    else{
        if(B){
            X = function(){
                return new B();
            };
        }
        else{
            throw new Error('no A or B');
        }
    }
    
    return new X();
}

6.4. 防篡改对象

6.4.1. 不可扩展对象 Object.preventExtensions

// 下面代码在谷歌浏览器中执行
> var person = {name: 'wdd'};
undefined
> Object.preventExtensions(person);
Object {name: "wdd"}
> person.age = 10
10
> person
Object {name: "wdd"}
> Object.isExtensible(person)
false

6.4.2. 密封对象Object.seal

密封对象不可扩展,而且不能删除对象的属性或者方法。可是属性值能够修改。

> var one = {name: 'hihi'}
undefined
> Object.seal(one)
Object {name: "hihi"}
> one.age = 12
12
> one
Object {name: "hihi"}
> delete one.name
false
> one
Object {name: "hihi"}

6.4.3. 冻结对象 Object.freeze

最严格的防篡改就是冻结对象。对象不可扩展,并且密封,不能修改。只能访问。

6.5. 高级定时器

6.5.1. 函数节流

函数节流的思想是:某些代码不能够没有间断的连续重复执行

var processor = {
    timeoutId: null,

    // 实际进行处理的方法
    performProcessing: function(){
        ...
    },

    // 初始化调用方法
    process: function(){
        clearTimeout(this.timeoutId);

        var that = this;

        this.timeoutId = setTimeout(function(){
            that.performProcessing();
        }, 100);
    }
}

// 尝试开始执行
processor.process();

6.5.2. 中央定时器

页面若是有十个区域要动态显示当前时间,通常来讲,能够用10个定时来实现。其实一个中央定时器就能够搞定。

中央定时器动画 demo地址:http://wangduanduan.coding.me...

var timers = {
        timerId: 0,
        timers: [],
        add: function(fn){
            this.timers.push(fn);
        },
        start: function(){
            if(this.timerId){
                return;
            }

            (function runNext(){
                if(timers.timers.length > 0){
                    for(var i=0; i < timers.timers.length ; i++){
                        if(timers.timers[i]() === false){
                            timers.timers.splice(i, 1);
                            i--;
                        }
                    }

                    timers.timerId = setTimeout(runNext, 16);
                }
            })();
        },
        stop: function(){
            clearTimeout(timers.timerId);
            this.timerId = 0;
        }
    };

7. 函数式编程

推荐阅读:JS函数式编程中文版

8. HTML的告诫

  • 使用input的时候,必定要加上maxlength属性。(你觉得只须要输入一个名字的地方,用户可能复制一篇文章放进去。)
  • 从input取值的时候,最好去除一下首尾空格

9. ajax的告诫

ajax在使用的时候,例如点击按钮,获取某个列表。须要注意如下方面

  1. ajax请求尚未结束时,按钮必定要disabled,防止屡次点击。请求结束时,才去掉按钮的disabled属性。
  2. 请求没结束的时候,必定要显示一个gif的动画,告诉用户请求还在loading。不要让用户觉得这垃圾程序又卡死了。
  3. 请求的结果若是是空的,必定要告诉用户: 很抱歉,暂时没有查询到相关记录之类的话语。不要给一个空白页面给用户。
  4. 最好考虑到请求报错的状况,给出友好的错误提醒。

10. 代码整洁之道

10.1. 函数整洁

  • 尽可能将全部代码封装在函数中,不要暴露全局变量
  • 每一个函数的函数体中,代码行越少越好,最好一个函数中就一句代码

11. 工程化与模块化

11.1. 前端构建工具必不可少

11.1.1. webpack

11.1.2. rollup

11.1.3. parcel

12. 协议 TCP IP HTTP

若是你认为前端不须要关于协议的知识,那么你就是大错特错了。其实不只仅是前端,全部的开发者都应该学习底层的协议。由于他们是互联网通讯的基石。

推荐三本必读的书籍

或者你一也能够看看关于协议方面的一些问题,以及若是你遇到过,你是否知道如何解决:

13. 推荐深度阅读

13.1. 推荐阅读技术书籍

13.2. 推荐阅读在线文章

13.3. 技术以外

14. 参考文献

相关文章
相关标签/搜索