JS三大原则
这是我参与「第四届青训营 」笔记创作活动的的第3天!
本次课程是分为上下两个半场的,我们这里先对上半场的知识进行一个记录。
我们知道,从前端学习的小白进阶到专业的前端工程师,其过程必不可少的是技术的累积和掌握,但在实现需求和代码规范上,我们同样需要注意自己的专业性,如何写好一段标准的,简单易懂的代码始终是我们应该追求的一个目标。
写好JS的三大原则主要有:
- 各司其职:让HTML,CSS和JavaScript的职能分离
- 组件封装:好的UI组件具备正确性,拓展性,以及复用性
- 过程抽象:应用函数式编程思想
下面我们会从这三个点出发来探究JS需求实现的一些细节和注意事项。
各司其职
首先我们来看第一个原则。假如给你一个需求,目标是实现一个浏览页面的白天阅读和夜间阅读的功能,你要如何去实现这个需求呢?
对于我们刚入门的前端小白,这个需求并不算是很难实现,具体的思路是我们可以通过获得一个button,标志为代表白天阅读的太阳和夜间阅读的月亮,同时在这个button上注册一个click事件,然后在click事件里面去添加一个判断,如果button的innerHTML是等于我们的“太阳”标记,我们的整个页面就变成黑色背景,白色字体,如果button的innerHTML是等于我们的“月亮”标记,就把整个页面变成白色背景,黑色字体,到这里我们的需求就基本实现了。
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的代码去操作了CSS,并没有体现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中定义一个body.class,并设置其name为night,若class等于night,则默认其为夜间模式,如果不等于,则class就表示白天模式。这个版本与上版本功能方面基本上是相同的,但在代码方面,这一版本明显更加简洁。
我们知道HTML是负责结构的,CSS是负责表现的,JS是负责行为的,这一案例主要想要告诉我们的是,要想成为一个合格的前端工程师,结构表现和行为分离是我们必须要掌握的基本原则。
这一原则分析给我们的结论主要体现在JS应当去体现行为变化,尽量不要直接去操作样式,同时我们也学习到了class可以去表示一个状态,我们可以去探求一个纯展示的交互方式来实现零JS方案。
组件封装
接下来我们来看第二个原则,组件封装。要理解组件封装的思想和原则,首先我们要了解的是组件的定义,那么什么是组件呢?
组件是指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的绝对定位来将图片重叠在同一个位置,同时使用修饰符来切换图片的状态,设置轮播图的切换动画为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来实现切换的行为,通过设计JS的行为API,具体通过getSelectedltem()得到当前选中的图片元素, getSelectedltemIndex()得到当前选中图片的下标,然后使用slideTo()到特定下标的图片,同时通过slideNext()和slidePrevious()向后轮播下一张图片和上一张图片,我们定义一个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);
在上述的实现过程中,我们主要是体现了将不同部分封装的思想,我们可以通过重构,用解耦的方式将HTML模板化,使得更容易拓展,同时我们将Slider组件框架抽象出来形成组件的通用模型,如果我们遇到类似的场景可以快速使用。同时我们也得出了以下结论:
过程抽象
我们的最后一个原则就是过程抽象,这一原则主要是用来处理局部细节控制的一些方法,主要体现了函数型思想的基础应用。而函数型思想主要是指,我们把函数本身看做是一个封装好的过程,我们关注点在它的输入和输出上。
这一块我们举一个关于操作次数限制的例子来说明,具体的功能描述为:我们要实现一个具体的学习列表,其功能为,每当我们完成一个具体的任务,点击一下任务列表前面的勾选,任务就会自动消失,直到没有任务。
const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
buttons.forEach((button) => {
button.addEventListener('click', (evt) => {
const target = evt.target;
target.parentNode.className = 'completed';
setTimeout(() => {
list.removeChild(target.parentNode);
}, 2000);
});
});
具体的思路是我们监测我们每一次的click,每点一次click触发一次事件,则状态调整为conpleted,则在setTimeout中计时两秒并把list中的清单remove一次。如果我们正常点击进行操作的话,是没有任务问题的,但如果连续快速点击,我们已经触发了remove的情况下,多次删除,就会报错。
对于这种情况我们就可以使用函数型思想去解决,首先我们是可以在后面加一个加一个once true来限制触发次数,但是如果我们遇到向服务器请求数据的情况,这样限制就使得服务器只能返回第一次的请求,这种处理明显是无法满足多次调用的需求的,这个时候我们就需要把这个过程封装成一个名为once的高阶函数,然后通过once来调用,就可以满足多次调用只输出一次的需求,而这个分析就是一个非常经典的过程抽象。
而我们在开发过程中,会遇到很多常见的高阶函数,它们往往能实现一些次数限制,节流,防抖的一些具体的操作性功能,他们都会return一个function用以判断和实现具体的功能。
到这里我们其实可以发现,高阶函数的使用其实是一种模式,而我们的JavaScript是一种现代的编程语言,它支持我们的声明式和命令式的编程范式,其具体的区别如下:
- 命令式:是一种过程式的编程范式,实现需求时更强调怎么做,并把具体做法写出来。
- 声明式:是一种简洁的编程范式,实现需求时强调做什么,主要定义和调用函数来实现。
而用一个简单的需求来体现,该需求为:定义一个数组,让数组中的每个数字都乘以2。我们来看命令式和声明式两种的代码实现
命令式:
let list = [1, 2, 3, 4];
let mapl = [];
for(let i = 0; i < list.length; i++) {
mapl.push(list[i] * 2);
}
声明式:
let list = [1, 2, 3, 4];
const double = x => x * 2;
list.map(double);
小结
今天的笔记,我们始终围绕的是如何去写出更好的JS代码,三大原则的介绍和具体的案例分析都证明了一段好的代码不只是实现基本功能,更多的需要开发者去注重应用场景,用户操作,以及优化空间等方向,在学习和实践过程中遵循三大原则,可以帮助我们初学者有序,合理的进行JS开发,同时也能提升我们的代码规范性,关于JS的学习还需要有更深刻的探索过程,今天的笔记分享就到这里啦!