百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

JavaScript函数式编程:提升你的代码品质和效率

yund56 2025-02-25 00:34 16 浏览

一、引言

函数式编程的历史已经很悠久了,但是最近几年却频繁的出现在大众的视野,很多不支持函数式编程的语言也在积极加入闭包,匿名函数等非常典型的函数式编程特性。大量的前端框架也标榜自己使用了函数式编程的特性,好像一旦跟函数式编程沾边,就很高大上一样,而且还有一些专门针对函数式编程的框架和库,比如:RxJS、cycleJS、ramdaJS、lodashJS、underscoreJS等。函数式编程变得越来越流行,掌握这种编程范式对书写高质量和易于维护的代码都大有好处,所以我们有必要掌握它。

二、什么是函数式编程

维基百科定义:

函数式编程(英语:functional programming),又称泛函编程,是一种编程范式,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。

三、纯函数(函数式编程的基石,无副作用的函数)



在初中数学里,函数f的定义是:对于输入x产生一个唯一输出y=f(x)。这便是纯函数。它符合两个条件:

1.此函数在相同的输入值时,总是产生相同的输出。函数的输出和当前运行环境的上下文状态无关。

2.此函数运行过程不影响运行环境,也就是无副作用(如触发事件、发起http请求、打印/log等)。

简单来说,也就是当一个函数的输出不受外部环境影响,同时也不影响外部环境时,该函数就是纯函数,也就是它只关注逻辑运算和数学运算,同一个输入总得到同一个输出。

javascript内置函数有不少纯函数,也有不少非纯函数。

纯函数:

Array.prototype.slice

Array.prototype.map

String.prototype.toUpperCase

非纯函数:

Math.random

Date.now

Array.ptototype.splice

这里我们以slice和splice方法举例:

let xs = [1,2,3,4,5];
// 纯的
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]

// 不纯的
xs.splice(0,3);
//=> [1,2,3]
xs.splice(0,3);
//=> [4,5]
xs.splice(0,3);
//=> []

我们看到调用数组的slice方法每次返回的结果完全相同,同时xs不会被改变,而调用splice方法每次返回值都不一样,同时xs变得面目全非。

这就是我们强调使用纯函数的原因,因为纯函数相对于非纯函数来说,在可缓存性、可移植性、可测试性以及并行计算方面都有着巨大的优势。

这里我们以可缓存性举例:

