Javascript | 青训营

59 阅读9分钟

JavaScript(简称“JS”)是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。JavaScript是web开发者必学的三种语言之一,它实现交互体验,可以处理复杂的函数,可以保证更高的效率和可用性。如何写好JS,让它发挥作用有三个原则:各司其职(让HTML、CSS和JavaScript职能分离)、组件封装(好的UI组件具备正确性、扩展性、复用性)、过程抽象(应用函数式编程思想)。

1、各司其职

以切换主题颜色为例,下面分别是三个不同版本的代码实现:
版本一:

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

版本二:

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

版本三:

<body>
<input id="modeCheckBox" type="checkbox">
<div id="container">
  <header>
    <label id="modeBtn" for="modeCheckBox"></label>
    <h1>夏目友人帐</h1>
  </header>
  ...
</body>
#modeCheckBox:checked+#container{
  background-color: black;
  color: white;
  transition: all is;
}
#modeCheckBox{
  display: none;
}

上述三个版本的代码实现的功能都是切换主题颜色,即普通的白天黑夜主题,但这三个版本的代码又各有不同。版本一的代码是在JS里面操作body.style以切换主题;版本二的代码是在JS里面操作body.className间接操作CSS中的样式以切换主题;而版本三中的代码是在前面加了一个,根据兄弟结点选择器#modeCheckBox:checked + #container去修改下面id选择器为contianer的内容。display:none;隐藏了checkbox,点击checkbox可以实现白天夜间模式切换。(注:因为这里的图标改为了label,并增加了for=”modeCheckBox”,所以点击图标和点击checkbox效果一样。)
总结:HTML负责结构,CSS负责表现,JS负责行为,结构、表现、行为分离是前端工程师需要掌握的基本原则。上述例子切换白天夜间模式是更换CSS样式,应当避免不必要的由JS直接操作样式,可以用纯class表示状态,纯展示类交互需求零JS方案。

2、组件封装

组件是指Web页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性和复用性。组件封装的基本方法:结构设计、展现效果、行为设计(API功能、Event控制流),下面以轮播图为例详细说明这三种基本方法。

结构设计:HTML

<div id="my-slider" class="slider-list">
  <ul>
    <li class="slider-list__item--selected">
      <img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F201705%2F11%2F20170511162412_KY2Zf.thumb.400_0.jpeg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1695568320&t=76da7fd7211523c896ec1731ecacec50">
    </li>
    <li class="slider-list__item">
      <img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F201808%2F08%2F20180808205849_sijwh.thumb.400_0.jpg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1695568351&t=c3badb0ead22021453eddec0e19dc46f">
    </li>
    <li class="slider-list__item">
      <img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F201912%2F11%2F20191211093109_ycgfm.thumb.400_0.jpg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1695568351&t=15fd225a1aa9a0590761c72c5b168ffe">
    </li>
    <li class="slider-list__item">
      <img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F202008%2F19%2F20200819012355_hygnu.thumb.400_0.png&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1695568351&t=55516b3767f3dd45f6b6e53c939df388">
    </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

行为设计: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)//2s更换一次图片
  • API设计应保证原子操作,职责单一,满足灵活性。
  • Slider
    • getSelectedItem():得到当前选中的图片元素
    • getSelectedItemIndex():得到当前选中的图片元素在列表里的下标
    • slideTo():slideTo到某个特定的Index的元素上
    • slideNext():轮播下一张图片
    • slidePrevious():轮播上一张图片

行为设计:控制流

在js文件中定义controller变量,代表轮播图下方的四个小圆点。在controller里面监听mouseover和mouseout事件,当鼠标移至小圆点时,mouseover监听到该操作把对应的图片slideTo,同时这里执行this.stop():结束轮播图的自动切换。当鼠标移除小圆点时,mouseout监听到该操作时重新开始执行this.start():轮播图自动切换
控制流实际上是使用自定义事件来解耦。轮播图自动切换时,切换到哪一张图片,下面对应的小圆点就变黑,实现这个功能,需要自定义事件进行监听:new一个slide CustomEvent,然后在controller里面监听这个slide事件。

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

组件除了上述三种基本方法外,还有可以改造的空间:重构(插件化、模板化、组件框架)

1、重构:插件化

解耦:将控制元素抽取成插件,插件与组件之间通过依赖注入方式建立联系。
以上述轮播图代码为例,如果我们不想要轮播图的左右切换功能,我们需要注销HTML、改动CSS和JS的代码,这样的组件不够灵活,所以考虑把组件插件化(代码如下)。下面的代码把切换上/下一张图片和小圆点切换图片的功能都进行抽象插件化,pluginController:表示下面的四个小圆点,pluginPrevious:表示切换上一张图片,pluginNext:切换下一张图片。如果我们不要下面的四个小圆点的切换功能,可以在js文件中把slider.registerPlugins(pluginController, pluginPrevious, pluginNext)中的pluginController注销,这样既不需要改动HTML和CSS的代码,也让代码更加简洁。

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

