这是我参与「第五届青训营 」伴学笔记创作活动的第3天
一、本堂课主要内容
- JavaScript 好代码的标准
- HTML/CSS/JS 各司其责
- 组件的定义解析及特征
- 组件封装基本方法
- 过程抽象概念
- 高阶函数使用模式
- JavaScript 编程范式
- 代码写作关注事项
- 代码实践
二、知识点介绍
本节课讲的是怎么写好JavaScript,因此一些语法、api之类的就不讲了,可以自己去学学,如JS基础语法、es6语法、DOM、BOM、Promise、async/await、原型链、闭包、事件循环等。
1. 各司其责
html、css、js各干各的活。html控制页面的内容结构,css控制样式、js控制页面行为,各司其责,这样代码结构清晰,便于阅读和维护。例:
切换页面黑白主题
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>深夜食堂</title>
<style>
body, html {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
body {
padding: 10px;
box-sizing: border-box;
}
img {
width: 100%;
}
#modeBtn {
font-size: 2rem;
float: right;
border: none;
background: transparent;
}
</style>
</head>
<body>
<header>
<button id="modeBtn">🌞</button>
<h1>深夜食堂</h1>
</header>
<main>
<img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
<div class="description">
<p>
这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返
[6] 。
</p>
</div>
</main>
<script>
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 = '🌞';
}
});
</script>
</body>
</html>
这里就是点击切换主题按钮后,js直接修改样式。我们做些改变,看看下面的代码
body { ... }
body.night { ... }
#modeBtn::after {
content: '🌞';
}
body.night #modeBtn::after {
content: '🌜';
}
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
这里改的不再是样式而是类名,然后不同主题的样式写在不同的类名里面。很明显,下面一种方法更优。
- 第二种方法改的是类名而不是样式,这样写css样式时有代码提示(very good)
- 第二种写法也能方便的修改样式,直接找到css对应位置轻松修改,而不是像第一种方法一样找到js中对应位置,再苦闷的修改,甚至项目复杂时,还可能js这边改一点还不够,得css那边也改一点,这样代码的可维护性就很差。
- 第二种写法也更容易改变不同主题的子元素的样式。比如黑夜模式下按钮要变成金色,第二种小手一挥就搞定,第一种又要写不知多少代码,复杂又臃肿
- 要添加多个主题时,也明显第二种层次更清晰
再来看一种无js写法
#modeCheckBox {
display: none;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
}
<input id="modeCheckBox" type="checkbox">
<div class="content">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<h1>深夜食堂</h1>
</header>
<main>
...
</main>
</div>
输入框隐藏,不会影响到页面布局,主题切换按钮(label)绑定输入框,点击label,输入框就会切换勾选状态,勾选时css样式就触发,主题就切换了。
这种方法也显得优雅,而且方便修改主题样式,多个label多个单选框也能弄多套主题。当然,这并不意味着第二种方法就不行了,其实第二种方法也足够简洁,而且写法也简单,层次分明,是大多数时候的选择方案,html、css、js配合起来使用很正常,不必刻意寻求零js解决方案。
2. 组件封装
这里展示一个轮播图案例,主要部分代码如下:
<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>
<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>
</div>
class Slider {
// 元素id,循环时间
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');
// 给下面N个点控制器添加事件监听,鼠标移上那个点就展示第几张图片
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();
});
}
}
getSelectedItem() {
let selected = this.container.querySelector('.slider-list__item--selected');
return selected
}
getSelectedItemIndex() {
return Array.from(this.items).indexOf(this.getSelectedItem());
}
slideTo(idx) {
let selected = this.getSelectedItem();
if (selected) {
selected.className = 'slider-list__item';
}
let item = this.items[idx];
if (item) {
item.className = 'slider-list__item--selected';
}
const detail = { index: idx }
// 定义自定义事件,并传参数进去
const event = new CustomEvent('slide', { bubbles: true, detail })
this.container.dispatchEvent(event)
}
slideNext() {
let currentIdx = this.getSelectedItemIndex();
let nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious() {
let currentIdx = this.getSelectedItemIndex();
let previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
start() {
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
stop() {
clearInterval(this._timer);
}
}
const slider = new Slider('my-slider');
slider.start();
效果如下
样式部分代码就不展示了,html部分为结构,4张图片,4个点,2个左右滑动,js就控制滑动、自动滑动等。
看起来代码还不错,但是耦合性太高了,不够灵活,比如要是要去掉下面控件、去掉左右2个控件,又得这改改那改改,要是是给别人用的,做成一个公共的,明显不行。
插件化思想
所有的控件都弄成插件,想要就添加,不要可不添加
class Slider {
constructor(id, cycle = 3000) {
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = cycle;
}
registerPlugins(...plugins) {
plugins.forEach(plugin => plugin(this));
}
...
}
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();
Slider.registerPlugins提供插件,函数pluginController,pluginPrevious,pluginNext则是注册3个插件,底下和左右的控件,这样想要什么控件,添加就是。
但是还不行,比如我要添加一个插件,就得html写上元素,css写好样式,再js注册插件,做些改进:
- 注册插件时不用获取元素,而是创建新的元素,这样就不用修改html对应部分了
- 图片部分也移到js,提供一个图片url数组,然后循环创建标签
代码如下:
<div id="my-slider" class="slider-list"></div>
class Slider {
constructor(id, opts = { images: [], cycle: 3000 }) {
this.container = document.getElementById(id);
this.options = opts;
// 向容器添加元素
this.container.innerHTML = this.render();
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = opts.cycle || 3000;
this.slideTo(0);
}
// 创建标签
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>`;
}
// 注册组件
registerPlugins(...plugins) {
plugins.forEach(plugin => {
const pluginContainer = document.createElement('div');
pluginContainer.className = '.slider-list__plugin';
pluginContainer.innerHTML = plugin.render(this.options.images);
this.container.appendChild(pluginContainer);
plugin.action(this);
});
}
...
}
const pluginController = {
render(images) {
return `
<div class="slide-list__control">
${images.map((image, i) => `
<span class="slide-list__control-buttons${i === 0 ? '--selected' : ''}"></span>
`).join('')}
</div>
`.trim();
},
action(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';
});
}
}
};
const slider = new Slider('my-slider', {
images: [
'https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'
],
cycle: 3000
});
slider.registerPlugins(pluginController);
slider.start();
这样就能很轻松的控制轮播图的各个部分了。当然,可能会有人说这不是与上面的各司其责相悖吗?其实不的,各司其责是各干各的活,而不是各找各的位置,这里面依旧是html控制结构、css控制样式、js控制行为的。
此外,还可以将通用的部分抽象成框架,像 这样
最后,记住组件的设计原则:封装性、正确性、扩展性、复用性。然后,其实上述代码还是有一些可以改进的地方的,比如获取图片元素时用的固定类名,获取SelectedItem的函数等等。
3. 过程抽象
- 用来处理局部细节的一些控制方法
- 函数式编程思想的基础应用
函数式编程就是重在逻辑结果,比如下面两段函数:
let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
function solution1(arr) {
let result = 0
for(let i = 0; i < arr.length; i++) {
if(i % 2 === 0) {
result += i * i
}
}
return result
}
function solution2(arr) {
return arr
.filter(i => i % 2 === 0)
.map(i => i * i)
.reduce((pre, cur) => pre + cur, 0)
}
solution2就是函数式编程,可以很清楚的看到都做了些什么,先是找出2的倍数,再把这些数平方,再相加。而solution1,命令式编程,就不易于看懂做了些什么(这个例子简单,所以其实容易看懂也很正常)。
函数式编程就性能上肯定是比不过命令式编程的,毕竟就这个例子来说,函数式编程调用了3次函数,还创建了新的数组,开销肯定大于命令式编程的。但是,现代计算机不差这点性能,除非你跑大型数据之类的,就我们平时的应用场景来说,使用函数式编程带来的性能额外开销是不足考虑的,而且使用函数式编程明显逻辑、流程更清晰。
再回到过程抽象,其实就是把一些可重复的过程提取成方法,比如多次调用函数只执行一次、节流、防抖,把这些过程提取出来然后就可以多次使用,而不需要每次使用时都得写一遍
// 只执行一次
function once(fn) {
return function (...params) {
if(fn) {
let res = fn.apply(this, params)
fn = null
return res
}
}
}
const log = once(() => console.log(1))
log()
log()
log()
// 节流,连续多次调用时,最多time(ms)执行一次
function throttle(fn, time = 500) {
let timer = null
return function(...params) {
if(timer) return
fn.apply(this, params)
timer = setTimeout(() => timer = null, time)
}
}
// 防抖,连续多次调用时(调用间隔在time(ms)内),只执行最后一个
function debounce(fn, time = 500) {
let timer = null
return function(...params) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, params)
}, time)
}
}
三、注意点
写代码时要关注风格、效率、约定、使用场景、设计。
这里要注意使用场景,比如你写一个排序算法,但只会用在小数据的排序上,就几个、十几个元素的排序,这时其实O(n^2)和O(nlogn)的区别就不大了,这时就不必揪着不放的想着怎么优化你的排序算法(只是说不必揪着不放,不是完全不需要优化)。再比如上面的轮播图,如果只用在你自己项目里,就用那一个地方,那其实第一种写法也无妨,最多就弄成数据驱动的,可以适应更多或更少的图片。
再揭示一个误区,就是打乱数组元素顺序时,可能很多人是这么写的
arr.sort(() => Math.random() - 0.5)
其实这个不够随机,因为sort函数内部对不同元素比对次数的问题,原本就在前面的元素更容易还是排在前面,这是因为小数据量时采用插入排序,后面的元素想排到前面要比对更多次数,而每次比对只有50%的概率往前移动。如下可以测试一下:
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
function randomSort(arr) {
return arr.sort(() => Math.random() - 0.5)
}
let res = Array(9).fill(0)
for (let i = 0; i < 1000000; i++) {
let temp = randomSort([...arr])
for(let i = 0; i < temp.length; i++) {
res[i] += temp[i]
}
}
console.table(res)
// ┌─────────┬─────────┐
// │ (index) │ Values │
// ├─────────┼─────────┤
// │ 0 │ 4519877 │
// │ 1 │ 4519880 │
// │ 2 │ 4975707 │
// │ 3 │ 5052507 │
// │ 4 │ 5058296 │
// │ 5 │ 5040230 │
// │ 6 │ 5068165 │
// │ 7 │ 5211657 │
// │ 8 │ 5553681 │
// └─────────┴─────────┘
可以改一下,思路这样:从1 - n个位置随机选一个,把这个元素移到最后,再1 - n-1个位置随机选一个,也移到最后,如此下去。每个数被选中的概率都一样,第一个为,第二个为 ,后面的也一样,都是1/n,不信的可以像上面一样去测试测试。
四、 个人总结
写JavaScript还是要注意很多原则和思路的,要多学习掌握,掌握各种方法,写出优秀的代码。