javascript函数式编程初探——什么是函数式编程?

(Javascript 函数式编程学习总结,如有不对,请多指教。)

什么是函数式编程?

函数式编程是是声明式的编程,其使用纯函数构建具有不变性的程序,将数据流的控制和操作抽象化,以消除副作用

什么是纯函数?什么叫数据流抽象化?什么是副作用?什么是声明式编程? 这定义太抽象了!这是中文吗!怎么每个字都认识,组合起来就看不懂了?

别急,我们一个个来看。

1. 什么是函数?

函数式编程中的“函数”指的是什么?

数学上来说是,函数是y(结果)和x(入参)存在一定的关系,例如:f(x) = 2x。 即,只要输入同样的元素,就会获取同样的结果(same inputs,same outputs),也被称为引用透明。可见,函数是输入元素与输出元素的映射关系。

🎯来看如下代码:

function addNumber(x = 0, y = 1, z = 2){
  var total = x + y + z
  console.log(total)
}
function extraNumbers(x = 2, ...args){
  return addNumber(x, ...args)
}

第一段代码没有 return 数据,只是一个命令程序(procedure),所以不是函数(注意,这里说的“函数”是函数式编程概念中的函数)。

第二段代码有 return,但是return的是一个命令程序,所以也不是函数。

这两段代码的输入和输出都没有构成关系。

🎯再来看如下代码:

var z = 1;
function addTwo(x, y){
  return x + y
}
function addAnother(x, y){
  return addTwo(x, y) + z
}
addAnother(2, 3)

上面这个 addAnother 是函数,因为它的入参和返回值存在固定的关系,并且只要输入同样的inputs,就能获得同样的output

但是需要注意的是,这个公共变量 z 虽然在当前代码中没有被更改,但并不代表在以后的代码中不会被更改。如果z的值变了,那么这个 addAnother 入参和返回值的关系也变了。如果出现这种情况,这个z就是我们说的副作用。

2. 什么是副作用?

简单来说,副作用就是,函数在执行过程中依赖于外部状态,或对外部环境造成了改变。

🎯来看如下代码:

let counter = 0
function increment(){
  return ++counter;
}

function getNewNum(num){
    return Math.random() + num
}

第一段代码,每次调用increment(),全局变量counter都会被影响,这是副作用。

第二段代码,每次调用getNum(),得到的数值都是不可预测的。它得到的值没有依赖我的输入,而是依赖于Math.random(),这是副作用。

副作用包括

改变全局变量、属性或数据结构;

I/O (文件读写,打印等);

数据库存储;

dom操作;

生成随机数(因为它的输出总是变化且不可预见);

生成时间戳(因为它的输出总是变化且不可预见);

...

3. 什么是纯函数?

知道了什么是副作用,我们就知道了什么是纯函数。

纯函数就是没有副作用的函数的调用

条件一:是函数;

条件二:没有副作用( 仅依赖提供的输入; 不会造成超出其作用域的变化;)

🎯还是这段代码:

var z = 1;
function addTwo(x, y){
  return x + y
}
function addAnother(x, y){
  return addTwo(x, y) + z
}
addAnother(2, 3)

上面这段代码是纯函数吗?

有人说不是,因为z是全局变量。它可以被重新赋值,会有副作用。那如果把z 改成const常量呢?或者直接在函数中用+1替换+z,这样就比上述代码更可靠了吗? 那如果我重新定义addTwo函数呢?毕竟除了变量,函数也是可以重新定义的。 有人说,那你这样就说不清楚了,没有什么是可靠的了。

没错。我们很难确保一个函数是或不是纯函数,但是我们可以说,这大概率是纯函数(记住这点很重要)。

上面的这个例子,虽然在函数内部使用全局变量z可能会造成副作用。但这段代码里,z没有被重新赋值,所以在这段代码里addAnother()可以称得上是纯函数但是!!阅读全代码才能知道一个函数是不是纯函数的函数称不上一个好的纯函数。 所以,不如把z变成入参。

🎯函数式编程思维下的addAnother() :

function addAnother(z){
  return function addTwo(x, y){
    return x + y + z
  }
}
addAnother(1)(2, 3)

这样就消除了全局变量这个副作用。

所以,避免副作用的一个方法是:在函数内应该仅依赖提供的输入,而非外部变量。

可能会有人说,我可以使用外部常量(const)来代替外部变量(let、var)。如果使用的字段明确规定了是const常量,那么是否会造成副作用也需要看具体情况。如果定义的是一个object,即使是const,object内部的值也是可以被改变并不报错的。