轮播图插件化代码:code.juejin.cn/pen/7271922…

2、重构:模板化

解耦:将HTML模板化,更易于扩展
之前的结构是写死在HTML里面的,实际上在js的UI组件里面首先要做到数据驱动,根据数据来生成HTML的模板。如果把图片写死在HTML代码里面,一旦图片数量变多,需要改动的代码就越多。如果是根据数据生成HTML模板,则不需要改动代码。 image.png
通过JS里的render()生成HTML代码,四张图片当成数据传给组件(new slider的时候传了四张图片进去)。四张图片通过调用render()方法,生成innerHTML,插入到content里面实现。同理,四个小圆点和上下切换功能是通过render()方法和action()方法实现的,render()方法渲染页面内容(生成四个小圆点),action()方法完成初始化(实现小圆点切换图片功能)。

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>`;
  }
  
const slider = new Slider('my-slider', {images: ['https://img2.baidu.com/it/u=2756958179,4279281762&fm=253&fmt=auto&app=138&f=JPEG?w=888&h=500',
'https://img2.baidu.com/it/u=475139972,1074921915&fm=253&fmt=auto&app=138&f=JPEG?w=888&h=500',
'https://img1.baidu.com/it/u=1875235429,2122274709&fm=253&fmt=auto&app=138&f=JPEG?w=890&h=500',
'https://img2.baidu.com/it/u=958150146,494355729&fm=253&fmt=auto&app=138&f=JPEG?w=888&h=500'], cycle:4000});)`

轮播图的模板化代码:code.juejin.cn/pen/7272255…

3、重构:组件框架

抽象:将通用的组件模型抽象出来,在模板化的基础上进一步抽象,把Slider组件模型抽象成通用的组件Component。Component里面包含两个方法:registerPlugins(...plugins)注册插件、render()渲染。
image.png
Slider会继承Component去实现render()方法,registerPlugins(...plugins)注册插件,其他的与模板化代码差不多。

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

轮播图的组件框架代码:code.juejin.cn/pen/7272272…

3、 过程抽象

过程抽象:

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

例子: image.png 这是一个待办事件的代码,但是它有bug,如果我们多次点击同一个待办事件就会报错。这是因为我们每点击一次就触发一次click事件,执行一次click事件就会removeChild一次,第一次执行click的时候就把待办事件移除了,所以之后再次点击会报错The node to be removed is not a child of this node.所以这个click事件只能执行一次。一个函数只能执行一次可以用参数once:true实现,这样这个事件就只能触发一次。

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);
    },{once:true});
});

为了能够让“只执行一次”的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。除了上述添加参数once:true实现事件只能触发一次外,还可以用高阶函数实现。

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

高阶函数

什么是高阶函数:以函数作为参数、以函数作为返回值、常用于作为函数装饰器。 image.png
常见的高阶函数:

  • Once:只能执行一次
  • Throttle:节流函数,指的是某个函数在一定时间间隔内(例如 3 秒)只执行一次,在这3秒内无视后来产生的函数调用请求,也不会延长时间间隔。
  • Debounce:防抖函数,是将一段时间内频繁被执行的数据,延迟到后面一次性做一个执行
  • Consumer:执行方法的时候不会直接执行,而是把它放到任务队列里,每隔多少毫秒去执行它。由此将同步的操作变成异步的操作。
  • Iterative:将一个函数,变成可迭代使用的,该函数通常用于给一组对象执行批量操作的时候。

为什么使用高阶函数?
函数分为两种,纯函数和非纯函数。

  • 纯函数:任何时候,以相同的参数调用纯函数,输出也是相同的
  • 非纯函数:非纯函数依赖外部环境,当外部环境参数改变时,即使用相同的参数调用,输出也会改变

显而易见,纯函数方便于后期的统一测试,而非纯函数还需要保证外部环境每次要统一(有时很难做到或很麻烦),所以现在更倾向于使用纯函数,而使用高阶函数可以减少使用非纯函数的可能性,高阶函数可以提高代码的复用性和可读性,从而间接地提高性能。

编程范式

JavaScript是一种既可以使用命令式又可以使用声明式的编程语言,命令式编程语言又可以分为面向过程和面向对象,声明式编程语言可以分为逻辑式和函数式。 image.png
命令式编程语言代码:强调的是怎么做,把具体做法写出来

let list=[1,2,3,4];
let map=[];
for(let i=0;i<list.length;i++){
  map.push(list[i]*2);
}

声明式编程语言代码:强调的是机器想要什么,让机器想出怎么做,声明式代码比命令式代码更具有扩展性

let list=[1,2,3,4];
const double = x => x*2;
list.map(double);

寄语:编写代码不仅仅只是正常运行实现功能,还要考虑代码的复用性、可读性和其间接影响的性能。要从实际出发,减低成本,增加效益。