JavaScript编码原则| 青训营笔记

97 阅读8分钟

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

本节课讲述的重点内容是JavaScript的编码原则。

对于一个前端工程师来说,JavaScript是一门很重要的语言,那么如何写好JavaScript呢?需我们需要要遵循以下原则:

  • 各司其职
  • 组件封装
  • 过程抽象

下面具体展开说明对应的原则。

一、各司其职

什么是各司其职?

就是要做到HTML CSS JS三个职责分开,各自负责各自的部分。

下面是这个深夜食堂的代码:

HTML

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

CSS

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

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

实现的结果如下:

image.png

这个版本虽然实现了基本功能,但是也存在一些问题:

  • 它没有做到各司其职,让JavaScript做了CSS应该做的事情。
  • 代码比较复杂,不够简洁。

针对上面存在的问题,有第二种版本进行优化:

HTML

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

CSS

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: '🌜';
}

JS

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

相比于第一种版本,第二种版本尽可能的把js中内容都抽离到 css 当中,这样更加能够做到各司其职。

最后我们再看第三个版本:

HTML

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

CSS

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: '🌜';
}

第三种版本最后只剩下HTML和 CSS,相比于第一种版本和第二种版本,这种更加简洁方便。

各司其职总结

通过这个例子,我们可以得到结论:

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

image.png

二、组件封装

首先我们了解一下什么是组件: 组件就是包含HTML CSS JSS 的单元。好的组件具备 封装性 正确性 扩展性 复用性

接下来我们通过轮播图的例子,对组件封装有更深一步的了解。

结构设计: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可以绝对定位将图片重叠在同一个位置,轮播图切换的状态使用修饰符, 轮播图的切换动画使用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(功能) 和 Event(控制流)

API

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

Event(控制流)

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

改进方案

  • 重构:插件化

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

  • 重构:模板化

    解耦操作,通过将 HTML 进行模板化,更易于扩展。

  • 重构:组件框架

    使用组件框架抽象,即将通用的组件模型给抽象出来。

组件封装总结

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

三、过程抽象

过程抽象是用来处理,局部细节控制的一些方法,是函数式编程思想的基础应用。 原理如下图所示:

image.png

通过下面的例子我们来理解过程抽象

下面是一个简单的UI实现学习列表的功能。

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

这个代码我们每点一次就会去触发 removeChild一次,而removeChild在第一次触发完之后,Dom节点就会消失。所以会出现错误。

针对这个错误,我们可以通过以下方式解决

使用Once函数

function once(fn) { 
return function (...args) { 
if(fn) { 
const set = fn.apply(this, args);
fn = null;
return ret; 
      } 
   };
} 
button.addEventListener('click', once((evt) => { 
const target = evt.target;
target.parentNode.className = 'completed'; 
setTimeout(()=> {
list.removeChild(target.parentNode);
}, 2000); 
}));

高阶函数

什么是高阶函数呢?

一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。高阶函数以函数作为参数,以函数作为返回值,常用于作为函数装饰器。

常用的高阶函数

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

编程范式

image.png

有以下例子:

过程抽象总结

过程抽象要理解HOF和函数装饰器,语言有命令式和声明式两种编程风格。

收获

今天通过对JS的学习,我学习了JS的一些基本内容。我们在编写JS应该要遵循各司其职,组件封装,过程抽象这三个原则,这样才能更好的设计出合适的编码。在编写时我们同样要注意编码的复用性、可扩展性、可维护性要满足需求。