这是我参与「第四届青训营 」笔记创作活动的的第5天,今天的课程是「跟着月影学JavaScript」,老师主要讲解了 如何评判代码 、 Leftpad事件的反思 、 交通灯案例 、 简单算法题 、 洗牌思路 、 分红包算法 等内容。
评估一段代码
看如下这样一段代码:
get layerTransformInvert() {
if(this[_layerTransformInvert]) return this[_layerTransformInvert];
const m = this.transformMatrix;
if(m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0) {
return null;
}
this[_layerTransformInvert] = mat2d.invert(m);
return this[_layerTransformInvert];
}
乍一看第二个if语句中写的十分啰嗦,但在这里却是更优的设计。这段代码用于渲染,每秒要调用很多次,如果使用for循环,就会在性能上有所欠缺,此处一个个进行比对反而是一种优势。因此每段代码的风格不能一眼进行评判,可能有独特的思想和道理。一段代码写的好与否,是要根据代码实际的使用场景去评判的。
当年的Leftpad事件
Leftpad是一个用于自动补齐的函数,之前应用于许多场景,但是后来该函数的作者将这个函数给下了,导致了很多库都不能用了,引起了广泛的吐槽。 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;
}
该事件本身的槽点:
- NPM 模块粒度
- 代码风格
- 代码质量/效率
因此之后有人重构了代码,使之效率提升的同时又更加简洁:
function leftpad(str, len, ch) {
str = "" + str;
const padLen = len - str.length;
if(padLen <= 0) {
return str;
}
return (""+ch).repeat(padLen)+str;
}
交通灯状态切换
实现一个切换多个交通灯状态切换的功能。
版本一
HTML:
<ul id="traffic" class="wait">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
CSS:
#traffic {
display: flex;
flex-direction: column;
}
#traffic li{
list-style: none;
width: 50px;
height: 50px;
background-color: gray;
margin: 5px;
border-radius: 50%;
}
#traffic.s1 li:nth-child(1) {
background-color: #a00;
}
#traffic.s2 li:nth-child(2) {
background-color: #aa0;
}
#traffic.s3 li:nth-child(3) {
background-color: #0a0;
}
#traffic.s4 li:nth-child(4) {
background-color: #a0a;
}
#traffic.s5 li:nth-child(5) {
background-color: #0aa;
}
JS:
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);
})();
总结:这样的代码很难去维护,因此代码需要进行优化。
版本二(数据抽象)
HTML:
<ul id="traffic" class="wait">
<li></li>
<li></li>
<li></li>
</ul>
CSS:
#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;
}
JS:
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);
总结:将交通灯的状态抽象出来,形成一个状态列表statelist,然后通过方法递归调用,使得状态进行对应改变,就比较优雅地实现了需求。但还可以进一步优化。
版本三(过程抽象)
JS文件:
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();
}
}());
总结:这个版本中将过程抽象出来,可以扩展到其他领域。但是代码非常复杂,要反思是否过度抽象了。
版本四(异步+函数式)
JS文件:
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的幂(leetcode简单题)
HTML:
<input id="num" value="65536"></input>
<button id="checkBtn">判断</check>
CSS:
#num {
color: black;
}
#num.yes {
color: green;
}
#num.no {
color: red;
}
JS:
// function isPowerOfFour(num) {
// num = parseInt(num);
// while(num > 1) {
// if(num % 4) return false;
// num /= 4;
// }
// return num === 1;
// }
// function isPowerOfFour(num) {
// num = parseInt(num);
// 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';
});
PS:
1、a & (a - 1):一个正整数a&a-1会使a的二进制里面的1减少一个。
2、满足num > 0 && (num & (num - 1)) === 0这两个条件,则num一定是2的幂。
洗牌
错误写法
HTML:
<div id="app">洗牌-错误写法</div>
<hr/>
<div id="log"></div>
<script>
window.console = JCode.logger(log);
</script>
JS:
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);
总结:进行1000000次循环,可见数字越小的牌出现在前面的概率越大。因为用的是
sort方法,交换次数不均,所以数字越小的牌被换到后面的可能性越小。
正确写法
JS:
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);
使用生成器
HTML:
<div id="app">洗牌-生成器</div>
<hr/>
<div id="log"></div>
<script>
window.console = JCode.logger(log);
</script>
JS:
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]);
分红包
切西瓜法
HTML:
<h2>红包生成器</h2>
<div id="setting">
<div><label>红包金额:<input id="amount" value=100.00></input></label></div>
<div><label>红包数量:<input id="count" value="10"></input></label></div>
<div><button id="generateBtn">随机生成</button></div>
</div>
<ul id="result">
</ul>
CSS:
#setting button {
margin-top: 10px;
}
#result {
padding: 0;
}
#result li {
list-style: none;
}
JS:
function generate(amount, count){
let ret = [amount];
while(count > 1){
//挑选出最大一块进行切分
let cake = Math.max(...ret),
idx = ret.indexOf(cake),
part = 1 + Math.floor((cake / 2) * Math.random()),
rest = cake - part;
ret.splice(idx, 1, part, rest);
count--;
}
return ret;
}
const amountEl = document.getElementById('amount');
const countEl = document.getElementById('count');
const generateBtn = document.getElementById('generateBtn');
const resultEl = document.getElementById('result');
generateBtn.onclick = function(){
let amount = Math.round(parseFloat(amountEl.value) * 100);
let count = parseInt(countEl.value);
let output = [];
if(isNaN(amount) || isNaN(count)
|| amount <= 0 || count <= 0){
output.push('输入格式不正确!');
}else if(amount < count){
output.push('钱不够分')
}else{
output.push(...generate(amount, count));
output = output.map(m => (m / 100).toFixed(2));
}
resultEl.innerHTML = '<li>' +
output.join('</li><li>') +
'</li>';
}
切西瓜法每次都选取最大的数值进行切分,但这种方法的算法复杂度不是很好,不算最优的算法,不过一般也够用。例如微信红包就会有抢红包的人数上限。
抽牌法
JS:
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];
}
}
// 0, 1, 2....9999
// 49 199
function generate(amount, count){
if(count <= 1) return [amount];
const cards = Array(amount - 1).fill(0).map((_, i) => i + 1);
const pick = draw(cards);
const result = [0];
for(let i = 0; i < count - 1; i++) {
result.push(pick.next().value);
}
result.sort((a, b) => a - b);
result.push(amount);
for(let i = result.length - 1; i > 0; i--) {
result[i] = result[i] - result[i - 1];
}
return result;
}
const amountEl = document.getElementById('amount');
const countEl = document.getElementById('count');
const generateBtn = document.getElementById('generateBtn');
const resultEl = document.getElementById('result');
generateBtn.onclick = function(){
let amount = Math.round(parseFloat(amountEl.value) * 100);
let count = parseInt(countEl.value);
let output = [];
if(isNaN(amount) || isNaN(count)
|| amount <= 0 || count <= 0){
output.push('输入格式不正确!');
}else if(amount < count){
output.push('钱不够分')
}else{
output.push(...generate(amount, count));
output = output.map(m => (m / 100).toFixed(2));
}
resultEl.innerHTML = '<li>' +
output.join('</li><li>') +
'</li>';
}
下半场的回放主要是老师对算法的一些分析,如何才能写出时空复杂度更低的代码,如何才能更准确地实现我们所需的功能。由此看来,在前端领域之中,算法依然是很重要的内容,要把数学能力提升上去,写出更优雅的代码!