这是我参与「第四届青训营 」笔记创作活动的的第4天
和月影大佬继续学习JavaScript,这次主要是学习写代码时最应该注意什么,
- 风格
- 效率
- 约定
- 使用场景
- 设计
以上几点其实都是很重要的,但是主要还是要根据场景来判定。
曾经npm就发生过很有名的left-pad时间,原因就是npm中的包依赖关系太强,而left-pad作者下架了这个包从而导致React、Babel等工具无法使用。也是因为这个原因,社群中的程序员看到left-pad代码之后认为left-pad代码不够符合规范,不够效率。以下是left-pad代码:
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模块粒度的问题,代码风格问题、代码质量/效率问题等,借这个例子我想要表达的是千人千面,可能会有很多中不同的写法,但是重要的是我们需要按照场景选择最优写法。
接下来让我们想想怎么写一个交通灯切换,月影大佬介绍了4种方法,我们都来看看
写法一
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;
}
JavaScript
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);
})();
写法一是最容易想到的写法,通过setTimeout来改变css样式名以打到红绿灯切换的功能,但是这么写很容易陷入“回调地狱”问题中。
写法二(数据抽象)
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;
}
JavaScript
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);
方法二改善了“回调地狱”的问题,但是代码的可读性并不高。
方法三(过程抽象)
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;
}
JavaScript
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();
}
}());
方法三使用过程抽象的方法,将中间过程抽象出来,虽然在可读性并没有提升,但是这么做的好处是代码复用性得到了很好的提升。
方法四(异步+函数式)
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;
}
JavaScript
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();
方法四使用异步async、wait使异步操作如同同步操作一般丝滑,既易于理解,也美观。
洗牌-错误写法
许多人包括我一开始没有深入理解的时候,都认为洗牌算法就是通过调用Math.random()就能实现真正的随机算法,这里我引入一个例子来证明Math.random()实际上并非真正的随机
var times = [0, 0, 0, 0, 0];
for (var i = 0; i < 100000; i++) {
let arr = [1, 2, 3, 4, 5];
arr.sort(() => Math.random() - 0.5);
times[arr[4]-1]++;
}
console.log(times)
输出的结果是:
[30636, 30906, 20456, 11743, 6259]
我们可以明显的看出每个数字出现的次数并不是随机的,而是从头到尾依此减少的,那么真正的乱序算法应该怎么写呢?这里的算法叫做Fisher–Yates,因为这是这两位作者发明的,算法如下:
function shuffle(a) {
var j, x, i;
for (i = a.length; i; i--) {
j = Math.floor(Math.random() * i);
x = a[i - 1];
a[i - 1] = a[j];
a[j] = x;
}
return a;
}
原理很简单,就是遍历数组元素,然后将当前元素与以后随机位置的元素进行交换,从代码中也可以看出,这样乱序的就会更加彻底。这里再写一个demo来验证一下
var times = 100000;
var res = {};
for (var i = 0; i < times; i++) {
var arr = shuffle([1, 2, 3]);
var key = JSON.stringify(arr);
res[key] ? res[key]++ : res[key] = 1;
}
// 为了方便展示,转换成百分比
for (var key in res) {
res[key] = res[key] / times * 100 + '%'
}
console.log(res)
结果如下:
可以看出我们已经实现了真正的乱序