跟着月影学JavaScript(下) | 青训营笔记

88 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的的第5天,今天的课程是「跟着月影学JavaScript」,老师主要讲解了 如何评判代码 、 Leftpad事件的反思 、 交通灯案例 、 简单算法题 、 洗牌思路 、 分红包算法 等内容。

评估一段代码

看如下这样一段代码:

get layerTransformInvert() {
    if(this[_layerTransformInvert]) return this[_layerTransformInvert];
    const m = this.transformMatrix;
    if(m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0) {
      return null;
    }
    this[_layerTransformInvert] = mat2d.invert(m);
    return this[_layerTransformInvert];
  }

乍一看第二个if语句中写的十分啰嗦,但在这里却是更优的设计。这段代码用于渲染,每秒要调用很多次,如果使用for循环,就会在性能上有所欠缺,此处一个个进行比对反而是一种优势。因此每段代码的风格不能一眼进行评判,可能有独特的思想和道理。一段代码写的好与否,是要根据代码实际的使用场景去评判的。

当年的Leftpad事件

Leftpad是一个用于自动补齐的函数,之前应用于许多场景,但是后来该函数的作者将这个函数给下了,导致了很多库都不能用了,引起了广泛的吐槽。 Leftpad函数如下:

function leftpad(str, len, ch) {
      str = String(str);
      var i = -1;
      if (!ch && ch !== 0) ch = ' ';
      len = len - str.length;
      while (++i < len) {
          str = ch + str;
      }
      return str;
  } 

该事件本身的槽点:

  • NPM 模块粒度
  • 代码风格
  • 代码质量/效率

因此之后有人重构了代码,使之效率提升的同时又更加简洁:

function leftpad(str, len, ch) {
      str = "" + str;
      const padLen = len - str.length;
      if(padLen <= 0) {
        return str;
      }
      return (""+ch).repeat(padLen)+str;
  } 

交通灯状态切换

实现一个切换多个交通灯状态切换的功能。

版本一

HTML:

<ul id="traffic" class="wait">
  <li></li>
  <li></li>
  <li></li>
  <li></li>
  <li></li>
</ul>

CSS:

#traffic {
  display: flex;
  flex-direction: column;
}

#traffic li{
  list-style: none;
  width: 50px;
  height: 50px;
  background-color: gray;
  margin: 5px;
  border-radius: 50%;
}

#traffic.s1 li:nth-child(1) {
  background-color: #a00;
}

#traffic.s2 li:nth-child(2) {
  background-color: #aa0;
}

#traffic.s3 li:nth-child(3) {
  background-color: #0a0;
}

#traffic.s4 li:nth-child(4) {
  background-color: #a0a;
}

#traffic.s5 li:nth-child(5) {
  background-color: #0aa;
}

JS:

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

(function reset(){
  traffic.className = 's1';
  
  setTimeout(function(){
      traffic.className = 's2';
      setTimeout(function(){
        traffic.className = 's3';
        setTimeout(function(){
          traffic.className = 's4';
          setTimeout(function(){
            traffic.className = 's5';
            setTimeout(reset, 1000)
          }, 1000)
        }, 1000)
      }, 1000)
  }, 1000);
})();

总结:这样的代码很难去维护,因此代码需要进行优化。

版本二(数据抽象)

HTML:

<ul id="traffic" class="wait">
  <li></li>
  <li></li>
  <li></li>
</ul>

CSS:

#traffic {
  display: flex;
  flex-direction: column;
}

#traffic li {
  display: inline-block;
  width: 50px;
  height: 50px;
  background-color: gray;
  margin: 5px;
  border-radius: 50%;
}

#traffic.stop li:nth-child(1) {
  background-color: #a00;
}

#traffic.wait li:nth-child(2) {
  background-color: #aa0;
}

#traffic.pass li:nth-child(3) {
  background-color: #0a0;
}

JS:

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

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

function start(traffic, stateList){
  function applyState(stateIdx) {
    const {state, last} = stateList[stateIdx];
    traffic.className = state;
    setTimeout(() => {
      applyState((stateIdx + 1) % stateList.length);
    }, last)
  }
  applyState(0);
}

start(traffic, stateList);

总结:将交通灯的状态抽象出来,形成一个状态列表statelist,然后通过方法递归调用,使得状态进行对应改变,就比较优雅地实现了需求。但还可以进一步优化。

版本三(过程抽象)

JS文件:

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

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();
  }
}());

总结:这个版本中将过程抽象出来,可以扩展到其他领域。但是代码非常复杂,要反思是否过度抽象了。

版本四(异步+函数式)

JS文件:

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

function wait(time){
  return new Promise(resolve => setTimeout(resolve, time));
}

function setState(state){
  traffic.className = state;
}

async function start(){
  //noprotect
  while(1){
    setState('wait');
    await wait(1000);
    setState('stop');
    await wait(3000);
    setState('pass');
    await wait(3000);
  }
}

