📜前端编程风格与最佳实践

2,252 阅读10分钟

“程序是写给人读的,只是偶尔让计算机执行一下” --- 鲁迅 😂😂😂

前言

还记得我在第一份工作接手过一些根本无法维护的代码,整份代码竟长达 4000 多行,后来只能重写了一遍,从那以后我就意识到可读性和可维护性是多么重要,所以当时我把如何写出好代码作为一个学习的目标,看了一些跟编程风格相关的书籍。前段时间在现公司部门分享会上分享了,加之最近有写博客锻炼下文笔(以前都在印象笔记上写着玩),所以也在这里分享一下!

编程风格

空格与缩进

a && b

for (let i = 0, len = x.length; i < len, i++) {
    // do something
}

let a = b;

let obj = {
    a: false,
    b: 1
    c: '2'
};


if () {

} else {

}

doSomething(1, true);

.class-name {
    position: relative;
}

换行

在运算符后换行,第二行加两个缩进

fn (arg1, arg2, arg3, arg4, 
        arg5)

分号

在《编写可维护的JavaScript》中作者是推荐结尾加分号,因为自动插入分号可能会出错,但是在 vue源码 中是不加分号的,所以这个还是看团队约定吧

空行:

代码应该由一段一段同系列的代码片段组成,而不是一整篇连续文本

应该按语义把代码分隔好,这将大大提高可读性!

if (true) {
    for (let i = 0; len = x.length; i < len; i++) {
        const a = x[i],
                b = '';

        if (a === 1) {

        }

        // 注释前面有内容的话也分隔开
        doSomething();
    }
}
<header class="header"></header>

<section class="intro">
    <h2 class="title"></h2>
    
    <div class="content">
        xxx
    </div>
</section>

<footer class="footer"></footer>
.header {
    height: 50px;
    
    .intro {
        
    }
    
    .content {
        
    }
}

命名

变量名:驼峰 && 名词开头

// 好
const name = ''
const count = ''

// 不好:像方法
const getCount = ''

函数:驼峰 && 动词开头

// 好
function getName() {}
function setName() {}

// 不好:像变量
function theName() {}

常量:大写与下划线组合

const URL = ''
const MAX_LENGTH = 20

构造函数:大驼峰

function Person() {}

常见的前缀

can: 能不能 -> canDelete
is:  是不是 -> isEmpty
has: 有没有 -> hasChild
get: 获取
set: 设置,用来保存一个值

工具:

使用直接量

// 好
const arr = []
const obj = {}
const name = 'cxk'

// 不好
const arr = new Array()
const obj = new Object()
const name = new String('cxk')

引号

在 html 中使用双引号 在 js 中使用单引号

<div class="name"></div>
const name = 'cxk'

注释

程序猿应该通过可读的代码来沟通,而不是依赖注释,注释应该加在难于理解或会被误认为错误的代码或浏览器 hack 代码

// 好

// 当 xx 时,在ie 会报错
if (xx) {
}

white (element && (element = 1)) { // 赋值操作
}

// 不好

// check to see if the employee is eligible for full benefit
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

// 改进
if (employee.isEligibleForFullBenefits())
  • switch

在 switch 语句中如果省略某个 break 那么会继续向下执行,如果确实需要连续执行应该加上注释;如果不需要 default 也加上注释

声明

《编写可维护的JavaScript》推荐把变量声明在代码块开头,但是个人觉得把变量定义到靠近使用的地方更好

function doSomething() {
    const a = '',
        b = false,
        c = 3
        
    if (d) {
        // xxx
    }
}

先声明后使用

const i = 1;
console.log(i);

// 虽然有函数提升 但是还是先声明后使用
function fun() {
    console.log('fun');
}
fun();

尽量避免

  1. with

  2. eval

  3. for in

  4. continue

  5. == !=

  6. 全局变量

最佳实践

UI层面松耦合

  1. 避免在 css 中写 js ,除非自带的如:rgb calc
  2. 避免在 js 中写 css ,可以通过操作 dom 的 class 代替
  3. 避免在 js 中写 html ,可使用 ejs handlebar 之类的代替
  4. 避免在 html 中写 js

对于这点 vue 的理念是:把模板、逻辑和样式在组件内部耦合再把组件搭配使用反而使得组件更加内聚且更可维护(所以写 vue 一定要做好组件化)

抽离应用逻辑

应用逻辑是指与应用相关的代码,例如显示弹窗、清空表单等,这些逻辑你不知道会在哪里用到,应该抽离复用

// 其实这个例子还有一点不好的地方,具体请往下看
const app = {
    handleClick(event) {
        this.showDialog(event)
    },
    
    // 抽离显示弹窗方法
    showDialog(event) {
        const dialog = document.getElementById('dialog')
        dialog.style.left = event.clientX + 'px'
        dialog.style.top = event.clientY + 'px'
        dialog.className = 'show'
    }
}

