最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 浅析JavaScript函数式编程

    正文概述 掘金(大转转FE)   2021-03-17   583

    前言

    随着React的流行,函数式编程在前端领域备受关注。尤其近几年,越来越多的类库偏向于函数式开发:lodash/fp,Rx.js、Redux的纯函数,React16.8推出的hooks,Vue3.0的composition Api...同时在ES5/ES6标准中也有体现,例如:箭头函数、迭代器、map、filter、reduce等。

    那么为什么要使用函数式编程呢?我们通过一个例子感受一下:在业务需求开发中,我们更多时候是对数据的处理,例如:将字符串数组进行分类,转为字符串对象格式

    // jsList => jsObj
    const jsList = [
      'es5:forEach',
      'es5:map',
      'es5:filter',
      'es6:find',
      'es6:findIndex',
      'add'
    ]
    
    const jsObj = {
      es5: ["forEach", "map", "filter"],
      es6: ["find", "findIndex"]
    }
    

    先通过我们最常用的命令式实现一遍:

    const jsObj = {}
    
    for (let i = 0; i < jsList.length; i++) {
      const item = jsList[i];
      const [vesion, apiName] = item.split(":")
        
      if (apiName) {
       	if (!jsObj[vesion]) {
          jsObj[vesion] = []
        }
    
        jsObj[vesion].push(apiName);   
      }
    }
    

    接下来再看函数式的实现:

    const jsObj = jsList
      .map(item => item.split(':'))
      .filter(arr => arr.length === 2)
      .reduce((obj, item) => {
        const [version, apiName] = item
        return {
          ...obj,
          [version]: [...(obj[version] || []), apiName]
        }
      }, {})
    

    两段代码对比下来,会发现命令式的实现过程中会产生大量的临时变量,还参杂大量的逻辑处理,通常只有读完整段代码才会明白具体做了什么。如果后续需求变更,又会添加更多的逻辑处理,想想脑壳都痛...

    反观函数式的实现:单看每个函数,就可以知道在做什么,代码更加语义化,可读性更高。整个过程就像一条完整的流水线,数据从一个函数输入,处理完成后流入下一个处理函数...每个函数都是各司其职

    接下来,让我们在窥探函数式编程的世界之前,先简单了解一下上面提到的编程范式。

    编程范式

    例如在面向对象编程中,程序员认为程序是一系列相互作用的对象;而在函数式编程中,程序会被当做一个无状态的函数计算的序列。常见的编程范式如下:

    命令式编程

    大部分命令式编程语言都支持四种基本的语句:

    1. 运算语句;
    2. 循环语句(for、while);
    3. 条件分支语句(if else、switch);
    4. 无条件分支语句(return、break、continue)。

    计算机执行的每一个步骤都是程序员控制的,所以可以更加精细严谨的控制代码,提高应用程序的性能;但是由于存在大量的流程控制语句,在处理多线程、并发问题时,容易造成逻辑紊乱

    声明式编程

    相较于命令式编程范式,不需要流程控制语言,没有冗余的操作步骤,使得代码更加语义化,降低了代码的复杂性;但是其底层实现的逻辑并不可控,不适合做更加精细的代码优化。

    总结下来,这两种编程范式最大的不同就是:

    1. How:命令式编程告诉计算机如何 计算,关心解决问题的步骤;
    2. What:声明式编程告诉计算机需要计算什么,关心解决问题的目标。

    函数式编程

    声明式编程是一个大的概念,其下包含一些有名的子编程范式:约束式编程、领域专属语言、逻辑式编程、函数式编程。其中领域专属语言(DSL)和函数式编程(FP)在前端领域的应用更加广泛,接下来开始我们今天的主角--函数式编程

    函数式编程并不是一种工具,而是一种可以适用于任何环境的编程思想,它是一种以函数使用为主的软件开发风格。这与大家都熟悉的面向对象编程的思维方式完全不同,函数式的目的是通过函数抽象作用在数据流的操作,从而在系统中消除副作用并减少对状态的改变

    为了充分理解函数式编程,我们先来看下它有哪些基本概念?

    概念

    函数是一等公民

    函数与其他数据类型一样,不仅可以赋值给变量,也可以当作参数传递,或者做为函数的返回值。例如:

    // 做为变量
    fn = () => {}
    // 做为参数
    function fn1(fn){fn()}
    // 做为函数返回值
    function fn2(){return () => {} }
    

    正是函数是‘一等公民’的前提,函数式编程才得以实现,而在JavaScript中,闭包和高阶函数成了中坚力量。

    纯函数

    提到纯函数,熟悉redux的同学可能再熟悉不过了,在redux中所有的修改都需要使用纯函数。纯函数具有以下特点:

    • 无状态:函数的输出仅取决于输入,而不依赖外部状态;
    • 无副作用:不会造成超出其作用域的变化,即不修改函数参数或全局变量等。
    function add(obj) {
      obj.num += 1
      return obj
    }
    
    const obj = {num: 1}
    add(obj)
    console.log(obj)
    // { num: 2 }
    

    这个函数不是纯的,因为js对象传递的是引用地址,函数内部的修改会直接影响外部变量,最后产生了预料之外的结果。接下来,我们改成纯函数的写法:

    function add(obj) {
      const _obj = {...obj}
      _obj.num += 1
      return _obj
    }
    
    const obj = {num: 1}
    add(obj)
    console.log(obj);
    // { num: 1 }
    

    通过在函数内部创建新的变量进行更改(是不是有想起redux的reducer写法~~),从而避免产生副作用。纯函数除了无副作用外,还有其他好处:

    1. 可缓存性 正是因为函数式声明的无状态特点,即:相同输入总能得到相同的输出。所以我们可以提前缓存函数的执行结果,实现更多功能。例如:优化斐波拉契数列的递归解法。
    2. 可移植性/自文档化 纯函数的依赖很明确,更易于观察和理解,配合类型签名可以使程序更加简单易读。
    // get :: a -> a
    const get = function (id) { return id}
    // map :: (a -> b) -> [a] -> [b]
    const map = curry(function (f, res){
        return res.map(f)
    })
    
    1. 可测试性 纯函数让测试更加简单,只需简单地给函数一个输入,然后断言输出就可以了。
    副作用

    函数的副作用是指在调用函数时,除了返回函数值外还产生了额外的影响。例如修改上个例子中的修改参数或者全局变量。除此之外,以下副作用也都有可能会发生:

    • 更改全局变量
    • 处理用户输入
    • 屏幕打印或打印log日志
    • DOM查询以及浏览器cookie、localstorage查询
    • 发送http请求
    • 抛出异常,未被当前函数捕获
    • ...

    副作用往往会影响代码的可读性和复杂性,从而导致意想不到的bug。在实际开发中,我们是离不开副作用的,那么在函数式编程中应尽量减少副作用,尽量书写纯函数。

    引用透明

    如果一个函数对于相同输出始终产生同一个输出结果,完全不依赖外部环境的变化,那么就可以说它是引用透明的。

    数据不可变

    所有数据被创建后不可更改,如果想要修改变量,需要新建一个新的对象进行修改(例如上面纯函数提到的例子)。

    说完这些概念,我们再来看一下在函数式编程中又有哪些常见的操作。

    柯里化(curry)

    F(a,b,c) => F(a)(b)(c)
    

    接下来我们实现一版简单的curry函数。

    function curry(targetFunc) {
      // 获取目标函数的参数个数
      const argsLen = targetFunc.length
      
      return function func(...rest) {
        return rest.length < argsLen ? func.bind(null, ...rest) : targetFunc.apply(null, rest)
      }
    }
    
    function add(a,b,c,d) {
      return a + b + c + d
    }
    
    console.log(curry(add)(1)(2)(3)(4));
    console.log(curry(add)(1, 2)(3)(4));
    // 10
    

    仔细的同学可能已经看出来,上面实现的curry函数并不是单纯柯里化函数,因为柯里化强调的是生成单元函数,但是单次传入多个参数也可以,更像是柯里化偏函数的综合应用。那偏函数又是怎么定义的呢?

    偏函数在创建的时候还可以传入预设的partials参数,类似bind的使用。通常情况下,我们不会自己写curry函数,像Lodash、Ramda这些库都实现了curry函数,这些库实现的curry函数和柯里化的定义也是不太一样的。

    const add = function (a, b, c) {return a + b + c}
    
    const curried = _.curry(add)
    curried(1)(2)(3)
    curried(1, 2)(3)
    curried(1, 2, 3)
    // 还实现了附加参数的占位符
    curried(1)(_, 3)(2)
    

    组合(compose)

    compose在函数式编程中也是一个很重要的思想。把复杂的逻辑拆分成一个个简单任务,最后组合起来完成任务,使得整个过程的数据流更明确、可控、可读。 这也印证了上面我们提到过:函数式编程像一条流水线,初始数据通过多个函数依次处理,最后完成整体输出。

    // 整个过程处理
    a => fn => b
    // 拆分成多段处理
    a => fn1 => fn2 => fn3 => b 
    

    接下来,我们实现一般简单的compose:

    function compose(...fns) {
      return fns.reduce((a,b) => {
        return (...args) => {
          return a(b(...args))
        }
      })
    }
    
    function fn1(a) {
      console.log('fn1: ', a);
      return a+1
    }
    
    function fn2(a) {
      console.log('fn2: ', a);
      return a+1
    }
    
    function fn3(a) {
      console.log('fn3: ', a);
      return a+1
    }
    
    console.log(compose(fn1, fn2, fn3)(1));
    // fn3:  1
    // fn2:  2
    // fn1:  3
    // 4
    

    分析上述compose的实现,可以看出fn3是先于fn2执行,fn2先于fn1执行,也就是说:compose创建了一个从右向左执行的数据流。如果要实现从左到右的数据流,可以直接更改compose的部分代码即可实现:

    • 更换Api接口:把reduce改为reduceRight
    • 交互包裹位置:把a(b(...args))改为b(a(...args))

    也可以使用Ramda中提供的组合方式:管道(pipe)。

    R.pipe(fn1, fn2, fn3)
    

    函数组合不仅让代码更富有可读性,数据流的整体流向也更加清晰,程序更加可控。接下来,我们看下函数式编程在具体业务中的实践。

    编程实践

    数据处理

    业务开发过程中,我们更多的时候是对接口请求数据或表单提交数据的处理,尤其是经常开发B端的同学更是深有体会。笔者之前就做过针对大量表单数据的处理需求,例如:针对用户提交的表单数据做一定的处理:1. 清除空格;2. 全部转为大写。

    首先我们站在函数式编程的思维上分析一下整个需求:

    1. 抽象:每个处理过程都是一个纯函数
    2. 组合:通过compose组合每一个处理函数
    3. 扩展:只需删除或添加对应的处理纯函数即可

    接下来,我们看一下整体的实现:

    // 1. 实现遍历函数
    function traverse (obj, handler) {
      if (typeof obj !== 'object') return handler(obj)
    
      const copy = {}
      Object.keys(obj).forEach(key => {
        copy[key] = traverse(obj[key], handler)
      })
    
      return copy
    }
    
    // 2. 实现具体业务处理的纯函数
    function toUpperCase(str) {
      return str.toUpperCase() // 转为大写
    }
    
    function toTrim(str) {
      return str.trim() // 删除前后空格
    }
    
    // 3. 通过compose执行
    // 用户提交数据如下:
    const obj = {
      info: {
        name: ' asyncguo '
      },
      address: {
        province: 'beijing',
        city: 'beijing',
        area: 'haidian'
      }
    }
    console.log(traverse(obj, compose(toUpperCase, toTrim)));
    /**
        {
         info: { name: 'ASYNCGUO' },
         address: { province: 'BEIJING', city: 'BEIJING', area: 'HAIDIAN' }
        }
    */
    

    redux中间件实现

    说到函数式在JavaScript中的实践,那就不得不聊一下redux。首先我们先实现一版简单redux:

    function createStore(reducer) {
      let currentState
      let listeners = []
    
      function getState() {
        return currentState
      }
    
      function dispatch(action) {
        currentState = reducer(currentState, action)
        listeners.map(listener => {
          listener()
        })
        return action
      }
    
      function subscribe(cb) {
        listeners.push(cb)
        return () => {}
      }
      
      dispatch({type: 'ZZZZZZZZZZ'})
    
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    
    // 应用实例如下:
    function reducer(state = 0, action) {
      switch (action.type) {
        case 'ADD':
          return state + 1
        case 'MINUS':
          return state - 1
        default:
          return state
      }
    }
    
    const store = createStore(reducer)
    
    console.log(store);
    store.subscribe(() => {
      console.log('change');
    })
    console.log(store.getState());
    console.log(store.dispatch({type: 'ADD'}));
    console.log(store.getState());
    

    首先使用reducer初始化store,后续事件产生时,通过dispatch更新store状态,同时通过getState获取store的最新状态。

    redux规范了单向数据流action只能由dispatch函数派发,并通过纯函数reducer更新状态state,然后继续等待下一次的事件。这种单向数据流的机制进一步简化事件管理的复杂度,并且还可以在事件流程中插入中间件(middleware)。通过中间件,可以实现日志记录、thunk、异步处理等一系列扩展处理,大大得增强事件处理的灵活性。

    接下来对上面的redux进一步增强优化:

    // 扩展createStore
    function createStore(reducer, enhancer){
      if (enhancer) {
          return enhancer(createStore)(reducer)
      }
    
      ...
    }
    // 中间件的实现
    function applyMiddleware(...middlewares) {
      return function (createStore) {
        return function (reducer) {
          const store = createStore(reducer)
          let _dispatch = store.dispatch
    
          const middlewareApi = {
            getState: store.getState,
            dispatch: action => _dispatch(action)
          }
    
          // 获取中间件数组:[mid1, mid2]
          // mid1 = next1 => action1 => {}
          // mid2 = next2 => action2 => {}
          const midChain = middlewares.map(mid => mid(middlewareApi))
    
          // 通过compose组合中间件:mid1(mid2(mid3())),得到最终的dispatch
          // 1. compse执行顺序:next2 => next1
          // 2. 最终dispatch:action1 (action1中调用next时,回到上一个中间件action2; action2中调用next时,回到最原始的dispatch)
          
          _dispatch = compose(...midChain)(store.dispatch)
    
          return {
            ...store,
            dispatch: _dispatch
          }
        }
      }
    }
    
    // 自定义中间件模板
    const middleaware = store => next => action => {
        // ...逻辑处理
        next(action)
    }
    

    通过compose组合所有的middleware,然后返回包装过的dispatch。接下来,在每次dispatch时,action会经过全部中间件进行一系列操作,最后透传给纯函数reducer进行真正的状态更新。任何middleware能够做到的事情,我们都可以通过手动包装dispatch调用实现,但是放在同一个地方统一管理使得整个项目的扩展变得更加容易。

    // 1. 手动包装dispatch调用,实现logger功能
    function dispatchWithLog(store, action) {
        console.log('dispatching', action)
        store.dispatch(action)
        console.log('next state', store.getState())
    }
    
    dispatchWithLog(store, {type: 'ADD'})
    
    // 2. 中间件方式包装dispatch调用
    const store = new Store(reducer, applyMiddleware(thunkMiddleware, loggerMiddleware))
    
    store.dispatch(() => {
        setTimeout(() => {
        store.dispatch({type: 'ADD'})
      }, 2000)
    })
      
    // 中间件执行过程
    thunk => logger => store.dispatch
    

    RxJS

    提到Rxjs,更多人想到应该是响应式编程(Reactive Programming, RP),即使用异步数据流进行编程。响应式编程使用Rx.Observale为异步数据提供统一的名为可观察的流(observeale stream)的概念,可以说响应式编程的世界就是的世界。想要提取其值,就必须先订阅它。例如:

    Rx.observale.of(1, 2, 3, 4, 5)
        .filter(x => x%2 !== 0)
        .map(x => x * x)
        .subscrible(x => console.log(`ext: ${x}`))
    

    通过上面的例子,可以发现响应式编程就是让整个编程过程流式化,就像一条流水线,同时以函数式编程为主,即流水线的每条工序都是无副作用的(纯函数)。所以更准确的说Rxjs应该是函数响应式编程(Functional Reactive Programming,FRP),顾名思义,FRP同时具有函数式编程和响应式编程的特点。(今天主要是讲函数式编程,更多Rxjs部分的内容,感兴趣的同学可以自行了解一下。笔者还是很推荐学习一下Rxjs在异步数据流上的处理~)

    总结

    函数式编程是一个很大的话题,今天我们主要是介绍了一下函数式编程的基础概念,当然还有更高级的概念:Functor(函子)MonadApplication Functor等还没有提到,真正掌握这些东西还是需要一定练习积累,感兴趣的同学可以自行了解一下,或者期待笔者后续的文章。

    对比面向对象编程,我们可以总结一下,函数式编程的优点:

    • 代码更加简明,流程更可控
    • 流式处理数据
    • 降低事件驱动代码的复杂性

    当然,函数式编程也存在一定的性能问题,在抽象层次往往因为过度包装,导致上下文切换的性能开销;同时由于数据不可变的特点,中间变量也会消耗更多内存空间

    在日常业务开发中,函数式编程应是与面向对象编程以互补的形式存在,根据具体的需求选择合适的编程范式。在面对一种新技术或新的编程方式时,若其优点值得我们学习和借鉴时,并不应该因为某个缺陷就一味的拒绝它,更多时候是应该能够想到与其互补的更优解。不以优而喜,不以劣而悲,与君共勉~

    推荐资料

    functional light JS

    Functional-Light-JS - github

    redux-middleware

    函数式编程浅析

    函数式编程在Redux/React中的应用

    函数式编程指北

    JavaScript函数式编程指南

    感谢你的阅读,有任何问题,欢迎评论区留言讨论,互相学习。


    起源地下载网 » 浅析JavaScript函数式编程

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元