如何写好JavaScript | 青训营

64 阅读4分钟

写好JS的原则

各司其职

  • 让HTML、CSS和JavaScript职能分离
  • 应当避免不必要的由JS直接操作样式
  • 可以用 class 来表示状态
  • 纯展示类交互寻求零JS解决方案

AUCNM`@0G{2EX9RQ6VXWZ7G.png

【例】写一段JS,控制一个网页,让它支持浅和深色两种浏览模式。

版本1

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 = '🌞';
  }
});

样式和行为没有分离,即没有做到职责分离。更改样式只能改动JavaScript中的代码。

版本2

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

通过不同类名绑定不同的CSS样式,达到样式变更的目的。尽可能的把样式都抽离到 CSS 当中,样式变更只需要修改对应类的CSS代码,职责分离,代码可维护性提升。

版本3

<body>
  <input id="modeCheckBox" type="checkbox">
  <div class="content">
    <header>
      <label id="modeBtn" for="modeCheckBox"></label>
      <h1>深夜食堂</h1>
    </header>
    <main>
      ...
    </main>
  </div>
</body>
#modeCheckBox:checked + .content {
  background-color: black;
  color: white;
  transition: all 1s;
}

样式的更改在HTML的基础上完全依靠CSS实现,没有使用JavaScript代码,但可维护性没有下降,真正体现了HTML\CSS\JavaScript各司其职。

组件封装:

  • 组件是指 Web 页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。
  • 好的组件具备封装性、正确性、扩展性、复用性。

【例】用原生 JS 写一个电商网站的轮播图

结构:HTML

轮播图是一个典型的列表结构,我们可以使用无序列表 <ul> 元素来进行实现。

<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/t01abe3351db853eb3.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/t01331ac159b58f5478.jpg" />
        </li>
    </ul>
</div>

表现:CSS

  • 使用 CSS 绝对定位将图片重叠在同一个位置
  • 轮播图切换的状态使用修饰符(modifier)
  • 轮播图的切换动画使用 CSS transition
#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;
}

行为:JS API: image.png

class Slider {
  constructor(id) {
    this.container = document.getElementById(id);
    this.items = this.container
    .querySelectorAll('.slider-list__item, .slider-list__item--selected');
    }
    getSlectedItem() {
      const selected = this.container
        .querySelector('.slider-list__item--selected');
      return selected;
    }
    getSlectedItemIndex() {
      return Array.from(this.items).indexOf(this.getSelectedItem());
    }
    slideTo(idx) {
      const selected = this.getSelectedItem();
      if(selected) {
        selected.className = 'slider-list__item';
      }
      const item = this.items[idx];
      if(item) {
        item.className = 'slider-list__item--selected';
      }
    }
    slideNext() {
      const currentIdx = this.getSlectedItemIndex();
      const nextIdx = (currentIdx + 1) % this.items.length;
      this.slideTo(nextIdx);
    }
    slidePrevious() {
      const currentIdx = this.getSelectedItemIndex();
      const previousIdx = (this.items.length + currentIdx - 1)
        % this.items.length;
      this.slideTo(previousIdx);
    }
}

控制流 使用自定义事件来进行解耦。

<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)

总结:基本方法

  • 结构设计 HTML
  • 展现效果 CSS
  • 行为设计 JS,包含 API(功能)和 Event(控制流)

上述代码存在构造函数太复杂,可扩展性和灵活性相对较小的问题—— 重构

  • 插件化

    解耦:将控制元素抽取成插件;插件与组件之间通过依赖注入方式建立联系

  • 模版化

    解耦:将HTML模板化,更易于扩展

  • 组件框架

    抽象:将通用的组件模型抽象出来

总结:

  • 组件设计的原则:封装性、正确性、扩展性、复用性
  • 实现组件的步骤:结构设计、展现效果、行为设计
  • 三次重构:插件化、模板化、抽象化(组件框架)

过程抽象:

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

image.png

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

高阶函数(HOF):以函数作为参数;以函数作为返回值;常用于作为函数装饰器

  • Once 一次函数:只执行一次

  • Throttle 节流:当持续触发事件时,保证一定时间段内只调用一次事件处理函数

  • Denounce 防抖:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次

  • Consumer:消费数据

  • Iterative:迭代器

使用高阶函数可以大大减少使用非纯函数的可能性。

纯函数:函数的返回结果只依赖于它的参数;如果输入的值是唯一的,且输出的值也是唯一的;并且在执行过程里面没有副作用。

使用纯函数能提高代码的可维护性。

编程范式

命令式与声明式

image.png

命令式编程:命令机器如何去做事情,这样不管你想要的是什么,它都会按照你的命令实现。

声明式编程:告诉机器你想要的是什么,让机器想出如何去做,这种方式的可扩展性会更高。

image.png

【例】

Toggle - 命令式

Toggle - 声明式

Toggle - 三态

学习心得:

过程抽象是指将一系列操作或功能抽象成一个高级的、可复用的功能单元,以提高代码的可维护性和复用性。在JavaScript中,过程抽象常常通过高阶函数来实现。过程抽象能够让我们将一些通用的操作抽象出来,使代码更加简洁、易读,并提高代码的可维护性和扩展性。它是函数式编程思想的基础应用,能够帮助我们更好地组织和管理代码。