Lesson 4 如何写好 JavaScript 学习笔记 | 青训营

74 阅读12分钟

Lesson 4 如何写好 JavaScript

VideoLink: 如何学好 Javascript - 掘金

CoursewareLink: 如何学好 Javascript - 掘金

各司其职

简单说就是

举例说明:写一段 JS,控制一个网页,让它支持浅色和深色两种浏览模式

  • Version One

    • Code
    • <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>深夜食堂</title>
      </head>
      <body>
        <header>
          <button id="modeBtn">🌞</button>
          <h1>深夜食堂</h1>
        </header>
        <main>
          <div class="pic">
            <img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
          </div>
          <div class="description">
            <p>
                这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
                眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返 [6]  。
            </p>
          </div>
        </main>
      </body>
      </html>
      
    • body, html {
        width: 100%;
        height: 100%;
        padding: 0;
        margin: 0;
        overflow: hidden;
      }
      body {
        padding: 10px;
        box-sizing: border-box;
      }
      div.pic img {
        width: 100%;
      }
      #modeBtn {
        font-size: 2rem;
        float: right;
        border: none;
        background: transparent;
      }
      
    • 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 改变样式,不再是各司其职
  • Version Two

    • Code
    • <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>深夜食堂</title>
      </head>
      <body>
        <header>
          <button id="modeBtn"></button>
          <h1>深夜食堂</h1>
        </header>
        <main>
          <div class="pic">
            <img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
          </div>
          <div class="description">
            <p>
                这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
                眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返 [6]  。
            </p>
          </div>
        </main>
      </body>
      </html>
      
    • body, html {
        width: 100%;
        height: 100%;
        max-width: 600px;
        padding: 0;
        margin: 0;
        overflow: hidden;
      }
      body {
        padding: 10px;
        box-sizing: border-box;
        transition: all 1s;
      }
      div.pic img {
        width: 100%;
      }
      #modeBtn {
        font-size: 2rem;
        float: right;
        border: none;
        outline: none;
        cursor: pointer;
        background: inherit;
      }
      
      body.night {
        background-color: black;
        color: white;
        transition: all 1s;
      }
      
      #modeBtn::after {
        content: '🌞';
      }
      body.night #modeBtn::after {
        content: '🌜';
      }
      
    • const btn = document.getElementById('modeBtn');
      btn.addEventListener('click', (e) => {
        const body = document.body;
        if(body.className !== 'night') {
          body.className = 'night';
        } else {
          body.className = '';
        }
      });
      
    • 用 Javascript 控制元素类, 用类的样式来控制元素
  • Version Three

    • Code

    • <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>深夜食堂</title>
      </head>
      <body>
        <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>
                  这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
                  眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返 [6]  。
              </p>
            </div>
          </main>
        </div>
      </body>
      </html>
      
    • body, html {
        width: 100%;
        height: 100%;
        max-width: 600px;
        padding: 0;
        margin: 0;
        overflow: hidden;
      }
      
      body {
        box-sizing: border-box;
      }
      
      .content {
        padding: 10px;
        transition: background-color 1s, color 1s;
      }
      
      div.pic img {
        width: 100%;
      }
      
      #modeCheckBox {
        display: none;
      }
      
      #modeCheckBox:checked + .content {
        background-color: black;
        color: white;
        transition: all 1s;
      }
      
      #modeBtn {
        font-size: 2rem;
        float: right;
      }
      
      #modeBtn::after {
        content: '🌞';
      }
      
      #modeCheckBox:checked + .content #modeBtn::after {
        content: '🌜';
      }
      
    • 无需 Javascript, 更加的各司其职,但不强求

由上面的例子得出的结论

  • HTML/CSS/JS 各司其职

  • 应当避免不必要的由 JS 直接操作样式

  • 可以用 class 来表示状态

  • 纯展示类交互寻求零 JS 方案

组件封装

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

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