参数

避免传递 event this 等,原因是

  1. 方法的参数应该保持透明,传入 event 或者 this 等并不能知道这些对象哪些属性是有用的,用来干嘛的
  2. 这些方法难以测试,真要测试的话要模拟一个事件对象

所以改进下上面的例子

const app = {
    handleClick(event) {
        this.showDialog(event.clientX, event.clientY)
    },
    
    // 抽离显示弹窗方法
    showDialog(clientX, clientY) {
        const dialog = document.getElementById('dialog')
        dialog.style.left = clientX + 'px'
        dialog.style.top = clientY + 'px'
        dialog.className = 'show'
    }
}

属性检测

应该使用 in 判断对象中是否含有某属性,而不是检测假值

// 不好 object.name 可能就是假值呢
if (object.name) {}

// 好
if ('name' in object) {}

把配置数据抽成常量

把一些多处引用甚至可能会变更的值抽离成常量,这里的作用有两个

  1. 需要修改时只改一处就行
  2. 提高可读性
// 下面展示提高可读性的例子

// 不好,会给人整懵逼,别人一眼看到 0 和 1 究竟是啥?
if (value === 0) {
    
} else if (value === 1) {
    
}

// 好
const type = {
    ADD: 1,
    DELETE: 0
}

if (value === type.DELETE) {
    
} else if (value === type.ADD) {
    
}

当看到如下时需要留意了:

  • url
  • 正则表达式
  • 需要展示给用户的字符串
  • 重复引用的值
  • 设置(类型等)
  • 可能发生变更的值

不要乱改对象

以下对象不要动(不覆盖 不修改 不删除)

  • 原生对象 Object Array……
  • DOM对象 document
  • BOM对象 window
  • 类库对象

网上有一些往原生对象加方法的例子,比如给 String 加个去头尾的方法之类的,其实这不太好,说不定 js 以后就支持了并且方法名和你的相同(prototype 库就有过这样的教训)

这个思路和 vue 中不要以单个单词作为组件名是一致的,说不定以后 html 就支持和你同名的标签了呢?

浏览器判断

最好用检测特性代替检测浏览器,比如检测是否有 document.getElementById ,而不是检测是不是 ie8

避免用特性推断浏览器,说不定以后其他浏览器也支持这些特性呢?

一个兼容各浏览器的方法,应当先检测标准方法,再检测各浏览器的方法,再提供通用的方法。

// 好
function setAnimate() {
    return window.requestAnimationFrame ||
           window.webkitRequestAnimationFrame ||
           window.mozRequestAnimationFrame ||
           function (callback) {
                window.setTimeout(callback, 6000 / 60);
           };
};

性能

script 在body闭标签前引入。

局部变量

尽可能使用局部变量,如果跨作用域的值被引用一次以上,则缓存。

// 不好
function init() {
    let bd = document.body,
        links = document.getElementsByTagName('a');
        i = 0,
        len = links.length;

    while(i < len) {
        update(links[i++]);
    }

    document.getElementById('goBtn').onclick = function() {
        start();
    }

    bd.className = 'active';
}

// 好 document处于作用域链中很深的位置,并且多次引用,应缓存起来。
function init() {
    let doc = document,
        bd = doc.body,
        links = doc.getElementsByTagName('a');
        i = 0,
        len = links.length;

    while(i < len) {
        update(links[i++]);
    }

    doc.getElementById('goBtn').onclick = function() {
        start();
    }

    bd.className = 'active';
}

with 与 try-catch

避免使用 with,with 使得局部变量处于作用域链中第二的位置,访问代价更高。

try catch 同理:会把错误对象推倒作用域首位,但是 try catch 是非常有用的语句,不建议弃用。 解决方法是将错误委托给函数。

// 好
try {
    func();
} catch(err) {
    handleErr(err); // 委托给一个函数处理
}

闭包

小心使用闭包,闭包可能使得活动对象无法销毁,存在更多内存开销,可配合以上说的将跨作用域的变量缓存,减轻负担。

对象深度

对象成员嵌套越深,读取越慢,localhost.href 总是比 window.location.href 要快。

变量缓存

如果一个对象属性被引用两次以上,就应该缓存起来,避免重复读取。

// 不好
function hasEitherClass(ele, className1, className2) {
    return element.classNam === className1 || element.classNam === className2;
}

// 好
function hasEitherClass(ele, className1, className2) {
    let curClassName = element.classNam;
    return curClassName === className1 || curClassName === className2;
}

dom

  • 访问dom非常低效,应减少dom访问。
// 不好
function innerHTMLLoop () {
    for (var count = 0; count < 1000; count++) {
        document.getElementById('here').innerHTML += 'a';
    }
}

// 好
function innerHTMLLoop () {
    var content = '';

    for (var count = 0; count < 1000; count++) {
        count += 'a';
    }
    document.getElementById('here').innerHTML = content;
}

