JavaScript 优雅之路(基础篇)

553 阅读3分钟

代码规范

为什么需要规范?

因为软件工程化需要协同开发,我们不得不接触其他开发者编写的代码,如果不注重编程规范,随意命名变量,代码毫无章法,阅读起来晦涩难懂,那么我们维护一个大型项目的难度势必登天。

选择代码风格

一种经过考虑,被大众接受的代码无疑会使代码阅读起来更熟悉、清爽。这里推荐ESLint进行代码规范和团队分享,你可以根据项目实际需求,选用一种风格或是进行自定义,并可设置一键代码格式化。

  1. Airbnb JavaScript Style
  2. JavaScript Standard Style
  3. Google JavaScript Style

声明

  • 命名应一目了然
    • 统一遵循驼峰命名或连字符命名法
    • 形参简写,减少无意义的说明,复数加s
    • 使用$作为存储jQuery对象的变量名前缀
    • 类和组件首字母大写,常量全大写,文件名全小写
  • 在当前作用域最顶部声明变量
    • 由于 js 声明提升的特性,变量不应在声明前使用
    • 使用字面量创建引用对象,尽可能的使用letconst进行声明
    • 为了直观的确定变量类型,最好在声明时给变量赋一个默认值
  • 若变量或方法绑定在全局对象window下,需要显式调用
// bad
var a, b;

// good
let num;
let obj;

// best
let num = 0;
const obj = {};  // 如果不希望对象的引用被修改

// bad
var car = {
  userName: 'tom',
  carColor: 'red'
};

// good
const car = {
  user: 'tom',
  color: 'red', // 换行保留逗号,提交记录更清爽
};

let arr = new Array(); // bad
let arr = [];  // good

const ul = $('.sidebar ul');  // bad
const $ul = $('.sidebar > ul'); // good

window.myFuc = function(){};
myFuc();  // bad
window.myFuc();  // good

代码块

  • 尽早return,减少代码嵌套
  • 为了使逻辑更清晰,应在逻辑语句外声明函数
  // bad
  function logic(bool) {
    if (bool) {
      something...
      return;
    } else {
      someElse...
    }
  }
  
  // good
  function logic(bool) {
    if (bool) {
      return doSomething()
    }
    doSomeElse()
  }
  function doSomething(){}
  function doSomeElse(){}

逻辑判断

大量的逻辑判断往往产出臃肿的代码,让我们更优雅地处理逻辑。

  • 由于 js 弱语言类型的特性
    • 除了undefinednull0NaN''false ,抽象方法 ToBoolean 会将其判断为 true
    • 使用简写,优先使用 ===!== ,在某些情况下逻辑判断会发生类型转换, ==!=是不严谨的
  • 巧用运算符
    • & 判断奇偶: 0001 & 0011 => 0001 (两位都是1才是1)
    • | 数字取整: 0001 | 0011 => 0011 (两位都是0才是0)
    • << 左移求幂: 1 << 2 => 4
if (name !== '') // bad
if (name)  // good

if (list.length > 0) // bad
if (list.length) // good

if (str == '2')  // bad
if (str === '2') // good

if ('red' === color  || 'blue' === color || 'pink' === color) // bad
if (['red', 'blue', 'pink'].includes(color)) // good

if (num % 2) // 奇数
if (num & 1) // 奇数

// 注意
if (true == []); // => false
if (true == ![]); // => false

我们知道,在js中利用短路与&&,短路或||求值很特别,举例来说:

let a1 = 0 && 'zero'; // 0
let a2 = '0' && 'zero'; // 'zero'
let a3 = '0' && NaN && 'zero'; // NaN

let b1 = '' || 'text'; // 'text'
let b2 = 'str' || 'text'; // 'str'
let b3 = undefined || 'text' || ''; // 'text'

let c = false && '0' || 'text' && null; // null
  • &&: 若每项都可转换为真,整体值为最后一个值,反之整体值为第一个假值
  • ||: 若每项都可转换为假,整体值为最后一个值,反之整体值为第一个真值
  • 优先级 &&||

举个例子,计算员工奖金:由于 tom、amy 业绩突出,分别奖励 5k,3k,其他人则为1k。

可以这样实现:

let bonus = 1000 * ('tom' === staff && 5 || 'amy' === staff && 3 || 1);

也可利用对象键值对之间的映射关系:

let bonus = 1000 * ({ tom: 5, amy: 3 }[staff] || 1);

如果需要处理更复杂的逻辑:

( { tom: tomDo, amy: amyDo }[staff] || elseDo )(); // 加()包裹防止js引擎将{}解析成代码块
function tomDo() {};
function amyDo() {};
function elseDo() {};

过滤falsy值

let res = [0, 1, '2', '', undefined, null, false, NaN].filter(Boolean); // [1,'2']

