什么是好的 JS 代码:各司其职、组件封装、过程抽象

使用 JS 解决实际问题:如何评价一段代码的好坏、写代码最应关注什么

# 如何写好 JavaScript - 笔记

# 各司其职

我们知道,前端 web 对于 HTML、CSS、JavaScript 的分工都很明确。

HTML 负责页面骨架、CSS 负责页面的渲染、JavaScript 负责页面的行为。

# 一个🌰

对于一个切换页面深色模式切换的需求,如果要用 JS,该怎么实现?

很容易想到:

  • 使用 button,监听点击事件,更改页面背景颜色和文字颜色

    const btn = document.getElementById('modeBtn');
    btn.addEventListener('click', (e) => {
        const body = document. body ;
        if(e.target.innerHTML === '🌞'){
            body.style.backgroundColor = 'black';
            body.style.color = 'white';
            e.target.innerHTML = '🌜';
        } else {
            body.style.backgroundColor = 'white';
            body.style.color = 'black ';
            e.target.innerHTML = '🌞';
        }
    });

但是这个版本的实现语义不清,如果让别人来阅读这段代码,可能一时间不知道是在实现什么功能。

于是,我们想出优化方案:

  • 同样使用 button,监听点击事件,但这次直接修改容器的 class,通过在 css 中写的 class 样式修改页面表现

    const btn = document.getElementById('modeBtn');
    btn.addEventListener('click', (e) => {
        const body = document. body ;
        if(body.className === 'night'){
            body.className = '';
        } else {
            body.className = 'night';
        }
    });

很明显,这个版本已经比上个版本好多了,我们一眼就能看出来这段代码是在做什么。

但实际上,我们还有一种更好的解决方案 —— 只使用 CSS 实现

  • 使用 checkbox + :checked 伪类 + 兄弟元素选择器来实现

那么,实际上来说,表现层的工作就让负责表现层的 CSS 来做才是最好的

总结下来就是以下几点:

  • HTML/CSS/JS 各司其责
  • 应当避免不必要的由 JS 直接操作样式
  • 可以用 class 来表示状态
  • 纯展示类交互寻求零 JS 方案

# 组件封装

组件是指 Web 页面上抽出来一个个包含模版(HTML)、功能 (JS)和样式 (CSS) 的单元。

好的组件具备封装性、正确性、扩展性、复用性。

# 如何实现一个轮播图组件?

  • HTML 结构设计

    <div id="my-slider" class="slider-list">
      <ul>
        <li class="slider-list__item--selected">
          <img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png"/>
        </li>
        <li class="slider-list__item">
          <img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg"/>
        </li>
        <li class="slider-list__item">
          <img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg"/>
        </li>
        <li class="slider-list__item">
          <img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg"/>
        </li>
      </ul>
    </div>
  • CSS 展现效果

    #my-slider{
      position: relative;
      width: 790px;
    }
    .slider-list ul{
      list-style-type:none;
      position: relative;
      padding: 0;
      margin: 0;
    }
    .slider-list__item,
    .slider-list__item--selected{
      position: absolute;
      transition: opacity 1s;
      opacity: 0;
      text-align: center;
    }
    .slider-list__item--selected{
      transition: opacity 1s;
      opacity: 1;
    }
  • 行为设计:API

    注意:API 设计应保证原子操作,职责单一,满足灵活性。

    image-20220117201004121

  • 行为设计:Event 控制流

    使用自定义事件来解耦

    <a class="slide-list__next"></a>
    <a class="slide-list__previous"></a>
    <div class="slide-list__control">
      <span class="slide-list__control-buttons--selected"></span>
      <span class="slide-list__control-buttons"></span>
      <span class="slide-list__control-buttons"></span>
      <span class="slide-list__control-buttons"></span>
    </div>
    const detail = {index: idx}
    const event = new CustomEvent('slide', {bubbles:true, detail})
    this.container.dispatchEvent(event)

总的来说,就是要遵循以下基本方法:

  • 结构设计
  • 展现效果
  • 行为设计
    • API(功能)
    • Event(控制流)

