如何写好JavaScript|青训营笔记

56 阅读8分钟

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

写好JS的一些原则

  • 各司其职:让HTML、CSS和JavaScript职能分离
  • 组件封装:好的UI组件具备正确性、扩展性、复用性
  • 过程抽象:应用函数式编程思想

各司其职

深夜食堂:写一段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来控制CSS的改变。

版本二:

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

提前在CSS文件中设置好类名为night的样式,通过修改类名来做到改变颜色。

版本三:

<input id="modeCheckBox" type="checkbox">
<div class="content">
    <header>
      <label id="modeBtn" for="modeCheckBox"></label>
      <h1>深夜食堂</h1>
    </header>
    <main>
      <div class="pic">
        <img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
      </div>
      <div class="description">
        <p>
            这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈眶。
        </p>
      </div>
    </main>
</div>

设置一个checkbox,将其隐藏并使用label进行绑定,编写checkbox被选中后状态的样式,通过点击即可改变,不通过JS来控制CSS。

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

结论:

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

组件封装

用原生 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:

image.png

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

轮播图左右键及轮播图下方圆点,通过自定义事件来解耦

<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模板化,更易于扩展

改进三:组件框架

  • 将组件通用模型抽象出来

总结

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

过程抽象

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

例:操作次数限制

  • 一些异步交互
  • 一次性的HTTP请求
  const list = document.querySelector('ul');
  const buttons = list.querySelectorAll('button');
  buttons.forEach((button) => {
    button.addEventListener('click', (evt) => {
      const target = evt.target;
      target.parentNode.className = 'completed';
      setTimeout(() => {
        list.removeChild(target.parentNode);
      }, 2000);
    });
  });

点击后删除li标签,有2000毫秒的过渡动画,但如果短时间内点击多次,click事件被多次触发,第一次removeChildli标签删除,之后的removeChild找不到对应节点,报出错误。

高阶函数

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

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

高阶函数意为:

  • 以函数作为参数
  • 以函数作为返回值
  • 常用于作为函数装饰器

image.png

function HOF0(fn) {
    return function(...args) {
      return fn.apply(this, args);
    }
}
  1. once仅执行一次函数

为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。执行一次后将fn设置为null,由于这是一个闭包函数,第二次执行时fn还是null,则不会再次执行。

  function once(fn) {
    return function(...args) {
      if(fn) {
        const ret = fn.apply(this, args);
        fn = null;
        return ret;
      }
    }
  }
  1. Throttle节流函数

首先创建一个timer,第一次执行时直接执行返回的函数,然后将time人设置为null,第二次执行如果时间不到500ms,setTimeout函数未执行,则不会执行if语句内部。

function throttle(fn, time = 500){
  let timer;
  return function(...args){
    if(timer == null){
      fn.apply(this,  args);
      timer = setTimeout(() => {
        timer = null;
      }, time)
    }
  }
}

3.Debounce防抖函数

传入一个时间,在时间结束后执行函数,而在这个时间内重复点击,则执行clearTimeout函数,重新设置setTimeout重新计时。

function debounce(fn, dur){
  dur = dur || 100;
  var timer;
  return function(){
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, dur);
  }
}

为什么要使用高阶函数

有一种函数叫做纯函数,它的定义是传入一个a他会固定输出一个b,就是说输入输出是一对一的。

纯函数的好处:方便测试,非纯函数由于在调用的时候有时会改变外界的值,所以在执行时需要构建好外部环境,非纯函数测试难度是比纯函数更高的,成本更大,系统中非纯函数越多,系统的可维护性更差。

编程范式

  • 命令式:面向过程、面向对象
  • 声明式:逻辑式、函数式

例:实现将数组中每个数乘以二

  • 命令式:命令式更加强调怎么做
  let list = [1, 2, 3, 4];
  let mapl = [];
  for(let i = 0; i < list.length; i++) {
    mapl.push(list[i] * 2);
  }
  • 声明式:声明式使用方法直接实现
  let list = [1, 2, 3, 4];
  const double = x => x * 2;
  list.map(double);

声明式的函数要比命令式更加简洁。

举例

实现一个多个交通信号灯的切换功能

  • 版本一

由于setTimeout是异步的,所以需要将其嵌套,但这样的代码很丑并且很难维护。

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);
})();
  • 版本二:数据抽象

设置好三种状态,将数据抽象出来,生成一个状态列表stateList,定义一个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);
  • 版本三:过程抽象

首先定义一个wait函数表示等待多久时间,再定义一个poll方法代表一个轮循,再利用setState方法设置类名并设置等待时间。利用过程抽象会写很多代码,但好处是抽象出了一个轮循方法可以处理其他问题。

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', 3000),
                            setState.bind(null, 'pass', 3000));

(async function() {
  // noprotect
  while(1) {
    await trafficStatePoll();
  }
}());
  • 版本四:异步+函数式

利用asyncawait写出最简单也是最容易理解的方式

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(3000);
    setState('pass');
    await wait(3000);
  }
}

start();

判断是否是4的幂

  • 版本一

最普通的方法,4的幂一直除以四最终是会等于一的,用while循环执行

while(num > 1) {
    if(num % 4) return false;
        num /= 4;
    }
    return num === 1;
}
  • 版本二

对版本一进行一定修改,版本一是一直除以四,这里是向右偏移,原理相同

function isPowerOfFour(num) {
    num = parseInt(num);

    while(num > 1) {
        if(num & 0b11) return false;
        num >>>=2;
    }
    return num === 1;
}
  • 版本三

已知一个数若是4的幂,那它一定是2的幂,前两个判断它是2的幂。(num & (num - 1)) === 0这个判断会使数字转换为二进制后从末尾减少一个一,已知2的幂的二进制数是只有一个一其他都是零,所以这个式子可以判断数字为2的幂。(num & 0xAAAAAAAAAAAAA) === 0,已知4的幂转换为二进制数后是一个一后面跟偶数个零,而0xAAAAA转换为二进制数是1010101010,进行&运算后,如果偶数位都是零,则最终结果为零,所以这个式子可以判断是否是4的幂。

function isPowerOfFour(num){
  num = parseInt(num);
  
  return num > 0 && (num & (num - 1)) === 0 && (num & 0xAAAAAAAAAAAAA) === 0;
}
  • 版本四

可以直接用正则匹配二进制数是否是一个一后面跟着偶数个零

function isPowerOfFour(num) {
  num = parseInt(num).toString(2);
  
  return /^1(?:00)*$/.test(num);
}

ppt