JS进阶指南--函数式编程

2,651 阅读8分钟

    谈到JS,大部分人会认为JS上手十分容易,变量没有限制,函数作为高等公民等等。诸如此种操作都会让大家觉得JS算是一门特别容易上手的语言。但事实上真的有想的那么容易吗?JS是否有更高阶的玩法呢?

IMG_9204.JPG

命令式与声明式

    常用的编程语言都会分为声明式和命令式。而 JS 就是同时具备声明式和命令式的一门语言。如果现在有一个改变样式的需求,按照命令式来处理的话

const root = document.querySelector('#root');
const btn = document.querySelector('#btn');
btn.addEventListener('click',() => {
  if(root.className == 'light') {
    root.className = ''
  } else {
    root.className = "light"
  }
})
 

    按照声明式来的话

const toggle(event, targetName) {
  if(event.target.className == targetName) {
    event.target.className = ""
  } else {
    event.target.className = targetName
  }
}

const root = document.querySelector('#root');
const btn = document.querySelector('#btn');
btn.addEventListener('click', toggle(event, 'light'));

    以上,命令式就是相当于给计算机下达命令,告知计算机每一步应该怎么做,但是这样的话代码的可读性就会降低,试想如果有其他人来阅读你写的代码,他们还需要花一定的时间来理解这段代码的含义,反观声明式,通过将重复逻辑抽离,封装成函数,达到见名知意的效果,你只需要告诉电脑你想做什么,电脑就会帮你去实现。传入对应的事件对象和需要切换的名字,在函数内部就可以进行切换,你不需要关注函数的过程,只需要关注函数的结果,这就是声明式语言的好处,同时也是函数式编程的要点。

何为函数式编程

    函数式编程属于编程范式中的一种,作为JS最为常用的一种编程范式,想要理解何为函数式编程,就必须了解高阶函数和纯函数的定义。

高阶函数(HOF)

    以一个函数作为参数,返回另一个另一个函数的函数称为高阶函数。

  1. throttle
  2. debounce
  3. once

    这些通过接受一个函数,并对该函数做一定的特殊处理,将处理后的新函数返回,此时新函数就具有了新的效果,这种函数就称为高阶函数,也常用于装饰器。我们来看一个经典的例子,对于输入框的输入,我们需要做一个防抖处理,避免多次向服务器发送请求,为了解决这个问题,我们决定做一个防抖的处理:

function debounce(fn, wait) {
  let timer
  // ... closures
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, ...args)
    }, wait)  
  }
}

    该函数接受一个需要处理的函数作为参数,并将其进行封装,返回一个在定时器中进行调用的函数。每次调用返回的函数时,都会检查是否存在定时器,如果有定时器的存在,就会将其清除并开启新的定时器,这样就能确保该函数只会在 wait 时间后执行一次,避免了快速输入时向服务器发起多次请求。

    而这样一个防抖的逻辑并不只有输入框需要使用,还有很多种业务要求进行防抖处理。基于这一点,我们将防抖的逻辑进行抽离,也就是解耦,将其封装成一个函数,只需要将处理的函数作为参数传递进去,我们就能得到处理后的结果,那么这个函数就被称为高阶函数。它能够替我们完成重复操作的部分,同时函数也能够做到见名知意。我们可以在任何地方调用它,只要我们需要。

纯函数

​ 我们首先引入一个叫做副作用的概念

const obj = {
	a: 1
}

function test() {
  obj.a++
}

    上面这个函数,在调用的时候会更改外部变量的值,这种函数就具有副作用,像是更改全局变量、引入其它变量,我们把这些现象称为函数具有副作用。而纯函数恰好与其相反,纯函数不具有副作用,它就像一个数学函数,在特定的输入下只能有特定的输出,回想刚刚写的防抖函数,它是不是也没有副作用。针对输入进来的同一个函数,它只会返回同一个封装好的函数,这就是纯函数。

​ eg: y = f(x)

函数式编程

    现在来看最重要的一部分,对于比较小的项目,即便我们使用纯命令式去构建,全篇没有封装任何一个函数,我们也不会很难弄懂,因为它的逻辑非常简单。但对于大型项目呢,我们还能不能用纯命令式去写?答案是可以,但不推荐。因为在大型项目中,他们的逻辑关系非常复杂,如果说没有做任何封装,在新人接触这个项目的时候,他们会难以理解其中的关系,对于其中的一些处理,需要花费很多时间去理解,如果还没有注释的话,那效果就会更差了。

    而现在有了函数式编程这个概念,一切都将变得简单起来,在函数式编程中,我们推荐尽量使用纯函数,将大部分的逻辑的主要功能进行抽离,封装成新的函数。在调用该函数的时候只需传入需要的配置,就能实现向实例中添加新功能的作用。

    比如一个轮播图,我们正常写的时候只是在 HTML、 CSS和 JavaScript中添加好对应的逻辑,在 html 中进行排版,在 JS 中添加点击事件和定时器事件。但这样的结果可想而知,可维护性非常差,当你需要添加新功能的时候,就会牵一发而动全身,大部分的代码都要修改,那我们能不能把轮播图当成一个组件来处理呢,将轮播图的逻辑全部封装在一起,答案是可以的。

  这是轮播图的 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){
      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);

    我们将轮播图的逻辑封装进类中,将其中的行为定义为函数,也就是定义一系列的API,如 slideTo 方法。这样我们只需要在 new 出实例后,通过调用实例上的 API 就能对轮播图组件进行操作。这样看起来是不是逻辑就清晰很多了呢?这就是函数式编程的好处。但在这里我得引入阮一峰前辈的一段话。

函数式编程入门教程

你可能注意到了,上面生成新的函子的时候,用了new命令。这实在太不像函数式编程了,因为new命令是面向对象编程的标志。 函数式编程一般约定,函子有一个of方法,用来生成新的容器。 下面就用of方法替换掉new

Functor.of = function(val) {
  return new Functor(val);
};

然后,前面的例子就可以改成下面这样。

Functor.of(2).map(function (two) {
  return two + 2;
});
// Functor(4)

这就更像函数式编程了。

    你看,这是不是将重复逻辑抽离成函数了?我们只要看到他的名字,就能想到他是构造实例的方法。

总结

    过程抽象、HOF、装饰器,这些是函数式编程的要点,在博主看来,函数式编程就是在编写代码的过程中尽量多使用能够见名知意的函数,将一些复杂的逻辑例如防抖处理提取到新的函数中,作为高阶函数。

    对于简单的逻辑我们也可以抽离成新的函数,比如加法函数,这样代码的可读性就会更好。我们只需要通过调用函数就能实现功能,而不需要去关注函数实现的过程。而对于常用的组件,我们尽量多去使用类,在其中构造一个控制器,在构造实例的时候传入配置,在构造器中注入配置,这就是依赖注入,针对这个轮播图组件,我们也可以用依赖注入去实现,这就是插件化的一种表现。

    总的来说,尽量使用纯函数,少使用非纯函数,将公共部分抽离,也就是解耦,减少复杂逻辑直接出现在代码中的几率,多去使用高阶函数,就是函数式编程的核心思想。