如何写好js代码+前端设计模式应用 | 青训营笔记

184 阅读5分钟

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

如何写好JS代码(下)

当年的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;
  } 

||

V

事件本身的槽点:

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

有 while 循环,所以它实际上它是一个时间复杂度O(n)的代码。

我们要补CH 的时候,就 repeat, repeat 是一个加时功能内置函数。但我们补 repeat 的时候,其实它可以用二次幂的快速的方法去优化,不用线性的 while 循环去加。

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

||

V

  • 代码更简洁
  • 效率提升

二次幂算法:假如说我们要 repeat 一个count ,那个这个时候 N 就等于这个 count,然后我们会去把这个 N 转换成 2 定制数,从那个最末位开始,依次地去判断 N 的每一位的值。如果这个值是 1 的话,我把我就把这个 string 给加到这个 result 上面去。同时这个 N 如果这个时候还有值的话,那么我们就下一次再继续循环来跑的时候。因为我每次循环的时候是把这个 N 给那个就是右移一位的。这个时候每移一位的话,实际上就相当于在二进制里面往高位移动一位。那这个时候把这个 string 乘以 2 就翻倍,所以就把 string 加上 string ,那这样的话就是它是一个二次幂的一个快速幂的算法

时间复杂度O(logn)

 /*! https://mths.be/repeat v1.0.0 by @mathias */

  'use strict';

  var RequireObjectCoercible = require('es-abstract/2019/RequireObjectCoercible');
  var ToString = require('es-abstract/2019/ToString');
  var ToInteger = require('es-abstract/2019/ToInteger');

  module.exports = function repeat(count) {
    var O = RequireObjectCoercible(this);
    var string = ToString(O);
    var n = ToInteger(count);
    // Account for out-of-bounds indices
    if (n < 0 || n == Infinity) {
      throw RangeError('String.prototype.repeat argument must be greater than or equal to 0 and not be Infinity');
    }

    var result = '';
    while (n) {
      if (n % 2 == 1) {
        result += string;
      }
      if (n > 1) {
        string += string;
      }
      n >>= 1;
    }
    return result;
  };
 /**
   * String.prototype.repeat() polyfill
   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat#Polyfill
  */
  if (!String.prototype.repeat) {
    String.prototype.repeat = function(count) {
      'use strict';
      if (this == null)
        throw new TypeError('can't convert ' + this + ' to object');

      var str = '' + this;
      // To convert string to integer.
      count = +count;
      // Check NaN
      if (count != count)
        count = 0;

      if (count < 0)
        throw new RangeError('repeat count must be non-negative');

      if (count == Infinity)
        throw new RangeError('repeat count must be less than infinity');

      count = Math.floor(count);
      if (str.length == 0 || count == 0)
        return '';

      // Ensuring count is a 31-bit integer allows us to heavily optimize the
      // main part. But anyway, most current (August 2014) browsers can't handle
      // strings 1 << 28 chars or longer, so:
      if (str.length * count >= 1 << 28)
        throw new RangeError('repeat count must not overflow maximum string size');

      var maxCount = str.length * count;
      count = Math.floor(Math.log(count) / Math.log(2));
      while (count) {
        str += str;
        count--;
      }
      str += str.substring(0, maxCount - str.length);
      return str;
    }
  }

交通灯状态切换

异步+函数式

<ul id="traffic" class="wait">
  <li></li>
  <li></li>
  <li></li>
</ul>
#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;
}
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的幂

<input id="num" value="65536"></input>
<button id="checkBtn">判断</check>
#num {
  color: black;
}

#num.yes {
  color: green;
}

#num.no {
  color: red;
}
//   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';
});

洗牌-错误写法

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

现在我们发现了一个很有意思的现象,就是我们的 index 越靠前,得到的总的数值就越小。也就是说它意味着什么呢?就意味着越小的这些数值,它的排布越靠前的几率越大。所以我们会看到 0 的时候只有 38 万多只有 380 多万。然后在 9 的时候,实际上有五百多万,就是这个值的分布是这样的,我们越小的序号出现在越前面的概率是越大的。所以就是这个是一个分布不均匀的算法。

那为什么分布不均匀呢?是因为我们用的是 sort 方法的随机交换。但我们知道 sort 方法它不是两位置都均匀的交换的,在每个位置他交换的次数是不一样的,所以他的越靠前的位置换到最后的概率是越低的。

