学习 JavaScript | 青训营笔记
这是我参与「第五届青训营」伴学笔记创作活动的第 3 天
0x1 各司其职
让 HTML、CSS 和 JavaScript 职能分离
举例:通过 JavaScript 控制页面的 日间模式 与 夜间模式 的切换
v1.0:依据需求直接编程
const btn = document.getElementById('dnchange');
btn.addEventListener('click', (e) => {
const body = document.body;
if(e.target.innerHTML === '日') {
body.style.backgroundColor = 'black';
body.style.color = 'white';
e.target.innerHTML = '夜';
} else {
body.style.backgroundColor = 'white';
body.style.color = 'black';
e.target.innerHTML = '日';
}
});
此时 HTML、CSS 同时混杂在 JavaScript 代码中,应当避免不必要的由 JS 直接操作样式
v2.0:将 CSS 部分分离独立
.day {
background: white;
color: black;
}
.night {
background: black;
color: white;
}
const btn = document.getElementById('dnchange');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = 'day';
}
});
由于当前页面属于纯展示页面,此时交互应寻求零 JS 的方案
v3.0:取消 JavaScript 部分,通过多选框的checked
的变化切换
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<style>
body {
margin: 0;
padding: 0;
}
.content {
background: white;
color: black;
transition: all 1s;
}
#btn {
display: none;
}
#btn:checked + .content {
background: black;
color: white;
transition: all 1s;
}
</style>
</head>
<body>
<input id="btn" type="checkbox" />
<div class="content">
<header>
<label id="lab" for="btn">[日夜切换]</label>
<h2>文章标题</h2>
</header>
<main>
<p>
段落内容
</p>
</main>
</div>
</body>
</html>
0x2 组件封装
好的 UI 组件应具备正确性、扩展性、复用性
举例:轮播图
HTML 结构:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<style>
#slider {
position: relative;
width: auto;
}
.list ul {
list-style-type: none;
position: relative;
margin: 0;
padding: 0;
}
.list_item,
.list_item--selected {
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
.list_item--selected {
transition: opacity 1s;
opacity: 1;
}
</style>
<script type="text/javascript">
class Slider{
constructor(id, cycle = 3000) {
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll('.list_item .list_item--selected');
this.cycle = cycle;
const controller = this.container.querySelector('.list_control');
if(controller) {
const buttons = container.querySelectorAll('.list_ctrl, .list_ctrl--selected');
controller.addEventListener('mousemove', evt => {
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0) {
this.slideTo(idx);
this.stop();
}
});
controller.addEventListener('mouseout', evt => {
this.start();
});
this.container.addEventListener('slide', evt => {
const idx = evt.detail.index;
const selected = controller.querySelector('.list_ctrl, .list_ctrl--selected');
if(selected) selected.className = 'list_ctrl';
buttons[idx].className = 'list_ctrl--selected';
});
}
const previous = this.controlled.querySelector('.list_previous');
if(previous) {
previous.addEventListener('click', evt => {
this.stop();
this.slidePrevious();
this.start();
evt.preventDefault();
});
}
const next = this.container.querySelector('.list_next');
if(next) {
next.addEventListener('click', evt => {
this.stop();
this.slideNext();
this.start();
evt.preventDefault();
});
}
}
getSelectedItem() { // 获取图片项
const selected = this.container.querySelector('.list_item--selected');
return selected;
}
getSelectedItemIndex() { // 获取图片在数组中的位置
return Array.from(this.items).indexOf(this.getSelectedItem());
}
slideTo(idx) { // 导航至
let selected = this.getSelectedItem();
if(selected) {
selected.className = 'list_item';
}
let item = this.items[idx];
if(item) {
item.className = 'list_item--selected';
}
/* 开发自定义事件 */
const detail = { index: idx };
const event = new CustomEvent('slide', { bubbles: true, detail });
this.container.dispatchEvent(event);
}
slideNext() { // 下一张
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious(){ // 上一张
const currentIdx = this.getSelectedItemIndex();
const previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
start() {
this.stop();
this._timer = setInteval(() => this.slideNext(), this.cycle);
}
stop() {
clearInterval(this._timer);
}
}
const slider = new Slider('slider');
slider.start();
</script>
</head>
<body>
<div id="slider" class="list">
<ul>
<li class="list_item--selected">
<img src="./1.png" />
</li>
<li class="list_item">
<img src="./2.png" />
</li>
<li class="list_item">
<img src="./3.png" />
</li>
<li class="list_item">
<img src="./4.png" />
</li>
</ul>
<a class="list_next"></a>
<a class="list_previous"></a>
<div class="list_control">
<span class="list_ctrl--selected"></span>
<span class="list_ctrl"></span>
<span class="list_ctrl"></span>
<span class="list_ctrl"></span>
</div>
</div>
</body>
</html>
对该组件进一步优化有以下方法:
-
重构:插件化
将控制元素抽取成插件,插件与组件之间通过 依赖注入 的方式建立联系
-
重构:模板化
将 HTML 模板化,使其更易于扩展
-
重构:抽象化
将组件通用模型抽象出来
组件封装基本方法:
- 结构设计
- 展现效果
- 行为控制——API(功能)、Event(控制流)
组件设计的原则:
- 封装性
- 正确性
- 扩展性
- 复用性
0x3 过程抽象
应用函数式编程思想
-
操作次数限制
- 一些异步交互
- 一次性的 HTTP 请求
const list = document.querySelector('ul'); const buttons = list.querySelectorAll('button'); buttons.forEach((button) => { buttons.addEventListener('click', (evt) => { const target = evt.target; target.parentNode.className = 'completed'; setTimeout(() => { list.removeChild(target.parentNode); }, 2000); }); });
上述代码中,当短时间内多次单击按钮会触发 BUG
为解决该 BUG,可通过对回调函数封装一个
once()
,修改如下:function once(fn) { return function(...args) { if(fn) { const ret = fn.apply(this, args); fn = null; return ret; } } } const list = document.querySelector('ul'); const buttons = list.querySelectorAll('button'); buttons.forEach((button) => { buttons.addEventListener('click', once((evt) => { const target = evt.target; target.parentNode.className = 'completed'; setTimeout(() => { list.removeChild(target.parentNode); }, 2000); })); });
-
高阶函数
-
比如上面例子中的
once()
函数,为了能让 “ 只执行一次 ” 的需求覆盖不同的事件处理,从而将该需求剥离出来。这个剥离过程称为过程抽象 -
函数装饰器
- 以函数作为参数
- 以函数作为返回值
function HOF0(fn) { return function(...args) { return fn.apply(this, args); } }
-
常用高阶函数(once 等),基于 HOF 进行延申
-
Throotle--节流函数
/* 500ms 点击计数 */ function throotle(fn, time = 500) { let timer; return function(...args) { if(timer == null) { fn.apply(this, args); timer = setTimeout(() => { timer = null; }, time) } } } /* 设置每次点击间隔 500ms */ btn.onclick = throotle(function(e) { circle.innerHTML = parseInt(circle.innerHTML) + 1; circle.className = 'fade'; setTimeout(() => circle.className='', 250); })
-
Debounce--防抖函数
/* 笨鸟先飞小游戏 */ var i = 0; setInterval(function(){ bird.className = "sprite " + 'bird' + ((i++) % 3); }, 1000/10); function debounce(fn, dur) { dur = dur || 100; var timer; return function() { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, arguments); }, dur); } } document.addEventListener('mousemove', debounce(function(evt){ var x = evt.clientX, y = evt.clientY, x0 = bird.offsetLeft, y0 = bird.offsetTop; console.log(x,y); var a1 = new Animator(1000, function(ep) { bird.style.top = y0 +ep * (y - y0) + 'px'; bird.style.top = x0 +ep * (x - x0) + 'px'; }, p => p*p); a1.animate(); }, 100));
-
Consumer--同步转异步
function consumer(fn, time) { let tasks = [], timer; return function(...args) { tasks.push(fn.bind(this, ...args)); if(timer == null) { timer = setInterval(() => { tasks.shift().call(this); if(tasks.length <= 0) { clearInterval(timer); timer = null; } }, time); } } } function add(ref, x) { const v = ref.value + x; console.log(`${ref.value} + ${x} = ${v}`); ref.value = v; return ref; } let consumerAdd = consumer(add, 1000); const ref = {value: 0}; for(let i = 0; i < 10; i++){ consumerAdd(ref, i); }
-
Iterative--可迭代函数
/* 将列表中奇数项的文本颜色设置为红色 */ const isIterable = obj => obj != null && typeof obj[Symbol.iteator] === 'function'; function iterative(fn) { return function(subject, ...rest) { if(isIterable(subject)) { const ret = []; for(let obj of subject) { ret.push(fn.apply(this, [obj, ...rest])); } return ret; } return fn.apply(this, [subject, ...rest]); } } const setColor = iterative((el, color) => { el.style.color = color; }); const els = document.querySelectorAll('li:nth-child(2n+1)'); setColor(els, 'red');
-
-
为什么要使用高阶函数
高阶函数是一种纯函数(pure function),通过高阶函数对非纯函数(inpure function)进行装饰,从而提高代码可维护性
-
-
编程范式
举例说明命令式与声明式的特点:将列表的数值翻一倍
let list = [1, 2, 3, 4]; let mapl = []; for(let i = 0; i < list.length; i++) { mapl.push(list[i] * 2); } console.log(mapl);
命令式特点:强调要做什么 What
let list = [1, 2, 3, 4]; const double = x => x*2; list = list.map(double); console.log(list);
声明式特点:强调要怎么做 How
举例:按钮样式切换
-
命令式
switcher.onclick = function(evt) { if(evt.target.className==='on'){ evt.target.className = 'off'; } else { evt.target.className = 'on'; } }
-
声明式
function toggle(...actions) { return function(...args) { let action = actions.shift(); actions.push(action); return action.apply(this, args); } } switcher.onclick = toggle( evt => evt.target.className = 'off', evt => evt.target.className = 'on' );
-
三态——当切换功能出现第三种状态时
命令式需要添加一段新的
else if
逻辑分支声明式需要添加一句新的
evt =>
函数语句体现了声明式优秀的可扩展性
-
0x4 质量优化
- 举例一:交通灯轮转
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<style>
li { color: grey; }
#traffic { display: flex; flex-direction: column; }
.stop li:nth-child(1) { color: red; }
.wait li:nth-child(2) { color: yellow; }
.pass li:nth-child(3) { color: green; }
</style>
<script type="text/javascript"></script>
</head>
<body>
<ul id="traffic" class="wait">
<li></li>
<li></li>
<li></li>
</ul>
</body>
</html>
v1.0:依据需求直接编程
const traffic = document.getElementById('traffic');
(function reset() {
traffic.className = 'stop';
setTimeout(function() {
traffic.className = 'wait';
setTimeout(function() {
traffic.className = 'pass';
setTimeout(reset, 1000);
}, 1000);
}, 1000);
}){};
v2.0:数据抽象
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);
v3.0:过程抽象
const traffic = document.getElementById('traffic');
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function poll(...fnList) { /* 轮询 */
let stateIdx = 0;
return async function(...args) {
let fn = fnList[stateIdx++ % 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() {
while(1) {
await trafficStatePoll();
}
}());
v4.0:异步与函数式
const traffic = document.getElementById('traffic');
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function setState(state) {
traffic.className = state;
}
async function start() {
while(1){
setState('wait');
await wait(1000);
setState('stop');
await wait(3000);
setState('pass');
await wait(3000);
}
}
start();
- 举例二:判断一个数是否为 4 的幂
// 方法一:直接循环除四直至为一
function isPowerOfFour_1(num) {
num = parseInt(num);
while(num > 1){
if(num % 4) return false;
num /= 4;
}
return num === 1;
}
// 方法二:通过基础位运算
function isPowerOfFour_2(num) {
num = parseInt(num);
while(num > 1){
if(num & 0b11) return false; // 判断二进制最后两位是否为 00
num >>>= 2;
}
return num === 1;
}
// 方法三:位运算进阶方法
function isPowerOfFour_3(num) {
num = parseInt(num);
return num > 0 &&
(num & (num - 1)) === 0 &&
(num & 0xAAAAAAAAAAAAA) === 0;
}
// 方法四:使用正则表达式方法
function isPowerOfFour_4(num) {
num = parseInt(num).toString(2);
return /^1(?:00)*$/.test(num);
}
- 举例三:号码抽奖
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function shuffle1(cards) { // 错误写法,中奖概率不均
return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}
function shuffle2(cards) { // 正确写法,中奖概率均衡
const a = [...cards];
for(let i = a.length; i > 0; i--) {
const idx = Math.floor(Math.random() * i);
[a[idx], a[i-1]] = [a[i-1], a[idx]];
}
return a;
}
function * draw(cards) { // 不需要跑完整个循环,即取即用,更适用于数值庞大且中奖数小的情况
const a = [...cards];
for(let i = a.length; i > 0; i--) {
const idx = Math.floor(Math.random() * i);
[a[idx], a[i-1]] = [a[i-1], a[idx]];
yield a[i - 1];
}
}
const result1 = Array(10).fill(0);
const result2 = Array(10).fill(0);
const result3 = draw(cards);
for(let i = 0; i < 1000000; i++) {
const c = shuffle1(cards);
const d = shuffle2(cards);
for(let j = 0; j < 10; j++) {
result1[j] += c[j];
result2[j] += d[j];
}
}
console.table(result1);
console.table(result2);
console.log([...result3]);
console.log([
result3.next().value,
result3.next().value,
result3.next().value
]);
- 举例四:抢红包
/* 切西瓜法:每次切割最大的部分 */
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;
}
console.table(generate(0.1, 10));
/* 抽牌法:在数列中添加分隔 */
function * draw(cards) {
const c = [...cards];
for(let i = c.length; i > 0; i--) {
const idx = Math.floor(Math.random() * i);
[c[idx], c[i-1]] = [c[i-1], c[idx]];
yield c[i - 1];
}
}
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 = [];
for(let i = 0; i< count; i++) {
result.push(pick.next().value);
}
result.sort((a, b) => a - b);
for(let i = count - 1; i> 0; i--) {
result[i] = result[i] - result[i - 1];
}
return result;
}
console.table(generate(10, 10));
0x5 Leftpad 事件
Leftpad 是一个用于添加前导零来对齐数字的 npm 模块(“ 00 01 ... 09 10 11 ... ”)。
它在被许多其他模块依赖时,其作者由于某些原因将其删除,导致许多依赖它的模块被迫无法使用。
有人指出该模块代码质量低、效率低,原代码如下:
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; }
经过改良后,代码如下:
function leftpad(str, len, ch) { str = "" + str; const padLen = len - str.length; if(padLen <= 0) { return str; } return ("" + ch).repeat(padLen) + str; }
上述代码中的
repeat
是 ES10 中的内置函数,起到了二次幂的方法提高效率
/* repeat 简易版 */ function repeat(str, cnt) { var result = ''; while(cnt) { if(cnt % 2 == 1){ result += str; } if(cnt > 1){ str += str; } n >>= 1; } return result; } console.log('*', 5);