缓存

在循环中缓存中间变量,避免每次访问值顺着链条查找,语义清晰,性能更好

len = data.length 现代浏览器已经做了优化,视情况可定

// bad 
for (var i = 0; i < data.length; i++) {
  data[i].index = i;
  data[i].type = 'object';
}

// good
for (let i = 0, len = data.length; i < len; i++) {
  const el = data[i];
  el.index = i;
  el.type = 'object';
}

// bad
$('.sidebar').show();
$('.sidebar').css({'width': '50px'});

// good
const $sidebar = $('.sidebar');
$sidebar
  .show()
  .css({'width': '50px'});

我们常常对变量验证赋值:

const person = {};
if (getSex()) person.sex = getSex(); // 'man'
if (getCar()) person.car = getCar(); // { id: 'abc' }
if (getName()) person.name = getName(); // 'tom'

观察一下,这里面get方法执行了两遍,如果get里进行了大量运算,无疑是一种性能损失

你说这简单:

const person = {};
const sex = getSex();
const car = getCar();
const name = getName();
if (sex) person.sex = sex;
if (car) person.car = car;
if (name) person.name = name;

这次仅执行了一遍,但是声明了更多的变量,代码看起来更臃肿了

我们可以利用中间变量,缓存每个属性的值/址:

let temp = null;
const person = {};
if (temp = getSex()) person.sex = temp;
if (temp = getCar()) person.car = temp;
if (temp = getName()) person.name = temp;

其中temp = getSex()整体value值为运算的结果

函数

  • 函数的参数个数不应超过三个,太多的参数不利于拓展重构
  • 参数过多可以将功能拆分,或将其组合成一个对象, 使用解构赋值并设置默认值
  • 如果调用函数时,是根据参数真假进行逻辑判断,可以利用字符串进行辅助说明
  • 不考虑this指向时尽量使用函数声明,函数表达式的变量名会被提升,但函数内容并不会
// bad
function ajax(url, data, method, success, error) {}
  
// good
function ajax({ url, data, method = 'GET', success, error }) {}


function getChildren(isDeep, hasBrother){}

// bad
getChildren(true, false);

// good
getChildren('isDeep', !'hasBrother');
console.log(named); // => undefined

named(); // => TypeError named is not a function

superPower(); // => ReferenceError superPower is not defined

var named = function superPower() {
};

两值交换

let a = 1;
let b = 2;
a = [b, b=a][0];
[a, b] = [b, a];

// 交换引用类型的两个属性
const obj = { c: 1, d: 2 };
const { c, d } = obj;
[obj.c, obj.d] = [d, c];

数组去重

我们有很多种方法实现这个常见需求,例如循环判断。但这种命令式的编程既繁琐,语义化也不强,下面我们将优雅地实现这个功能。

  • Mapfilter
function unique(arr) {
  const map = new Map();
  return array.filter(el => map.has(el) ? false : map.set(el));
}
function unique(arr) {
  return arr.filter((el, index, array) => index === array.indexOf(el))
}
  • reduce
const unique = arr => arr.reduce((a, c) => a.includes(c) ? a : a.concat(c), []);
  • Set
const unique = arr => [...new Set(arr)];
const unique = arr => Array.from(new Set(arr));

正则表达式

  • 正则可以对字符串进行过滤、匹配、提取,简洁强大。暂且按下不表,举几个小例子
let price = "¥35.5/kg";
price.replace(/[^(0-9).]/ig,'') //35.5

tpl = tpl.replace(/(\r|\n)/ig, ''); // 去除换行符

数组扁平化

  • stack
function flat(arr) {
  let res = [];
  let stack = [];
  function forEachRight(arr) {
    for (let i = arr.length - 1; i >= 0; i--) {
      stack.push(arr[i]);
    }
  }
  forEachRight(arr);
  while (stack.length) {
    let cur = stack.pop();
    if (Array.isArray(cur)) forEachRight(cur);
    else res.push(cur);
  }
  return res;
}
  • JSON.parseJSON.stringify
const flat = arr => JSON.parse(`[${JSON.stringify(arr).replace(/\[|]/g, '')}]`);
  • reduce
const flat = arr => arr.reduce((a, c) => a.concat(Array.isArray(c) ? flat(c): c), []);
  • while
const flat = arr => {
  while(arr.some(item => Array.isArray(item))) {
    arr = [].concat(...arr)
  }
  return arr
};
  • Generator
function* flat(arr) {
  for (const a of arr) {
    if (Array.isArray(a)) {
      yield* flat(a)
    } else {
      yield a
    }
  }
}
[...flat(arr)];

为指定索引插入元素

Array.prototype.insertItem = (index, item) => {
  const head = this.slice(0, index);
  const foot = this.slice(index, this.length);
  return [...head, item, ...foot];
}