JavaScript函数式编程实现介绍

目录
  • 为什么要学习函数式编程
  • 什么是函数式编程
  • 前置知识
    • 函数是一等公民
    • 函数可以储存在变量中
    • 函数作为参数
    • 函数作为返回值
  • 高阶函数
    • 什么是高阶函数
    • 使用高阶函数的意义
    • 常用高阶函数
  • 闭包
  • 纯函数
    • 纯函数概念
    • 纯函数的好处
    • 副作用
  • 柯里化
  • 函数组合
  • Functor(函子)
  • MayBe 函子
  • Either函子

为什么要学习函数式编程

Vue进入3.*(One Piece 海贼王)世代后,引入的setup语法,颇有向老大哥React看齐的意思,说不定前端以后还真是一个框架的天下。话归正传,框架的趋势确实是对开发者的js功底要求更为严格了,无论是hooks、setup,都离不开函数式编程,抽离代码可复用逻辑,更好地组织及复用代码,有一点我感到很高兴的是,终于可以抛弃烦人的this了,当然,这也不是我为偷懒而生出这样的感想,人家道格拉斯老爷子可是在他的新书《JavaScript悟道》里极力吐槽了一下this,所以,也算是像js大佬看齐了。所以,要想不被前端日新月异的新技术给冲昏头脑,还是适时回来重学一下JavaScript吧。

什么是函数式编程

函数式编程(Functional Programming, FP),FP 是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程。

面向对象编程:面向对象有三大特性,通过封装、继承和多态来演示事物之间的联系,如果更宽泛来说,抽象也应该算进去,但是由于面向对象的本质就是抽象,其不算是三大特性也不为过。

函数式编程:函数式编程的思想主要就是对运算过程进行抽象,它更像一个黑盒,你给入特定的输出,进过黑盒运算后再返回运算结果。你可以将其理解为数学中的y = f(x)。

  • 程序的本质:根据输入进行某种运算得到相应的输出。
  • x -> f(联系、映射) -> y, y = f(x)
  • 函数式编程中的函数其实对应数学中的函数,即映射关系。
  • 相同的输入始终要得到相同的输出(纯函数)
  • 可复用

前置知识

函数是一等公民

作为一名有一定经验的前端开发者,你一定对JavaScript中“函数是一等公民”这一说法不陌生。

这里给出权威文档MDN的定义:当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数。例如,在这门语言中,函数可以被当作参数传递给其他函数,可以作为另一个函数的返回值,还可以被赋值给一个变量。

函数可以储存在变量中

let fn = function() {
  console.log('Hello First-class Function')
}
fn()

函数作为参数

function foo(arr, fun) {
  for (let i = 0; i < arr.length; i++) {
    fun(arr[i])
  }
}
const array = [1, 2, 3, 4]
foo(array, function(a) { console.log(a) })

函数作为返回值

function fun() {
  return function () {
    consoel.log('哈哈哈')
  }
}
const fn = fun()
fn()

高阶函数

什么是高阶函数

高阶函数

  • 可以把函数作为参数传递给另外一个函数
  • 可以把函数作为另外一个函数的返回结果

函数作为参数(为了避免文章篇幅过长,后面的演示代码就不给出测试代码了,读者可自行复制文章代码在本地编辑器上调试)

function filter(array, fn) {
    let results = []
    for (let i = 0; i < array.length; i++) {
        if (fn(array[i])) {
            results.push(array[i])
        }
    }
    return results
}
// 测试
let arr = [1, 3, 4, 7, 8]
const results = filter(arr, function(num) {
    return num > 7
})
console.log(results) // [8]

函数作为返回值

// 考虑一个场景,在网络延迟情况下,用户点击支付,你一定不想要用户点完支付没反应后点击下一次支付再重新支付一次,不然,你的公司就离倒闭不远了。
// 所以考虑一下once函数
function once(fn) {
    let done = false
    return function() {
        if (!done) {
            done = true
            return fn.apply(this, arguments)
        }
    }
}
let pay = once(function (money) {
    console.log(`支付: ${money} RMB`)
})
pay(5)
pay(5)
pay(5)
pay(5)
// 5

使用高阶函数的意义

  • 抽象可以帮我们屏蔽细节,只需要关注目标
  • 高阶函数是用来抽象通用的问题

常用高阶函数

  • forEach(已实现)
  • map
const map = (array, fn) => {
  let results = []
  for (let value of array) {
    results.push(fn(value))
  }
  return results
}
  • filter
  • every
const every = (array, fn) => {
  let result = true
  for (let value of array) {
    result = fn(value)
    if (!result) {
      break
    }
  }
  return result
}
  • some
