JS编码原则

557 阅读9分钟

写好js的一些原则

  • 各司其职
    • HTML CSS JS职能分离
    • 深夜食堂
  • 组件封装
    • 好的UI组件具备正确性,扩展性,复用性。
    • 轮播图
  • 过程抽象
    • 应用函数式编程思想,拥有更好的扩展性,更好的通用能力

各司其职

结构、样式、行为分离。 image.png

示例 深夜食堂

例子:写一段js,控制一个网页,使他支持浅色和深色两种浏览模式。

image.png

在浅色模式(白色背景,黑色文字)下,点击'🌞',页面变成深色模式(黑色背景,白色文字),图标变为'🌜'。反之。

结构 & 样式

结构,采用语义化标签,头部,主部分:header,main。

头部包含标题和按钮;主要部分包含图片和一段描述。

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

不使用浮动进行排版,使用flex布局,让float回归最原始的作用:图文混排。

header{
  display: flex;
  flex-direction: row-reverse;
  justify-content: space-between;
}
#modeBtn{
  background-color:transparent;
  border:none;
  font-size:2em;
}
.pic img{
  width:100%;
}

版本一

给图标按钮注册点击事件,根据点击的目标元素(e.target)得到其内部的图标,根据图标切换。

const btn = document.getElementById('modeBtn');
btn.addEventListener('click',(e)=>{
  const body = document.body;
  if(e.target.innerHTML == '🌞'){
    //目前正处于浅色模式,需要切换成深色模式,改变图标
    body.style.color = '#fff';
    body.style.backgroundColor = '#000';
    e.target.innerHTML = '🌜';
  }else{
    //目前正处于深色模式,需要切换成浅色模式,改变图标
    body.style.color = '#000';
    body.style.backgroundColor = '#fff';
    e.target.innerHTML = '🌞';
  }
})
  • js直接操作body的style属性,修改了元素的css样式,使js去做了css的事情
  • 开发人员只看代码无法理解原始需求含义,可读性差

版本二

在css中设置深色模式的规则-》class选择器,使用js为body添加/删除相应的class,从而实现切换,避免使用js直接操作css。

结构和样式改动:使用伪元素来制作图标,这样可以使用body类名的不同来对其内容进行切换,不再需要js操作html。

body{
  transition: all 1s;
}
body.dark{
  color:#fff;
  background-color: #000;
  transition: all 1s;
}

#modeBtn::after{
  content:'🌞'
}
.dark #modeBtn::after{
  content:'🌜'
}
const btn = document.getElementById('modeBtn');
btn.addEventListener('click',(e)=>{
  const body = document.body;
  if(body.className != 'dark'){
    //目前正处于浅色模式,需要切换成深色模式,添加dark
    body.className = 'dark'
  }else{
    //目前正处于深色模式,需要切换成浅色模式,删除dark
    body.className = ''
  }
})
  • 浅色和深色的切换,只是去改变html的展现效果,没有其他的行为逻辑,本质上这个代码是控制样式的
  • 纯css实现

版本三

纯展示类交互,零JS方案

使用多选框的选中/不选中两个状态来表示深色/浅色两种模式

使用label标签将多选框与图标关联。

使用伪类选择器匹配元素状态(:checked)。

使用兄弟选择器,选中.content。(mode:checked + .content)

总结

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

组件封装

组件是指web页面上抽出来一个一个包含模板(html),功能(js)和样式(css)的单元。

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

版本一轮播图

原生JS写轮播图

结构

使用无序列表表示。

使用特殊的类名表示当前显示的元素

    <div class="slider-list" id="my-slider">
        <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>

样式

  • 需要哪个元素显示,就为他加上slider-list__item--selected类名,设置opacity:1
  • 在ul上取消项目符号,删除列表自带的内边距和外边距
  • 使用绝对定位让所有li元素重叠在一起,在一个位置出现。
  • 使用opacity属性控制li元素的显示。
  • 切换的过渡动画使用transition
#my-slider{
    width:790px;
    position: relative;
}
#my-slider ul{
    list-style: none;
    position: relative;
    padding: 0;
    margin: 0;
}
.slider-list__item--selected
,.slider-list__item{
    position: absolute;
    transition: opacity 1s;
    opacity: 0;
}
.slider-list__item--selected{
    transition: opacity 1s;
    opacity: 1;
}

行为:API

控制轮播图功能的类:Slider,在类内部定义函数:职责单一,满足灵活性。

  • getSelectedItem()
  • getSelectedItemIndex()
  • slideTo(idx)
  • slideNext()
  • slidePrevious()
