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

发表于 5年以前  | 总阅读数:1629 次
  • 版本一
  • 版本二
  • 版本三
  • 版本四
  • 版本五
  • 版本六
  • 总结

这是 奇舞前端特训营 JavaScript 培训课程 的节选。很多同学觉得写 JavaScript 很简单,只要能写出功能来,效果能实现就好。还有一些培训机构,专门教人写各种"炫酷特效",以此让许多人觉得这些培训很"牛逼"。然而事实上,能写 JavaScript 和写好 JavaScript 这中间还有很遥远的距离。成为专业前端,注定在 JavaScript 路途上需要一步步扎实的修炼,没有捷径。

看一个简单的例子:

实现一个类似于"交通灯"的效果,让三个不同颜色的圆点每隔 2 秒循环切换。

对应的 HTML 和 CSS 如下:

<ul id="traffic" class="wait">
      <li><span></span></li>
      <li><span></span></li>
      <li><span></span></li>
    </ul>
#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 都暴露出来了,而且以全局变量的形式,这么做很不好,需要优化。

版本三

const traffic = document.getElementById("traffic");

    function start(traffic, stateList){
      var currentStateIndex = 0;

      setInterval(function(){
        var state = stateList[currentStateIndex];
        traffic.className = state;
        currentStateIndex = (currentStateIndex + 1) % stateList.length;
      }, 2000);
    }

    start(traffic, ["wait", "stop", "pass"]);

版本三是中规中矩的一版,也是一般我们在工作中比较常用的思路。应该将暴露出来的 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.bind(null, 1000))
        .then(setState.bind(null, "stop"))
        .then(wait.bind(null, 2000))
        .then(setState.bind(null, "pass"))
        .then(wait.bind(null, 3000))
        .then(reset);
    }

    reset();

版本五的思路是,既然我们需要考虑不同的持续时间,那么我们需要将等待时间抽象出来

function wait(time){
      return new Promise(resolve => setTimeout(resolve, time));
    }

这一版本里我们用了 Promise 来处理回调问题,当然对 ES6 之前的版本,可以用 shim 或 polyfill、第三方库,也可以选择不用 Promise。

版本五抽象出的 wait 方法也还比较通用,可以用在其他地方。这是版本五好的一点。

版本六

我们还可以进一步抽象,设计出版本六,或者类似的对象模型

const trafficEl = document.getElementById("traffic");

    function TrafficProtocol(el, reset){
      this.subject = el;
      this.autoReset = reset;
      this.stateList = [];
    }

    TrafficProtocol.prototype.putState = function(fn){
      this.stateList.push(fn);
    }

    TrafficProtocol.prototype.reset = function(){
      let subject = this.subject;

      this.statePromise = Promise.resolve();
      this.stateList.forEach((stateFn) => {
        this.statePromise = this.statePromise.then(()=>{
          return new Promise(resolve => {
            stateFn(subject, resolve);
          });
        });
      });
      if(this.autoReset){
        this.statePromise.then(this.reset.bind(this));
      }
    }

    TrafficProtocol.prototype.start = function(){
      this.reset();
    }

    var traffic = new TrafficProtocol(trafficEl, true);

    traffic.putState(function(subject, next){
      subject.className = "wait";
      setTimeout(next, 1000);
    });

    traffic.putState(function(subject, next){
      subject.className = "stop";
      setTimeout(next, 2000);
    });

    traffic.putState(function(subject, next){
      subject.className = "pass";
      setTimeout(next, 3000);
    });

    traffic.start();

这一版本里,我们设计了一个 TrafficProtocol 类,它有 putState、reset、start 三个方法:

  • putState 接受一个函数作为参数,这个函数自身有两个参数,一个是 subject,是由 TrafficProtocol 对象初始化时设定的 DOM 元素,一个是 next,是一个函数,表示结束当前 state,进入下一个 state。

  • reset 结束当前状态循环,开始新的循环。

  • start 开始执行循环,这里的实现是直接调用 reset。

看一下 reset 的实现思路:

TrafficProtocol.prototype.reset = function(){
      let subject = this.subject;

      this.statePromise = Promise.resolve();
      this.stateList.forEach((stateFn) => {
        this.statePromise = this.statePromise.then(()=>{
          return new Promise(resolve => {
            stateFn(subject, resolve);
          });
        });
      });
      if(this.autoReset){
        this.statePromise.then(this.reset.bind(this));
      }
    }

在这里我们创建一个 statePromise,然后将 stateList 中的方法(通过 putState 添加的)依次绑定到 promise 上。如果设置了 autoReset,那么我们在 promise 的最后绑定 reset 自身,这样就实现了循环切换。

有了这个模型,我们要添加新的状态,只需要通过 putState 添加一个新的状态就好了。这一模型不仅仅可以用在这个需求里,还可以用在任何需要顺序执行异步请求的地方。

最后,我们看到,版本六用到了面向对象、过程抽象、Promise等模式,它的优点是 API 设计灵活,通用性和扩展性好。但是版本六也有缺点,它的实现复杂度比前面的几个版本都高,我们在做这样的设计时,也需要考虑是否有过度设计的嫌疑。

总结

  • 设计是把双刃剑,繁简需要权衡,尺度需要把握。
  • 写代码简单,程序设计不易,需要走心。

上面六个版本大家喜欢哪一个,或者有什么其他的思路,欢迎在评论区讨论。

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:8月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:8月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:8月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:8月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:8月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:8月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:8月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:8月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:8月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:8月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:8月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:8月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:8月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:8月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:8月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:8月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:8月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:8月以前  |  398次阅读  |  详细内容 »
 相关文章
为Electron程序添加运行时日志 4年以前  |  19610次阅读
Node.js下通过配置host访问URL 5年以前  |  5636次阅读
用 esbuild 让你的构建压缩性能翻倍 4年以前  |  5371次阅读
 目录