JavaScript 函数式编程

昨天进行了人人网的面试,其中在二面中面试官问了“什么是函数式编程”这个问题,我只是听说过这个东西,但是让我讲就讲不出来,最后结束的时候给我的评价就是

js基础还行, 但是深入理解不够, 还有一个最关键的就是知识点看到了最好搞懂搞透彻, 不可以自己推测, 但是可以自己做demo实验自己的推测

经过这次面试后也发现和认识了自己的不足的地方,在这里也总结记录一下自己学习的JS的函数式编程

正文

一、什么是函数式编程?

我的理解是函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数可以看成没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的。.函数式编程不是用函数来编程,也不是传统的面向过程编程。主旨在于将复杂的函数符合成简单的函数(计算理论,或者递归论,或者拉姆达演算)。运算过程尽量写成一系列嵌套的函数调用。函数式编程我觉得更贴近于数学思想或者说计算。

二、函数式编程的特性

1.函数是"第一等公民"

所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
举例来说,下面代码中的print变量就是一个函数,可以作为另一个函数的参数。

var print = function(i){ console.log(i);};
  [1,2,3].forEach(print);
2.只用”表达式",不用"语句"

“表达式”(expression)是一个单纯的运算过程,总是有返回值;“语句”(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以就被排斥在外。
当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

3.没有"副作用"

所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

4.不修改状态

在函数式编程中,我们通常理解的变量在函数式编程中也被函数代替了:在函数式编程中变量仅仅代表某个表达式。这里所说的’变量’是不能被修改的。所有的变量只能被赋一次初值。(下面会详细介绍变量被函数代替)

5.引用透明性

函数程序通常还加强引用透明性,即如果提供同样的输入,那么函数总是返回同样的结果。就是说,表达式的值不依赖于可以改变值的全局状态。

三、函数式编程常用核心概念和技术

1.纯函数

什么是纯函数呢?

对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态的函数,叫做纯函数。
举个栗子:

var xs = [1,2,3,4,5];// Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
xs.slice(0,3);
xs.slice(0,3);
xs.splice(0,3);// Array.splice会对原array造成影响,所以不纯
xs.splice(0,3);
2.函数柯里化(高级函数)

高阶函数,就是把函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。
我的理解:传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

看一个例子:

// 柯里化之前
function add(x, y) {
    return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
    return function (x) {
        return x + y;
    };
}
addX(2)(1) // 3

柯里化可以理解为是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法。

3.函数组合

为了解决函数嵌套过深,洋葱代码:h(g(f(x))),我们需要用到“函数组合”,我们可以
来用柯里化来改他,让多个函数像拼积木一样。

函数编程的函数组合:两个纯函数组合之后返回了一个新函数,举个例子:

const compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

const toUpperCase = function(x) {
return x.toUpperCase();
};

const exclaim = function(x) {
return x + "!";
};
 
const shout = compose( exclaim , toUpperCase );
 
console.log(shout("hello world")); //HELLO WORLD!

可以对他简化一下:

const compose=(f,g)=>(x=>f(g(x)))
const toUpperCase=x=>x.toUpperCase()
const exclaim=x=>x+'!'
const shout=compose(exclaim,toUpperCase)
console.log(shout('hello world'));  //HELLO WORLD!

compose中的参数的顺序是随意的,这就类似于乘法中的交换律 xy=yx,
所以函数式编程贴近于数学计算。
在这里插入图片描述

4.递归与尾递归

指函数内部的最后一个动作是函数调用。 该调用的返回值, 直接返回给函数。 函数调用自身, 称为递归。 如果尾调用自身, 就称为尾递归。 递归需要保存大量的调用记录, 很容易发生栈溢出错误, 如果使用尾递归优化, 将递归变为循环, 那么只需要保存一个调用记录, 这样就不会发生栈溢出错误了。通俗点说,尾递归最后一步需要调用自身,并且之后不能有其他额外操作。
举个例子:我的理解为满足递归条件相当于捕鱼时先撒网,等达到递归边界即捕到鱼时,再返回自身及收网;尾递归就为当捕到鱼时鱼已经到你手里了,具体举个例子:

// 不是尾递归,无法优化
function factorial(n) {
    if (n === 1) return 1;
    return n * factorial(n - 1);
}   //这里的递归当到达边界条件时会把值返回给上一个调用它的函数
 
function factorial(n, total) {
    if (n === 1) return total;
    return factorial(n - 1, n * total);
} //ES6强制使用尾递归
// 这里的尾递归就是当到底边界条件时直接将结果 return 出来

我们看一下递归和尾递归执行过程:
递归:

function sum(n) {
    if (n === 1) return 1;
    return n + sum(n - 1);
}
sum(5)
(5 + sum(4))
(5 + (4 + sum(3)))
(5 + (4 + (3 + sum(2))))
(5 + (4 + (3 + (2 + sum(1)))))
(5 + (4 + (3 + (2 + 1))))
(5 + (4 + (3 + 3)))
(5 + (4 + 6))
(5 + 10)
15      //递归非常消耗内存,因为需要同时保存很多的调用帧,这样,就很容易发生“栈溢出”

尾递归:

function sum(x, total) {
    if (x === 1) {
        return x + total;
    }
    return sum(x - 1, x + total);
}
sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14)
15