洗牌-正确写法

O(n) 时间复杂度的算法:

我们可以遍历每一张牌。这么做:一开始的时候,我们从这里总的这个牌里面抽一张出来,随机抽一张出来,把它换到最后面的位置去,换到那个位置以后它就不动了。接下来我们再从剩下这 9 张牌里面随机地抽一张牌出来,再把它塞到最后的位置去。然后我们从剩下的八张牌里头再随机取张牌,再把它塞到最后的位置去,就相当于是一张牌随机的抽出来,抽出来以后就把它放在那里。这样是可以确保说那个每张牌被抽到任何一个位置的概率都均等。

就这个问题,也可以通过数学归纳法证明: 假设我们现在只有两张牌,就是 A 和 B 。这个时候我们抽牌的时候,A 有 50% 的概率被抽到和 B 交换。交换后的结果就是最后就得到BA 。还有 50% 概率是直接抽到 B ,直接抽到 B 的话它也是跟 B 自己交换。所以剩下的 50% 的概率就是 AB ,所以概率是均等的。

<div id="app">洗牌-正确写法</div>
<hr/>
<div id="log"></div>
<script>
  window.console = JCode.logger(log);
</script>
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);

洗牌-使用生成器

但是有的时候比如说我们要抽奖,在 100 个人里面,抽出 10 个中奖的,那就没有必要把这 100 张牌全部都洗完,只需要从这 100 张牌里抽出 10 张牌来就可以了。在这种情况下,就是在 javascript 里面,我们可以把刚才的那个版本的代码给改成一个生成器,区别是我们之前是把 for 循环跑完了,然后把整个牌给返回。但现在我们不这么做,不跑完这个 for 循环,我们直接取了一张牌,就直接把这张牌给 add 出来。那么这样我们也一样是能够洗牌的。把牌洗了以后,直接用 sprint 操作符把它展开,这就相当于把这个牌给洗完了。

<div id="app">洗牌-生成器</div>
<hr/>
<div id="log"></div>
<script>
  window.console = JCode.logger(log);
</script>
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 块钱的红包,也就是 1 万分,我们把它看成是一个数列,就是一个从 0 到 99 的数列。在这个数列里面,随机的插入就是一个范围,比如说一直到 99 的一个数列,在这个数列里面,插入 9 个分隔符。比如第一个分隔符在49,这个时候我就把四毛九的钱给他。然后比如说另外一个分隔符在199。那就把一块九毛九减去四毛九,然后把剩下的一块 5 再分给另外一个人。然后往序列里面随机的插入。


前端设计模式应用

软件设计中常见问题的解决方案模型

  • 历史经验的总结
  • 与特定语言无关

设计模式背景

1.模式语言:城镇、建筑、建造 2.设计模式:可复用面向对象软件的基础

23种设计模式

  • 创建型:如何创建一个对象
  • 结构型:如何灵活的将对象组装成较大的结构
  • 行为型:负责对象间的高校通信和职责划分

浏览器中的设计模式

单例模式

定义

全局唯一访问对象

应用场景

缓存,全局状态管理等

发布订阅模式

定义

一种订阅机制,可在被订阅对象发生变化时通知订阅者

应用场景

从系统架构之间的解耦,到业务中一些实现模式,像邮件订阅、上线订阅等,应用广泛。

JS中的设计模式

原型模式

定义

复制已有对象来创建新的对象

应用场景

JS中对象创建的基本模式

代理模式

定义

可自定义控制对原对象的访问方式,并且允许在更新前后做一些额外处理

应用场景

监控、代理工具,前端框架实现等等

迭代器模式

定义

在不保留数据类型的情况下访问集合中的数据

应用场景

数据结构中有多种数据类型,列表、树等,提供通用操作接口。

前端框架中的设计模式

  • 代理模式
  • 组合模式

Vue组件实现定时器

前端框架中对DOM操作的代理

  • 更改DOM属性->视图更新
  • 更改DOM属性->更改虚拟DOM-Diff->视图更新

组合模式

定义

可多个对象组合使用,也可单个对象独立使用

应用场景

DOM、前端组件、文件目录、部门

练习题:使用组件模式实现一个文件夹结构

  • 为个文件夹可以包含文件和文件夹
  • 文件有大小
  • 可以获取每个文件夹下文件的整体大小