const some = (array, fn) => {
  let result = false
  for (let value of array) {
    result = fn(value)
    if (result) {
      break
    }
  }
  return result
}
  • find/findIndex
  • reduce
  • sort

闭包

闭包 (Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。

闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。

function makePower(power) {
    return function (num) {
        return Math.pow(num, power)
    }
}
// 求平方及立方
let power2 = makePower(2)
let power3 = makePower(3)
console.log(power2(4)) // 16
console.log(power2(5)) // 25
console.log(power3(4)) // 64
function maekSalary(base) {
    return function (performance) {
        return base + performance
    }
}
let salaryLevel1 = makeSalary(12000)
let salaryLevel2 = makeSalary(15000)
console.log(salaryLevel1(2000)) // 14000
console.log(salaryLevel2(3000)) // 18000

其实上面这两个函数都是差不多的,都是通过维持对原函数内部成员的引用。具体可以通过浏览器调试工具自行了解。

纯函数

纯函数概念

纯函数:相同的输入永远会得到相同的输出

lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法。有人可能会有这样的疑惑,随着ECMAScript的演进,lodash中很多方法都已经在ES6+中逐步实现了,那么学习其还有必要吗?其实不然,lodash中还是有很多很好用的工具函数的,比如说,防抖节流是前端工作中经常用到的,你可不想每次都手写一个函数吧?更何况没有一点js功底还写不出来呢。

话归正传,来看看数组的两个方法:slice和splice。

  • slice 返回数组中的指定部分,不会改变原数组
  • splice 对数组进行操作返回该数组,会改变原数组
let array = [1, 2, 3, 4, 5]
// 纯函数
console.log(array.slice(0, 3))
console.log(array.slice(0, 3))
console.log(array.slice(0, 3))
// 不纯的函数
console.log(array.splice(0, 3))
console.log(array.splice(0, 3))
console.log(array.splice(0, 3))

纯函数的好处

可缓存

因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来

function getArea(r) {
    console.log(r)
    return Math.PI * r * r
}
function memoize(f) {
    let cache = {}
    return function() {
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || f.apply(f, arguments)
        return cache[key]
    }
}
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
// 4
// 50.26548245743669
// 50.26548245743669
// 50.26548245743669

可测试

纯函数让测试更方便

并行处理

在多线程环境下并行操作共享的内存数据很可能会出现意外情况

纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)

副作用

// 不纯的
let mini = 18
function checkAge (age) {
  return age >= mini
}
// 纯的(有硬编码,后续可以通过柯里化解决)
function checkAge (age) {
  let mini = 18
  return age >= mini
}

副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。

柯里化

柯里化的概念:当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果。

柯里化就可以解决上面代码中的硬编码问题

// 普通的纯函数
function checkAge(min, age) {
    return age >= min
}
// 函数的柯里化
function checkAge(min) {
    return function(age) {
        return age >= min
    }
}
// 当然,上面的代码也可以用ES6中的箭头函数来改造
const checkAge = (min) => (age => age >= min)

下面来手写一个curry函数

function curry(func) {
  return function curriedFn(...args) {
    if (args.length < func.length) {
      return function() {
        return curriedFn(...args.concat(Array.from(arguments)))
      }
    }
    return func(...args)
  }
}

函数组合

看了这么多代码,你肯定会觉得函数里面有很多return看起来不是很好看,事实也确是如此,所以这就要引出函数组合这个概念。

纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))

获取数组的最后一个元素再转换成大写字母, .toUpper(.first(_.reverse(array))) (这些都是lodash中的方法)

函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

你可以把其想象成一根管道,你将fn管道拆分成fn1、fn2、fn3三个管道,即将不同处理逻辑封装在不同的函数中,然后通过一个compose函数进行整合,将其变为一个函数。

fn = compose(f1, f2, f3)
b = fn(a)

Functor(函子)

什么是Functor

  • 容器:包含值和值的变形关系(这个变形关系就是函数)
  • 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 行一个函数对值进行处理(变形关系)
// Functor 函子 一个容器,包裹一个值
class Container {
    constructor(value) {
        this._value = value
    }
    // map 方法,传入变形关系,将容器里的每一个值映射到另一个容器
    map(fn) {
        return new Container(fn(this._value))
    }
}
let r = new Container(5)
    .map(x => x + 1)
    .map(x => x * x)

console.log(r)  // 36

总结

  • 函数式编程的运算不直接操作值,而是由函子完成
  • 函子就是一个实现了 map 契约的对象
  • 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
  • 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
  • 最终 map 方法返回一个包含新值的盒子(函子)

可能你不习惯在代码中看到new关键字,所以可以在容器中实现一个of方法。

