什么是好的 JS 代码:各司其职、组件封装、过程抽象
使用 JS 解决实际问题:如何评价一段代码的好坏、写代码最应关注什么
# 如何写好 JavaScript - 笔记
# 各司其职
我们知道,前端 web 对于 HTML、CSS、JavaScript 的分工都很明确。
HTML 负责页面骨架、CSS 负责页面的渲染、JavaScript 负责页面的行为。
# 一个🌰
对于一个切换页面深色模式切换的需求,如果要用 JS,该怎么实现?
很容易想到:
-
使用 button,监听点击事件,更改页面背景颜色和文字颜色
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 = '🌞';
}
});
但是这个版本的实现语义不清,如果让别人来阅读这段代码,可能一时间不知道是在实现什么功能。
于是,我们想出优化方案:
-
同样使用 button,监听点击事件,但这次直接修改容器的 class,通过在 css 中写的 class 样式修改页面表现
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document. body ;
if(body.className === 'night'){
body.className = '';
} else {
body.className = 'night';
}
});
很明显,这个版本已经比上个版本好多了,我们一眼就能看出来这段代码是在做什么。
但实际上,我们还有一种更好的解决方案 —— 只使用 CSS 实现:
- 使用
checkbox
+:checked
伪类 + 兄弟元素选择器来实现
那么,实际上来说,表现层的工作就让负责表现层的 CSS 来做才是最好的
总结下来就是以下几点:
- HTML/CSS/JS 各司其责
- 应当避免不必要的由 JS 直接操作样式
- 可以用 class 来表示状态
- 纯展示类交互寻求零 JS 方案
# 组件封装
组件是指 Web 页面上抽出来一个个包含模版(HTML)、功能 (JS)和样式 (CSS) 的单元。
好的组件具备封装性、正确性、扩展性、复用性。
# 如何实现一个轮播图组件?
-
HTML 结构设计
<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 展现效果
#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;
}
-
行为设计:API
注意:API 设计应保证原子操作,职责单一,满足灵活性。
-
行为设计:Event 控制流
使用自定义事件来解耦
<a class="slide-list__next"></a>
<a class="slide-list__previous"></a>
<div class="slide-list__control">
<span class="slide-list__control-buttons--selected"></span>
<span class="slide-list__control-buttons"></span>
<span class="slide-list__control-buttons"></span>
<span class="slide-list__control-buttons"></span>
</div>
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
总的来说,就是要遵循以下基本方法:
- 结构设计
- 展现效果
- 行为设计
- API(功能)
- Event(控制流)
# 优化
# 解耦
- 将控制元素抽取成插件
- 插件与组件之间通过依赖注入方式建立联系
- 将 HTML 模板化,更易于扩展
# 抽象
- 将通用的组件模型抽象出来,形成组件框架
# 过程抽象
# 什么是过程抽象?
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
# 过程抽象有什么好处?
一个🌰:操作次数限制
假如有一个需求,要求对某个函数的调用设置次数限制,我们可以直接在这个函数里面写上限制代码。
但是实际上,这个需求是可以通用的,如果对每一个函数都是有需求时更改内部代码,未免显得有点重复。
所以我们实际上可以通过一个代理函数 (高阶函数),写一个新的函数,接收一个函数参数,对其封装,并返回封装好的新函数,这样我们就完美地实现了这个需求。
function once(fn){ | |
return (...args) => { | |
if(fn){ | |
fn.apply(this, args); | |
fn = null; | |
} | |
} | |
} |
# 常用高阶函数
- Function Once:只能执行一次
- Function Throttle:节流,每隔一段时间可以调用一次
- Function Debounce:防抖,停下来一段时间后再调用
- Function Consumer:缓存队列,延迟执行
- Function Iteraticve:让函数支持批量操作
# 为什么要使用高阶函数?
函数分为两种,纯函数和非纯函数。
纯函数的意思是:任何时候,以相同的参数调用纯函数,输出也是相同的
那么其实非纯函数的意思就是相对的:非纯函数依赖外部环境,当外部环境参数改变时,即使用相同的参数调用,输出也会改变
显而易见,纯函数方便于后期的统一测试,而非纯函数还需要保证外部环境每次要统一(有时很难做到或很麻烦),所以现在更倾向于使用纯函数
// 纯函数 | |
function add(a, b) { | |
return a + b; | |
} | |
// 非纯函数 | |
let sum = function() { | |
let res = 0; | |
return (value) => res += value; | |
} |
当使用高阶函数时,由于高阶函数一般都是纯函数,这样的话,由高阶函数封装的函数在测试时,就只需要测试原始函数即可,降低了测试成本
# 编程范式
JavaScript 是一种既可以使用命令式又可以使用声明式的编程语言,例如:
// 命令式 | |
function toggle(event) { | |
if(event.target.className === 'on'){ | |
event.target.className = 'off'; | |
}else{ | |
event.target.className = 'on'; | |
} | |
} | |
// 声明式 | |
function toggleBuilder(...actions) { | |
return function(...args){ | |
let action = actions.shift(); | |
actions.push(action); | |
return action.apply(this, args); | |
} | |
} | |
let toggle = toggleBuilder( | |
event => event.target.className = 'off', | |
event => event.target.className = 'on' | |
); |
那么我们应该使用什么编程范式呢?
思考这样一个问题,对于状态切换这个需求,如果我们需要调整状态的数量、先后,那么:
-
对于命令式的
toggle
来说,我们需要直接修改这个函数,当需求频繁变化时,就会即为耗费人力和时间; -
而对于声明式的
toggleBuilder
来说,我们只要在构建toggle
时,调整传入的行为参数即可,既简单又直观。
发现了吗,声明式的函数要优于命令式的函数
但是在实际开发中,到底是使用哪种范式,还需要具体问题具体分析,在两种范式之间选择最适合的,才是最好的
# 使用 JS 解决实际问题
# 如何评价一段代码的好坏
先来看一段代码:
function isUnitMatrix2d(m) { | |
return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0; | |
} |
这段代码好不好?为什么?
其实有两种观点:
乍一看,对于简洁度和可读性来说,不好,明明可以使用更优雅的迭代来解决,却要用这么笨的方法
但是,其实这个函数的效率极高,如果这个函数需要在 requestAnimationFrame
中被高频调用,那么这种写法也不失为一种好的解决方案
# 写代码最应关注什么?
风格 vs 效率
实际上我们应该根据使用场景来判断,对于效率优先的情况下,肯定要先考虑实现的效率问题,而如果多人协作开发和效率问题起冲突,那么我们就要在这两者之间做权衡了
所以其实没有绝对的判断代码好坏的标准,过度的优化、过度的设计难免会让理解成本成倍增加,所以一切都要从实际出发,结合实际考虑
抽象程度越高,可复用性就越高,同时理解成本也会越高
# 参考资料
-
字节青训营课程
-
MDN 中文文档