JavaScript | 青训营笔记

97 阅读7分钟

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

一、本堂课重点内容:

JavaScript三大原则之各司其职、组件封装、过程抽象
代码质量优化

二、详细知识点介绍:

js三大原则

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

各司其职

主题切换

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

用JavaScript控制CSS,没有做到各司其职

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

body {
  transition: all 1s; //动画效果 更加自然
}

做到各司其职,但还能改进,直接用纯css实现

版本3
//html
<input id="modeCheckBox" type="checkbox">
<label id="modeBtn" for="modeCheckBox"></label> //使用for将modeBtn和modeCheckBox绑定,当点击modeBtn时,触发input的click事件

//css
#modeCheckBox {
  display: none;//隐藏复选框
}

#modeCheckBox:checked + .content {//切换成夜间主题,content的颜色发生变化
  background-color: black;
  color: white;
  transition: all 1s;
}

//:after伪类的作用就是在指定的元素内容(而不是元素本身)之后插入一个包含content属性指定内容的行内元素
#modeBtn::after {
  content: '🌞';
}

#modeCheckBox:checked + .content #modeBtn::after {
  content: '🌜';
}

纯CSS,利用伪类选择器,隐藏modeCheckBox,最优

组件封装

用原生 JS 写一个电商网站的轮播图

版本1

行为:api

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

//js
class Slider{
  constructor(id){//获取.slider-list__item和.slider-list__item--selected元素
    this.container = document.getElementById(id);
    this.items = this.container
    .querySelectorAll('.slider-list__item, .slider-list__item--selected');
  }
  getSelectedItem(){//获取selected元素
    const selected = this.container
      .querySelector('.slider-list__item--selected');
    return selected
  }
  getSelectedItemIndex(){//获取selected的元素在items里的下标
    return Array.from(this.items).indexOf(this.getSelectedItem());
  }
  slideTo(idx){//
    const selected = this.getSelectedItem();//当前选择的元素
    if(selected){ 
      selected.className = 'slider-list__item';//把当前选择的元素的ClassName改为非选择状态
    }
    const item = this.items[idx];//要选择的元素
    if(item){
      item.className = 'slider-list__item--selected';//把要选择的元素的ClassName改为选中状态
    }
  }
  slideNext(){
    const currentIdx = this.getSelectedItemIndex();//当前选中元素的index
    const nextIdx = (currentIdx + 1) % this.items.length;//当前选中元素的下一个的index
    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);

轮播图是一个典型的列表结构,我们可以使用无序列表ul元素来实现
使用 CSS 绝对定位将图片重叠在同一个位置
轮播图切换的状态使用修饰符(modifier)
轮播图的切换动画使用 CSS transition
定义一个类Slider,在类里面实现api

Slider
	+getSelectedItem()
	+getSelectedItemIndex()
	+slideTo()
	+slideNext()
	+slidePrevious()
版本2

行为:控制流 使用自定义事件来解耦

//html 
<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>

//js
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');
      //小圆点监听mouseover事件,当鼠标放在小圆点上显示该位置的图片,并把自动播放停止
      controller.addEventListener('mouseover', evt=>{
        const idx = Array.from(buttons).indexOf(evt.target);
        if(idx >= 0){
          this.slideTo(idx);
          this.stop();
        }
      });
      //小圆点监听mouseout事件,当鼠标离开小圆点,继续自动播放
      controller.addEventListener('mouseout', evt=>{
        this.start();
      });
      //监听slide事件,将图片和小圆点设置为选中状态,来修改其样式
      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';
      })
    }
    //上一页,调用slidePrevious()事件,在调用之前停止自动播放,调用后打开自动播放
    const previous = this.container.querySelector('.slide-list__previous');
    if(previous){
      previous.addEventListener('click', evt => {
        this.stop();
        this.slidePrevious();
        this.start();
        evt.preventDefault();
      });
    }
    //下一页,调用slideNext()事件,在调用之前停止自动播放,调用后打开自动播放
    const next = this.container.querySelector('.slide-list__next');
    if(next){
      next.addEventListener('click', evt => {
        this.stop();
        this.slideNext();
        this.start();
        evt.preventDefault();
      });
    }
  }
  getSelectedItem(){
    let selected = this.container.querySelector('.slider-list__item--selected');
    return selected
  }
  getSelectedItemIndex(){
    return Array.from(this.items).indexOf(this.getSelectedItem());
  }
  //自定义事件,实现在container里监听slider事件
  slideTo(idx){
    let 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(){
    let currentIdx = this.getSelectedItemIndex();
    let nextIdx = (currentIdx + 1) % this.items.length;
    this.slideTo(nextIdx);
  }
  slidePrevious(){
    let currentIdx = this.getSelectedItemIndex();
    let previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
    this.slideTo(previousIdx);  
  }
  //定时器循环播放
  start(){
    this.stop();
    this._timer = setInterval(()=>this.slideNext(), this.cycle);
  }
  //停止播放
  stop(){
    clearInterval(this._timer);
  }
}