class Slider{
    constructor(id) {
        //根据id获得外部容器
        this.container = document.getElementById(id);
        //获得所有的li元素;
        this.items = this.container.querySelectorAll('.slider-list__item,.slider-list__item--selected');
    }
    //得到当前选中的li元素
    getSelectedItem(){
        const selected = this.container.querySelector('.slider-list__item--selected')
        return selected;
    }
    //得到当前选中的li元素的索引
    getSelectedItemIndex(){
        //this.items 不是数组,把他转化为数组,才可以用数组的方法
        return Array.from(this.items).indexOf(this.getSelectedItem());
    }
    //切换到哪一张,参数为索引
    slideTo(idx){
        //切换class
        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();
        //减一防止为负数,故先加length
        const previousIdx = (currentIdx - 1 + this.items.length) % this.items.length;
        this.slideTo(previousIdx);
    }
}

行为:控制流

在Slider类的构造函数内操作。

  • 图片自动切换:定时器;移入/移出事件
  • 箭头:上一张/下一张;点击事件
  • 原点:切换到某一张;移入/移出事件;
  • 原点与图片元素存在状态耦合,采用自定义事件 slide 使图片元素和原点状态对应。(slideTo的时候触发,改变原点状态)

箭头和原点都需要绝对定位,相对于ul元素。

因为箭头需要垂直居中,需要用到外部容器的高度,但是因为他们内容都是绝对定位,脱离了常规流,所以外部容器高度都为0;加高度;

#my-slider{
    width:790px;
    height:340px;
    position: relative;
}
#my-slider ul{
    list-style: none;
    position: relative;
    padding: 0;
    margin: 0;
    width:100%;
    height: 100%;
}

箭头使用伪元素生成。

        <a class="slider-list__next"></a>
        <a class="slider-list__previous"></a>
        <div class="slider-list__control">
            <span class="slider-list__control-buttons--selected"></span>
            <span class="slider-list__control-buttons"></span>
            <span class="slider-list__control-buttons"></span>
            <span class="slider-list__control-buttons"></span>
        </div>

箭头的样式

.slider-list__next
,.slider-list__previous{
    /*定位*/
    position: absolute;
    top:50%;
    margin-top:-25px;
    /*设置宽高、背景、字体大小*/
    width:30px;
    height:50px;
    color:#fff;
    font-size:24px;
    
    /*设置垂直居中和水平居中*/
    /* 因为伪元素默认是inline,即行级元素*/
    /*所以生成的是行级排版上下文,text-align,并不控制块元素自己的对齐,只控制它的行内内容的对齐*/
    line-height:50px;
    text-align: center;
    
    opacity: 0;
    transition: opacity 0.5s;
    cursor: pointer;
    background: rgba(0,0,0,0.3);
}
#my-slider:hover .slider-list__next,
#my-slider:hover .slider-list__previous{
    opacity: 1;
}
.slider-list__previous{
    left:0;
}
.slider-list__next{
    right: 0;
}
.slider-list__next::after{
    content:'>'
}
.slider-list__previous::after{
    content:'<'
}

圆点样式

外部容器:

  • display:table
  • margin:auto
  • position:relative
  • bottom:40px;

image.png

.slider-list__control{
    /*在ul下面生成,现在ul有高度了,所以他在显示在图片下面*/
    position:relative;
    bottom: 40px;
   
    display: table;
    
    background-color: rgba(255,255,255,0.5);
    border-radius: 15px;
    
    /*水平居中*/
    margin: auto;
    padding:5px;
}
.slider-list__control-buttons--selected
,.slider-list__control-buttons{
    display: inline-block;
    width:15px;
    height:15px;
    border-radius: 50%;
    background-color: #fff;
    margin:0 5px;
    cursor: pointer;
}
.slider-list__control-buttons--selected{
    background-color: #f40;
}

增加的API

  • start
  • stop
  • 构造函数内部
    • 左右箭头的点击事件
    • 原点控制器
      • 鼠标滑进事件
      • 鼠标滑出事件
      • 容器触发的自定义事件,为原点设置状态
  • slideTo函数,增加自定义事件
    • CustomEvent(type,Map配置)
    • EventTarget.dispatchEvent(event)

image.png

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

基本方法

  • 结构设计
  • 展现效果
  • 行为设计
    • API(功能)
    • Event(控制流)
    • 使用自定义事件进行数据解耦

版本2插件化

  • 版本一中Slider构造函数内部太多代码
  • 将next,previous,control等控制元素抽离出来,做成插件
  • 组件与插件之间通过依赖注入方式建立联系

image.png

function pluginNext(slide){
  const next = slide.container.querySelector('.slide-list__next');
    if(next){
      next.addEventListener('click',(ev)=>{
        //先停止动画
        slide.stop();
        slide.slideNext();
        slide.start();
        ev.preventDefault();
      })
    }
}

注册插件

  pluginRegister(...plugins){
    plugins.forEach((plugin)=>{
      plugin(this);
    })
  }

版本3模板化

