写好JavaScript的原则(2)
这是我参与「第四届青训营」笔记创作活动的的第5天!
思想: 关注一段代码的好与坏,并不完全取决于代码是否优雅
,有时候比较低级的代码以及冗长的代码反而有着较高的性能
,这取决于我们是否从需要以性能作为指标,如果在数据量较小的前提下,那么则可以考虑优雅性
本文将通过介绍一些例子,来分析代码的优化~
LeftPad的优化
将while循环变成repeat进行优化,为啥repeat能优化呢?
- 由于repeat进行
快速幂
的优化
--- repeat 伪码如下 ---
let result = ''
while (n) {
if (n % 2 == 1) {
result += string
}
if (n > 1) {
string += string
}
n >>= 1
}
repeat(5, '*')
/*
* 当对2取余为1时,说明是一个奇数,则进行一次拼接
* 如果此时n是大于1的情况,那么可以再拼接一次,这就实现了一次循环两次拼接的效果
* 最后进行除2操作
*/
交通灯状态
方法一: 直接通过setTimeOut进行嵌套,通过递归的方式,在最里层再次调用外层方法即可
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);
})();
方法二: 以数据抽象的方式,将数据对象抽离出来,依旧是自己调自己的递归调用,更改每次传递的索引值
<style>
#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;
}
</style>
<ul id="traffic" className="wait">
<li></li>
<li></li>
<li></li>
</ul>
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);
方法三: 通过async和await并结合过程抽象来实现
- +wait(),专门用来做等待操作,方法主体为一个Promise,里面进行一个setTimeOut异步操作
- +setState(),用来做状态的改变和wait方法调用,每一次执行就是一次切换加等待
- +poll(),依赖于闭包思想,并且结合bind和apply的延迟调用,通过传入的setState的bind好的方法列表,在内部进行apply的调用,调用时依赖于await进行等待操作
具体代码如下:
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();
}
}());
// - 可以不通过traffiStatePoll的方式 -
// 直接以setState加wait方法的方式加在while里面来实现
async function start(){
//noprotect
while(1){
setState('wait');
await wait(1000);
setState('stop');
await wait(3000);
setState('pass');
await wait(3000);
}
}
判断是否是4的幂
方法一: 直接对4取余,进行除以4的操作,直到结果成为1,也就整除成功,是4的幂。如果中间出现不能整除的情况,又或者除外最后的数不为1,返回false。
- 这是最简单,也是最暴力无脑的方式,效率低下
function isPowerOfFour(num) {
num = parseInt(num);
while(num > 1) {
if(num % 4) return false;
num /= 4;
}
return num === 1;
}
方法二: 按位操作,当二进制的最后两位与0b11做&操作时,如果结果为0b01、0b11、0b10,说明不是4的幂,则直接返回false,其实就是上个方法的按位判断的版本
function isPowerOfFour(num) {
num = parseInt(num);
while(num > 1) {
if(num & 0b11) return false;
num >>>=2;
}
return num === 1;
}
方法三: 极致性能版本(一)
前提:
- num > 0 才有可能是4的幂
- 一个数与该数减1的值做&操作,结果是该数在数量上减少一个1,如 1111 & 1110 => 1110, 1101 & 1100 => 1100, 1000 & 0111 => 0000
- 如果要是4的幂,那么只有保证该数在偶数位上不能是1,如1000(✖️), 100(✔️), 10000(✔️)
- 所以十三个A的十六进制数就能代表01010101…在js中的最大值
function isPowerOfFour(num){
num = parseInt(num);
return num > 0 &&
(num & (num - 1)) === 0 &&
(num & 0xAAAAAAAAAAAAA) === 0;
}
方法四: 极致性能版本(二)
通过转化成字符串,然后转成二进制,以正则表达式的方式进行判断,只有1后面跟着n个(00)的情况代表4的幂
function isPowerOfFour(num) {
num = parseInt(num).toString(2);
return /^1(?:00)*$/.test(num);
}
洗牌算法
✖️ 错误的sort洗牌: 由于sort是前一个数和后一个数进行交互的,所以最前面的数交换到最后面的数概率是比较小的,所以我们以生成一个随机数判断0.5的左右区间再然后进行sort排序的方法是错误
的
function shuffle(cards) {
return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}
✔️ 争取的洗牌算法: 正确的方式还是得通过选一个随机数作为索引和未洗牌部分最后一个取出来交换的方式,保证他它的绝对随机性
[6,4,1,2,5,7,3] => [6,4,3,2,5,7,1] === 随机取出数值1和未洗牌部分的最后一个数值3进行交换
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;
}
✔️ 洗牌生成器: 通过生成器函数generator每次抽取一个牌出来
🎄 应用场景: 一百人里面抽出是十中奖的人,经历十次洗牌就好,无需一次性洗完再取
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]);
分红包问题
- 切西瓜法 => 每次切完找最大的一块在进行切分
- 划分大数组,找几个红包就几个分隔符,依据洗牌算法的生成器生成,第一个分隔符就是就是第一个人的钱,后一个人就减去前面的人的钱就是他的钱