如何写好 JavaScript | 青训营笔记

64 阅读4分钟

如何写好 JavaScript | 青训营笔记

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

知识回顾

我们已经学习了前端三大件的前两个:负责页面内容和结构的 HTML 以及负责页面排版和样式的 CSS ,接下来要学习的是负责页面交互行为的 JavaScript。

JavaScript - 学习 Web 开发 | MDN

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

这个版本的问题:用 JavaScript 去操作 CSS 样式,没有做到职能分离。

版本二

版本二

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

这个版本的问题:

版本三

版本三

  #modeCheckBox {
    display: none;
  }

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

因为只涉及到对样式的操作,就使用纯 CSS 操作。选择器的高级应用。

总结

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

组件封装

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

例子

用原生 JS 写一个电商网站的轮播图,应该怎么实现?

结构: 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>

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

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

行为: JS(API)

行为:JS (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);

行为: API

  • Slider
    • +getSelectedItem()
    • +getSelectedItemIndex()
    • +slideTo()
    • +slideNext()
    • +slidePrevious()

行为: JS(控制流)

行为:JS (控制流)

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

行为:控制流

  • 使用自定义事件来解耦。

总结

组件封装基本方法

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

思考

改进空间:如果让你来重构这个轮播图组件,你会怎么做?

重构:插件化

重构:插件化

  function pluginController(slider){
    const controller = slider.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){
          slider.slideTo(idx);
          slider.stop();
        }
      });

      controller.addEventListener('mouseout', evt=>{
        slider.start();
      });

      slider.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';
      });
    }  
  }

  function pluginPrevious(slider){
    const previous = slider.container.querySelector('.slide-list__previous');
    if(previous){
      previous.addEventListener('click', evt => {
        slider.stop();
        slider.slidePrevious();
        slider.start();
        evt.preventDefault();
      });
    }  
  }

  function pluginNext(slider){
    const next = slider.container.querySelector('.slide-list__next');
    if(next){
      next.addEventListener('click', evt => {
        slider.stop();
        slider.slideNext();
        slider.start();
        evt.preventDefault();
      });
    }  
  }

解耦

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

重构:模板化

重构:模板化

  class Slider{
    constructor(id, opts = {images:[], cycle: 3000}){
      this.container = document.getElementById(id);
      this.options = opts;
      this.container.innerHTML = this.render();
      this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
      this.cycle = opts.cycle || 3000;
      this.slideTo(0);
    }
    render(){
      const images = this.options.images;
      const content = images.map(image => `
        <li class="slider-list__item">
          <img src="${image}">
        </li>    
      `.trim());
      
      return `<ul>${content.join('')}</ul>`;
    }
    ...
  }

解耦

  • 将HTML模板化,更易于扩展

组件框架

组件框架

  class Component{
    constructor(id, opts = {name, data:[]}){
      this.container = document.getElementById(id);
      this.options = opts;
      this.container.innerHTML = this.render(opts.data);
    }
    registerPlugins(...plugins){
      plugins.forEach(plugin => {
        const pluginContainer = document.createElement('div');
        pluginContainer.className = `.${name}__plugin`;
        pluginContainer.innerHTML = plugin.render(this.options.data);
        this.container.appendChild(pluginContainer);
        
        plugin.action(this);
      });
    }
    render(data) {
      /* abstract */
      return ''
    }
  }

抽象

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

总结

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

过程抽象

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

例子

操作次数限制

操作次数限制

  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);
    });
  });
  • 一些异步交互
  • 一次性的HTTP请求

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);
    }
  }
  • 以函数作为参数
  • 以函数作为返回值
  • 常用于作为函数装饰器

常用高阶函数

思考

为什么要使用高阶函数?

使用纯函数维护性高。鼓励使用高阶函数可以减少使用非纯函数的可能性。

编程范式

JavaScript 既有命令式编程语言的特点也有声明式编程语言的特点

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

例子

声明式比命令式有更好的可扩展性。

写代码应该关注的方面

先来看一段代码

// 判断一个mat2d矩阵是不是单位矩阵
function isUnit(m) {
  return m[0] === 1 && m[1] === 0 && m[2] === 0
    && m[3] === 1 && m[4] === 0 && m[5] === 0;
}

上面是一段真实的代码

因为需要达到很高的帧率,留给计算的时间不多,所以这里不用循环而是直接取地址判断是否为单位矩阵。

  • 风格?
  • 效率?
  • 约定?
  • 使用场景?
  • 设计?

判断一个代码好不好应该从实际使用场景和功能出发去综合考虑,而不仅仅是看它的风格与方法。

当年的 Leftpad 事件

  function leftpad(str, len, ch) {
      str = String(str);
      var i = -1;
      if (!ch && ch !== 0) ch = ' ';
      len = len - str.length;
      while (++i < len) {
          str = ch + str;
      }
      return str;
  } 

事件本身的槽点

  • NPM 模块粒度
  • 代码风格
  • 代码质量/效率