const slider = new Slider('my-slider');
slider.start();

不够灵活?-> 改进

改进1

重构:插件化
解耦:
1.将控制元素抽取成插件
2.插件与组件之间通过依赖注入方式建立联系

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

当不需要底部小圆点时,可以通过删除html中的小圆点控件,同时slider.registerPlugins(/*pluginController,*/ pluginPrevious, pluginNext);即可

当需要增加插件时,如需添加一个按钮
1.在html中增加<button id="randomGet">随机图片</button>
2.在js中增加
function pluginRandomGet(slider){
  randomGet.addEventListener('click',()=>{
    const idx=Math.floor(slider.items.length*Math.random());//长度乘随机数然后向下取整
    slider.stop();
    slider.slideTo(idx);
    slider.start();
  })
}
3.在js中把插件pluginRandomGet注册进去
slider.registerPlugins(pluginController, pluginPrevious, pluginNext ,pluginRandomGet);

但是我还是要去修改html,能不能不去修改html?

改进2

HTML模板化

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

//js
class Slider{
  constructor(id, opts = {images:[], cycle: 3000}){
    this.container = document.getElementById(id);
    this.options = opts;
    this.container.innerHTML = this.render();//调用render()
    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
    this.cycle = opts.cycle || 3000;
    this.slideTo(0);
  }
  render(){//通过slider里的render()方法生成html代码
    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);//调用插件里的render方法
      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();

如果不需要底部的小圆点,直接把pluginController注释掉即可,不需要再去修改html
slider.registerPlugins(/*pluginController,*/pluginPrevious, pluginNext);
改进3

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

image-20230118010540438.png

//js
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{//继承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){//重写render方法
    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 = (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){
    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});//此处的name相当于id,控制相应的class名

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

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

过程抽象

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

高阶函数:以函数作为参数、 以函数作为返回值、常用于作为函数装饰器

Once 只执行一次

操作次数限制

用户疯狂点击同一个会报错,每点一次就会触发click事件,都会removeChild,而第一次就已经remove了,后面的点击就会报错Uncaught NotFoundError: Failed to execute 'removeChild' on 'Node'

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);
  });
});
修改 方法1

加上{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});
});

然而有的时候不是仅仅针对于click事件。比如向服务器get请求数据,这类多次调用都只允许一次的情况,可以再次改进

修改 方法2

可以抽象出来一个高阶函数once(),保证function只被执行一次
第一次调用的时候接受的是一个函数,不是null,会返回值。第一次调用之后fn = null,第二次调用时接受的函数为null,不会调用到里面的方法

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

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

Throttle 节流

事件触发频率太高会有一定性能的消耗,当只需要每隔几秒触发一次时可以用节流函数限制其频率
第一次调用timer为null,500ms内timer都是有值的,调用后不会执行函数如果,当500ms之后,timer为null,就可以被再次调用执行

function throttle(fn, time = 500){//传入function和频率
  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);
});

Debounced 防抖

永远只会被调用最后一次
每次调用functin都会把timer清空,只有当不再重复调用function并且dur时间到了以后才会执行

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

Consumer

consumer每隔time时间去调用function

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

例1
每隔一定时间输出一行计算结果
例2
每点击一次按钮就会显示+1,当快速点击后,它会间隔相同的时间显示+1,有延时显示的效果

Iterative

如果subject是可迭代的,比如list,则subject的每一个元素调用functi方法,否则只调用一次

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

把list中下标为奇数的元素颜色设为红色

编程范式

Toggle - 命令式
switcher.onclick = function(evt){
  if(evt.target.className === 'on'){
    evt.target.className = 'off';
  }else{
    evt.target.className = 'on';
  }
}

需要添加新的样式会很复杂

Toggle - 声明式
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',
);

需要添加新的样式,则只需添加如下即可

//css
#switcher.warn {
  background-color: yellow;
}
#switcher.warn:after {
  content: 'warn';
  color: black;
}

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

//js
switcher.onclick = toggle(
  evt => evt.target.className = 'warn', //加入
  evt => evt.target.className = 'off',
  evt => evt.target.className = 'on'
);

JavaScript 代码质量优化

交通灯状态切换

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

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

直接用setTimeout嵌套,很难维护

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