举例说明: 用原生 JS 写一个电商网站的轮播图

  • 结构设计:HTML,轮播图是一个典型的列表结构,我们可以使用无序列表<ul> 元素来实现

  • 展示效果:CSS

    • 使用 CSS 绝对定位将图片重叠在同一个位置
    • 轮播图切换的状态使用修饰符(modifier)
    • 轮播图的切换动画使用 CSS transition
  • 行为设计:API

    • API设计应保证原子操作,职责单一,满足灵活性。
    • getSelectedItem()
    • getSelectedItemIndex()
    • slideTo()
    • slideNext()
    • slidePrevious()
  • 行为设计:控制流

    • 使用自定义事件来解耦
  • 代码实现:

    • Version One: API 的简单实现
    • <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>
      
    • #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;
      }
      
    • 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);
      
    • Version Two:控制流
    • <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>
        <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>
      </div>
      
    • #my-slider{
        position: relative;
        width: 790px;
        height: 340px;
      }
      
      .slider-list ul{
        list-style-type:none;
        position: relative;
        width: 100%;
        height: 100%;
        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;
      }
      
      .slide-list__control{
        position: relative;
        display: table;
        background-color: rgba(255, 255, 255, 0.5);
        padding: 5px;
        border-radius: 12px;
        bottom: 30px;
        margin: auto;
      }
      
      .slide-list__next,
      .slide-list__previous{
        display: inline-block;
        position: absolute;
        top: 50%;
        margin-top: -25px;
        width: 30px;
        height:50px;
        text-align: center;
        font-size: 24px;
        line-height: 50px;
        overflow: hidden;
        border: none;
        background: transparent;
        color: white;
        background: rgba(0,0,0,0.2);
        cursor: pointer;
        opacity: 0;
        transition: opacity .5s;
      }
      
      .slide-list__previous {
        left: 0;
      }
      
      .slide-list__next {
        right: 0;
      }
      
      #my-slider:hover .slide-list__previous {
        opacity: 1;
      }
      
      
      #my-slider:hover .slide-list__next {
        opacity: 1;
      }
      
      .slide-list__previous:after {
        content: '<';
      }
      
      .slide-list__next:after {
        content: '>';
      }
      
      .slide-list__control-buttons,
      .slide-list__control-buttons--selected{
        display: inline-block;
        width: 15px;
        height: 15px;
        border-radius: 50%;
        margin: 0 5px;
        background-color: white;
        cursor: pointer;
      }
      
      .slide-list__control-buttons--selected {
        background-color: red;
      }
      
    • class Slider{
        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();
            });
      
            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();
            });
          }
      
        }
      
        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';
      
          const detail = {index: idx};
          const event = new CustomEvent('slide', {bubbles: true, detail});
          this.container.dispatchEvent(event);
        }
      
        slideNext(){
          const currentIdx = this.getSelectedItemIndex();
          const nextIdx = (currentIdx + 1) % this.items.length;
          this.slideTo(nextIdx);
        }
      
        slidePrevious(){
          const currentIdx = this.getSelectedItemIndex();
          const previousIdx = (currentIdx - 1 + this.items.length) % this.items.length;
          this.slideTo(previousIdx);
        }
      
        start(){
          this.stop();
          // 这里使用了箭头函数(()=>{}),箭头函数中的this绑定到它被创建的上下文,通常是定义它的对象,这里的this将绑定到当前对象上。所以,每当setInterval触发时,将会调用this.slideNext()方法。
          this._timer = setInterval(()=>this.slideNext(), this.cycle);
      
          // 这里直接调用了this.slideNext()方法,并且将其返回值传递给setInterval。由于setInterval需要一个函数作为第一个参数,而不是函数调用的结果,这个语句并不会按预期工作。实际上,这会立即调用this.slideNext()方法,并将返回值(如果有的话)传递给setInterval,而不是在定时间隔触发时调用该方法。
          // 正确的写法是第一条语句,使用箭头函数确保在定时间隔内调用this.slideNext()方法,而不是在创建定时器时就立即调用它。
          // this._timer = setInterval(this.slideNext(), this.cycle);
        }
        stop(){
          clearInterval(this._timer);
        }
      
      }
      
      const slider = new Slider('my-slider');
      slider.start();
      

=> 是箭头函数(Arrow Function)的语法。它是ES6(ECMAScript 2015)中引入的一种新的函数定义方式,用于简化函数的声明和编写。

箭头函数有两种形式:

  1. 单参数、单语句的箭头函数:
(param) => statement;
  1. 多参数、多语句的箭头函数:
(param1, param2, ...) => {
  // multiple statements
};

箭头函数相比传统函数表达式具有以下特点:

  1. 简洁:由于语法简洁,通常在只有一个参数和单个表达式的情况下使用,可以让代码更加紧凑。
  2. 没有this绑定:箭头函数没有自己的this绑定,而是继承外部作用域的this。这在一定程度上解决了传统函数中this作用域问题。
  3. 没有arguments对象:箭头函数也没有自己的arguments对象,而是继承外部函数的arguments对象。