优化

  function leftpad(str, len, ch) {
      str = "" + str;
      const padLen = len - str.length;
      if(padLen <= 0) {
        return str;
      }
      return (""+ch).repeat(padLen)+str;
  } 
  • 代码更简洁
  • 效率提升

快速幂优化

  /*! https://mths.be/repeat v1.0.0 by @mathias */

  'use strict';

  var RequireObjectCoercible = require('es-abstract/2019/RequireObjectCoercible');
  var ToString = require('es-abstract/2019/ToString');
  var ToInteger = require('es-abstract/2019/ToInteger');

  module.exports = function repeat(count) {
    var O = RequireObjectCoercible(this);
    var string = ToString(O);
    var n = ToInteger(count);
    // Account for out-of-bounds indices
    if (n < 0 || n == Infinity) {
      throw RangeError('String.prototype.repeat argument must be greater than or equal to 0 and not be Infinity');
    }

    var result = '';
    while (n) {
      if (n % 2 == 1) {
        result += string;
      }
      if (n > 1) {
        string += string;
      }
      n >>= 1;
    }
    return result;
  };

性能更好

  /**
   * String.prototype.repeat() polyfill
   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat#Polyfill
  */
  if (!String.prototype.repeat) {
    String.prototype.repeat = function(count) {
      'use strict';
      if (this == null)
        throw new TypeError('can\'t convert ' + this + ' to object');

      var str = '' + this;
      // To convert string to integer.
      count = +count;
      // Check NaN
      if (count != count)
        count = 0;

      if (count < 0)
        throw new RangeError('repeat count must be non-negative');

      if (count == Infinity)
        throw new RangeError('repeat count must be less than infinity');

      count = Math.floor(count);
      if (str.length == 0 || count == 0)
        return '';

      // Ensuring count is a 31-bit integer allows us to heavily optimize the
      // main part. But anyway, most current (August 2014) browsers can't handle
      // strings 1 << 28 chars or longer, so:
      if (str.length * count >= 1 << 28)
        throw new RangeError('repeat count must not overflow maximum string size');

      var maxCount = str.length * count;
      count = Math.floor(Math.log(count) / Math.log(2));
      while (count) {
        str += str;
        count--;
      }
      str += str.substring(0, maxCount - str.length);
      return str;
    }
  }

总结

对于一些代码可以优化,但在他的使用场景下优化以后性能提升不明显。

算法实例

交通灯状态切换

实现一个切换多个交通灯状态切换的功能

版本一:

版本一

  • 无脑嵌套,不易维护。

版本二(数据抽象):

版本二(数据抽象)

  • 将交通灯的状态抽象出来(状态列表),递归方式。

版本三(过程抽象):

版本三(过程抽象)

  • 使用过程式编程范式,抽象出通用方法,扩展性强;但较为复杂,可能过度抽象。

版本四(异步 + 函数式):

版本四(异步 + 函数式)

  • 设置状态,循环更新。

判断是否是 4 的幂

function isPowerOfFour(num){

  num = parseInt(num);

  return num > 0 &&

         (num & (num - 1)) === 0 &&

         (num & 0xAAAAAAAAAAAAA) === 0;

}

判断是否是 4 的幂

方法:二进制表示中 1 的位置

如果 nn 是 2 的幂,则其与 n1n-1 按位与后应等于 0。

如果 nn 是 4 的幂,那么 nn 的二进制表示中有且仅有一个 1,并且这个 1 出现在从低位开始的第偶数个二进制位上(这是因为这个 1 后面必须有偶数个 0)。这里我们规定最低位为第 0 位,例如 n=16n=16 时,nn 的二进制表示为:

(10000)2(10000)_{2}

唯一的 1 出现在第 4 个二进制位上,因此 nn 是 4 的幂。

由于题目保证了 nn 是一个 32 位的有符号整数,因此我们可以构造一个整数 maskmask,它的所有偶数二进制位都是 0,所有奇数二进制位都是 1。这样一来,我们将 nnmaskmask 进行按位与运算,如果结果为 0,说明 nn 二进制表示中的 1 出现在偶数的位置,否则说明其出现在奇数的位置。

根据上面的思路,maskmask 的二进制表示为:

mask=(10101010101010101010101010101010)2mask = (10101010101010101010101010101010)_2

我们也可以将其表示成 16 进制的形式,使其更加美观:

mask=(AAAAAAAA)16mask = (AAAAAAAA)_{16}

洗牌

错误写法:

错误写法

  • sort 交换不是两两均匀交换,数字小的越容易靠前,概率不均等

正确写法:

正确写法

  • 随机抽一张牌放到最后,保证每张牌在各个位置概率均等

使用生成器:

使用生成器

  • 不需要洗每一张牌,直接随机抽取需数目的牌

分红包

切西瓜法

切西瓜法

  • 从剩下的挑出最大的部分继续分

抽牌法

抽牌法

  • 抽出隔板,随机分为一定份数,每个区间金额随机

总结

  • 评价代码的好坏应该从使用场景出发
  • 前端也需要算法