如何写好JS | 青训营笔记

83 阅读4分钟

这是我参与「第四届青训营」笔记创作活动的第2天

如何写好 JavaScript

1. 各司其职

举个例子,写一段JS,控制一个网页,让它支持浅色和深色两种浏览模式。如果实现?

很容易想到一个版本,给按钮绑定事件,然后直接去使用 JS 操作样式。

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

上述代码的问题就在直接使用js操作了样式,没有做到各司其职,修改一下得到:

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 实现该功能:

<input id="modeCheckBox" type="checkBox">
<div class="content">
  <header>
    <label id="modeBtn" for="modeCheckBox"></label>
  </header>
  ...
</div>
#modeCheckBox {
  display: none;
}

#modeCheckBox:checked + .content {
  background-color: black;
  color: white;
  transition: all 1s;
}

第三版代码根据checkBox的状态改变实现样式改变,同时将checkBox隐藏,主要用到了 CSS 的伪类选择器。

样例结论:

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

2. 组件封装

组件

组件是指 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/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

  • 利用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

API保证指责单一,保证组件灵活性。

// 创建 Slider 类,封装 API
class Slider{
  constructor(id){
    this.container = document.getElementById(id);
    this.items = this.container
    .querySelectorAll('.slider-list__item, .slider-list__item--selected');
  }
  // 获取当前选中图片
  getSelectedItem(){
    const selected = this.container
      .querySelector('.slider-list__item--selected');
    return selected
  }
  // 获取当前选中图片的索引
  getSelectedItemIndex(){
    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.getSelectedItemIndex();
    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);  
  }
}
// 尝试调用
const slider = new Slider('my-slider');
slider.slideTo(3);
// 或者可以设置定时器,自动播放
setInterval(() => {
  slider.slideNext();
}, 2000);

行为 —— JS: 控制流

// 需要添加进行控制的 html 元素和 css 样式,没有在笔记中记录
// 使用自定义事件来解耦
constructor(id, cycle = 3000){
    this.container = document.getElementById(id);
    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
    this.cycle = cycle;

    const controller = this.container.querySelector('.slide-list__control');
    if(controller){
      const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
      // 鼠标经过小圆点时停止轮播 将该圆点对应的图片显示出来
      controller.addEventListener('mouseover', evt=>{
        const idx = Array.from(buttons).indexOf(evt.target);
        if(idx >= 0){
          this.slideTo(idx);
          this.stop();
        }
      });
      // 鼠标离开圆点 继续轮播
      controller.addEventListener('mouseout', evt=>{
        this.start();
      });
      // 注册 slide 事件
      this.container.addEventListener('slide', evt => {
        const idx = evt.detail.index
        const selected = controller.querySelector('.slide-list__control-buttons--selected');
        if(selected) selected.className = 'slide-list__control-buttons';
        buttons[idx].className = 'slide-list__control-buttons--selected';
      })
    }
    // 向前翻页
    const previous = this.container.querySelector('.slide-list__previous');
    if(previous){
      previous.addEventListener('click', evt => {
        this.stop();
        this.slidePrevious();
        this.start();
        evt.preventDefault();
      });
    }
    // 向后翻页
    const next = this.container.querySelector('.slide-list__next');
    if(next){
      next.addEventListener('click', evt => {
        this.stop();
        this.slideNext();
        this.start();
        evt.preventDefault();
      });
    }
  }

总结 —— 基本方法

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

3. 过程抽象

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

Once

  • 为了能够让"只执行一次"的需求覆盖不同的事件处理,我们可以将这个需求剥离出来,这个过程我们称为过程抽象
function once(fn) {
  return function(...args) {
    if(fn) {
      const ret = fn.apply(this, args);
      fn = null;
      return ret;
    }
  }
}

高阶函数

  • HOF:以函数作为参数、以函数作为返回值、常用于作为函数装饰器。
function HOF0(fn) {
  return function(...args) {
    return fn.apply(this. args);
  }
}

HOF0是高阶函数的等价范式,fn与HOF0(fn)是完全等价的。

  • JS数组中 every、map、filter、forEach、reduce、sort等API是高阶函数

常用高阶函数

  • 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;
  var timer;
  return function() {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, dur);
  }
}
  • consumer函数
// consumer函数 相当于将同步操作变成一个异步操作
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)
    }
  }
}

// 应用 1: 每隔一秒执行一次 add
function add(ref, x) {
  const v = ref.value + x;
  console.log(`${ref.value} + ${x} = ${v}`);
  ref.value = v;
  return ref;
}
let consumerAdd = consumer(add, 1000);
const ref = {value: 0};
for(let i = 0; i < 10; i++) {
  consumerAdd(ref, i);
}

// 应用 2: 快速点击慢慢执行
btn.onclick = consumer((evt) => {
  let t = parseInt(count.innerHTML.slice(1)) + 1;
  count.innerHTML = `+${t}`;
  count.className = 'hit';
  let r = t * 7 % 256,
      g = t * 17 % 128,
      b = t * 31 % 128;
  count.style.color = `rgb(${r}, ${g}, ${b})`.trim();
  setTimeout(() => {
    count.calssName = 'hide';
  }, 500);
}, 800)
  • iterative函数
function interative(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])
  }
}
// 应用: 迭代对象 改变颜色
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function'
const setColor = iterative((el, color) => {
  el.style.color = color;
})
const els = document.querySelector('li:nth-child(2n+1)');
setColor(els, 'red')
  • 拦截函数deprecate
function deprecate(fn, oldApi, newApi) {
  const msg = `The ${oldApi} is deprecated. Please use the ${newApi} instead.`
  return function(...args) {
    console.log(msg);
    return fn.apply(this, args);
  }
}
// 不修改代码本身,而是对这个API进行修饰,修饰的过程可以抽象为拦截它的输入或输出。

4. 编程范式

  • 命令式 关注怎么做 How
  • 声明式 关注做什么 What
  • JS既可以写命令式代码也可以写声明式代码,处理复杂逻辑时,建议使用声明式,抽象程度更高,拓展性更强

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',
)
  • 声明式扩展性更强:
// 如果想多一种状态 只需要添加一行代码
switcher.onclick = toggle(
  evt => evt.target.className = 'warn',
  evt => evt.target.className = 'off',
  evt => evt.target.className = 'on',
)