作者:十年踪迹
链接:www.h5jun.com/post/how-to-write-better-js-code.html
很多同学觉得写 JavaScript 很简单,只要能写出功能来,效果能实现就好。还有一些培训机构,专门教人写各种“炫酷特效”,以此让许多人觉得这些培训很“牛逼”。然而
事实上,能写 JavaScript 和写好 JavaScript 这中间还有很遥远的距离。成为专业前端,注定在 JavaScript 路途上需要一步步扎实的修炼,没有捷径。
看一个简单的例子:
实现一个类似于“交通灯”的效果,让三个不同颜色的圆点每隔 2 秒循环切换。
对应的 HTML 和 CSS 如下:
id
=
"traffic"
class
=
"wait"
>
#traffic > li
{
display
:
block
;
}
#traffic span
{
display
:
inline-block
;
width
:
50px
;
height
:
50px
;
background-color
:
gray
;
margin
:
5px
;
border-radius
:
50%
;
}
#traffic.stop li:nth-child(1) span
{
background-color
:
#a00
;
}
#traffic.wait li:nth-child(2) span
{
background-color
:
#aa0
;
}
#traffic.pass li:nth-child(3) span
{
background-color
:
#0a0
;
}
那么这一功能的 JavaScript 该如何实现呢?
有的同学说,这个实现还不简单嘛?直接用几个定时器一下切换不就好了:
const
traffic
=
document
.
getElementById
(
"traffic"
);
(
function
reset
(){
traffic
.
className
=
"wait"
;
setTimeout
(
function
(){
traffic
.
className
=
"stop"
;
setTimeout
(
function
(){
traffic
.
className
=
"pass"
;
setTimeout
(
reset
,
2000
)
},
2000
)
},
2000
);
})();
没错,就这个功能本身,这样实现就 OK 了。但是这样实现有什么问题呢?
首先是
过程耦合
,状态切换是wait->stop->pass 循环,在上面的设计里,实际上操作顺序是耦合在一起的,要先 ‘wait’,然后等待 2000 毫秒再 ‘stop’,然后再等待 2000 毫秒在 ‘pass’,这中间的顺序一旦有调整,需求有变化,代码都需要修改。
其次,这样的异步嵌套是会产生
callback hell
的,如果需求不是三盏灯,而是五盏灯、十盏灯,代码的嵌套结构就很深,看起来就很难看了。
所以我们说,版本一方法虽然直接,但因为抽象程度很低(几乎没有提供任何抽象 API),它的扩展性很不好,因为异步问题没处理,代码结构也很不好。如果只能写这样的代码,是不能说就写好了 JavaScript 的。
要解决版本一的
过程耦合
问题,最简单的思路是将状态
['wait','stop','pass']
抽象出来:
const
traffic
=
document
.
getElementById
(
"traffic"
);
var
stateList
=
[
"wait"
,
"stop"
,
"pass"
];
var
currentStateIndex
=
0
;
setInterval
(
function
(){
var
state
=
stateList
[
currentStateIndex
];
traffic
.
className
=
state
;
currentStateIndex
=
(
currentStateIndex
+
1
)
%
stateList
.
length
;
},
2000
);
这是一种数据抽象的思路,应用它我们得到了上面的这个版本。
这一版本比前一版本要好很多,但是它也有问题,最大的问题就是
封装性很差
,它把 stateList 和 currentStateIndex 都暴露出来了,而且以全局变量的形式,这么做很不好,需要优化。
版本三是中规中矩的一版,也是一般我们在工作中比较常用的思路。应该将暴露出来的 API 暴露出来(本例中的 stateList)。将不应该暴露出来的数据或状态隐藏(本例中的 currentStateIndex)。
有许多同学觉得说写出这一版本来已经很不错的。的确,应该也还不错,但这一版的抽象程度其实也不是很高,或者说,如果考虑适用性,这版已经很好了,但是如果考虑可复用性的话,这版依然有改进空间。
我们再看一个思路上较有意思的版本。
const traffic = document.getElementById("traffic");
function
poll
(...
fnList
){
let
stateIndex
=
0
;
return
function
(...
args
){
let
fn
=
fnList
[
stateIndex
++ %
fnList
.
length
];
return
fn
.
apply
(
this
,
args
);
}
}
function
setState
(
state
){
traffic
.
className
=
state
;
}
let
trafficStatePoll
=
poll
(
setState
.
bind
(
null
,
"wait"
),
setState
.
bind
(
null
,
"stop"
),
setState
.
bind
(
null
,
"pass"
));
setInterval
(
trafficStatePoll
,
2000
);
这一版用的是
过程抽象
的思路,而过程抽象,是
函数式编程
的基础。在这里,我们抽象出了一个
poll(...fnList)
的高阶组合函数,它将一个函数列表组合起来,每次调用时依次轮流执行列表里的函数。
我们说,程序设计的本质是抽象,而
过程抽象
是一种与
数据抽象
对应的思路,它们是两种不同的抽象模型。数据抽象比较基础,而过程抽象相对高级一些,也更灵活一些。数据抽象是研究函数如何操作数据,而过程抽象则在此基础上研究函数如何操作函数。所以说如果把抽象比作数学,那么数据抽象是初等数学,过程抽象则是高等数学。同一个问题,既可以用初等数学来解决,又可以用高等数学来解决。用什么方法解决,取决于问题的模型和难度等等。
好了,上面我们有了四个版本,那么是否考虑了这些版本就足够了呢?
并不是。因为需求是会变更的。假设现在需求变化了:
需求变更:让 wait、stop、pass 状态的持续时长不相等,分别改成 1秒、2秒、3秒。
那么,我们发现 ——
除了版本一之外,版本二、三、四全都跪了……
那是否意味着我们要
回归到版本一
呢?
当然并不是。
const
traffic
=
document
.
getElementById
(
"traffic"
);
function
wait
(
time
){
return
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
time
));
}
function
setState
(
state
){
traffic
.
className
=
state
;
}
function
reset
(){
Promise
.
resolve
()
.
then
(
setState
.
bind
(
null
,
"wait"
))
.
then
(
wait