# 优化

# 解耦

  • 将控制元素抽取成插件
  • 插件与组件之间通过依赖注入方式建立联系
  • 将 HTML 模板化,更易于扩展

# 抽象

  • 将通用的组件模型抽象出来,形成组件框架

# 过程抽象

# 什么是过程抽象?

  • 用来处理局部细节控制的一些方法
  • 函数式编程思想的基础应用

# 过程抽象有什么好处?

一个🌰:操作次数限制

假如有一个需求,要求对某个函数的调用设置次数限制,我们可以直接在这个函数里面写上限制代码。

但是实际上,这个需求是可以通用的,如果对每一个函数都是有需求时更改内部代码,未免显得有点重复。

所以我们实际上可以通过一个代理函数 (高阶函数),写一个新的函数,接收一个函数参数,对其封装,并返回封装好的新函数,这样我们就完美地实现了这个需求。

function once(fn){
    return (...args) => {
        if(fn){
            fn.apply(this, args);
            fn = null;
        }
    }
}

# 常用高阶函数

  • Function Once:只能执行一次
  • Function Throttle:节流,每隔一段时间可以调用一次
  • Function Debounce:防抖,停下来一段时间后再调用
  • Function Consumer:缓存队列,延迟执行
  • Function Iteraticve:让函数支持批量操作

# 为什么要使用高阶函数?

函数分为两种,纯函数和非纯函数。

纯函数的意思是:任何时候,以相同的参数调用纯函数,输出也是相同的

那么其实非纯函数的意思就是相对的:非纯函数依赖外部环境,当外部环境参数改变时,即使用相同的参数调用,输出也会改变

显而易见,纯函数方便于后期的统一测试,而非纯函数还需要保证外部环境每次要统一(有时很难做到或很麻烦),所以现在更倾向于使用纯函数

// 纯函数
function add(a, b) {
    return a + b;
}
// 非纯函数
let sum = function() {
    let res = 0;
    return (value) => res += value;
}

当使用高阶函数时,由于高阶函数一般都是纯函数,这样的话,由高阶函数封装的函数在测试时,就只需要测试原始函数即可,降低了测试成本

# 编程范式

JavaScript 是一种既可以使用命令式又可以使用声明式的编程语言,例如:

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

那么我们应该使用什么编程范式呢?

思考这样一个问题,对于状态切换这个需求,如果我们需要调整状态的数量、先后,那么:

  • 对于命令式的 toggle 来说,我们需要直接修改这个函数,当需求频繁变化时,就会即为耗费人力和时间;

  • 而对于声明式的 toggleBuilder 来说,我们只要在构建 toggle 时,调整传入的行为参数即可,既简单又直观。

发现了吗,声明式的函数要优于命令式的函数

但是在实际开发中,到底是使用哪种范式,还需要具体问题具体分析,在两种范式之间选择最适合的,才是最好的

# 使用 JS 解决实际问题

# 如何评价一段代码的好坏

先来看一段代码:

function isUnitMatrix2d(m) {
    return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
}

这段代码好不好?为什么?

其实有两种观点:

乍一看,对于简洁度和可读性来说,不好,明明可以使用更优雅的迭代来解决,却要用这么笨的方法

但是,其实这个函数的效率极高,如果这个函数需要在 requestAnimationFrame 中被高频调用,那么这种写法也不失为一种好的解决方案

# 写代码最应关注什么?

风格 vs 效率

实际上我们应该根据使用场景来判断,对于效率优先的情况下,肯定要先考虑实现的效率问题,而如果多人协作开发和效率问题起冲突,那么我们就要在这两者之间做权衡了

所以其实没有绝对的判断代码好坏的标准,过度的优化、过度的设计难免会让理解成本成倍增加,所以一切都要从实际出发,结合实际考虑

抽象程度越高,可复用性就越高,同时理解成本也会越高

# 参考资料

  • 字节青训营课程

  • MDN 中文文档

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

TagBug 微信支付

微信支付

TagBug 支付宝

支付宝