image.png

  • render(images) 渲染html结构
  • action(slide) 定义控制流插件功能
  render(){
    const images = this.opts.images;
    const htmlStr = images.map((iamge)=>{
      return `<li class="slider-list__item">
                <img src="${iamge}"/>
              </li>`
    }).join('')
    return `<ul>${htmlStr}</ul>`
  }

插件注册,这里每一个plugin是一个对象,包含两个函数:render()和action()

  pluginRegister(...plugins){
    plugins.forEach((plugin)=>{
      const  container = document.createElement('div');
      container.className = 'slide-list__plugin';
      container.innerHTML = plugin.render(this.opts.images);
      this.container.appendChild(container);

      plugin.action(this);
    })
  }
const pluginNext = {
  render(){
    return `<a class="slide-list__next"></a>`;
  },
  action(slide){
    const next = slide.container.querySelector('.slide-list__next');
    if(next){
      next.addEventListener('click',(ev)=>{
        //先停止动画
        slide.stop();
        slide.slideNext();
        slide.start();
        ev.preventDefault();
      })
    }
  }
}

版本4组件化

image.png

过程抽象

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

image.png

操作次数限制 once函数

点击任务列表

  • 点击后,button背景颜色变化,任务列表透明度变化(transition)
  • 最后删除该任务
  • 点击事件
    const list = document.querySelector('ul');
    const buttons = list.querySelectorAll('button');
    buttons.forEach((button)=>{
      button.addEventListener('click',(ev)=>{
        const el = ev.target;
        //el-><i>  iconfont元素  li->button->i
        el.parentNode.parentNode.className = "completed";
        setTimeout(()=>{
        list.removeChild(el.parentNode.parentNode);
        },1000)
      })
    })
    

因为有setTimeout的存在,当点击后,该任务并不会立即消失,且opacity设置了过度,当连续点击时,会出发多次click事件,因此会发生错误

image.png 为了能够让“只执行一次”的需求覆盖不同的事件处理,需要将这个需求剥离出来,在各个过程叫过程抽象

function once(fn){
  return function (...args){
    if(fn){
      const ret = fn.apply(this,...args);
      fn = null;
      return ret;
    }
  }
}

EventTarget.addEventListener中的第三个参数可以配置once;

image.png

高阶函数

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

image.png

  • once
  • Throttle (节流)
  • Debounce (防抖)
    • 注意事项,防抖函数timer的赋值
    • 小鸟自动画-》扇动翅膀,使用toggle函数切换类名,不同的类对应着背景图片不同的位置-》精灵图
    • 小鸟随鼠标移动;鼠标移动事件,需要防抖;
    • 位置的确定
      • 事件对象中的ev.clientX-》表示鼠标离可视化区域左边界,
      • 但如果让bird.style.left = ev.clientX,bird.style.top = ev.clientY
      • 对应的是左上顶点
      • 需要小鸟的中心对应着鼠标的位置;

image.png

image.png

image.png

//节流函数;两次函数调用之间必须间隔time时间。在time时间内,触发,不管用
function Throttle(fn,time = 500){
  const timer = null;
  return function(...args){
    if(timer == null){
      fn.apply(this,args);
      timer = setTimeout(()=>{
        timer = null;
      },time)
    }
  }
}
//防抖函数
//常用于鼠标滑动mousemove;滚动条滚动scroll事件;
//在time这一段时间中,只要有调用,就将计时器清掉,重新计时;
//只有当time时间内没有调用时,在time之后才可调用
//setTimeout的返回值代表了计时器的标志,一定要赋值到timer中,
//否则,前面清除不管用,清除的不是下面的计时器,会出现触发了好几个计时器的情况下,防抖无效。
function Debounce(fn,time){
  time = time || 100
  const timer = null;
  return function(...args){
    clearInterval(timer);
   timer =  setTimeout(()=>{
      fn.apply(this,args);
    })
  }
}
//消费者  把一个同步的变成异步的,隔一段时间调用。
//延迟调用的一个效果。
function Consumer(fn,time){
  const tasks = [],timer = null;
  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)
    }
  }
}
//jquery中存在批量操作,即调用一个api同时操作一组元素
//处理迭代对象;
//如果subject是可迭代的,则让fn去处理subject每一个元素obj;
//如果是不可迭代的,就让fn只处理subject
const isInterable = obj => obj != null && typeof obj[Symbol.interator] == 'function';
function Iterative(fn){
  return function(subject,...rest){
    if(isInterable(obj)){
      const ret = [];
      for(const obj of subject){
        ret.push(fn.apply(this,[obj,...rest]));
      }
      return ret;
    }
    return fn.apply(this,[subject,...rest])
  }
}

纯函数

  • 没有副作用的
  • 结果是可预期的
  • 输入确定,输出就确定了
  • 容易确定他的正确性

编程范式

  • 命令式-》过程,怎么做
    • 面向过程
    • 面向对象
  • 声明式 做什么 过程抽象
    • 逻辑式
    • 函数式

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