箭头函数的使用场景包括但不限于:

  • 简化回调函数的定义。
  • 作为简单函数的快捷方式。
  • 在需要保留外部作用域的this时使用,避免this指向出现问题。

需要注意的是,由于箭头函数没有自己的thisarguments对象,所以在一些情况下,传统函数可能更适合,特别是需要动态绑定this或访问arguments对象时。

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

  HTML 与 CSS 同上

class Slider{
  constructor(id, cycle = 3000){
    this.container = document.getElementById(id);
    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
    this.cycle = cycle;
  }
  registerPlugins(...plugins){
    plugins.forEach(plugin => plugin(this));
  }
  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';
    }

    const detail = {index: idx}
    const event = new CustomEvent('slide', {bubbles:true, detail})
    this.container.dispatchEvent(event)
  }
  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);  
  }
  addEventListener(type, handler){
    this.container.addEventListener(type, handler)
  }
  start(){
    this.stop();
    this._timer = setInterval(()=>this.slideNext(), this.cycle);
  }
  stop(){
    clearInterval(this._timer);
  }
}

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


const slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
  • Version Four:解耦,将 HTML 模板化,更易于拓展。

<div id="my-slider" class="slider-list"></div>

  CSS 代码同上

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>`;
  }
  registerPlugins(...plugins){
    plugins.forEach(plugin => {
      const pluginContainer = document.createElement('div');
      pluginContainer.className = '.slider-list__plugin';
      pluginContainer.innerHTML = plugin.render(this.options.images);
      this.container.appendChild(pluginContainer);
      
      plugin.action(this);
    });
  }
  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';
    }
    let item = this.items[idx];
    if(item){
      item.className = 'slider-list__item--selected';
    }
    
    const detail = {index: idx}
    const event = new CustomEvent('slide', {bubbles:true, detail})
    this.container.dispatchEvent(event)
  }
  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);  
  }
  addEventListener(type, handler){
    this.container.addEventListener(type, handler);
  }
  start(){
    this.stop();
    this._timer = setInterval(()=>this.slideNext(), this.cycle);
  }
  stop(){
    clearInterval(this._timer);
  }
}

const pluginController = {
  render(images){
    return `
      <div class="slide-list__control">
        ${images.map((image, i) => `
            <span class="slide-list__control-buttons${i===0?'--selected':''}"></span>
         `).join('')}
      </div>    
    `.trim();
  },
  action(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';
      });
    }    
  }
};

const pluginPrevious = {
  render(){
    return `<a class="slide-list__previous"></a>`;
  },
  action(slider){
    const previous = slider.container.querySelector('.slide-list__previous');
    if(previous){
      previous.addEventListener('click', evt => {
        slider.stop();
        slider.slidePrevious();
        slider.start();
        evt.preventDefault();
      });
    }  
  }
};

const pluginNext = {
  render(){
    return `<a class="slide-list__next"></a>`;
  },
  action(slider){
    const previous = slider.container.querySelector('.slide-list__next');
    if(previous){
      previous.addEventListener('click', evt => {
        slider.stop();
        slider.slideNext();
        slider.start();
        evt.preventDefault();
      });
    }  
  }
};

const slider = new Slider('my-slider', {images: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
     'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
     'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
     'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'], cycle:3000});

slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
  • Version Five:组件框架实现,将通用的组件模型抽象出来

  HTML 与 CSS 同上

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 ''
    }
}

class Slider extends Component {
    constructor(id, opts = { name: 'slider-list', data: [], cycle: 3000 }) {
        super(id, opts);
        this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
        this.cycle = opts.cycle || 3000;
        this.slideTo(0);
    }

    render(data) {
        const content = data.map(image => `
        <li class="slider-list__item">
            <img src="${image}"/>
        </li>
        `.trim());
        return `<ul>${content.join('')}</ul>`;
    }

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

        const detail = { index: idx };
        const event = new CustomEvent('slide', { bubbles: true, detail });
        this.container.dispatchEvent(event);
    }

    slideNext() {
        const currentIdx = this.getSelectedItemIndex();
        const nextIdx = (currentIdx + 1) % this.items.length;
        this.slideTo(nextIdx);
    }

    slidePrevious() {
        const currentIdx = this.getSelectedItemIndex();
        const previousIdx = (currentIdx - 1 + this.items.length) % this.items.length;
        this.slideTo(previousIdx);
    }

    addEventListener(type, handler) {
        this.container.addEventListener(type, handler);
    }

    start() {
        this.stop();
        this._timer = setInterval(() => this.slideNext(), this.cycle);
    }

    stop() {
        clearInterval(this._timer);
    }

}

const pluginController = {
    render(images) {
        return `
        <div class="slide-list__control">
          ${images.map((image, i) => `
              <span class="slide-list__control-buttons${i === 0 ? '--selected' : ''}"></span>
           `).join('')}
        </div>    
      `.trim();
    },
    action(slider) {
        let controller = slider.container.querySelector('.slide-list__control');

        if (controller) {
            let buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
            controller.addEventListener('mouseover', evt => {
                var 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;
                let 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 pluginPrevious = {
    render() {
        return `<a class="slide-list__previous"></a>`;
    },
    action(slider) {

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

const pluginNext = {
    render() {
        return `<a class="slide-list__next"></a>`;
    },
    action(slider) {
        let previous = slider.container.querySelector('.slide-list__next');
        if (previous) {
            previous.addEventListener('click', evt => {
                slider.stop();
                slider.slideNext();
                slider.start();
                evt.preventDefault();
            });
        }
    }
};

const slider = new Slider('my-slider', {
    name: 'slide-list', data: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
        'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
        'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
        'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'], cycle: 3000
});

slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();

过程抽象

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

高阶函数

Once

  • 操作次数限制

    • 一些异步交互
    • 一次性的 HTTP 请求

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

像这样的函数也称为高阶函数

高阶函数

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

常用的高阶函数有:

  • Once
  • Throttle
  • Debounce
  • Consumer / 2
  • Iterative

// 让 fn 函数只能执行一次
function once(fn) {
    return function (...args) {
        if (fn) {
            const ret = fn.apply(this, args)
            fn = null
            return ret;
        }
    };
}

// 让 fn 函数在 time 内只能执行一次
function throttle(fn, time = 500) {
    let timer;
    return function (...args) {
        if (timer == null) {
            fn.apply(this, args);
            timer = setTimeout(() => {
                timer = null;
            }, time)
        }
    }
}

// 当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次;如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
function debounce(fn, dur) {
    dur = dur || 100;
    var timer;
    return function () {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, arguments);
        }, dur);
    }
}

// 每隔 time 时间调用 fn, 实现一个 delay 调用
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)
        }
    }
}

// 对可迭代对象批量操作
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
function iterative(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]);
    }
}

throttle Example

每500毫秒可记录一次

<button id="btn">点我</button>

<div id="circle">0</div>
#circle {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background-color: red;
  line-height: 50px;
  text-align: center;
  color: white;
  opacity: 1.0;
  transition: opacity .25s;
}

#circle.fade {
  opacity: 0.0;
  transition: opacity .25s;
}
function throttle(fn, time = 500){
  let timer;
  return function(...args){
    if(timer == null){
      fn.apply(this,  args);
      timer = setTimeout(() => {
        timer = null;
      }, time)
    }
  }
}

btn.onclick = throttle(function(e){
  circle.innerHTML = parseInt(circle.innerHTML) + 1;
  circle.className = 'fade';
  setTimeout(() => circle.className = '', 250);
});

Debounce Example:

<script src="https://s1.qhres2.com/!bd39e7fb/animator-0.2.0.min.js"></script>
<div id="bird" class="sprite bird1"></div>
html, body {
  margin:0;
  padding:0;
}

.sprite {
  display:inline-block; overflow:hidden; 
  background-repeat: no-repeat;
  background-image:url(https://p1.ssl.qhimg.com/d/inn/0f86ff2a/8PQEganHkhynPxk-CUyDcJEk.png);
}

.bird0 {width:86px; height:60px; background-position: -178px -2px}
.bird1 {width:86px; height:60px; background-position: -90px -2px}
.bird2 {width:86px; height:60px; background-position: -2px -2px}

#bird{
  position: absolute;
  left: 100px;
  top: 100px;
  transform: scale(0.5);
  transform-origin: -50% -50%;
}
var i = 0;
setInterval(function(){
  bird.className = "sprite " + 'bird' + ((i++) % 3);
}, 1000/10);

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

document.addEventListener('mousemove', debounce(function(evt){
  var x = evt.clientX,
      y = evt.clientY,
      x0 = bird.offsetLeft,
      y0 = bird.offsetTop;
  
  console.log(x, y);
  
  var a1 = new Animator(1000, function(ep){
    bird.style.top = y0 + ep * (y - y0) + 'px';
    bird.style.left = x0 + ep * (x - x0) + 'px';
  }, p => p * p);
  
  a1.animate();
}, 100));

Consumer Example:

<div id="main">
  <button id="btn">Hit</button>
  <span id="count">+0</span>
</div>
#main {
  padding-top: 20px;
  font-size: 26px;
}

#btn {
  font-size: 30px;
  border-radius: 15px;
  border: solid 3px #fa0;
}

#count {
  position: absolute;
  margin-left: 6px;
  opacity: 1.0;
  transform: translate(0, 10px);
}

#count.hit {
  opacity: 0.1;
  transform: translate(0, -20px);
  transition: all .5s;
}
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)
    }
  }
}

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.className = 'hide';
  }, 500);
}, 800)

Iterative Example:

<ul>
  <li>a</li>
  <li>b</li>
  <li>c</li>
  <li>d</li>
  <li>e</li>
  <li>f</li>
  <li>g</li>
</ul>
const isIterable = obj => obj != null 
  && typeof obj[Symbol.iterator] === 'function';

function iterative(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 setColor = iterative((el, color) => {
  el.style.color = color;
});

const els = document.querySelectorAll('li:nth-child(2n+1)');
setColor(els, 'red');

编程范式

此处为命令式与声明式,命令式强调How to do 而声明式强调 What to do,所以命令式不可避免的比声明式复杂。

Toggle Example:

<div id="switcher" class="on"></div>
#switcher {
  display: inline-block;
  background-color: black;
  width: 50px;
  height: 50px;
  line-height: 50px;
  border-radius: 50%;
  text-align: center;
  cursor: pointer;
}

#switcher.on {
  background-color: green;
}

#switcher.off {
  background-color: red;
}

#switcher.on:after {
  content: 'on';
  color: white;
}

#switcher.off:after {
  content: 'off';
  color: white;
}
  • 命令式
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'
);

虽然看起来声明式的代码更加长,但是声明式的代码更容易维护

例如,为 Toggle 添加一个状态

#switcher {
  display: inline-block;
  background-color: black;
  width: 50px;
  height: 50px;
  line-height: 50px;
  border-radius: 50%;
  text-align: center;
  cursor: pointer;
}

#switcher.on {
  background-color: green;
}

#switcher.warn {
  background-color: yellow;
}

#switcher.off {
  background-color: red;
}

#switcher.on:after {
  content: 'on';
  color: white;
}

#switcher.warn:after {
  content: 'warn';
  color: black;
}

#switcher.off:after {
  content: 'off';
  color: white;
}

声明式只需添加一行

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 = 'warn',
  evt => evt.target.className = 'off',
  evt => evt.target.className = 'on'
);

而命令式需要 else if 的逻辑判断

后言

要写好JavaScript代码,可以遵循以下几个原则和技巧:

  1. 熟悉JavaScript语言规范和最佳实践:了解JavaScript的语法、数据类型、运算符、控制流等基础知识,并了解JavaScript编码规范和最佳实践,例如使用严格模式、避免全局变量等。

  2. 使用合理的命名和注释:给变量、函数、类等起有意义的名字,并添加适当的注释,提高代码的可读性和可维护性。

  3. 使用模块化和封装:将代码分解成小的模块,每个模块负责特定的功能,并封装相关的变量和函数,避免全局污染和代码重复。

  4. 避免使用全局变量:全局变量容易引发命名冲突和意外修改,应该尽量避免使用全局变量,可以使用模块化的方式管理变量的作用域。

  5. 异步编程:JavaScript是单线程的,使用异步编程可以提高程序的性能和响应性。可以使用Promise、async/await、回调函数等方式来处理异步任务。

  6. 错误处理:对于可能出现错误的代码,需要进行适当的错误处理,避免程序崩溃或者产生不可预料的结果。

  7. 性能优化:注意代码的性能问题,避免无效的循环、重复的计算等,可以使用工具进行性能分析和优化。

  8. 测试和调试:编写测试用例并进行测试,使用调试工具来定位和修复问题。

  9. 学习和掌握常用的JavaScript库和框架:学习并熟练使用常用的JavaScript库和框架,例如React、Vue等,可以提高开发效率和代码质量。

  10. 不断学习和提升:JavaScript语言和相关技术在不断发展,要保持学习的态度,关注最新的技术动态和最佳实践,不断提升自己的技能水平。

这条很重要,当时第一次学js的我以为学的(至少是学完了)结果很多ES6的特性都没有