这是我参与「第四届青训营」笔记创作活动的第2天
如何写好 JavaScript
1. 各司其职
举个例子,写一段JS,控制一个网页,让它支持浅色和深色两种浏览模式。如果实现?
很容易想到一个版本,给按钮绑定事件,然后直接去使用 JS 操作样式。
const btn = document.getElementById('modeBtn');
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 = '☀';
}
});
上述代码的问题就在直接使用js操作了样式,没有做到各司其职,修改一下得到:
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
第二版通过添加删除类名,结合 CSS 的相关样式,实现了切换浏览模式的功能,基本实现各司其职,更进一步的,还可以完全使用 CSS 实现该功能:
<input id="modeCheckBox" type="checkBox">
<div class="content">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
</header>
...
</div>
#modeCheckBox {
display: none;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
第三版代码根据checkBox的状态改变实现样式改变,同时将checkBox隐藏,主要用到了 CSS 的伪类选择器。
样例结论:
- HTML、CSS、JS各司其职
- 应当避免不必要的由 JS 直接操作样式
- 可以用 class 来表示状态
- 纯展示类交互寻求零 JS 方案
2. 组件封装
组件
组件是指 web 页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元。
好的组件具备封装性、正确性、扩展性、复用性。
举个例子,用原生 JS 写一个电商网站的轮播图,应该怎么实现?
轮播图案例
结构 —— HTML
轮播图是一个典型的列表结构,我们可以使用无序列表 ul 元素来实现。
<div id="my-slider" class="slider-list">
<ul>
<li class="slider-list__item--selected">
<img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png"/>
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg"/>
</li>
<li class="slider-list__item">
<img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg"/>
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg"/>
</li>
</ul>
</div>
表现 —— CSS
- 利用CSS绝对定位将图片重叠在同一个位置
- 轮播图切换的状态使用修饰符(modifier)
- 轮播图的切换动画使用 CSS transition
#my-slider {
position: relative;
width: 790px;
}
.slider-list ul {
list-style-type: none;
position: relative;
padding: 0;
margin: 0;
}
.slider-list__item,
.slider-list__item--selected {
/* 使用绝对定位,将多张图片重叠在一起 */
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
.slider-list__item--selected {
transition: opacity 1s;
opacity: 1;
}
行为 —— JS: API
API保证指责单一,保证组件灵活性。
// 创建 Slider 类,封装 API
class Slider{
constructor(id){
this.container = document.getElementById(id);
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
}
// 获取当前选中图片
getSelectedItem(){
const selected = this.container
.querySelector('.slider-list__item--selected');
return selected
}
// 获取当前选中图片的索引
getSelectedItemIndex(){
return Array.from(this.items).indexOf(this.getSelectedItem());
}
// 跳转到某张图片
slideTo(idx){
const selected = this.getSelectedItem();
if(selected){
selected.className = 'slider-list__item';
}
const item = this.items[idx];
if(item){
item.className = 'slider-list__item--selected';
}
}
// 跳转到下一张图片
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);
}
}
// 尝试调用
const slider = new Slider('my-slider');
slider.slideTo(3);
// 或者可以设置定时器,自动播放
setInterval(() => {
slider.slideNext();
}, 2000);
行为 —— JS: 控制流
// 需要添加进行控制的 html 元素和 css 样式,没有在笔记中记录
// 使用自定义事件来解耦
constructor(id, cycle = 3000){
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = cycle;
const controller = this.container.querySelector('.slide-list__control');
if(controller){
const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
// 鼠标经过小圆点时停止轮播 将该圆点对应的图片显示出来
controller.addEventListener('mouseover', evt=>{
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0){
this.slideTo(idx);
this.stop();
}
});
// 鼠标离开圆点 继续轮播
controller.addEventListener('mouseout', evt=>{
this.start();
});
// 注册 slide 事件
this.container.addEventListener('slide', evt => {
const idx = evt.detail.index
const selected = controller.querySelector('.slide-list__control-buttons--selected');
if(selected) selected.className = 'slide-list__control-buttons';
buttons[idx].className = 'slide-list__control-buttons--selected';
})
}
// 向前翻页
const previous = this.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {
this.stop();
this.slidePrevious();
this.start();
evt.preventDefault();
});
}
// 向后翻页
const next = this.container.querySelector('.slide-list__next');
if(next){
next.addEventListener('click', evt => {
this.stop();
this.slideNext();
this.start();
evt.preventDefault();
});
}
}
总结 —— 基本方法
- 结构设计
- 展现效果
- 行为设计
- API(功能)
- Event(控制流)
3. 过程抽象
过程抽象用来处理局部细节控制的一些方法,是函数式编程思想的基础应用
Once
- 为了能够让"只执行一次"的需求覆盖不同的事件处理,我们可以将这个需求剥离出来,这个过程我们称为过程抽象。
function once(fn) {
return function(...args) {
if(fn) {
const ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
高阶函数
- HOF:以函数作为参数、以函数作为返回值、常用于作为函数装饰器。
function HOF0(fn) {
return function(...args) {
return fn.apply(this. args);
}
}
HOF0是高阶函数的等价范式,fn与HOF0(fn)是完全等价的。
- JS数组中 every、map、filter、forEach、reduce、sort等API是高阶函数
常用高阶函数
- Throttle 节流函数
function Throttle(fn, time = 500) {
let timer;
return function(...args) {
if(timer == null) {
fn.apply(this, args);
timer = setTimeout(() => {
timer = null;
}, time)
}
}
}
- debounce 防抖函数
function debounce(fn, dur) {
dur = dur || 100;
var timer;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, dur);
}
}
- consumer函数
// 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)
}
}
}
// 应用 1: 每隔一秒执行一次 add
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);
}
// 应用 2: 快速点击慢慢执行
btn.onclick = consumer((evt) => {
let t = parseInt(count.innerHTML.slice(1)) + 1;
count.innerHTML = `+${t}`;
count.className = 'hit';
let r = t * 7 % 256,
g = t * 17 % 128,
b = t * 31 % 128;
count.style.color = `rgb(${r}, ${g}, ${b})`.trim();
setTimeout(() => {
count.calssName = 'hide';
}, 500);
}, 800)
- iterative函数
function interative(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 isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function'
const setColor = iterative((el, color) => {
el.style.color = color;
})
const els = document.querySelector('li:nth-child(2n+1)');
setColor(els, 'red')
- 拦截函数deprecate
function deprecate(fn, oldApi, newApi) {
const msg = `The ${oldApi} is deprecated. Please use the ${newApi} instead.`
return function(...args) {
console.log(msg);
return fn.apply(this, args);
}
}
// 不修改代码本身,而是对这个API进行修饰,修饰的过程可以抽象为拦截它的输入或输出。
4. 编程范式
- 命令式 关注怎么做 How
- 声明式 关注做什么 What
- JS既可以写命令式代码也可以写声明式代码,处理复杂逻辑时,建议使用声明式,抽象程度更高,拓展性更强
toggle案例:
- 命令式:
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',
)
- 声明式扩展性更强:
// 如果想多一种状态 只需要添加一行代码
switcher.onclick = toggle(
evt => evt.target.className = 'warn',
evt => evt.target.className = 'off',
evt => evt.target.className = 'on',
)