JavaScript(简称“JS”)是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。JavaScript是web开发者必学的三种语言之一,它实现交互体验,可以处理复杂的函数,可以保证更高的效率和可用性。如何写好JS,让它发挥作用有三个原则:各司其职(让HTML、CSS和JavaScript职能分离)、组件封装(好的UI组件具备正确性、扩展性、复用性)、过程抽象(应用函数式编程思想)。
1、各司其职
以切换主题颜色为例,下面分别是三个不同版本的代码实现:
版本一:
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 = '🌞';
}
});
版本二:
const btn = document.getElementById('modeBtn');
btn.addEventListener('click',(e)=>{
const body=document.body;
if(body.className!=='night'){
body.className='night';
}else{
body.className='';
}
})
版本三:
<body>
<input id="modeCheckBox" type="checkbox">
<div id="container">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<h1>夏目友人帐</h1>
</header>
...
</body>
#modeCheckBox:checked+#container{
background-color: black;
color: white;
transition: all is;
}
#modeCheckBox{
display: none;
}
上述三个版本的代码实现的功能都是切换主题颜色,即普通的白天黑夜主题,但这三个版本的代码又各有不同。版本一的代码是在JS里面操作body.style以切换主题;版本二的代码是在JS里面操作body.className间接操作CSS中的样式以切换主题;而版本三中的代码是在前面加了一个,根据兄弟结点选择器#modeCheckBox:checked + #container去修改下面id选择器为contianer的内容。display:none;隐藏了checkbox,点击checkbox可以实现白天夜间模式切换。(注:因为这里的图标改为了label,并增加了for=”modeCheckBox”,所以点击图标和点击checkbox效果一样。)
总结:HTML负责结构,CSS负责表现,JS负责行为,结构、表现、行为分离是前端工程师需要掌握的基本原则。上述例子切换白天夜间模式是更换CSS样式,应当避免不必要的由JS直接操作样式,可以用纯class表示状态,纯展示类交互需求零JS方案。
2、组件封装
组件是指Web页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性和复用性。组件封装的基本方法:结构设计、展现效果、行为设计(API功能、Event控制流),下面以轮播图为例详细说明这三种基本方法。
结构设计:HTML
<div id="my-slider" class="slider-list">
<ul>
<li class="slider-list__item--selected">
<img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F201705%2F11%2F20170511162412_KY2Zf.thumb.400_0.jpeg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1695568320&t=76da7fd7211523c896ec1731ecacec50">
</li>
<li class="slider-list__item">
<img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F201808%2F08%2F20180808205849_sijwh.thumb.400_0.jpg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1695568351&t=c3badb0ead22021453eddec0e19dc46f">
</li>
<li class="slider-list__item">
<img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F201912%2F11%2F20191211093109_ycgfm.thumb.400_0.jpg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1695568351&t=15fd225a1aa9a0590761c72c5b168ffe">
</li>
<li class="slider-list__item">
<img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F202008%2F19%2F20200819012355_hygnu.thumb.400_0.png&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1695568351&t=55516b3767f3dd45f6b6e53c939df388">
</li>
</ul>
</div>
轮播图是一个典型的列表结构,我们可以用无序列表<ul>元素来实现。
展现效果: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;
}
- 使用CSS绝对定位将图片重叠在同一个位置
- 轮播图切换的状态使用修饰符(modifier)
- 轮播图的切换动画使用CSS transition
行为设计: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)//2s更换一次图片
- API设计应保证原子操作,职责单一,满足灵活性。
- Slider
- getSelectedItem():得到当前选中的图片元素
- getSelectedItemIndex():得到当前选中的图片元素在列表里的下标
- slideTo():slideTo到某个特定的Index的元素上
- slideNext():轮播下一张图片
- slidePrevious():轮播上一张图片
行为设计:控制流
在js文件中定义controller变量,代表轮播图下方的四个小圆点。在controller里面监听mouseover和mouseout事件,当鼠标移至小圆点时,mouseover监听到该操作把对应的图片slideTo,同时这里执行this.stop():结束轮播图的自动切换。当鼠标移除小圆点时,mouseout监听到该操作时重新开始执行this.start():轮播图自动切换
控制流实际上是使用自定义事件来解耦。轮播图自动切换时,切换到哪一张图片,下面对应的小圆点就变黑,实现这个功能,需要自定义事件进行监听:new一个slide CustomEvent,然后在controller里面监听这个slide事件。
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
组件除了上述三种基本方法外,还有可以改造的空间:重构(插件化、模板化、组件框架)
1、重构:插件化
解耦:将控制元素抽取成插件,插件与组件之间通过依赖注入方式建立联系。
以上述轮播图代码为例,如果我们不想要轮播图的左右切换功能,我们需要注销HTML、改动CSS和JS的代码,这样的组件不够灵活,所以考虑把组件插件化(代码如下)。下面的代码把切换上/下一张图片和小圆点切换图片的功能都进行抽象插件化,pluginController:表示下面的四个小圆点,pluginPrevious:表示切换上一张图片,pluginNext:切换下一张图片。如果我们不要下面的四个小圆点的切换功能,可以在js文件中把slider.registerPlugins(pluginController, pluginPrevious, pluginNext)中的pluginController注销,这样既不需要改动HTML和CSS的代码,也让代码更加简洁。
function pluginController(slider){
const controller = slider.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){
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener('mouseout', evt=>{
slider.start();
});
slider.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';
});
}
}
function pluginPrevious(slider){
const previous = slider.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
}
function pluginNext(slider){
const next = slider.container.querySelector('.slide-list__next');
if(next){
next.addEventListener('click', evt => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
}
const slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
轮播图插件化代码:code.juejin.cn/pen/7271922…
2、重构:模板化
解耦:将HTML模板化,更易于扩展
之前的结构是写死在HTML里面的,实际上在js的UI组件里面首先要做到数据驱动,根据数据来生成HTML的模板。如果把图片写死在HTML代码里面,一旦图片数量变多,需要改动的代码就越多。如果是根据数据生成HTML模板,则不需要改动代码。
通过JS里的render()生成HTML代码,四张图片当成数据传给组件(new slider的时候传了四张图片进去)。四张图片通过调用render()方法,生成innerHTML,插入到content里面实现。同理,四个小圆点和上下切换功能是通过render()方法和action()方法实现的,render()方法渲染页面内容(生成四个小圆点),action()方法完成初始化(实现小圆点切换图片功能)。
render(){
const images = this.options.images;
const content = images.map(image => `
<li class="slider-list__item">
<img src="${image}"/>
</li>
`.trim());
return `<ul>${content.join('')}</ul>`;
}
const slider = new Slider('my-slider', {images: ['https://img2.baidu.com/it/u=2756958179,4279281762&fm=253&fmt=auto&app=138&f=JPEG?w=888&h=500',
'https://img2.baidu.com/it/u=475139972,1074921915&fm=253&fmt=auto&app=138&f=JPEG?w=888&h=500',
'https://img1.baidu.com/it/u=1875235429,2122274709&fm=253&fmt=auto&app=138&f=JPEG?w=890&h=500',
'https://img2.baidu.com/it/u=958150146,494355729&fm=253&fmt=auto&app=138&f=JPEG?w=888&h=500'], cycle:4000});)`
轮播图的模板化代码:code.juejin.cn/pen/7272255…
3、重构:组件框架
抽象:将通用的组件模型抽象出来,在模板化的基础上进一步抽象,把Slider组件模型抽象成通用的组件Component。Component里面包含两个方法:registerPlugins(...plugins)注册插件、render()渲染。
Slider会继承Component去实现render()方法,registerPlugins(...plugins)注册插件,其他的与模板化代码差不多。
class Component{
constructor(id, opts = {name, data:[]}){
this.container = document.getElementById(id);
this.options = opts;
this.container.innerHTML = this.render(opts.data);
}
registerPlugins(...plugins){
plugins.forEach(plugin => {
const pluginContainer = document.createElement('div');
pluginContainer.className = `.${name}__plugin`;
pluginContainer.innerHTML = plugin.render(this.options.data);
this.container.appendChild(pluginContainer);
plugin.action(this);
});
}
render(data) {
/* abstract */
return ''
}
}
class Slider extends Component{
constructor(id, opts = {name: 'slider-list', data:[], cycle: 3000}){
super(id, opts);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = opts.cycle || 3000;
this.slideTo(0);
}
render(data){
const content = data.map(image => `
<li class="slider-list__item">
<img src="${image}"/>
</li>
`.trim());
return `<ul>${content.join('')}</ul>`;
}
轮播图的组件框架代码:code.juejin.cn/pen/7272272…
3、 过程抽象
过程抽象:
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
例子:
这是一个待办事件的代码,但是它有bug,如果我们多次点击同一个待办事件就会报错。这是因为我们每点击一次就触发一次click事件,执行一次click事件就会removeChild一次,第一次执行click的时候就把待办事件移除了,所以之后再次点击会报错The node to be removed is not a child of this node.所以这个click事件只能执行一次。一个函数只能执行一次可以用参数once:true实现,这样这个事件就只能触发一次。
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);
},{once:true});
});
为了能够让“只执行一次”的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。除了上述添加参数once:true实现事件只能触发一次外,还可以用高阶函数实现。
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) => {
button.addEventListener('click',once((evt) => {
const target=evt.target;
target.parentNode.className = 'completed';
setTimeout(() => {
list.removeChild(target.parentNode);
}, 2000);
}));
})
高阶函数
什么是高阶函数:以函数作为参数、以函数作为返回值、常用于作为函数装饰器。
常见的高阶函数:
- Once:只能执行一次
- Throttle:节流函数,指的是某个函数在一定时间间隔内(例如 3 秒)只执行一次,在这3秒内无视后来产生的函数调用请求,也不会延长时间间隔。
- Debounce:防抖函数,是将一段时间内频繁被执行的数据,延迟到后面一次性做一个执行
- Consumer:执行方法的时候不会直接执行,而是把它放到任务队列里,每隔多少毫秒去执行它。由此将同步的操作变成异步的操作。
- Iterative:将一个函数,变成可迭代使用的,该函数通常用于给一组对象执行批量操作的时候。
为什么使用高阶函数?
函数分为两种,纯函数和非纯函数。
- 纯函数:任何时候,以相同的参数调用纯函数,输出也是相同的
- 非纯函数:非纯函数依赖外部环境,当外部环境参数改变时,即使用相同的参数调用,输出也会改变
显而易见,纯函数方便于后期的统一测试,而非纯函数还需要保证外部环境每次要统一(有时很难做到或很麻烦),所以现在更倾向于使用纯函数,而使用高阶函数可以减少使用非纯函数的可能性,高阶函数可以提高代码的复用性和可读性,从而间接地提高性能。
编程范式
JavaScript是一种既可以使用命令式又可以使用声明式的编程语言,命令式编程语言又可以分为面向过程和面向对象,声明式编程语言可以分为逻辑式和函数式。
命令式编程语言代码:强调的是怎么做,把具体做法写出来
let list=[1,2,3,4];
let map=[];
for(let i=0;i<list.length;i++){
map.push(list[i]*2);
}
声明式编程语言代码:强调的是机器想要什么,让机器想出怎么做,声明式代码比命令式代码更具有扩展性
let list=[1,2,3,4];
const double = x => x*2;
list.map(double);
寄语:编写代码不仅仅只是正常运行实现功能,还要考虑代码的复用性、可读性和其间接影响的性能。要从实际出发,减低成本,增加效益。