class Container {
    static of (value) {
        return new Container(value)
    }
    constructor(value) {
        this._value = value
    }
    map(fn) {
        return Container.of(fn(this._value))
    }
}

MayBe 函子

上面的代码中如果传入一个null 或 undefined的话,代码就会抛出错误,所以需要再实现一个方法

class MayBe {
    static of(value) {
        return new MayBe(value)
    }
    constructor(value) {
        this._value = value
    }
    map(fn) {
        return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
    }
    isNothing() {
        return this._value == null // 此处双等号等价于this._value === null || this._value === undefined
    }
}

你看下上面的代码,是不是健壮性就好一点了呢?

Either函子

在MayBe函子中,很难确认哪一步产生的空值问题。所以就有了Either

class Left {
    static of(value) {
        return new Left(value)
    }
    constructor(value) {
        this._value = value
    }
    map(fn) {
        return this
    }
}
class Right {
    static of(value) {
        return new Right(value)
    }
    constructor(value) {
        this._value = value
    }
    map(fn) {
        return Right.of(fn(this._value))
    }
}
function parseJSON(str) {
    try {
        return Right.of(JSON.parse(str))
    } catch (e) {
        return Left.of({ error: e.message })
    }
}

到此这篇关于JavaScript函数式编程实现介绍的文章就介绍到这了,更多相关JS函数式编程内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • js面向对象编程OOP及函数式编程FP区别

    目录 写在前面 javscript 中函数和对象的关系 面向对象编程(OOP) 继承 多态 封装 函数编程编程(FP) 闭包和高阶函数 柯里化 偏函数 组合和管道 函子 写在最后 写在前面 浏览下文我觉得还是要有些基础的!下文涉及的知识点太多,基本上每一个拿出来都能写几篇文章,我在写文章的过程中只是做了简单的实现,我只是提供了一个思路,更多的细节还是需要自己去钻研的,文章内容也不少,辛苦,如果有其他的看法或者意见,欢迎指点,最后纸上得来终觉浅,绝知此事要躬行 javscript 中函数和对象的关

  • JavaScript函数式编程(Functional Programming)组合函数(Composition)用法分析

    本文实例讲述了JavaScript函数式编程(Functional Programming)组合函数(Composition)用法.分享给大家供大家参考,具体如下: 组合(Composition)函数,就是把两个或以上的函数组合到一块儿,整成一个新的函数.我找到了一个很好的例子,很好地解释了组合函数这个概念. 比如一个应用主要是记录一下日常的花销(expenses),应用里的数据看起来像这样: const expenses = [ { name: '租金', price: 3000, type:

  • javascript函数式编程基础

    目录 一.引言 二.什么是函数式编程 三.纯函数(函数式编程的基石,无副作用的函数) 四.函数柯里化 五.函数组合 六.声明式和命令式代码 七.Point Free 八.示例应用 九.总结 一.引言 函数式编程的历史已经很悠久了,但是最近几年却频繁的出现在大众的视野,很多不支持函数式编程的语言也在积极加入闭包,匿名函数等非常典型的函数式编程特性.大量的前端框架也标榜自己使用了函数式编程的特性,好像一旦跟函数式编程沾边,就很高大上一样,而且还有一些专门针对函数式编程的框架和库,比如:RxJS.cy

  • JS函数式编程之纯函数、柯里化以及组合函数

    目录 前言 纯函数 纯函数的概念 副作用 纯函数案例 柯里化 柯里化的概念 函数柯里化的过程 函数柯里化的特点及应用 自动柯里化函数的实现 组合函数 前言 函数式编程(Functional Programming),又称为泛函编程,是一种编程范式. 早在很久以前就提出了函数式编程这个概念了,而后面一直长期被面向对象编程所统治着,最近几年函数式编程又回到了大家的视野中,JavaScript是一门以函数为第一公民的语言,必定是支持这一种编程范式的. 下面就来谈谈JavaScript函数式编程中的核心

  • JavaScript中的函数式编程详解

    函数式编程 函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,它把计算看作是对数学函数的评估,避免了状态的变化和数据的可变,与函数式编程相对的是命令式编程.我们有这样一个需求,给数组的每个数字加一: // 数组每个数字加一, 命令式编程 let arr = [1, 2, 3, 4]; let newArr = []; for(let i = 0; i < arr.length; i++){ newArr.push(arr[i] + 1); } console.log(newArr)

  • Javascript函数式编程简单介绍

    几十年来,函数式编程一直是计算机科学狂热者的至爱,由于数学的纯洁性和谜一般的本质, 它被埋藏在计算机实验室,只有数据学家和有希望获得博士学位的人士使用.但是现在,它正经历一场复兴, 这要感谢一些现代语言比如Python,Julia,Ruby,Clojure以及--但不是最后一个--Javascript. 你是说Javascript?这个WEB脚本语言?没错! Javascript已经被证明是一项长期以来都没有消失的重要的技术.这主要是由于它扩展的一些框架和库而使其具有重生的能力, 比如backb

  • 《JavaScript函数式编程》读后感

    本文章记录本人在学习 函数式 中理解到的一些东西,加深记忆和并且整理记录下来,方便之后的复习. 在近期看到了<JavaScript函数式编程>这本书预售的时候就定了下来.主要目的是个人目前还是不理解什么是函数式编程.在自己学习的过程中一直听到身边的人说面向过程编程和面向对象编程,而函数式就非常少.为了自己不要落后于其他同学的脚步,故想以写笔记的方式去分享和记录自己阅读中所汲取的知识. js 和函数式编程 书中用了一句简单的话来回答了什么是函数式编程: 函数式编程通过使用函数来将值转换为抽象单元

  • 探究JavaScript函数式编程的乐趣

    编程范式 编程范式是一个由思考问题以及实现问题愿景的工具组成的框架.很多现代语言都是聚范式(或者说多重范式): 他们支持很多不同的编程范式,比如面向对象,元程序设计,泛函,面向过程,等等. 函数式编程范式 函数式编程就像一辆氢燃料驱动的汽车--先进的未来派,但是还没有被广泛推广.与命令式编程相反,他由一系列语句组成,这些语句用于更新执行时的全局状态.函数式编程将计算转化作表达式求值.这些表达式全由纯数学函数组成,这些数学函数都是一流的(可以被当做一般值来运用和处理),并且没有副作用. 函数式编程

  • javascript函数式编程实例分析

    本文实例讲述了javascript函数式编程.分享给大家供大家参考.具体分析如下: js像其他动态语言一样是可以写高阶函数的,所谓高阶函数是可以操作函数的函数.因为在js中函数是一个彻彻底底的对象,属于第一类公民,这提供了函数式编程的先决条件. 下面给出一个例子代码,出自一本js教程,功能是计算数组元素的平均值和标准差,先列出非函数式编程的一种写法: var data = [1,1,3,5,5]; var total = 0; for(var i = 0;i < data.length;i++)

  • JavaScript函数式编程(Functional Programming)声明式与命令式实例分析

    本文实例讲述了JavaScript函数式编程(Functional Programming)声明式与命令式.分享给大家供大家参考,具体如下: 函数式编程属于声明式编程(declarative programming)的范畴,经常跟声明式编程一块儿讨论的是命令式编程(imperative programming),因为它们是两种不太一样的风格. 命令式编程一般就是说清楚具体要怎么样得到一个结果:先这样做,再这样做,然后再这样,如果这样,就这样做 - 声明式编程就是声明(说明)一下你想得到的结果是什

  • JavaScript函数式编程(Functional Programming)箭头函数(Arrow functions)用法分析

    本文实例讲述了JavaScript函数式编程(Functional Programming)箭头函数(Arrow functions)用法.分享给大家供大家参考,具体如下: 箭头函数在 JavaScript 里面,是 ES6(ES2015)才加入进来的.因为函数里有个像箭头一样的符号:=>,所以叫箭头函数,英文经常也会称为 Fat arrow functions,胖乎乎的箭头函数.这种函数也称为 lambda 表达式.箭头函数不能当作构造函数使用. 语法 一个箭头函数看起来像这样: const

  • JavaScript函数式编程(Functional Programming)高阶函数(Higher order functions)用法分析

    本文实例讲述了JavaScript函数式编程(Functional Programming)高阶函数(Higher order functions)用法.分享给大家供大家参考,具体如下: 高阶函数(higher-order functions),就是返回其它函数的函数,或者使用其它函数作为它的参数的函数. 使用函数作为参数 因为函数本身就是一个值,所以可以让函数作为参数传递给其它的函数.JavaScript 有些函数就需要用到函数类型的参数,比如 Array.map. 比如我有一组数据: con

  • JavaScript函数式编程(Functional Programming)纯函数用法分析

    本文实例讲述了JavaScript函数式编程(Functional Programming)纯函数用法.分享给大家供大家参考,具体如下: 函数式编程鼓励我们多创建纯函数(pure functions),纯函数只依赖你交给它的东西,不使用任何函数以外的东西,也不会影响到函数以外的东西.跟纯函数对应的就是不纯函数(impure functions),也就是不纯函数可能会使用函数以外的东西,比如使用了一个全局变量.也可能会影响到函数以外的东西,比如改变了一个全局变量的值. 多使用纯属函数是因为它更可靠

随机推荐