专栏名称: 程序员大咖
为程序员提供最优质的博文、最精彩的讨论、最实用的开发资源;提供最新最全的编程学习资料:PHP、Objective-C、Java、Swift、C/C++函数库、.NET Framework类库、J2SE API等等。并不定期奉送各种福利。
目录
相关文章推荐
程序员的那些事  ·  趣图:纯种牛马求聘用 ·  3 天前  
程序员的那些事  ·  事业编一年 8 万 40 年退休挣 320 ... ·  2 天前  
码农翻身  ·  11w*14薪,进DeepSeek了! ·  2 天前  
OSC开源社区  ·  地表最强「开源版PS」——GIMP ... ·  5 天前  
51好读  ›  专栏  ›  程序员大咖

如何写出好的 JavaScript —— 浅谈 API 设计

程序员大咖  · 公众号  · 程序员  · 2017-01-17 19:18

正文

作者:十年踪迹

链接: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







    请到「今天看啥」查看全文