把交通灯的状态数据抽象出来,变成状态列表,递归实现当前状态结束之后切换下一个状态

版本三(过程抽象)
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();
  }
}());

复杂,过度抽象

版本四(异步+函数式)
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的幂

//法1:
//一直除以4
function isPowerOfFour(num) {
  num = parseInt(num);

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

//法2:
//用位运算
function isPowerOfFour(num) {
  num = parseInt(num);

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

//法3:
/*num为二进制数,num & (num - 1)会使num中1的个数减少一个
如 ...1 & ...0 =...0

num & 0xAAAAAAAAAAAAA 即 num& 010101010..10 
只要有一个偶数位上是1,结果就不是0*/

function isPowerOfFour(num){
  num = parseInt(num);
  
  return num > 0 &&
         (num & (num - 1)) === 0 &&  //num是一个2的幂
         (num & 0xAAAAAAAAAAAAA) === 0;
}

//法4:
//转换成二进制字符串,用正则表达式匹配
function isPowerOfFour(num) {
  num = parseInt(num).toString(2);
  
  return /^1(?:00)*$/.test(num);
}

洗牌

错误写法
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function shuffle(cards) {
  return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}
console.log(shuffle(cards));

//测试
const result = Array(10).fill(0);

for(let i = 0; i < 1000000; i++) {
  const c = shuffle(cards);
  for(let j = 0; j < 10; j++) {
    result[j] += c[j];
  }
}
console.table(result);

数字越小,排在前面的概率就越大,sort交换不是均匀交换,不能做到真正意义上的乱序

正确写法
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function shuffle(cards) {
  const c = [...cards];
  for(let i = c.length; i > 0; i--) {
    const pIdx = Math.floor(Math.random() * i);
    [c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
  }
  return c;
}
console.log(shuffle(cards));
//测试
const result = Array(10).fill(0);
for(let i = 0; i < 10000; i++) {
  const c = shuffle(cards);
  for(let j = 0; j < 10; j++) {
    result[j] += c[j];
  }
}
console.table(result);

[a1-ak],每次从k中选择一个数放到前面k-1任意一个位置,概率p=(k-1)/k+1/(k-1), 每个数在自己原本的位置的概率为1/k

使用生成器
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

function * draw(cards){
    const c = [...cards];

  for(let i = c.length; i > 0; i--) {
    const pIdx = Math.floor(Math.random() * i);
    [c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
    yield c[i - 1];
  }
}

const result = draw(cards);
console.log([...result]);

不洗牌,直接用随机数取
如果需要前三张牌,可以用如下方法,不需要把整个都洗牌,性能提升

console.log([
  result.next().value,
  result.next().value,
  result.next().value
  ]);

分红包

切西瓜法
function generate(amount, count){
  let ret = [amount]; 
  while(count > 1){
    //挑选出最大一块进行切分
    let cake = Math.max(...ret),//用Math.max取出数组中最大值
        idx = ret.indexOf(cake),
        part = 1 + Math.floor((cake / 2) * Math.random()),
        rest = cake - part;  
    ret.splice(idx, 1, part, rest);  
    count--;
  }
  return ret;
}

先把红包分成两份,从剩下的中挑选出最大的继续分,以此类推,每次选择的都是剩下的中最大的,这样就不会出现不够分的情况
每次分的都是最大的,相对均匀

抽牌法
function * draw(cards){
  const c = [...cards];

  for(let i = c.length; i > 0; i--) {
    const pIdx = Math.floor(Math.random() * i);
    [c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
    yield c[i - 1];
  }
}

function generate(amount, count){
  if(count <= 1) return [amount];
  const cards = Array(amount - 1).fill(0).map((_, i) => i + 1);
  const pick = draw(cards);
  const result = [];
  for(let i = 0; i < count - 1; i++) {
    result.push(pick.next().value);
  }
  result.sort((a, b) => a - b);
  result.push(amount);
  for(let i = result.length - 1; i > 0; i--) {
    result[i] = result[i] - result[i - 1];
  }
  return result;
}

使用洗牌的算法,随机排序,如果要分成n分,就插入n-1个隔板,把每两个隔板之间的数相加给同一个人
相对不均匀,更加刺激

三、课后个人总结:

通过这节课的学习,对组件实现有了更深入的了解,没想到组件如此复杂,需要从结构设计、效果展示、行为设计一步步实现,还需要进行重构,包括插件化、模板化、抽象化。这都是我以前没有接触过的,收获颇丰。还学习了一些简单的JavaScript算法例子,一步步的优化也拓宽了我的思考。

四、引用参考:

前端入门 - 基础语言篇