【提到const,这里顺带一提,const 的意思不是我们给字段一个final值,这个值不能再改变了。而是我们只能给字段赋值一次。 例如 const obj = { a: 1} 。 我不能给obj重新赋值 obj = {a: 1, b: 2} ,但是我可以改变obj的值: obj.b = 2。例如 const a = 1 。我不能再给a赋值其它了。同时1也改变不成其它数字,毕竟原始值只要改变就是重新给a赋值了。】

🎯再来看代码:

let obj = { name: 'John' }
function changeName(myObj){
    myObj.name = 'Linda'
    return myObj
}
let newObj = changeName(obj)

上面这段代码是不是纯函数呢?

入参和结果存在一定的关系,所以可以称得上是函数 —— 满足了条件一。

那么是否满足条件二 —— 没有副作用呢?

虽然这个函数中使用的是直接的入参,没有在函数其它地方使用外部变量。但是,它对obj造成了改变,产生了副作用。所以这不是一个纯函数。

引用类型作为入参的时候,它和外部变量指向的是同一个地址。所以在使用引用类型作为入参的时候一定要做好拷贝,不能对作用域外有所影响。例如,可以使用 object.freeze(obj)、object.assign((),obj)、拓展运算符等规避这种情况。

🎯终极一问来了,下面这段代码是不是纯函数?

function getId(obj){
  return obj.id;
}

有人会说,这妥妥的纯函数啊。 首先,入参是obj,获取的结果是obj的属性id,传入同样obj肯定能获取同样的结果。这明显建立了非常明确的数学关系,所以符合函数的定义。 其次,这个函数里没有对全局变量的操作,虽然入参obj是引用类型,但也没对它进行操作,只是返回了它的一个属性而已。所以没有产生副作用。

我们再来看一遍定义:纯函数就是没有副作用的函数的调用

为什么说是“函数的调用”?

比如我这样调用:

let obj = { id: '1' }
let myId = getId(obj)

是纯函数没错。

那么如果我这样调用呢:

function getId(obj){
  return obj.id;
}

getId({
  get id(){
    return Math.random()
  }
})

每次获取的id都是不同的。这显然不是纯函数。

所以,看一个函数是不是纯函数,比起看这个函数是怎么定义,我们更应该看它是怎么被调用的。 所以再次说明,我们很难确保一个函数是或不是纯函数,但是我们可以说,这大概率是纯函数或大概率不是

4. 声明式VS命令式

声明式编程(declarative)将程序的描述和求值分离,告诉计算机要做什么。让计算机去考虑具体的实现。

命令式编程(imperative)是具体告诉计算机如何执行某个任务。

🎯比如要把数组的值都乘2。

声明式编程(只对每个数组元素的行为进行关注,将循环交给系统处理):

let arr = [1,2,3];
let newArr = arr.map(item => item*2)

命令式编程(每次都要手动*2,push到新数组,再循环+1):

let arr = [1,2,3]
let newArr = []
for(let i = 0; i < arr.length; i++){
    newArr.push(arr[i]*2)
}

🎯再比如在js中对dom进行操作。

声明式编程:

export default function Header(){
  return (
      <header id='header'>
          <div className='nav-item'>nav</div>
      </header>
  )
}

命令式编程(一步步告诉计算机要如何实现dom):

let header = document.getElementById('header');
let div = document.createElement('div');
div.className = 'nav-item'
header.appendChild('div')

总结:

回到什么是函数式编程。

函数式编程,其实就是以声明式的纯函数为主的编程。

就像制作巧克力。

原本我们每个步骤都跟A提醒:A你先把温度调到xx,我们把巧克力熔化。熔化好了,我们把它放在模具里冷却,然后冷却好了,我们给它进行包装,然后来下一个巧克力...

现在我们让A专门处理熔化(不管是巧克力还是其它,只要给他物品和物品的熔点),让B专门凝固东西(给他熔化好的物品和模型),让C专门包装(给他凝固好物品和包装盒)。我只要给A待加工的巧克力,C就能给我包装好的新巧克力,不用去管ABC做了什么。

这整个流程就像是声明式函数。其中ABC就是纯函数,这里的巧克力就是数据流。

函数式编程优点

1 可拓展性,避免业务改变导致的不断重构代码来支持额外功能;

2 易模块化,避免更改一个文件导致其它文件受到影响;

3 重用性高,避免很多重复代码;

4 可测性,更易于编写单元测试;

5 易推理性,代码有bug更容易推理问题在哪。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值