这是我参与「第四届青训营 」笔记创作活动的第2天
一、写好JavaScript的三个原则
-
各司其责
让HTML、CSS、JavaScript职能分离
-
组件封装
好的UI组件具备正确性、扩展性、复用性
-
过程抽象
应用函数式编程思想
1. 各司其职
HTML负责结构
CSS负责样式表现
JavaScript负责行为
要保证结构、表现、行为分离。
场景:模式切换
点击按钮,切换场景的白天/黑夜模式
版本一代码:
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中控制了style的具体内容,js和css没有分离开。
对于以上这段代码,主要存在以下几点问题:
- 样式和行为没有分离,即没有做到职责分离
- 全局污染
- 复用性不强
- 封装性不好
版本二:
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
相比版本一,通过class切换比通过style切换分离程度更高。将黑夜模式和白天模式通过class写好,然后通过label的点击事件控制className的切换。
版本三:
<input id="modeCheckBox" type="checkbox">
<div class="content">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<h1>深夜食堂</h1>
</header>
<main>
<div class="pic">
<img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
</div>
<div class="description">
<p>
这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返 [6] 。
</p>
</div>
</main>
</div>
#modeCheckBox {
display: none;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
利用了CSS的高级功能,通过伪类选择器来匹配元素的状态。
- 首先在内容板块前面添加一个checkbox,用于控制模式;
- 通过label的for属性,将label与checkbox联动起来;
- 通过checkbox的伪类选择器来匹配元素状态,并修改样式;
- 最后通过display:none把checkbox隐藏起来即可。
版本三通过css直接完成了样式的切换,完全与js分离。
总结
- HTML、CSS、JavaScript各司其职
- 应当避免不必要的由JavaScript直接操作样式
- 可以用样式的class来表示状态
- 纯展示类交互寻求零JS方案
2. 组件封装
组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性。
场景:图片轮播
-
HTML结构:列表结构,可以通过无序列表ul将图片加载进来;
-
CSS表现:
- 使用 CSS 绝对定位将图片重叠在同一个位置
- 轮播图切换的状态使用修饰符(modifier)
- 轮播图的切换动画使用 CSS transition
-
JavaScript行为:
-
定义Slider类,再封装接口;
getSelectedItem ()getSelectedItemIndex ()slideTo ( id )slideNext ()slidePrevious ()
-
控制流:自定义事件来绑定状态,进行解耦。
-
总结:写组件的基本方法
-
HTML结构设计
-
CSS展现效果
-
JavaScript行为设计
- API功能
- Event控制流
重构
解耦:
-
插件化
- 将控制元素抽取成插件
- 插件与组件之间通过依赖注入方式进行联系。(即通过插件实现功能,然后将其注册到组件中,实现组件和插件的联系)
-
模板化
- 将HTML结构和CSS样式进行模板化(动态地修改组件)
- 更易于扩展
-
抽象化——组件框架
- 将组件通用模型抽象出来
- 可以进一步考虑父子组件,将子组件作为父组件的插件来使用。
3. 过程抽象
-
用来处理局部细节控制的一些方法
-
过程抽象,是一种思维方法,也是一种编程范式
-
函数式编程思想的基础应用。
比如:为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来,写为Once函数。这个过程我们称为过程抽象。
高阶函数
如果一个函数return另外一个函数,则它为高阶函数。
HOF:以函数作为参数、以函数作为返回值,常用于作为函数装饰器
function HOF0(fn){
return function(...args){
return fn.apply(this,args);
}
}
常用的高阶函数:
Once函数:可以防止函数不必要的重复执行,只让其执行一次。
function once(fn) {
return function(...args) {
if(fn) {
const ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
Throttle节流函数:节流的意思就是函数在一段时间内的多次调用,仅第一次有效。
Debounce防抖函数:函数在一段时间内的多次调用,仅使得最后一次调用有效。
- 节流---王者荣耀释放技能
- 防抖---王者荣耀回城
Consumer/2:
Iterative函数:迭代方法
为什么要用高阶函数?
事实上,在一个系统中,纯函数越多时,这个系统的可维护性是越好的。
纯函数: 如果输入的值是唯一的,且输出的值也是唯一的,那它就是纯函数。比如:
function add(x, y) {
return x + y;
}
add(1, 2); // 3
非纯函数:就是输入和输出的值不是唯一的,那它就不是纯函数。比如:
let x = 10;
function foo() {
return x++;
}
function bar() {
return x * 2;
}
foo();
foo();
bar();
bar();
假设我们稍微调整了函数的顺序,那么它的执行结果都是不一样的,这种就称为了非纯函数。
之所以要使用纯函数的原因在于,它能使得我们代码的可维护性和可扩展性变得更好。
编程范式
-
命令式编程语言
- 面向过程(fortran、C)
- 面向对象(C++、Java)
-
声明式编程语言
- 逻辑式
- 函数式编程
js既可以命令式,也可以函数式。对于命令式编程来说,它指的是命令机器如何去做事情 (how) ,这样不管你想要的是什么 (waht) ,它都会按照你的命令实现。
而对于声明式编程来说,它旨在告诉机器你想要的是什么 (what) ,让机器想出如何去做 (how) ,这种方式的可扩展性会更高。
二、如何评价代码的好坏?
首先我们先来看一段代码。具体代码如下:
//判断一个mat2d矩阵是否是单位矩阵
function isUnitMatrix2d(m) {
return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
}
我们先来思考一个问题:上面的代码写的好不好?为什么?
其实,上面这段代码是一段真实的代码,其来源于一个开源库。具体地址:spritejs
get layerTransformInvert() {
if(this[_layerTransformInvert]) return this[_layerTransformInvert];
const m = this.transformMatrix;
if(m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0) {
return nul l;
}
this[_layerTransformInvert] = mat2d.invert(m);
return this[_layerTransformInvert];
}
现在,我们来分析一下上面这段代码写的好不好。具体分析结果如下:
单从代码优雅性的角度来看的话,这段代码确实不够优雅。
但是呢,上面这个库是一个图形库,且这段代码负责的是在渲染之前,计算我们图层的 transform 矩阵的逻辑代码。
也就是说,我们在计算每一帧的时候,都需要进行一个计算。因此呢,在这样的一个场景下,我们首先要去关注的是如何达到性能最优化。
而与其他任何类型的写法来比,以上这种写法能够达到性能最大化,所以,以上这段代码在这样的一个场景下是没有任何问题滴。
同时呢,如果是对于其他场景来说,如果堆性能优化没有这么敏感的话,可以不用这么写。
所以,一般来说,代码的好坏要结合它的使用场景来分析。
三、写代码最应该关注什么?
写代码我们应该要注重风格、效率、约定、 使用场景(算法) 和设计等方面;
- 风格:选择什么风格都没有错,关键是风格要统一(分号、行尾花括号等等);
- 效率:在写代码时要考虑什么样的代码写起来效率是最高的,能写高效率的代码就不要写低效率的代码;当然,也要追求一个平衡就是,要结合它的场景来使用;
- 约定:在开发前,团队要约定好代码规范和风格,比如
eslint、airbnb等等;
四、当年的 Left-pad 事件
我们来了解下当年 github 的 Left-pad 事件。先来这个事件中的一段代码,具体如下
function leftpad (str, len, ch) {
str = String(str);
var i = -1;
if (!ch && ch !== 0) ch = '';
len = len - str.length;
while(++i < len) {
str = ch - str;
}
return str;
}
这个作者想要实现的功能就是,比如说我现在有一段字符串,然后呢,我想要把这段字符串拼接成同样长度的字符串。
这个使用场景通常会放在一些展示类的地方,比如排序。当时这个模块一开始被用于很多的 npm 包中,但是后面被作者下线了,所以引起了很大的风波,因为很多人在用的库突然被下线了,试想,开发者岂不是要哭辽。
那这个事件本身的槽点呢,主要有以下三点:
- npm模块粒度
- 代码风格
- 代码质量和代码效率
如果要考虑效率的话,那么我们可以对代码进行改进。比如:
function leftpad(str, len, ch = '') {
str = "" + str;
// 判断要补充的代码长度
const padLen = len - str.length;
if(padLen <= 0) {
return str;
}else {
return ("" + ch).repeat(padLen) + str;
}
}
通过这样的改进,使得代码更简洁,同时也极大的提升了运行效率。