项目中的代码优化总结

1,530 阅读7分钟

记住,您编写的每一行代码都会有相应的阅读成本。看这代码的人可能是团队的成员,甚至是你未来的自己。

一、软件设计3R层次结构

  • readable可读性
  • reuseable可复用性
  • refactorable可重构性

关键思想:

  • 保持代码一致:一致的风格比“正确”的风格更重要。——编码规范、ESlint
  • 易于阅读和理解:应当使别人理解它所需的时间最小化。——命名、函数式编程
  • 易于维护。——设计模式思想

二、为什么我极力推荐eslint

据广泛估计,开发人员将70%的代码维护时间花在阅读上以理解它。这真让人大开眼界。居然达到了70%。难怪程序员每天编写的代码行数的平均值大约是10行。我们每天花7个小时来阅读代码,去理解这10行怎么运行!

团队合作带来的问题

看别人的代码

  • 团队其他人和自己书写代码的风格不一致,很难去适应别人的编程风格。
  • 甚至一个文件有多个人改动过,有多种编程风格,看着不仅难受,阅读代码也较吃力。

改别人的代码

  • 代码没有格式化便提交,增加团队成员的阅读成本。
  • 团队成员使用不同的编辑器,每个人都自有一套格式化代码风格,很容易因格式化代码生成多余的改动,从而产生冲突,也不利于解决。
  • 代码提交到git上,由于格式化带来的改动,反而不利于查看这次提交实际改动了哪些代码。

eslint解决了什么

  • 不要使用编辑器自带的格式化,用在项目中配置的eslint的规则去格式化代码,团队使用一致的编程风格,会让代码更容易阅读。
  • 设置每次保存的时候就格式化代码,不用每次都手动输入空格或者分号来保持编程风格,也不用每次都手动格式化代码。
  • 每次提交代码,不会因为代码格式化问题产生多余的改动,干净的提交,便于以后查看提交记录解决问题。

经过一年多使用eslint的切身体会,我确实感受到eslint的好用之处,以及在团队协作中带来的好处,让你在修改别人写的代码的时候,也不会因为不统一的代码风格难受。

三、项目中的优化

下面我们将从代码的易读、性能等方面,举例说明在实际项目中我们可以怎么做来优化我们的代码。

命名

坏味道:命名采用缩写。

推荐:选择具体有实际意义的词,不要用别人看不懂的缩写。

函数

坏味道:函数参数太多,命名模糊。

推荐:函数参数不要超过2个,超过的话可以考虑用对象传参,获取参数的时候用解构赋值。

坏味道:函数过大,难于阅读。

推荐:一个函数只做一件事原则,把代码片段提取出来放入一个独立的函数,这样的好处是函数名也起到了注释的效果,有助于阅读代码,提取出来的函数有助于代码复用。

值的不变性

重新赋值

var/let声明的值可被重新赋值,var可能引起变量提升,带来隐藏的问题。

let x = 1;
x = 2;

const声明的值不可被重新赋值,但如果声明的变量是引用类型,值的内部仍然可被改变

const x = 2;
// 试着改变`x`看看!
x = 3;      // Error!

const x = [ 2 ];
x[0] = 3; // 可以改变
x = [1]; // Error!

推荐:程序中尽量避免var的使用,因为var会带来作用域的问题;用const声明不会被改变的变量。

值的冻结

Object.freeze:将一个可变的对象/数组/函数转换成一个“不可变值”,但只针对最顶层。

var x = Object.freeze( [ 2, 3, [4, 5] ] );

// 不允许:
x[0] = 42;

// 允许:
x[2][0] = 42;

Object.freeze(..)提供了浅的、单一的不变性,如果您想要一个非常不可变的值,就必须手动遍历整个对象/数组结构,并对每个子对象/数组应用Object.freeze(..)。

推荐:不会被修改的对象/数组用Object.freeze“冻结”起来。

for循环

效率优化

for循环中的代码会在每次循环的时候都会执行,所以不要在循环体里写无意义的代码,也可以提前结束循环,优化效率。

推荐

  • 将一些操作放在循环体外面
  • 如果要提前进入下一次循环,用continue
  • 如果要提前退出循环,用break

可读性优化

我们处理数组或者类数组的数据的时候,可以用数组的内置方法(map,filter等等)的,就不要使用for语句自己写一遍,这些内置方法可读性明显优于我们写for语句。

// 扁平化数组arr,结果存放在result中
const arr = [[12, 19], [20, 123, 12], [23, 54]];
const result = [];

// 坏味道
for(let i = 0, len = arr.length; i < len; i++){
  result = result.concat(arr[i]);
}

// 推荐
result = arr.flat();

坏味道:for循环使代码可读性更差

推荐:要善于利用js中一些内置方法,直接用封装好的方法有更好的可读性,也增加了开发效率。

多个弹窗的情况

我们页面中有多个弹窗,需要用变量控制弹窗的显示隐藏

坏味道:一个变量控制一个弹窗的显示隐藏,变量数很多,容易混淆哪个变量控制哪个弹窗

