JavaScript 代码质量优化之路|青训营

129 阅读9分钟

JavaScript 代码质量优化之路

交通灯状态切换

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

交通灯:版本一

首先是 once 函数的定义:

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

这段JavaScript代码创建了一个用于交通指示灯动画的函数,并将其立即调用。

首先,通过 document.getElementById('traffic') 获取到 traffic 元素,然后定义了一个名为 reset 的函数。

reset 函数的作用是设置 traffic 元素的类名(className)为 's1',即设置交通指示灯的初始状态。

接下来,通过 setTimeout 函数在一定的时间间隔后依次改变 traffic 元素的类名,模拟交通指示灯的变化。通过嵌套的 setTimeout 函数,实现了一个循环,使交通指示灯可以根据一定的时间间隔不断重复变化。

在每个 setTimeout 的回调函数中,利用赋值语句 traffic.className = 's2';traffic.className = 's3';traffic.className = 's4';traffic.className = 's5';traffic 元素的类名更新为 's2''s3''s4''s5'。这样就可以通过改变类名来触发不同状态下的交通指示灯颜色的变化。

最后,在最内层的 setTimeout 回调函数中,调用了 reset 函数,实现了动画的循环播放。

整体上,这段代码通过使用定时器和不断改变类名的方式,实现了交通指示灯动画的效果。

交通灯:版本二(数据抽象)

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

这段代码是一个模拟交通信号灯的效果。代码中定义了一个表示交通信号灯的元素节点traffic,以及一个状态列表stateList,其中包含了三个状态: 'wait'、'stop'和'pass',并且每个状态持续的时间。

函数start用于启动交通信号灯的模拟效果。它通过递归调用applyState函数来切换交通信号灯的状态。applyState函数根据stateIdx参数获取当前状态的内容和持续时间,并将traffic元素节点的className设置为对应的状态。然后使用setTimeout函数延迟指定的时间后,再次调用applyState函数,参数为下一个状态的索引(stateIdx + 1) % stateList.length,实现状态的循环切换。

最后,调用start函数,并传入traffic元素节点和stateList状态列表,即可开始交通信号灯的模拟效果。

交通灯:版本三(过程抽象)

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

这段代码也是用于模拟交通信号灯的效果,但与之前的代码不同,它使用了Promise和async/await来实现异步操作。

代码中首先定义了一个元素节点traffic,然后定义了一个wait函数,它返回一个Promise对象,通过setTimeout函数实现延迟指定时间后resolve的功能。

接下来定义了一个poll函数,它接受一组函数作为参数,并返回一个异步函数。这个异步函数会依次调用传入的函数列表,并按循环方式进行循环调用。

然后定义了一个setState函数,它用于设置交通信号灯的状态。在函数中,先设置traffic元素节点的className为指定的状态,然后通过await关键字等待指定的时间。

在变量trafficStatePoll中,通过调用poll函数并传入三个setState函数的绑定版本,创建了一个循环调用的异步函数。

最后,在一个匿名的async函数中,使用无限循环来调用trafficStatePoll函数。通过await关键字等待每次调用的完成后,再进行下一次调用,从而实现交通信号灯的模拟效果。

最后一行的"noprotect"注释是为了使代码在某些在线代码编辑器中能够正常运行,可以忽略它。

优点:把poll过程抽象出来了,方便扩展; 缺点:写的代码太冗长了

交通灯:版本四(异步+函数式)

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的幂

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

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

该函数使用循环和位运算来判断一个数字是否是4的幂。它首先将输入的num转换为整数类型。然后通过循环,不断将num右移两位(相当于除以4),并检查num的最后两位是否为非零值。如果最后两位不全为零,即num & 0b11的结果不为0,那么说明num不是4的幂,函数返回false。如果num全部右移完毕,变为1,那么说明num是4的幂,函数返回true。

function isPowerOfFour(num) {
  num = parseInt(num).toString(2);
  
  return /^1(?:00)*$/.test(num);
}
  1. num = parseInt(num).toString(2);

    • 此行使用基数为 2 的函数将输入转换为二进制字符串表示形式,然后将其转换回字符串。num``parseInt
  2. return /^1(?:00)*$/.test(num);

    • 此行测试二进制字符串是否与指定的正则表达式模式匹配。num
    • 正则表达式匹配以“1”开头的字符串,后跟零个或多次出现的“00”。/^1(?:00)*$/
    • 该方法用于检查二进制字符串是否与模式匹配。如果是这样,该函数返回 ,表明该数字是 4 的幂。否则,它将返回 ..test``num``true``false

