快速入门函数式编程——以Javascript为例
函数式编程是在不改变状态和数据的情况下使用表达式和函数来编写程序的一种编程范式。
通过遵守这种范式,我们能够编写更清晰易懂、更能抵御bug的代码。这是通过避免使用流控制语句(for、while、break、continue、goto)来实现的,这些语句会使代码更难理解。此外,函数式编程要求我们编写纯的、确定性的函数,这些函数不太可能有bug。
在开始函数式编程之前,我们需要了解一下什么是纯函数和非纯函数。
纯函数就是对于固定的输入一定会有固定的输出。而且,它们对外界没有任何副作用。
const add = (a, b) => a + b;
这里,add就是一个纯函数。这是因为,对于固定值a和b,输出总是相同的。
const SECRET = 2019;
const getId = (a) => SECRET * a;
getId就不是一个纯函数。因为它使用了全局变量SECRET。如果SECRET发生变化,getId函数将为相同的输入返回不同的值。因此,它不是一个纯函数。
let id_count = 0;
const getId = () => ++id_count;
这也是一个非纯函数,原因如下:(1)它使用一个非局部变量来计算它的输出,(2)它修改了外部世界中的一个变量,产生了副作用。
getId()——>1
getId()——>2
getId()——>3
???每次调用结果都不一样?我们如果调试这段代码可能会五脸蒙B。
id_count的当前值是多少?哪些其他函数正在修改id_count?是否有其他函数依赖于id_count?
为了避免这些不确定性,使得代码更健壮,所以我们要使用纯函数。
函数式编程的三大原则(重要!!!)
1.不改变数据
2.使用纯函数:固定的输入总能得到固定的输出,而且妹有副作用
插播一下 副作用是啥玩意来着?
大家如果学过C语言的话肯定接触过
比如a=b= 50
从C语言的角度来讲,目的是对表达式求值(这个语句的结果是50)。
但是使用我们写这个赋值表达式根本目的就是使用其副作用(C语言本身目的——运算 之外的作用效果):将a和b变量的值设置为50。
3.使用表达式不使用语句
"表达式"(expression)是一个单纯的运算过程,总是有返回值;
"语句"(statement)是执行某种操作,没有返回值。
也就是说,每一步都是单纯的运算,而且都有返回值。
现在看完这三点可能不太懂,但是对照下面的例子相信你就能理解了。
JavaScript中的函数式编程
JavaScript有const关键字,它非常适合函数式编程,因为不会改变数据。
让我们来康康JavaScript提供的一些纯函数。
Filter
见名知义,就是对数组进行过滤。
array.filter(condition);
这个condition(过滤条件)是一个函数,它拿到数组的每一项然后决定是否留下它。(通过返回true)
const filterEven = x => x%2 === 0;
[1, 2, 3].filter(filterEven);
// [2]
注意,filterEven是一个纯函数。如果它是不纯的,那么整个filter函数也不能称之为纯函数了。
Map
map将数组的每一项传给一个函数,然后根据函数的返回值创建一个新数组。
array.map(mapper)
mapper(映射器)就是拿到数组的每一项然后返回一个值作为新数组的元素。
const double = x => 2 * x;
[1, 2, 3].map(double);
// [2, 4, 6]
Reduce
reduce直译是减少,为什么叫减少呢,是因为它把整个数组变成了一个值。
array.reduce(reducer);
reducer是一个函数,它根据已有的结果和数组中的下一项返回一个新值(新值继续作为已有的结果)。
const sum = (accumulatedSum, arrayItem) => accumulatedSum + arrayItem
[1, 2, 3].reduce(sum);
//
我们可以发现这个reduce完成的就是数组的累加,那过程是怎样的呢?
第一次已有的结果默认为第一个元素,因此第一次sum函数的参数是(1,2),返回值为3,
3又作为accumulatedSum,第二次sum函数调用即为(3,3),返回值为6.
concat
concat在原有数组基础上拼接新的元素以创建新数组。它不同于push(),因为push()会改变数据(向原来的数组添加元素),从而成为非纯函数。
[1, 2].concat([3, 4])
// [1, 2, 3, 4]
用ES6的话,可以用解构赋值运算符...实现。
[1, 2, ...[3, 4]]
简单吧,记住这个语法哦,很常用。
Object.assign
Object.assign会拷贝给定对象的值给新对象。由于函数式编程是基于不可变数据的,所以我们使用它,来根据现有对象创建新对象。
const obj = {a : 2};
const newObj = Object.assign({}, obj);
newObj.a = 3;
obj.a;
//
简写方法:(ES6的解构赋值又来了)
const newObj = {...obj};
创建我们自己的纯函数
我们也可以创建自己的纯函数。让我们来做一个复制一个字符串n次的例子。
const duplicate = (str, n) =>
n < 1 ? '' : str + duplicate(str, n-1);
duplicate('bokeyuan!', 3)
// bokeyuan!bokeyuan!bokeyuan!
高阶函数(Higher-order Functions, 后文简称HOF)
HOF是接受函数作为参数或返回函数的函数。通常,它们用于给函数添加功能(类似Java的AOP或Python的装饰器,有学过的小伙伴可以类比一下)。
const withLog = (fn) => {
return (...args) => {
console.log(`calling ${fn.name}`);
return fn(...args);
};
};
我们创建了一个withLog HOF,它接受一个函数并返回一个函数,该函数在传入的函数运行之前会记录一条消息。
const add = (a, b) => a + b;
const addWithLogging = withLog(add);
addWithLogging(3, 4);
// calling add
//
withLog HOF也可以与其他函数一起使用,它不会产生任何冲突,也不会编写额外的代码。这就是HOF的美丽之处。
const addWithLogging = withLog(add);
const hype = s => s + '!!!';
const hypeWithLogging = withLog(hype);
hypeWithLogging('Sale');
// calling hype
// Sale!!!
也可以简单点直接调用HOF的返回值。
withLog(hype)('Sale');
// calling hype
// Sale!!!
柯里化(Currying)
柯里化意味着将一个多参数的函数分解成高阶函数。看完这句话目测大家内心又是崩溃的。
我们还拿add函数来举栗子。
const add = (a, b) => a + b;
当我们要把它柯里化时,我们改写它,将这一个函数的多个参数分布到多个函数,如下所示。
const add = a => {
return b => {
return a + b;
};
};
add(3)(4);
//
柯里化的好处就是记忆性。我们可以在函数调用中记忆某些参数,以便以后可以重用它们,而不需要重新计算或者写冗余代码。
举个栗子来看一下
add(4, getOffsetNumber());
add(6, getOffsetNumber());
add(10, getOffsetNumber());
我们假设调用getOffsetNumer()这个函数非常消耗资源,因此上面的代码,第一,造成了耗资源操作的多次重新计算,第二,也产生了冗余代码,一样的调用写三遍。
而如果我们采用柯里化之后的add函数,偶们来看一下。
const addOffset = add(getOffsetNumber());
addOffset(4);
addOffset(6);
这个好处就不用我说,不言自明了吧。
拓展
我们还可以进一步优化柯里化函数的写法,使其看起来更简洁。
这是因为每一层的函数调用都是一个单行返回语句。因此,我们可以在ES6中使用箭头函数重构它,如下所示。
const add = a => b => a + b;
(这块看不懂没关系,语法不重要,重在理解柯里化的意义)
复合函数(Composition)
高中数学俺们都学过,复合函数函数套函数,就是把一个函数的输出传递给另一个函数作为输入,从而产生一个组合输出。
那我们讲一下复合函数在函数式编程里面的应用。我们先准备点素材。
第一个函数叫 range,接收俩参数a和b,产生一个数组,里面内容是从a到b的每个数。
const range = (a, b) => a > b ? [] : [a, ...range(a+1, b)];
然后我们再来一个函数叫multiply,接收一个数组,然后把里面所有元素累乘起来。
const multiply = arr => arr.reduce((p, a) => p * a);
然后我们利用这俩玩意去实现阶乘。
const factorial = n => multiply(range(1, n));
factorial(5);
// 120
factorial(6);
// 720
factorial这个函数就类似于f(x) = g(h(x)),满足复合函数的性质。
总结
俺们学习了纯函数和非纯函数、函数式编程、新的JavaScript特性以及函数式编程中的一些关键概念。
我们希望这篇文章能激起你对函数式编程的兴趣,然后泥,多在代码中尝试运用它。
函数式编程是一种经过充分研究的健壮的计算机程序编写范例。随着ES6的引入,JavaScript提供了比以前更好的函数式编程体验。
关键概念扫盲
1.函数式编程好处都有啥?谁说对了就给他!美国,圣地亚哥!
函数式编程确保了代码中更简单的流程控制,并避免了变量和状态更改可能会给代码带来的一些不确定性,给我们造成惊喜,哦不,惊吓。函数式编程可以帮助我们避免bug并轻松理解代码。
2.ES6是啥玩意?
ES6全称ECMAScript 6是一个新版本的JavaScript,介4里妹有用过的船新版本,挤需体验三番钟,里奏会....ES6包含了许多贼酷炫的新特性像箭头函数, 常量, 害有解构赋值运算符等等等。
快速入门函数式编程——以Javascript为例的更多相关文章
- JavaScript快速入门(四)——JavaScript函数
函数声明 之前说的三种函数声明中(参见JavaScript快速入门(二)——JavaScript变量),使用Function构造函数的声明方法比较少见,我们暂时不提.function func() { ...
- 从 Racket 入门函数式编程
一直想学学LISP,今天总算开了个头.如今学习LISP不是为了立就可以以用于实际项目的应用,而是为了学习一下函数式的思维方式,可以更加深入的了解计算的本质,可以更好的用C++, Java, Pytho ...
- 用函数式编程对JavaScript进行断舍离
译者按: 当从业20的JavaScript老司机学会函数式编程时,他扔掉了90%的特性,也不用面向对象了,最后发现了真爱啊!!! 原文: How I rediscovered my love for ...
- html5快速入门(四)—— JavaScript
前言: 1.HTML5的发展非常迅速,可以说已经是前端开发人员的标配,在电商类型的APP中更是运用广泛,这个系列的文章是本人自己整理,尽量将开发中不常用到的剔除,将经常使用的拿出来,使需要的朋友能够真 ...
- 【python】 入门 - 函数式编程
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数 http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8b ...
- Scala编程入门---函数式编程之集合操作
集合的函数式编程: 实战常用: //map案例实战:为List中的每个元素都添加一个前缀. List("leo","Jen","peter" ...
- Python学习总结之五 -- 入门函数式编程
函数式编程 最近对Python的学习有些怠慢,最近的学习态度和学习效率确实很不好,目前这种病况正在好转. 今天,我把之前学过的Python中函数式编程简单总结一下,分享给大家,也欢迎并感谢大家提出意见 ...
- JavaScript快速入门(二)——JavaScript变量
变量声明 JavaScript的变量声明分为显式声明跟隐式声明. 显式声明 即带var关键字声明,例如 var example = example; 要注意JavaScript里面声明的关键字只有fu ...
- JavaScript快速入门(一)——JavaScript概览
JavaScript是什么? JavaScript的诞生 在1995年前后,当时世界上的主流带宽为28.8Kbps,现在世界平均下载带宽为21.9Mbps(数据来源于http://www.netind ...
随机推荐
- 数据结构(四十七)归并排序(O(nlogn))
一.归并排序的定义 归并排序(Merging Sort)就是利用归并的思想实现的排序方法.它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n ...
- accesskey附上一些实例
HTML accesskey属性与web自定义键盘快捷访问 本文地址:http://www.zhangxinxu.com/wordpress/?p=6142 可能很多小伙伴都不知道,我们只要在HTML ...
- 基于jquery,php实现AJAX长轮询(LongPoll),类似推送机制
HTTP是无状态.单向的协议,用户只能够通过客服端向服务器发送请求并由服务器处理发回一个响应.若要实现聊天室.WEBQQ.在线客服.邮箱等这些即时通讯的应用,就要用到“ 服务器推送技术(Comet)” ...
- Go netpoll I/O 多路复用构建原生网络模型之源码深度解析
导言 Go 基于 I/O multiplexing 和 goroutine 构建了一个简洁而高性能的原生网络模型(基于 Go 的I/O 多路复用 netpoll),提供了 goroutine-per- ...
- [考试反思]1002csp-s模拟测试56:凌乱
放假回来状态回升??(玩够了-但是稍困) T1打的不完全对,但是过掉了.很快的想到了二分吧喇叭啦.. 然后T2也挺快想出来了但是挂细节没发现,考试快结束的时候才发现出锅了. 改了过来是正解,但是出题人 ...
- Java基础系列4:抽象类与接口的前世今生
该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架. 1.抽象类: 当编写 ...
- 国际C语言混乱代码大赛优胜作品详解之“A clock in one line”
原文链接:https://blog.csdn.net/herorenme/article/details/8864351 摘要:IOCCC,即国际混乱C语言代码大赛是一项著名的国际编程赛事迄今已举办2 ...
- Linux Shell | 解析xml节点
01 xml文件 # user.xml <user> <name>Toy</name> <sex>man</sex> <room/&g ...
- 一道笔试题(vue,react)
题目: vue代码 <template> <div class="colculate"> <div> <select v-model=&q ...
- drf
跨域同源 django做跨域同源 需要把csrf去掉 跨站请求伪造 同源 同源机制:域名.协议.端口号相同的同源 简单请求 不写头部请求 跨域会拦截报错缺少请求信息 (1) 请求方法是以下三种方法之一 ...