如何写好js(下)| 青训营笔记

70 阅读3分钟

这是我参与「第四届青训营 」笔记创作活动的第2天

功能实现--代码优化

为什么需要代码优化?

  • 提高性能
  • 增强可维护性
  • 提高复用性

优化方法

可以考虑以下几个方面:

  • 运算复杂度
  • 效率
  • 简洁性
  • 严禁度(BUG)

数据抽象

数据抽象是指:把需要用到的数据通过对象的形式用字符串或者数组抽象描述出来。 比如:交通灯功能实现:

const stateList = [
  {state: 'wait', last: 1000},
  {state: 'stop', last: 3000},
  {state: 'pass', last: 3000},
];

过程抽象(便于维护和扩展)

过程抽象是指:把每一个功能抽离出来单独作为一个函数。最后通过不同功能的函数组合实现最终效果。

缺陷:代码量多,较为复杂


const traffic = document.getElementById('traffic');

//通过promise的方式替代计时器进行延时操作
function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function poll(...fnList){
  let stateIndex = 0;
  
  return async function(...args){
    let fn = fnList[stateIndex++ % fnList.length];
    return await fn.apply(this, args);
  }
}

async function setState(state, ms){
  traffic.className = state;
  await wait(ms);
}

let trafficStatePoll = poll(setState.bind(null, 'wait', 1000),
                            setState.bind(null, 'stop', 3000),
                            setState.bind(null, 'pass', 3000));

(async function() {
  // noprotect
  while(1) {
    await trafficStatePoll();
  }
}());

🌰判断是否是四的幂

// function isPowerOfFour(num) {
//   num = parseInt(num);

//   while(num > 1) {
//     if(num % 4) return false;
//     num /= 4;
//   }
//   return num === 1;
// }

// function isPowerOfFour(num) {
//   num = parseInt(num);

//   while(num > 1) {
//     if(num & 0b11) return false;
//     num >>>=2;
//   }
//   return num === 1;
// }

// function isPowerOfFour(num) {
//转换为二进制字符串,然后用正则进行匹配
//   num = parseInt(num).toString(2);
  
//   return /^1(?:00)*$/.test(num);
// }

🌰洗牌

错误写法:使用sort进行随机排序。(实际上是不均匀的,索引值越大,出现的概率也越大),本质原因是sort方法每个位置交换的次数不一样

const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

function shuffle(cards) {
  return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}

正确写法:每次随机抽取一张牌放到最后,末尾的排保持不变。后面依次选一张放到倒数第二位,类推。这样在一次抽取中,抽到每一张牌的概率相同。

const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

function shuffle(cards) {
  const c = [...cards];
  for(let i = c.length; i > 0; i--) {
    const pIdx = Math.floor(Math.random() * i);
    [c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
  }
  return c;
}

生成器版:不需要洗完所有的牌,取出十张就行

function * draw(cards){
    const c = [...cards];

  for(let i = c.length; i > 0; i--) {
    const pIdx = Math.floor(Math.random() * i);
    [c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
    yield c[i - 1];
  }
}

const result = draw(cards);

🌰分红包

切西瓜法(均匀性大)

思路:第一次随机切成两部分,第二次从较大的那一块开始切。每次切都从最大的那部分开始分。

初版本:

function generate(amount, count){
  let ret = [amount];
  
  while(count > 1){
    //挑选出最大一块进行切分
    let cake = Math.max(...ret),
        idx = ret.indexOf(cake),
        part = 1 + Math.floor((cake / 2) * Math.random()),
        rest = cake - part;
    
    ret.splice(idx, 1, part, rest);
    //切一次count减一
    count--;
  }
  return ret;
}

抽牌法(空间复杂度高)

思路:所有的钱看作一个序列,通过随机插入10个值。(此处以10份举例) 每两个相邻值的差为分到的钱

function * draw(cards){
  const c = [...cards];

  for(let i = c.length; i > 0; i--) {
    const pIdx = Math.floor(Math.random() * i);
    [c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
    yield c[i - 1];
  }
}
function generate(amount, count){
  if(count <= 1) return [amount];
  const cards = Array(amount - 1).fill(0).map((_, i) => i + 1);
  const pick = draw(cards);
  const result = [];
  for(let i = 0; i < count - 1; i++) {
    result.push(pick.next().value);
  }
  result.sort((a, b) => a - b);
  result.push(amount);
  for(let i = result.length - 1; i > 0; i--) {
    result[i] = result[i] - result[i - 1];
  }
  return result;
}