青训营——JavaScript

99 阅读3分钟

Left-pad的实现

版本一

  // 传入三个参数,分别为需要填充的字符串str,填充后的长度len、填充字符ch
 const leftpad = (str, len, ch) => {
     str = String(str) //保证str为字符串
     //对ch的处理,防止null等不合法的值
     if(!ch && ch !== 0) ch = '' 
     let i = 0
     //计算需要填充字符的个数
     len = len - str.length
     while(i++ < len) str = ch + str
     return str
 }

这个版本为命令式编程,虽然逻辑清晰,但是代码冗余,效率低下。

版本二

 //ch设置了默认值''
 const leftpad = (str, len, ch='') => {
     str = "" + str //保证str为字符串
     len = len - str.length
     if (len <= 0) return str
     return ch.repeat(len) + str
 }

这个版本代码比较精简,使用声明式方法repeat,更加高效。

版本三

 const leftpad = (str, len, ch='') => {
     str = "" + str //保证str为字符串
     len = len - str.length
     if (len <= 0) return str
     let rpt = ''
     while(true){
         if ((len & 1) == 1) rpt += ch
         len >>>= 1 //二进制向右移一位
         if (len === 0) break
         ch += ch //将自身重复一倍,如 'aaa' -> 'aaaaaa'
     }
     return rpt + str
 }

这个版本虽然代码略长一些,但是把时间复杂度从O(n)降低到了O(logn)

版本四

 const leftpad = (str, len, ch='') => {
     str = "" + str //保证str为字符串
     len = len - str.length
     if (len <= 0) return str
     let rpt = ''
     do{
         rpt += ch
         ch += ch
         len &= (len-1)
     }while(len)
     return rpt + str
 }

思路:减去1后的数和原数相与,能将最低位的1置0,每次操作都是如此。所以**能操作的次数为原数二进制表示中1的个数**。因此,可以使用这中方法快速得到循环的次数。

交通信号灯算法

现在有个需求,有三个信号灯,按照一定的时间间隔进行切换。

setTimeout回调嵌套

很容易想到的就是使用setTimeout进行递归延迟,完整代码如下

 <!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <title>Document</title>
     <style>
         #traffic li{
             height: 40px;
             width: 40px;
             background-color: gray;
             border-radius: 20px;
             list-style: none;
             margin: 10px;
         }
         #traffic.stop li:nth-child(1){
             background-color: red;
         }
         #traffic.wait li:nth-child(2){
             background-color: yellow;
         }
         #traffic.pass li:nth-child(3){
             background-color: green;
         }
     </style>
 </head>
 <body>
     <ul id="traffic" class="pass">
         <li></li>
         <li></li>
         <li></li>
     </ul>
     <script>
         const traffic = document.getElementById('traffic');
         (function reset(){
             traffic.className = 'stop';
             setTimeout(() => {
                 traffic.className = 'wait';
                 setTimeout(() => {
                     traffic.className = 'pass';
                     setTimeout(reset, 1000)
                 }, 1000)
             }, 1000)
         })()
     </script>
 </body>
 </html>

但是这样有一种回调地狱的感觉,而且要是改成了四个五个六个信号等,reset部分的代码要大改,改起来很麻烦。

状态列表法

我们可以把状态和持续时间放到一个列表中,这样我们循环遍历这个列表,并将对应的类名和持续时间进行应用即可。js部分的代码如下:

 const traffic = document.getElementById('traffic');
 const stateList = [
     {state: 'stop', last: 1000},
     {state: 'wait', last: 2000},
     {state: 'pass', last: 3000}
 ]
 const start = (traffic, stateList) => {
     const len = stateList.length
     const applyState = stateIdx => {
         const {state, last} = stateList[stateIdx]
         traffic.className = state
         setTimeout(() => {
             applyState((stateIdx + 1) % len)
         }, last)
     }
     applyState(0)
 }
 start(traffic, stateList)

可以看到,如果我们想修改或增加状态,对stateList进行修改即可,且代码清晰易懂。

Promise封装

我们可以使用Promise对这个过程进行扁平化,代码如下:

 const traffic = document.getElementById('traffic');
 const wait = time => new Promise(resolve => setTimeout(resolve, time))
 const setState = state => traffic.className = state
 const start = async () => {
     for(;;){
         setState('stop')
         await wait(1000)
         setState('wait')
         await wait(2000)
         setState('pass')
         await wait(3000)
     }
 }
 start()

这种方法更加直接,将时间的切换和状态的改变进行拆解,更加灵活,且无需占用空间存贮状态,简明。

洗牌算法

如果我们想让一个数组里的元素随机排序,很容易想到以下方法:利用sort函数的参数和Math.random方法

 // 创建数组[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 let cards = Array(10).fill(0).map((_, index) => index)
 const shuffle = cards => [...cards].sort(() => Math.random() > 0.5 ? -1 : 1)

但是这种方法并不平均,使用以下代码,将这个算法执行1000次,我们统计每个位置的数值之和,以查看每个位置产生的数字概率是否和其他位置的一致。

 const result = Array(10).fill(0)
 for(let i = 0; i < 1000; i++){
     const s = shuffle(cards)
     for(let j = 0; j < 10; j++){
         result[j] += s[j]
     }
 }

这种方法产生的结果如下,我们发现,每个位置的数字之和差距很大,越靠后的数字之和越大,且还需要sort函数排序,时间和空间复杂度较高。

 [3938, 3906, 4489, 4507, 4670, 4309, 4416, 4736, 4882, 5147]

下面我们换一种思路,从前开始,随机和后面的数字进行交换,这样保证了随机性。

 const shuffle = cards => {
     for(let i = 0, len = cards.length; i < len; i++){
         const index = Math.floor(Math.random()*(len-1)) + 1;
         [cards[i], cards[index]] = [cards[index], cards[i]]
     }
     return cards
 }

而用这种方法统计出来的结果,其数值基本上都是在4500左右,比较接近,说明这种方法比较公平,且算法的复杂度很低。

 [4542, 4504, 4481, 4479, 4338, 4531, 4575, 4548, 4536, 4466]

分红包算法

  const generate = (amount, count) => {
      let ret = [amount]
      while(count > 1){
          let cake = Math.max(...ret)
          let idx = ret.indexOf(cake)
          let part = 1 + Math.floor((cake / 2) * Math.random())
          let rest = cake - part
          ret.splice(idx, 1, part, rest)
          count --
      }
      return ret
  }

这种算法的思路是每次只对最大的那份红包进行切分,这样能够保证大家抢到的红包数额相对来说比较均衡。

个人总结

虽然这些代码都是老师PPT上的, 一些经过了我个人的修改,但是自己思考一遍,敲出来,自己实现一把,是有很多收获的, 尤其是在细节方面,只有自己敲出来的才能体会到,对于知识的理解也会更加深入。其中一些关键带啊吗增加了我的个人注释,方便大家理解。代码都是可以执行的,没有伪代码,希望可以帮助到大家。