推荐:通过给变量赋值,判断变量是否与值相等来控制弹窗显示隐藏

合理使用数据结构

// 坏味道
data() {
  return {
    total: 0,
    pageSize: 12,
    currentPage: 1,
    name: '',
  }
}

// 推荐
data() {
  return {
    queryInfo: {
      total: 0,
      pageSize: 12,
      currentPage: 1,
      name: '',
    },
  }
}

坏味道:数据过于扁平,变量很多

推荐:把相关功能的几个变量封装在一个对象里面,对象名起到注释作用,说明这些变量的作用

在调后端接口获取数据传参的时候,如果对象的属性和我们要传给接口的参数一直,那么传参的时候只需要传一个对象就可以了,不用再写一遍。

适当复用代码

DRY(dont repeat yourself)原则。

1.常用工具方法

坏味道:在组件里面用到什么方法复制粘贴一遍

推荐:像格式化时间、判断浏览器类型、防抖节流等等,整理出来放在一个文件里面,导出方法,在用到的地方导入

2.使用mixins

坏味道:在组件把类似功能的代码复制粘贴一遍

推荐:类似的功能提取出来放在mixins里面,同时也要考虑复用性。比如table相关的变量和方法

3.使用状态管理器

我们做后台管理平台的时候经常会遇到这种情况:一组枚举值在列表的下拉筛选会用到,在新增或者修改页面也会用到。

坏味道:在两个组件重复定义枚举值,复制粘贴一遍,如果值是从后台接口获取的,那么会在每个用到的组件中都去请求接口。

推荐:把枚举值放在状态管理器中去管理,只定义一次(或者只请求接口一次),然后在用到的组件里去获取枚举值。

条件判断

多重条件,即代码逻辑中有很多条件判断语句,比如if-else分支。

1.使用布尔值的快捷方式

// 坏味道
if (isValid === true) {}
if (arr.length === 0) {}

// 推荐
if (isValid) {}
if (!arr.length) {}

2.合理使用三元语句

简单的两重条件判断可以用三元表达式代替:

// 坏味道
if (a === b) {
  res = true;
} else {
  res = false;
}

// 推荐
res = a === b ? true : false;

同时也要避免不必要的三元表达式:

// 坏味道
const res = a ? a : b;

// 推荐
const res = a || b;

3.把复杂的条件分支语句提炼成函数

// 坏味道
var getPrice = function (price) {
  var date = new Date();
  if (date.getMonth() >= 6 && date.getMonth() <= 9) {
    // 夏天
    return price * 0.8;
  }
  return price;
};

// 推荐
var isSummer = function () {
  var date = new Date();
  return date.getMonth() >= 6 && date.getMonth() <= 9;
};
var getPrice = function (price) {
  if (isSummer()) {
    // 夏天
    return price * 0.8;
  }
  return price;
};

4.避免条件判断的重复过程

// 惰性加载函数
// 坏味道
var addEvent = function (elem, type, handler) {
  if (window.addEventListener) {
    return elem.addEventListener(type, handler, false);
  }
  if (window.attachEvent) {
    return elem.attachEvent("on" + type, handler);
  }
};
// 推荐
var addEvent = function (ele, type, handler) {
  if (window.addEventListener) {
    addEvent = function (ele, type, handler) {
      ele.addEventListener(ele, type, handler, false);
    };
  } else if (window.attachEvent) {
    addEvent = function (ele, type, handler) {
      ele.addEventListener(ele, type, handler);
    };
  }

  addEvent(ele, type, handler);
};

5.巧用return

1.提前return

// 坏味道
var del = function (obj) {
  var ret;
  if (!obj.isReadOnly) {
    // 不为只读的才能被删除
    if (obj.isFolder) {
      // 如果是文件夹
      ret = deleteFolder(obj);
    } else if (obj.isFile) {
      // 如果是文件
      ret = deleteFile(obj);
    }
  }
  return ret;
};

// 推荐
var del = function (obj) {
  if (obj.isReadOnly) {
    // 反转 if 表达式
    return;
  }
  if (obj.isFolder) {
    return deleteFolder(obj);
  }
  if (obj.isFile) {
    return deleteFile(obj);
  }
};

优点

  • 更少的嵌套,看起来更简洁
  • 程序不用再往下执行

缺点:提前返回导致函数有多个输出,可能难以读取函数以了解其输出行为,在流控制结构(if逻辑等等)中,会导致代码可读性更差。

现在大多数文章都在推崇尽可能早的return,但我们也不应该忽视它可能会带来的问题,所以在实际项目中,我们也要合理选择在什么时候return。

2.用return退出多重循环

// 坏味道
var func = function () {
  var flag = false;
  for (var i = 0; i < 10; i++) {
    for (var j = 0; j < 10; j++) {
      if (i * j > 30) {
        flag = true;
        break;
      }
    }
    if (flag === true) {
      break;
    }
  }
  
  // do something
  console.log(i);
};

// 推荐
var print = function (i) {
  console.log(i);
};
var func = function () {
  for (var i = 0; i < 10; i++) {
    for (var j = 0; j < 10; j++) {
      if (i * j > 30) {
        return print(i);
      }
    }
  }
};