start();

总结:该方法最简单,也最容易理解。

判断是否是4的幂(leetcode简单题)

HTML:

<input id="num" value="65536"></input>
<button id="checkBtn">判断</check>

CSS:

#num {
  color: black;
}

#num.yes {
  color: green;
}

#num.no {
  color: red;
}

JS:

// 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);
// }

function isPowerOfFour(num){
  num = parseInt(num);
  
  return num > 0 &&
         (num & (num - 1)) === 0 &&
         (num & 0xAAAAAAAAAAAAA) === 0;
}

num.addEventListener('input', function(){
  num.className = '';
});

checkBtn.addEventListener('click', function(){
  let value = num.value;
  num.className = isPowerOfFour(value) ? 'yes' : 'no';
});

PS:
1、a & (a - 1):一个正整数a&a-1会使a的二进制里面的1减少一个。
2、满足num > 0 && (num & (num - 1)) === 0这两个条件,则num一定是2的幂。

洗牌

错误写法

HTML:

<div id="app">洗牌-错误写法</div>
<hr/>
<div id="log"></div>
<script>
  window.console = JCode.logger(log);
</script>

JS:

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

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

console.log(shuffle(cards));

const result = Array(10).fill(0);

for(let i = 0; i < 1000000; i++) {
  const c = shuffle(cards);
  for(let j = 0; j < 10; j++) {
    result[j] += c[j];
  }
}

console.table(result);

image.png 总结:进行1000000次循环,可见数字越小的牌出现在前面的概率越大。因为用的是sort方法,交换次数不均,所以数字越小的牌被换到后面的可能性越小。

正确写法

JS:

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;
}

console.log(shuffle(cards));

const result = Array(10).fill(0);

for(let i = 0; i < 10000; i++) {
  const c = shuffle(cards);
  for(let j = 0; j < 10; j++) {
    result[j] += c[j];
  }
}

console.table(result);

使用生成器

HTML:

<div id="app">洗牌-生成器</div>
<hr/>
<div id="log"></div>
<script>
  window.console = JCode.logger(log);
</script>

JS:

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

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);
console.log([...result]);

分红包

切西瓜法

HTML:

<h2>红包生成器</h2>
<div id="setting">
  <div><label>红包金额:<input id="amount" value=100.00></input></label></div>
  <div><label>红包数量:<input id="count" value="10"></input></label></div>
  <div><button id="generateBtn">随机生成</button></div>
</div>

<ul id="result">
</ul>

CSS:

#setting button {
  margin-top: 10px;
}

#result {
  padding: 0;
}

#result li {
  list-style: none;
}

JS:

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--;
  }
  return ret;
}

const amountEl = document.getElementById('amount');
const countEl = document.getElementById('count');
const generateBtn = document.getElementById('generateBtn');
const resultEl = document.getElementById('result');

generateBtn.onclick = function(){
  let amount = Math.round(parseFloat(amountEl.value) * 100);
  let count = parseInt(countEl.value);
  
  let output = [];
  
  if(isNaN(amount) || isNaN(count) 
     || amount <= 0 || count <= 0){
    output.push('输入格式不正确!');
  }else if(amount < count){
    output.push('钱不够分')
  }else{
    output.push(...generate(amount, count));
    output = output.map(m => (m / 100).toFixed(2));
  }
  resultEl.innerHTML = '<li>' + 
                        output.join('</li><li>') +
                       '</li>';
}

image.png 切西瓜法每次都选取最大的数值进行切分,但这种方法的算法复杂度不是很好,不算最优的算法,不过一般也够用。例如微信红包就会有抢红包的人数上限。

抽牌法

JS:

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];
  }
}

// 0, 1, 2....9999
// 49 199
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 = [0];
  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;
}

const amountEl = document.getElementById('amount');
const countEl = document.getElementById('count');
const generateBtn = document.getElementById('generateBtn');
const resultEl = document.getElementById('result');

generateBtn.onclick = function(){
  let amount = Math.round(parseFloat(amountEl.value) * 100);
  let count = parseInt(countEl.value);
  
  let output = [];
  
  if(isNaN(amount) || isNaN(count) 
     || amount <= 0 || count <= 0){
    output.push('输入格式不正确!');
  }else if(amount < count){
    output.push('钱不够分')
  }else{
    output.push(...generate(amount, count));
    output = output.map(m => (m / 100).toFixed(2));
  }
  resultEl.innerHTML = '<li>' + 
                        output.join('</li><li>') +
                       '</li>';
}

下半场的回放主要是老师对算法的一些分析,如何才能写出时空复杂度更低的代码,如何才能更准确地实现我们所需的功能。由此看来,在前端领域之中,算法依然是很重要的内容,要把数学能力提升上去,写出更优雅的代码!