总而言之,该函数将输入数字转换为二进制字符串,并检查它是否与 4 的幂模式匹配。如果是这样,则返回 ;否则,它将返回 .isPowerOfFour``true``false

正则匹配方法速度也很快。

function isPowerOfFour(num){
  num = parseInt(num);
  
  return num > 0 &&
         (num & (num - 1)) === 0 &&
         (num & 0xAAAAAAAAAAAAA) === 0;
}   
  • //a&(a-1)
  • //x....1{k个0} & x....0{k个1} ->x...{k+1个0}

函数逻辑:

  • 首先,将传入的num转换为整数。

  • 然后,通过位运算和逻辑运算来确定num是否为4的幂。

    • 第一个条件 (num & (num - 1)) === 0 检查num是否为2的幂,即是否只有一个比特位是1.
    • 第二个条件 (num & 0xAAAAAAAAAAAAA) === 0 检查num在二进制表示下是否满足4的幂的特征:只有一个1,并且这个1的位置必须是第偶数个比特位(从0开始计数)。
  • 最后,如果以上两个条件都满足,则返回true,否则返回false。

这个版本的复杂度是O(1),很神奇的方法。

&

  • 当与两个数字一起使用时,它会对它们的二进制表示形式执行按位 AND 运算。它比较两个数字的相应位,并返回一个新数字,其中每个位仅当两个数字的相应位均为 1 时才设置为 1。否则,位设置为 0。
  • 例如, 5 & 3``1``5``101``3``011``001``1

洗牌-错误写法

我们来验证这个洗牌算法的正确性,如何验证呢? 我们将这个洗牌程序重复一百万次,result数组用来记录每个位置出现过的数字之和,如果这是一个公平的算法的话,result数组中的数字应该都很相近。

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

得到的结果是

0,5,7,9,3,2,6,4,1,8
(index)Value
03858256
13870417
24544021
34647499
44670139
54356062
64363476
74723600
84851572
95114958

可以看出这个结果是呈现递增的,而且第一个和最后一个位置的所有数字之和相差还比较大,也就是说,越大的数字出现在数组后面的概率要大一些。每个元素被安排在每个位置的概率是不同的,这是一个不公平的算法。

参考YK菌文章

如何解决这个问题呢?

洗牌-正确写法

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

现有十张牌,在十张中选一张放置在位置10上,固定10位置上的牌不动; 在前面九张牌选一张放到位置9上,固定; 依此类推 这样可以确保每张牌放到任意位置的可能性是均等的,可以用数学归纳法证明。

洗牌-使用生成器

比如我们要抽奖,可以直接取一个任意位置上的元素就行了

Math.floor(Math.random() * length)

但是我们的抽奖是一个过程,比如抽出一等奖,二等奖,三等奖,幸运奖之类的,就需要封装一下,采用我们上面的洗牌算法 将函数改成生成器,将return改成yield,就能够实现部分洗牌,或者用作抽奖

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

也可以只选取部分,实现部分洗牌,或者说抽奖的功能

100个号随机抽取5个

let items = [...new Array(100).keys()];

let n = 0;
// 100个号随机抽取5个
for (let item of shuffle(items)) {
  console.log(item);
  if (n++ >= 5) break;
}
// 24 62 60 16 42 21 

分红包-切西瓜法

优先切最大的那块西瓜

((cake / 2) * Math.random()):此表达式将生成的随机数乘以值的一半,有效地产生介于 0 和cake / 2 之间的随机分数。

Math.floor((cake / 2) * Math.random()):删除小数位并返回小于或等于给定数字的最大整数。

1 + Math.floor((cake / 2) * Math.random()):最后,添加到向下舍入的值,以确保分配的值至少为 1。1``part

总之,这行代码生成一个介于 1 和 cake / 2 之间的随机整数。

分红包-抽牌法

举例:金额 100元 分成 10个红包 相当于数列0~9999(所以该方法空间复杂度高) 插入9个分隔符 分成10份

可以调用前面生成器抽牌算法 抽9个数出来,排序

切西瓜法较为均匀,抽牌法可能会相差很大

总结

前端也需要学习算法,判断哪个算法更高明也是需要根据问题具体需求来看的。

参考文章链接:blog.csdn.net/weixin_4497…