青训营月影老师告诉我写好JavaScript的四大技巧——封装函数

Posted YK菌

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了青训营月影老师告诉我写好JavaScript的四大技巧——封装函数相关的知识,希望对你有一定的参考价值。

如何写好javascript是每一个前端工程师一直以来在思考的问题,月影老师告诉我们一些写好JavaScript的原则,同时也教了一些我们如何写好JavaScript的技巧,今天来继续跟着月影老师学JavaScript吧~~

起步

今天我们来讨论函数封装,我们要把JavaScript代码写好,那一定要考虑代码的封装性,我们通过一个案例来学习学习函数封装的思想。

来看一个状态切换交通灯案例

案例:异步状态切换(交通灯)

这个例子的具体需求是,模拟交通灯信号,每隔一段时间,显示不同的颜色,循环切换状态

未封装:菜鸟版

这个拿给我们新手菜鸟,可能会这样写:

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

(function reset(){
  traffic.className = 's1';
  
  setTimeout(function(){
      traffic.className = 's2';
      setTimeout(function(){
        traffic.className = 's3';
        setTimeout(function(){
          traffic.className = 's4';
          setTimeout(function(){
            traffic.className = 's5';
            setTimeout(reset, 1000)
          }, 1000)
        }, 1000)
      }, 1000)
  }, 1000);
})();

上面的这段菜鸟版代码虽然实现了我们的需求,但是它在设计上有很大的缺陷

个缺陷:reset函数访问了外部环境traffic,而它在函数内部不具有意义
这么做有两个问题:

  1. 如果我们修改了html代码,元素不叫做traffic了,这个函数就不工作了。
  2. 如果我们想把这个函数复用到其他地方,我们还得在那个地方重建这个traffic对象。

个缺陷是 回调地狱,我们要手写那么多次回调套回调,如果要增加或者减少状态,会很麻烦

出现这些问题的原因就是我们没有做到对函数进行封装!!!

所以,我们要封装函数,不能直接将traffic这个对象直接写在函数中,也不能将状态切换的具体数据直接写在函数中

封装数据:数据抽象版

我们先对数据进行抽象,或者可以说将数据从函数中解耦出来~

首先,我们将traffic变量作为函数的参数传入我们的start函数中,这样函数体内部就没有完全来自外部环境的变量了

然后我们将状态(数据)抽象出来,形成一个对象数组,存着状态的名称state和等待时间last。将数组传入我们的start函数中,递归调用applyState函数,来实现状态切换。

这样做对函数进行了封装,将数据抽象出来,我们在修改数据的时候,不用修改函数体中的内容,使得我们函数的封装性得到了很大的提升。

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

const stateList = [
  {state: 'wait', last: 1000},
  {state: 'stop', last: 3000},
  {state: 'pass', last: 3000},
];

function start(traffic, stateList){
  function applyState(stateIdx) {
    const {state, last} = stateList[stateIdx];
    traffic.className = state;
    setTimeout(() => {
      applyState((stateIdx + 1) % stateList.length);
    }, last)
  }
  applyState(0);
}

start(traffic, stateList);

数据抽象就是把数据定义并聚合成能被过程处理的对象,交由特定的过程处理。 简单来说就是数据的结构化。

这里做出了两点改进

  1. 将外部变量变成参数传进函数 traffic
  2. 将状态数据与函数进行解耦,抽象数据 stateList
    都提升了函数封装性和可复用

封装行为:过程抽象版

在之前我们说到的三大原则的过程抽象 中,我们知道,不仅可以对数据进行抽象,也可以对过程进行抽象,下面我们来通过抽象过程来进行函数封装。

这次我们抽象出过程的两种操作:① 改变类名 ② 等待时间

① 改变类名封装成函数setState

function setState(state){
  traffic.className = state;
}

② 等待时间 就是用wait函数封装setTimeout,我们把定时器的操作抽象出来,进行promise化,提高我们代码的可读性

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

将setTimeout函数封装成一个返回Promise的wait函数。
然后配合async/await语法,可以用同步代码风格写异步代码

这段代码与之前的代码相比,它的可读性是不是提高了很多?并且我们把他的过程都抽象出来了

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

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

function setState(state){
  traffic.className = state;
}

async function start(){
  //noprotect
  while(1){
    setState('wait');
    await wait(1000);
    setState('stop');
    await wait(1000);
    setState('pass');
    await wait(1000);
  }
}

start();

封装行为:宏观版(封装轮询操作)

这里我们将改变类名和等待时间的操作封装在一起到setstate中去,

将设置状态封装成函数,将这些状态作为轮询函数的参数

async function setState(state, ms){
  traffic.className = state;
  await wait(ms);
}

主要是要封装轮询函数

将循环播放抽象成一个轮询函数,用来切换状态

function poll(...fnList){
  let stateIndex = 0;
  
  return async function(...args){
    let fn = fnList[stateIndex++ % fnList.length];
    return await fn.apply(this, args);
  }
}

就可以这样使用

let trafficStatePoll = poll(setState.bind(null, 'wait', 1000),
                            setState.bind(null, 'stop', 3000),
                            setState.bind(null, 'pass', 3000));

最终,完整的代码就是如下所示

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

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

function poll(...fnList){
  let stateIndex = 0;
  
  return async function(...args){
    let fn = fnList[stateIndex++ % fnList.length];
    return await fn.apply(this, args);
  }
}

async function setState(state, ms){
  traffic.className = state;
  await wait(ms);
}

let trafficStatePoll = poll(setState.bind(null, 'wait', 1000),
                            setState.bind(null, 'stop', 1000),
                            setState.bind(null, 'pass', 1000));

(async function() {
  // noprotect
  while(1) {
    await trafficStatePoll();
  }
}());

这样加强了我们函数的灵活性,我们状态切换的具体内容可以更改,而且将状态抽象出来,改变的状态的数量也很方便修改,直接在trafficStatePoll中添加即可

总结

  1. 函数要做好封装,降低函数耦合性
  2. 要确保函数尽量不要直接使用和修改外部的变量,要用到外部变量,应该使其成为参数传入函数中
  3. 函数是一个处理数据的最小单元。它包含数据处理过程
  4. 做好数据抽象,将用到的数据抽象出去形成对象或数组,可以提高函数的复用性
  5. 做好过程抽象,将过程进行抽象形成独立的函数,可以提高函数的复用性、灵活性
  6. 将异步操作进行promise化,可以提高函数的可读性

更多相关博文

【青训营】月影老师告诉我写好JavaScript的三大原则——各司其责

【青训营】月影老师告诉我写好JavaScript的三大原则——组件封装

【青训营】月影老师告诉我写好JavaScript的三大原则——过程抽象

【青训营】月影老师告诉我写好JavaScript的四大技巧——风格优先

【青训营】月影老师告诉我写好JavaScript的四大技巧——保证正确

也可以关注专栏:
【青训营笔记专栏】

以上是关于青训营月影老师告诉我写好JavaScript的四大技巧——封装函数的主要内容,如果未能解决你的问题,请参考以下文章

青训营月影老师告诉我写好JavaScript的四大技巧——风格优先

青训营月影老师告诉我写好JavaScript的四大技巧——风格优先

青训营月影老师告诉我写好JavaScript的四大技巧——妙用特性

青训营月影老师告诉我写好JavaScript的三大原则之——过程抽象

青训营月影老师告诉我写好JavaScript原则与技巧大总结

青训营月影老师告诉我写好JavaScript原则与技巧大总结