Day03:JavaScript编码原则 | 青训营笔记

71 阅读5分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 3 天。

写好JS的一些原则

  • 各司其职:HTML(内容)、CSS(样式)与JS(行为)职能分离

    注意是职能分离,而不是形式上分开写!

  • 组件封装:好的UI组件具备正确性、扩展性、复用性
  • 过程抽象:应用函数式编程思想

各司其职

应当避免不必要的由JS直接操作样式:

  • 方案A:通过设置类的方式,将样式管理交给CSS
  • 方案B:纯展示类交互寻求零JS方案(采用隐藏式的复选框控件记录状态+labelfor属性绑定实际的按钮,辅以:checked伪类选择器和后代选择器判断状态来设置样式)

组件封装:以轮播图为例

  • 内容结构(HTML):
    • 轮播图主体是一个典型的列表结构,可以使用无序列表<ul>元素来实现
    • 轮播图的状态(显示若干个小圆点表示当前是第几个图,可以用<span>实现)
    • 轮播图的切换按钮(<a>标签实现)
  • 样式表现(CSS):
    • 多张图片显示在同一位置:CSS绝对定位
    • 图片切换:修饰符(modifier)
    • 图片切换动画(渐入渐出):CSS过渡(transition)
  • 交互行为(JS):API设计应保证原子操作职责单一、满足灵活性
    • 设计类与API:
      • 构造器:获取轮播图元素(根据ID获取),获取<li>中的图片元素
      • 获取当前显示的图片(选择具有指定类的图片)与图片索引(利用indexOf()函数)
      • 切换至给定索引图片(获取当前显示的图片,取消设置类:显示,获取指定索引的图片,设置类:显示)
      • 切换到上一张图/下一张图(类似切换至给定索引图片,但是需要处理索引+1/-1的问题,并注意对索引取模来实现循环)
    • 自定义事件:与状态绑定的行为建议使用此方法,CustomEvent

改进与重构:插件化

原先的组件不够灵活(例如有时候我们不需要切换按钮,有时候不需要显示轮播图的状态),同时构造函数已经超过25行有效代码,且负责的工作比较冗杂,其实控制组件的部分可以单独提取成可选的“插件”,插件与组件之间通过依赖注入的方式建立联系,即所谓的解耦

改进与重构:模板化

如果需要改动插件,仍然需要同时修改HTML和JS,这样修改图片也不方便,为此我们可以采用“数据驱动”的方式,HTML只提供一个容器,使用JS来填充具体的数据。

改进与重构:抽象化(封装组件)

可以进一步提取重复代码,抽象出组件类,通过类继承的方式实现代码复用。这实际上就是一个简单的组件框架。

这种设计没有考虑嵌套、子组件等问题,也没有考虑CSS模板化问题(CSS仍然需要手动修改),还可以进一步扩展。

过程抽象

例如,为了实现只运行一次的效果,可以:

  • addEventListener()中实现时,可以附加选项{ once: true }来保证只执行一次
  • 可以建立一个高阶函数once来实现

为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象

高阶函数

返回函数的函数被称为高阶函数(Higher-Order Function,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):限制触发频率,超过不再触发

    function throttle(fn, time = 500){
      let timer;
      return function(...args){
        if(timer == null){
          fn.apply(this,  args);
          timer = setTimeout(() => {
            timer = null;
          }, time)
        }
      }
    }
    
  • 防抖动(Debounce):等到数据稳定后再触发

    function debounce(fn, dur){
      dur = dur || 100; // dur 是 undefined 的时候取 100
      var timer;
      return function(){
        clearTimeout(timer);
        timer = setTimeout(() => {
          fn.apply(this, arguments);
        }, dur);
      }
    }
    
  • 消费(Consumer,例1 / 例2):生成一个执行队列,拉开每次触发的执行间隔

    function consumer(fn, time){
      let tasks = [],
          timer;
      
      return function(...args){
        tasks.push(fn.bind(this, ...args));
        if(timer == null){
          timer = setInterval(() => {
            tasks.shift().call(this)
            if(tasks.length <= 0){
              clearInterval(timer);
              timer = null;
            }
          }, time)
        }
      }
    }
    
  • 迭代函数(Iterative):把原本只接受单个元素的函数改造为接收多个元素(每个元素依次执行原来的函数)

    const isIterable = obj => obj != null 
      && typeof obj[Symbol.iterator] === 'function';
    
    function iterative(fn) {
      return function(subject, ...rest) {
        if(isIterable(subject)) {
          const ret = [];
          for(let obj of subject) {
            ret.push(fn.apply(this, [obj, ...rest]));
          }
          return ret;
        }
        return fn.apply(this, [subject, ...rest]);
      }
    }
    

为什么需要高阶函数

  • 减少重复逻辑的实现
  • 降低非纯函数的测试成本(不需要为每个类似的非纯函数做测试,只需要测试基本的函数+高阶函数即可)

编程范式

  • 命令式(imperative):需要告知具体怎么做
    let list = [1, 2, 3, 4];
    let mapl = [];
    for(let i = 0; i < list.length; i++) {
      mapl.push(list[i] * 2);
    }
    
    • 面向过程(procedural)
    • 面向对象(object-oriented)
  • 声明式(declarative):只需要说结果是什么
    let list = [1, 2, 3, 4];
    const double = x => x * 2;
    list.map(double);
    
    • 逻辑式(logic)
    • 函数式(functional)

声明式相对命令式的优势

  • 隐藏了实现细节
  • 添加状态/数据较为容易,可扩展性强

命令式和声明式区别的例子:Toggle

  • 命令式
    switcher.onclick = function(evt){
      if(evt.target.className === 'on') {
        evt.target.className = 'off';
      } else {
        evt.target.className = 'on';
      }
    }
    
  • 声明式
    function toggle(...actions){
      return function(...args){
        let action = actions.shift();
        actions.push(action);
        return action.apply(this, args);
      }
    }
    
    switcher.onclick = toggle(
      evt => evt.target.className = 'off',
      evt => evt.target.className = 'on'
    );