减少重排与重绘,避免重排队列强制刷新

使用事件委托。

循环

  • 提高循环性能可减少迭代次数或减轻每次迭代处理的事务

  • 避免 for in 循环,除非要循环对象的原型属性。

  • 循环时缓存 length,避免多次读取。

  • 颠倒循环更快(减少了一次判断)。

let arr = [3,2,1];
for(let i = arr.length; i--; ) {
    console.log(arr[i]); // 1 2 3
}

let j = arr.length;
while(j--) {
    console.log(arr[j]); // 1 2 3
}
  • 如果循环次数在1000以上,使用达夫设备。

  • for 循环比 forEach 快,因 forEach 要对每项调用方法。

  • 条件数量大时 switch 比 if-else 快。

if else 优化

  • 确保最可能出现的条件放首位
  • if else 嵌套
  • 用查找表的方法代替
// 好 if else 嵌套
if (value < 6){
  if (value < 3){
    if (value == 0){
      return result0;
    } else if (value == 1){
      return result1;
    } else {
      return result2;
    }
  } else {
  if (value == 3){
      return result3;
    } else if (value == 4){
      return result4;
    } else {
      return result5;
    }
  }
} else {
  if (value < 8){
    if (value == 6){
      return result6;
    } else {
      return result7;
    }
  } else {
    if (value == 8){
      return result8;
    } else if (value == 9){
      return result9;
    } else {
      return result10;
    }
  }
}

// 好 查找表代替 if else
// 数组
var results = [result0, result1, result2, result3...]
return results[value];

//对象
var obj = {
    num1: '未发货',
    num2: '已发货',
    num3: '已到货',
}
//对象成员查找
return obj[num1];

拆分任务

若一个任务运行过长(超过100毫秒),则利用定时器分割成小任务或者使用 Web Workers。

function save () {
    let tasks = [open, write, close];

    setTimeout(() => {
        let task = tasks.shift();
        task();

        if (tasks.length) {
            setTimeout(arguments.callee, 25);
        }
    }, 25);
}

避免每次都做同一件事

// 不好 函数每次执行都判断一次是否支持addEventListener
function addHandle(target, eventType, handler) {
    if (target.addEventListener) {
        target.addEventListener(eventType, handler, false);
    } else {
        target.attachEvent('on' + eventType, handler);
    };
}

// 好 第一次执行判断,判断完成覆盖掉原方法。
function addHandle(target, eventType, handler) {
    if (target.addEventListener) {
        addHandle = function() {
            target.addEventListener(eventType, handler, false);
        }
    } else {
        addHandle = function() {
            target.attachEvent('on' + eventType, handler);
        }
    };
}

// 好
let addHandle = document.body.addEventListener ?
        function() {
           target.addEventListener(eventType, handler, false);
        }:
        function() {
            target.attachEvent('on' + eventType, handler);
        };

css

  • 避免嵌套过深,不超过 4 层
  • 个人习惯的顺序是
  1. 写决定属性的 例如: display position
  2. 写决定位置的 例如: 宽高 left rignt padding magin
  3. 写字体颜色
  4. 写背景边框
  5. 写其他 例如: transition transform

vue

对于 vue 编写风格基本上参考 官方风格指南 即可

编写 vue 一定要做好组件化,那些一个页面上千行的基本上是无法维护的。

大概说下我们的约定,

  • 把页面各个部分分割成业务组件或通用组件
  • 接口在 actions 中管理,在 pages 或者说 views 中请求数据,再把数据传到组件中使用
  • 组件中是不请求数据的,一些请求数据的操作可以通过 emit 传到 pages 中执行
<template>
 <div class="environment-config">
    <environment-list
      class="environment-config-list"
      v-loading="listLoading"
      v-model="currentEnvironment"
      :data="environmentList"
      @add-environment="handleEnvironmentAdd"
      @delete-environment="handleEnvironmentDelete"
      @environment-click="handleEnvironmentClick">
    </environment-list>
    
    <environment-table
      :data="environmentTable"
      @edit-name="handleEnvironmentNameEdit">
    </environment-table>

    <environment-form
      class="environment-config-form"
      ref="environmentConfigForm"
      v-loading="formLoading"
      :data="environmentForm"
      @submit="handleEnvironmentSubmit">
    </environment-form>
 </div>
</template>

这样好处是:

  • 组件化,能控制好每个页面行数;
  • 易定位,数据问题找 pages/views 或者 actions ,页面哪部分出问题,找哪部分组件接口,比如上面的 environment-table 有 bug,只需 ctrl + p ,输入 environment-table 跳到对应的组件 debug 即可

参考

《编写可维护的JavaScript》

《高性能JavaScript》

《JavaScript语言精粹》

《代码整洁之道》

《编写可维护的JavaScript》脑图