如何写好JS(2) | 青训营笔记

100 阅读4分钟

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

和月影大佬继续学习JavaScript,这次主要是学习写代码时最应该注意什么,

  • 风格
  • 效率
  • 约定
  • 使用场景
  • 设计

以上几点其实都是很重要的,但是主要还是要根据场景来判定。

曾经npm就发生过很有名的left-pad时间,原因就是npm中的包依赖关系太强,而left-pad作者下架了这个包从而导致ReactBabel等工具无法使用。也是因为这个原因,社群中的程序员看到left-pad代码之后认为left-pad代码不够符合规范,不够效率。以下是left-pad代码:

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模块粒度的问题,代码风格问题、代码质量/效率问题等,借这个例子我想要表达的是千人千面,可能会有很多中不同的写法,但是重要的是我们需要按照场景选择最优写法。

接下来让我们想想怎么写一个交通灯切换,月影大佬介绍了4种方法,我们都来看看

写法一

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

JavaScript

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

写法一是最容易想到的写法,通过setTimeout来改变css样式名以打到红绿灯切换的功能,但是这么写很容易陷入“回调地狱”问题中。

写法二(数据抽象)

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

JavaScript

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

方法二改善了“回调地狱”的问题,但是代码的可读性并不高。

方法三(过程抽象)

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

JavaScript

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

方法三使用过程抽象的方法,将中间过程抽象出来,虽然在可读性并没有提升,但是这么做的好处是代码复用性得到了很好的提升。

方法四(异步+函数式)

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

JavaScript

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

方法四使用异步async、wait使异步操作如同同步操作一般丝滑,既易于理解,也美观。

洗牌-错误写法

许多人包括我一开始没有深入理解的时候,都认为洗牌算法就是通过调用Math.random()就能实现真正的随机算法,这里我引入一个例子来证明Math.random()实际上并非真正的随机

var times = [0, 0, 0, 0, 0];

for (var i = 0; i < 100000; i++) {
    
    let arr = [1, 2, 3, 4, 5];
    
    arr.sort(() => Math.random() - 0.5);
    
    times[arr[4]-1]++;

}

console.log(times)

输出的结果是:

[30636, 30906, 20456, 11743, 6259]

我们可以明显的看出每个数字出现的次数并不是随机的,而是从头到尾依此减少的,那么真正的乱序算法应该怎么写呢?这里的算法叫做Fisher–Yates,因为这是这两位作者发明的,算法如下:

function shuffle(a) {
    var j, x, i;
    for (i = a.length; i; i--) {
        j = Math.floor(Math.random() * i);
        x = a[i - 1];
        a[i - 1] = a[j];
        a[j] = x;
    }
    return a;
}

原理很简单,就是遍历数组元素,然后将当前元素与以后随机位置的元素进行交换,从代码中也可以看出,这样乱序的就会更加彻底。这里再写一个demo来验证一下

var times = 100000;
var res = {};

for (var i = 0; i < times; i++) {
    var arr = shuffle([1, 2, 3]);

    var key = JSON.stringify(arr);
    res[key] ? res[key]++ :  res[key] = 1;
}

// 为了方便展示,转换成百分比
for (var key in res) {
    res[key] = res[key] / times * 100 + '%'
}

console.log(res)

结果如下:

image.png

可以看出我们已经实现了真正的乱序