const squareNumber  = memoize(function(x){ return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // 从缓存中读取输入值为 4 的结果
//=> 16

那我们如何把一个非纯函数变纯呢?比如下面这个函数:

const minimum = 21;
const checkAge = age => {
  return age >= minimum;
};

这个函数的返回值依赖于可变变量minimum的值,它依赖于系统状态。在大型系统中,这种对于外部状态的依赖是造成系统复杂性大大提高的主要原因。

const checkAge = age => {
  const minimum = 21;
  return age >= minimum;
};

通过改造,我们把checkAge变成了一个纯函数,它不依赖于系统状态,但是minimum是通过硬编码的方式定义的,这限制了函数的扩展性,我们可以在后面的柯里化中看到如何优雅的使用函数式解决这个问题。所以把一个函数变纯的基本手段是不要依赖系统状态。

四、函数柯里化

curry 的概念很简单:将一个低阶函数转换为高阶函数的过程就叫柯里化。


用一个形象的比喻就是:


比如对于加法操作:var add = (x, y) => x + y,我们可以这样柯里化:

//es5写法
const add = function(x) {
  return function(y) {
    return x + y;
  };
};

//es6写法
const add = x => (y => x + y);

//试试看
const increment = add(1);
const addTen = add(10);

increment(2);  // 3

addTen(2);  // 12

对于加法这种极其简单的函数来说,柯里化并没有什么用。

还记得上面的checkAge函数吗?我们可以这样柯里化它:

const checkage = min => (age => age > min);
const checkage18 = checkage(18);
checkage18(20);
// =>true

这表明函数柯里化是一种“预加载”函数的能力,通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。从某种意义上来讲,这是一种对参数的缓存,是一种非常高效的编写函数的方法:

const curry = require('lodash').curry;

//柯里化两个纯函数
const match = curry((what, str) => str.match(what));
const filter = curry((f, ary) => ary.filter(f));

//判断字符串里有没有空格
const hasSpaces = match(/\s+/g);

hasSpaces("hello world");  // [ ' ' ]
hasSpaces("spaceless");  // null

const findSpaces = filter(hasSpaces);

findSpaces(["tori_spelling", "tori amos"]);  // ["tori amos"]

五、函数组合

假设我们需要对一个字符串做一些列操作,如下,为了方便举例,我们只对一个字符串做两种操作,我们定义了一个新函数shout,先调用toUpperCase,然后把返回值传给exclaim函数,这样做有什么不好呢?

不优雅,如果做得事情一多,嵌套的函数会非常深,而且代码是由内往外执行,不直观,我们希望代码从右往左执行,这个时候我们就得使用组合。

const toUpperCase = function(x) { return x.toUpperCase(); };
const exclaim = function(x) { return x + '!'; };

const shout = function(x){
  return exclaim(toUpperCase(x));
};

shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"

使用组合,我们可以这样定义我们的shout函数:

//定义compose
const compose = (...args) => x => args.reduceRight((value, item) => item(value), x);

const toUpperCase = function(x) { return x.toUpperCase(); };
const exclaim = function(x) { return x + '!'; };

const shout = compose(exclaim, toUpperCase);

shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"

代码从右往左执行,非常清晰明了,一目了然。

我们定义的compose像N面胶一样,可以将任意多个纯函数结合到一起。

这种灵活的组合可以让我们像拼积木一样来组合函数式的代码:

const head = function(x) { return x[0]; };
const reverse = reduce(function(acc, x){ return [x].concat(acc); }, []);
const last = compose(head, reverse);

last(['jumpkick', 'roundhouse', 'uppercut']);
//=> 'uppercut'

六、声明式和命令式代码

命令式代码:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。

声明式代码:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。

与命令式不同,声明式意味着我们要写表达式,而不是一步一步的指示。

以 SQL 为例,它就没有“先做这个,再做那个”的命令,有的只是一个指明我们想要从数据库取什么数据的表达式。至于如何取数据则是由它自己决定的。以后数据库升级也好,SQL 引擎优化也好,根本不需要更改查询语句。这是因为,有多种方式解析一个表达式并得到相同的结果。

这里为了方便理解,我们来看一个例子:

// 命令式
const makes = [];
for (var i = 0; i < cars.length; i++) {
  makes.push(cars[i].make);
}

// 声明式
const makes = cars.map(function(car){ return car.make; });

命令式的循环要求你必须先实例化一个数组,而且执行完这个实例化语句之后,解释器才继续执行后面的代码。然后再直接迭代 cars 列表,手动增加计数器,就像你开了一辆零部件全部暴露在外的汽车一样。这不是优雅的程序员应该做的。

声明式的写法是一个表达式,如何进行计数器迭代,返回的数组如何收集,这些细节都隐藏了起来。它指明的是做什么,而不是怎么做。除了更加清晰和简洁之外,map 函数还可以进一步独立优化,甚至用解释器内置的速度极快的 map 函数,这么一来我们主要的业务代码就无须改动了。

函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。

相反,不纯的不函数式的代码会产生副作用或者依赖外部系统环境,使用它们的时候总是要考虑这些不干净的副作用。在复杂的系统中,这对于程序员的心智来说是极大的负担。

七、Point Free

pointfree 模式指的是,永远不必说出你的数据。它的意思是说,函数无须提及将要操作的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协作起来非常有助于实现这种模式。

// 非 pointfree,因为提到了数据:word
const snakeCase = function (word) {
  return word.toLowerCase().replace(/\s+/ig, '_');
};

// pointfree
const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。当然,为了在一些函数中写出Point Free的风格,在代码的其它地方必然是不那么Point Free的,这个地方需要自己取舍。

八、示例应用

拥有了以上的知识,我们是时候该写一个示例应用了。

这里我们使用了 ramda ,没有用 lodash 或者其他类库。ramda 提供了 compose、curry 等很多函数。

我们的应用将做四件事:

1.根据特定搜索关键字构造 url
2.向 flickr 发送 api 请求
3.把返回的 json 转为 html 图片
4.把图片放到屏幕上

上面提到了两个不纯的动作,即从 flickr 的 api 获取数据和在屏幕上放置图片这两件事。我们先来定义这两个动作,这样就能隔离它们了。这里我们只是简单包装了一下jQuery的getJSON函数,把它变为一个 curry 函数,还有就是把参数位置也调换了下,我们把它们放在 Impure 命名空间下以用来隔离,这样我们就知道它们都是危险函数。

运用函数柯里化和函数组合的技巧,我们就可以创建一个函数式的实际应用了:


看看,多么美妙的声明式规范啊,只说做什么,不说怎么做。现在我们可以把每一行代码都视作一个等式,变量名所代表的属性就是等式的含义。

九、总结

我们已经见识到如何在一个小而不失真实的应用中运用新技能了,但是异常处理以及代码分支呢?如何让整个应用都是函数式的,而不仅仅是把破坏性的函数放到命名空间下?如何让应用更安全更富有表现力?我会在下一篇文章中介绍函数式编程的更加高阶一些的知识,例如Functor、Monad、Applicative等概念。

相关推荐

今日起,办理游戏版号这么做就行了!真的太方便了

  在“大众创业,万众创新”的浪潮下,我国很多创业者也看到了游戏的前景,准备在游戏行业分一杯羹。  但根据国家新闻出版广电总局颁布的《关于移动游戏出版服务管理的通知》,游戏需要通过国家新闻出版广电总局...

给大家推荐些好的c语言代码的网站

C语言,那就来推荐几个吧,部分含有C++:1、TheLinuxKernelArchives(kernel.org)Linux内核源码,仅限于C,但内核庞大,不太适合新手;2、redis(redi...

手游平台没有源码的三大危害

搭建一款属于自己的手游平台可以直接和游戏研发商对接游戏,既减少中介的差价,还能根据自己需求去选择游戏。对于玩家而言,手游平台给予了玩家更多的选择机会,对于运营者而言,借助平台可以更好地服务玩家,通过对...

游戏源代码开发时需要什么,需要哪些团队成员?

游戏由于她轻松娱乐,对战刺激,寓教于乐等特点,吸引住了一大批不一样年龄阶段的用户,例如喜爱竞技游戏的年轻群体,需要益智游戏的儿童等。游戏源代码是游戏构建的基础,尽管将开发时分成开发软件和游戏开发2个概...

育碧经典游戏《孤岛惊魂1》源代码遭泄露,玩家表示可以运行

IT之家7月3日消息,一份名为“FarCry1.34Complete”的游戏源代码已经出现在了互联网档案网站“Archive.org”上,并且在Reddit论坛和各种社交媒体上得到...

神秘网站倒数结束 令人一头雾水

还记得那个疑似小岛秀夫作品的《黑色猎犬》倒计时网站吗?现在该网站已经停止倒计时,仅剩一段话“这里原来有一个倒计时,现在没了”……点击这句话会跳转到国外网站Funhaus的一个莫名其妙的视频,然而评论的...

LOL源代码娜美免费领取地址 LOL源代码娜美领取活动网址分享

[海峡网]在英雄联盟中近日国服的服务器一直不稳定,繁出现卡顿和功能错误等问题,官方现在正在努力维护,为表歉意将免费赠送给玩家一款“源代码·娜美”的皮肤,那么这个皮肤要怎么领取呢,小编相信小伙伴们一定都...

个人网站集成js小游戏《圈小猫》教程及源码

今天在某网站浏览帖子的时候,发现帖子被删除了,然后弹出了404页面,页面上集成了一个小游戏,小游戏长什么样子呢?看下面这个图!第一步查看小游戏源码,发现这个小游戏完全是由JavaScript编写的,因...

Scratch创意编程-数学问答游戏

项目名称:数学问答游戏目标年龄群体:8-12岁项目简介:在这个Scratch创意编程项目中,学生们将扮演数学家,通过解答数学题目来挑战自己的数学技能。游戏中包含了加法、减法、乘法和除法等基本算术题,以...

少时不努力长大程序猿 酷比魔方AI百变编程套件体验测评

本文产品为厂家送测,坚持独立的评价观点是笔者创作的基本底线,绝不会因商品来源不同而有所偏颇,请各位放心。写在开始讲讲今天男主的故事这篇体验到的目标群体是跟我一样,家中有个在上小学二年级的小学生。首先...

孩子的scratch作品只能演示?教你把它三步变为电脑软件

随着少儿编程的发展,越来越多的家长和孩子开始投身其中。对于初学者来说,最好的编程工具就是Scratch,它是麻省理工学院的“终身幼儿园团队”开发的图形化编程工具,主要面对青少年开放。这是对孩子最好的编...

打地鼠小游戏制作教程

打地鼠这个小游戏貌似比我的年龄都要大,这次我们使用scratch3.0图形化编程软件来制作一款我们自己的“打地鼠”。我们先准备4样角色,分别是:地鼠角色、锤子角色、地洞角色、草地角色。地鼠→使用猫...

Scratch2.0接苹果小游戏讲义整理

Scratch2.0接苹果小游戏概貌见动图:这又是一款经典的Scratch小游戏,是孩子们学习Scratch编程软件的良好载体,不容错过。(一)玩法说明接到慢速的红苹果一个加1分;接到中速的红苹果一个...

少儿编程太难?原来可以闯关玩游戏啊

随着编程学习全球化的趋势,国内编程学习热潮日盛,越来越多的家长开始让孩子接触学习编程。然而我们都不了解这个少儿编程是到底是什么,近年来,许多家长开始给小孩报编程学习班。最小的从幼儿园开始就在学习...

如何在Scratch中创建一个两人赛艇游戏

本分步指南将教您如何使用Scratch程序创建划船游戏。完成对这个简单游戏的编程后,两条船将使用按键命令一起竞赛。步骤1.打开Scratch。2.删除名为“Sprite1”的猫。您可以通过右键单击它...