整个计算过程是线性的,调用一次sum(x, total)后,会进入下一个栈,相关的数据信息和跟随进入,不再放在堆栈上保存。当计算完最后的值之后,直接返回到最上层的sum(5,0)。这能有效的防止堆栈溢出。 在ECMAScript 6,我们将迎来尾递归优化,通过尾递归优化,javascript代码在解释成机器码的时候,将会向while看起,也就是说,同时拥有数学表达能力和while的效能。

5.惰性计算(求值)

在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。延迟的计算使你可以编写可能潜在地生成无穷输出的函数。因为不会计算多于程序的其余部分所需要的值,所以不需要担心由无穷计算所导致的 out-of-memory 错误。简单的来说就是:

按需索取,能不多做事,绝不多做

这里有自己实现的简单的惰性求值
用JavaScript写一个惰性求值的最简实现
惰性求值官方文档
官方文档

四、函数式编程总结

1.函数式编程中的每个符号都是 const 的,于是没有什么函数会有副作用。谁也不能在运行时修改任何东西,也没有函数可以修改在它的作用域之外修改什么值给其他函数继续使用。这意味着决定函数执行结果的唯一因素就是它的返回值,而影响其返回值的唯一因素就是它的参数。
2.函数式编程不需要考虑”死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。
3.函数式编程中所有状态就是传给函数的参数,而参数都是储存在栈上的。这一特性让软件的热部署变得十分简单。只要比较一下正在运行的代码以及新的代码获得一个diff,然后用这个diff更新现有的代码,新代码的热部署就完成了。

参考文献

JavaScript函数式编程指南

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
大学生参加学科竞赛有着诸多好处,不仅有助于个人综合素质的提升,还能为未来职业发展奠定良好基础。以下是一些分析: 首先,学科竞赛是提高专业知识和技能水平的有效途径。通过参与竞赛,学生不仅能够深入学习相关专业知识,还能够接触到最新的科研成果和技术发展趋势。这有助于拓展学生的学科视野,使其对专业领域有更深刻的理解。在竞赛过程中,学生通常需要解决实际问题,这锻炼了他们独立思考和解决问题的能力。 其次,学科竞赛培养了学生的团队合作精神。许多竞赛项目需要团队协作来完成,这促使学生学会有效地与他人合作、协调分工。在团队合作中,学生们能够学到如何有效沟通、共同制定目标和分工合作,这对于日后进入职场具有重要意义。 此外,学科竞赛是提高学生综合能力的一种途径。竞赛项目通常会涉及到理论知识、实际操作和创新思维等多个方面,要求参赛者具备全面的素质。在竞赛过程中,学生不仅需要展现自己的专业知识,还需要具备创新意识和解决问题的能力。这种全面的综合能力培养对于未来从事各类职业都具有积极作用。 此外,学科竞赛可以为学生提供展示自我、树立信心的机会。通过比赛的舞台,学生有机会展现自己在专业领域的优势,得到他人的认可和赞誉。这对于培养学生的自信心和自我价值感非常重要,有助于他们更加积极主动地投入学习和未来的职业生涯。 最后,学科竞赛对于个人职业发展具有积极的助推作用。在竞赛中脱颖而出的学生通常能够引起企业、研究机构等用人单位的关注。获得竞赛奖项不仅可以作为个人履历的亮点,还可以为进入理想的工作岗位提供有力的支持。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值