if-else的替代写法

实际项目中的例子: image.png 程序中大量的if-else阅读起来比较吃力,但实际我们可以有很多写法来代替。下面会举例一些场景的代码,再结合设计模式中的一些思想,说说怎么处理这些多重条件的情况更好。

1 策略模式

定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

JavaScript语言的特性,决定了我们实用策略模式很简单。

// 坏味道
function travel(type) {
  if (type === "plane") {
    //...
  } else if (type === "train") {
    //...
  } else if (type === "bus") {
    //...
  }
}

// 推荐
function travel(type) {
  const options = {
    plane: () => {},
    train: () => {},
    bus: () => {},
  };

  return options[type]();
}

2 迭代器模式

定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示,又叫遍历器。

Array.prototype.includes采用的就是迭代器的思想:

// 坏味道
if (color === "red" || color === "green" || color === "blue") {
  console.log(color + "属于三原色");
}

// 推荐
if (["red", "green", "blue"].includes(color)) {
  console.log(color + "属于三原色");
}

我们再看一个更复杂的文件上传例子

// 迭代器实现 上传文件
// 坏味道
var getUploadObj = function () {
  try {
    return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE 上传控件
  } catch (e) {
    if (supportFlash()) {
      // supportFlash 函数未提供
      var str = '<object type="application/x-shockwave-flash"></object>';
      return $(str).appendTo($("body"));
    } else {
      var str = '<input name="file" type="file"/>'; // 表单上传
      return $(str).appendTo($("body"));
    }
  }
};

// 推荐
var getActiveUploadObj = function () {
  try {
    return new ActiveXObject("EXFTNActiveX.FTNUpload");
  } catch (e) {
    return false;
  }
};

var getFlashUploadObj = function () {
  if (supportFlash()) {
    var str = '<object type="application/x-shockwave-flash"></object>';
    return $(str).appendTo($("body"));
  }
  return false;
};

var getFormUpladObj = function () {
  var str = '<input name="file" type="file" class="ui-file"/>'; // 表单上传
  return $(str).appendTo($("body"));
};

var uploadObj = iteratorUploadObj(
  getActiveUploadObj,
  getFlashUploadObj,
  getFormUpladObj
);

其中迭代器可以这样去实现

// 迭代器
var iteratorUploadObj = function () {
  for (var i = 0, fn; (fn = arguments[i++]); ) {
    var uploadObj = fn();
    if (uploadObj !== false) {
      return uploadObj;
    }
  }
};

3 职责链模式

定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间 的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

我们利用after去实现职责链

Function.prototype.after = function (fn) {
  var _this = this;
  return function () {
    var ret = _this.apply(this.arguments);
    fn.apply(this, arguments);
    return ret;
  };
};

同样是上面那个文件上传的例子,用迭代器模式这样实现

// 职责链模式
var getActiveUploadObj = function () {
  try {
    return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE 上传控件
  } catch (e) {
    return "nextSuccessor";
  }
};

var getFlashUploadObj = function () {
  if (supportFlash()) {
    var str = '<object type="application/x-shockwave-flash"></object>';

    return $(str).appendTo($("body"));
  }

  return "nextSuccessor";
};

var getFormUpladObj = function () {
  return $('<form><input name="file" type="file"/></form>').appendTo($("body"));
};

var getUploadObj = getActiveUploadObj
  .after(getFlashUploadObj)
  .after(getFormUpladObj);

console.log(getUploadObj());

4 状态模式

定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

Light.prototype.buttonWasPressed = function () {
  if (this.state === "off") {
    console.log("弱光");
    this.state = "weakLight";
  } else if (this.state === "weakLight") {
    console.log("强光");
    this.state = "strongLight";
  } else if (this.state === "strongLight") {
    console.log("关灯");
    this.state = "off";
  }
};

// 状态模式
var Light = function () {
  this.currState = FSM.off; // 设置当前状态
  this.button = null;
};
Light.prototype.press = function () {
  this.currState.buttonWasPressed.call(self); // 把请求委托给 FSM 状态机
};
var FSM = {
  off: {
    buttonWasPressed: function () {
      console.log("弱光");
      this.currState = "weakLight";
    },
  },
  weakLight: {
    buttonWasPressed: function () {
      console.log("强光");
      this.currState = "strongLight";
    },
  },
  strongLight: {
    buttonWasPressed: function () {
      console.log("关灯");
      this.currState = "off";
    },
  },
};
var light = new Light();
light.press();

时间的写法

// 坏味道
const time = 180000;

// 推荐
const time = 3 * 60 * 1000;

坏味道:直接写成毫秒,不直观。

推荐:时间推荐写成day * hour * minute * second * millisecond的形式,能很直观的看出表示的时间。

四、团队需要完善的地方

  1. 团队编码规范
  2. eslint统一规范
  3. 定期code review

五、参考

一名合格前端工程师必备素质:代码整洁之道

你所需要知道的代码整洁之道

JavaScript轻量级函数式编程