这是我参与「第四届青训营 」笔记创作活动的的第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、前端组件、文件目录、部门
练习题:使用组件模式实现一个文件夹结构
- 为个文件夹可以包含文件和文件夹
- 文件有大小
- 可以获取每个文件夹下文件的整体大小