函数式编程入门实践(一)

在文章之前,先和大家讲一下对于函数式编程(Functional Programming, aka. FP)的理解(下文我会用FP指代函数式编程):

  1. FP需要保证函数都是纯净的,既不依赖外部的状态变量,也不产生副作用。基于此前提下,那么纯函数的组合与调用,在时间顺序上就不会产生依赖,改变多个函数的调用顺序也不必担心产生问题,因此也会消灭许多潜在的bug。
  2. 函数必须有输入输出。如果一个函数缺乏输入或输出,那么它其实是一段处理程序procedure而已。
  3. 函数尽可能的保持功能的单一,如果一个函数做了多件事情,那么它理论上应当被拆分为多个函数。
  4. FP的意义之一就是,在适当的时机使用声明式编程,抽象了程序流的控制与表现,从理解和维护的角度上会胜于命令式编程。
  5. FP是一种范式,但并不意味这和OOP(面向对象编程)冲突,两者当然是可以和谐共存的。个人认为 React 其实就是一个很好的栗子~
  6. Javascript的函数一等公民以及闭包的特性,决定了Javascript的确是适合施展FP的舞台

理解闭包

闭包对于 Javascript 来说,当然十分重要。然而对于函数式编程来说,这更加是必不可少的,必须掌握的概念,闭包的定义如下:

Closure is when a function remembers and accesses variables from outside of its own scope, even when that function is executed in a different scope.

相信大部分同学都对闭包有不错的理解,但是由于对FP的学习十分重要。接下来我还是会啰嗦的带大家过一遍。闭包就是能够读取其他函数内部变量的函数

简单示例如下

// Closure demo
function cube(x) {
 let z = 1;
 return function larger(y) {
 return x * y * z++;
 };
}

const makeCube = cube(10);
console.log(makeCube(5)); // 50
console.log(makeCube(5)); // 100

那么有没有想过在函数makeCube,或者也可以说是函数larger是怎么记住原本不属于自己作用域的变量x和z的呢?在控制台查看makeCube.prototype,点开会发现原来是有个[[Scopes]]这个内置属性里的Closure(cube)记住了函数larger返回时记住的变量x和z。如果多嵌套几层函数,也会发现多几个Closure(name)在[[Scopes]]的Scopes[]数组里,按序查找变量。

再看下图测试代码:

function cube(x) {
 return function wrapper(y) {
 let z = 1;
 return function larger() {
  return x * y * z++;
 };
 }
}

const makeCubeY = cube(10);
const makeCube = makeCubeY(5);
const $__VAR1__ = '1. This var is just for test.';
let $__VAR2__ = '2. This var is just for test.';
var $__VAR3__ = '3. This var is just for test.';
console.log(makeCubeY.prototype, makeCube.prototype);
console.log(makeCube()); // 50
console.log(makeCube()); // 100

打印makeCubeY.prototype:

打印makeCube.prototype:

通过这几个实验可以从另一个角度去理解Javascript中闭包,一个闭包是怎么去查找不是自己作用域的变量呢?makeCube函数分别从[[Scopes]]中的Closure(wrapper)里找到变量y、z,Closure(cube)里找到变量x。至于全局let、const声明的变量放在了Script里,全局var声明的变量放在了Global里。

在学习FP前,理解闭包是尤为重要的~ 因为事实上大量的FP工具函数都使用了闭包这个特性。

工具函数

unary

const unary = fn => arg => fn(arg);

一元函数,应用于当只想在某个函数上传递一个参数情况下使用。尝试考虑以下场景:

console.log(['1', '2', '3'].map(parseInt)); // [1, NaN, NaN]
console.log(['1', '2', '3'].map(unary(parseInt))); // [1, 2, 3]

parseInt(string, radix)接收两个参数,而map函数中接收的回调函数callback(currentValue[, index[, array]]),第二个参数是index,此时如果parseInt的使用就是错误的。当然除了Array.prototype.map,大量内置的数组方法中的回调函数中都不止传递一个参数,如果存在适用的只需要第一个参数的场景,unary函数就发挥了它的价值,无需修改函数,优雅简洁地就接入了。(对于unary函数,fn就是闭包记忆的变量数据)

identity

const identity = v => v;

有同学会看到identity函数会觉得莫名其妙?是干嘛的?我第一眼看到也很迷惑?但是考虑以下场景:

console.log([false, 1, 2, 0, '5', true].filter( identity )); // [1, 2, "5", true]
console.log([false, 0].some( identity )); // false
console.log([-2, 1, '3'].every( identity )); // true

怎么样?眼前一亮吧,没想到identity函数原来深藏不露,事实上虽然identity返回了原值,但是在这些函数中Javascript会对返回的值进行类型装换,变成了布尔值。比如filter函数。我们可以看MDN定义filter描述如下(看标粗的那一句)。

filter() calls a provided callback function once for each element in an array, and constructs a new array of all the values for which callback returns a value that coerces to true.

constant

const constant = v => () => v;

同样,这个函数...乍一看,也不知道具体有什么用。但是考虑场景如下:

onst p1 = new Promise((resolve, reject) => {
 setTimeout(() => {
 resolve('Hello!');
 }, 200);
});
p1.then(() => 'Hi').then(console.log); // Hi!
p1.then(constant('Hi')).then(console.log); // Hi!
p1.then('Hi').then(console.log); // Hello!

由于Promise.prototype.then只接受函数,如果我仅仅只需要传递一个值时,那么constant便会提供这种便利。当然这个并没有什么功能上的提升,但是的确提高了可阅读性,也是函数式编程的一个优点。

spreadArgs & gatherArgs

const spreadArgs = fn => argsArr => fn( ...argsArr );
const gatherArgs = fn => (...argsArr) => fn( argsArr );

嗯这两个函数见名知义。分别用于展开一个函数的所有参数和收集一个函数所有参数,这两个函数明显对立,那么它们的应用场景又是什么呢?

spreadArgs函数示例如下:

function cube(x, y, z) {
 return x * y * z;
}

function make(fn, points) {
 return fn(points);
}

console.log(make(cube, [3, 4, 5])); // NaN
console.log(make(spreadArgs(cube), [3, 4, 5])); // 60

gatherArgs函数示例如下:

function combineFirstTwo([v1, v2]) {
 return v1 + v2;
}

console.log([1, 2, 3, 4, 5].reduce(combineFirstTwo)); // Uncaught TypeError
console.log([1, 2, 3, 4, 5].reduce(gatherArgs(combineFirstTwo))); // 15

看完以上代码,简单的两个工具函数,轻易的做到了对一个函数的转换,从而使其适用于另一个场景。如果从此应该可以瞥见函数式编程的一点点魅力,那么下面的两个函数将给大家带来更多的惊喜。

partial & curry

const partial = (fn, ...presetArgs) => (...laterArgs) =>
 fn(...presetArgs, ...laterArgs);

const curry = (fn, arity = fn.length, nextCurried) =>
 (nextCurried = prevArgs => nextArg => {
 const args = [...prevArgs, nextArg];

 if (args.length >= arity) {
  return fn(...args);
 } else {
  return nextCurried(args);
 }
 })([]);

相信大家对函数柯里化应该或多或少有点了解。维基百科定义:

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

当然得益于闭包的强大威力,柯里化这个武器得以诞生于Javascript世界。请大家先精读以上关于partiel、curry函数的代码。

喝一杯咖啡~

先模拟一个ajax函数如下:

function ajax(url, params, callback) {
 setTimeout(() => {
 callback(
  `GET ${url} \nparams: ${params} \ndata: Hello! ${params} `
 );
 });
}

考虑partial使用场景如下:

const fetchPerson = partial( ajax, "http://some.api/person" );

fetchPerson('Teddy Bear', console.log);
/*
GET http://some.api/person
params: Teddy Bear
data: Hello! Teddy Bear
*/

考虑curry使用场景如下:

const fetchPerson = curry(ajax)('http://some.api/person');
const fetchUncleBarney = fetchPerson('Uncle Barney');

fetchUncleBarney(console.log);
/*
GET http://some.api/person
params: Uncle Barney
data: Hello! Uncle Barney
*/

partial和curry函数功能相似,但又有具体的不同应用场景,但总体来说curry会比partial更自动化一点。
但是!相信看完示例的同学又会有一连串问号?为什么好好地参数不一次性传入,而非要分开多次传入这么麻烦?原因如下:

  1. 最首要的原因是partial和curry函数都允许我们通过参数控制将一个函数的调用在时间和空间上分开了。传统函数需要一次性将参数凑齐才能调用,但是有时候我们可以提前预置部分参数,在最终需要触发此函数时,才将剩余参数传入。这时候partial和curry就会变得十分有用。
  2. partial和curry的存在让函数组合(compose)会更加便利。(函数组合也计划之后和大家分享,这里就不详细说了)。
  3. 当然最重要是也提升了可阅读性!一开始可能不这么以为,但是如果你实践操作感受之后,也许会改观。

P.S. 关于函数式编程的实践,大家可以使用lodash/fp模块进行入门实践。

一些思考
因为我也是函数式编程的初学者,如有不正确的地方,欢迎大家纠正~

接下来还是会继续整理FP的学习资料,学习实践,连载一些我对于函数式编程的学习与思考,希望和大家一起进步~

谢谢大家(●´∀`●)~

以上所述是小编给大家介绍的Javascript函数式编程详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • javascript 函数式编程

    JavaScript的函数式编程的对象本质: function a() {     var x="sth";       return b(){         //do with x;      } } var c = a(); 等价于 function a() {         this.x = "dosth";         this.b = function(){               //do with this.x         } } va

  • JavaScript与函数式编程解释

    作者:月影 牢记:函数式编程不是用函数来编程!!!23.4函数式编程  23.4.1 什么是函数式编程 什么是函数式编程?如果你这么直白地询问,会发现它竟是一个不太容易解释的概念.许多在程序设计领域有着多年经验的老手,也无法很明白地说清楚函数式编程到底在研究些什么.函数式编程对于熟悉过程式程序设计的程序员来说的确是一个陌生的领域,闭包(closure),延续(continuation),和柯里化(currying)这些概念看起来是这么的陌生,同我们熟悉的if.else.while没有任何的相似之

  • Javascript函数式编程简单介绍

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

  • JavaScript 函数式编程实践(来自IBM)第1/3页

    函数式编程简介 说到函数式编程,人们的第一印象往往是其学院派,晦涩难懂,大概只有那些蓬头散发,不修边幅,甚至有些神经质的大学教授们才会用的编程方式.这可能在历史上的某个阶段的确如此,但是近来函数式编程已经在实际应用中发挥着巨大作用了,而更有越来越多的语言不断的加入诸如 闭包,匿名函数等的支持,从某种程度上来讲,函数式编程正在逐步"同化"命令式编程. 函数式编程思想的源头可以追溯到 20 世纪 30 年代,数学家阿隆左 . 丘奇在进行一项关于问题的可计算性的研究,也就是后来的 lambd

  • javascript函数式编程程序员的工具集

    如果你仔细看了到目前为止出现过的示例代码,你会发现这里面的一些方法不太熟悉. 它们是map().filter()和reduce()函数,它们对任何语言的函数式编程都至关重要. 它们可以让你不必使用循环和语句,写出更简洁的代码. map().filter()和reduce()函数组成了函数式程序员工具集的核心部分,这个工具集包括一系列纯的. 高阶的函数,它们是函数式方法的主力.实际上,它们是纯函数和高阶函数的典型,它们以一个函数为输入, 返回一个输出结果,并且不产生副作用. 然而它们是浏览器中EC

  • 深入探讨javascript函数式编程

    有时,优雅的实现是一个函数.不是方法.不是类.不是框架.只是函数. - John Carmack,游戏<毁灭战士>首席程序员 函数式编程全都是关于如何把一个问题分解为一系列函数的.通常,函数会链在一起,互相嵌套, 来回传递,被视作头等公民.如果你使用过诸如jQuery或Node.js这样的框架,你应该用过一些这样的技术, 只不过你没有意识到. 我们从Javascript的一个小尴尬开始. 假设我们需要一个值的列表,这些值会赋值给普通的对象.这些对象可能包含任何东西:数据.HTML对象等等. v

  • 用函数式编程技术编写优美的 JavaScript_ibm

    因为函数式编程采用了完全不同的组织程序的方式,所以那些习惯于采用命令式范例的程序员可能会发现函数式编程有点难学.在这篇文章中,您将了解一些关于如何采用函数式风格,用 JavaScript 编写良好的.优美的代码的示例.我将讨论: 函数式编程概念,包括匿名函数.调用函数的不同方法,以及将函数作为参数传递给其他函数的方式. 函数式概念的运用,采用的示例包括:扩展数组排序:动态 HTML 生成的优美代码:系列函数的应用. 函数式编程概念 请告诉每个人.请把这个提交给: Digg Slashdot 在那

  • javascript函数式编程实例分析

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

  • 理解javascript函数式编程中的闭包(closure)

    闭包(closure)是函数式编程中的概念,出现于 20 世纪 60 年代,最早实现闭包的语言是 Scheme,它是 LISP 的一种方言.之后闭包特性被其他语言广泛吸纳. 闭包的严格定义是"由函数(环境)及其封闭的自由变量组成的集合体."这个定义对于大家来说有些晦涩难懂,所以让我们先通过例子和不那么严格的解释来说明什么是闭包,然后再举例说明一些闭包的经典用途. 什么是闭包 通俗地讲, JavaScript 中每个的函数都是一个闭包,但通常意义上嵌套的函数更能够体 现出闭包的特性,请看

  • Javascript函数式编程语言

    函数式编程语言 函数式编程语言是那些方便于使用函数式编程范式的语言.简单来说,如果具备函数式编程所需的特征, 它就可以被称为函数式语言.在多数情况下,编程的风格实际上决定了一个程序是否是函数式的. 是什么让一个语言具有函数式特征? 函数式编程无法用C语言来实现.函数式编程也无法用Java来实现(不包括那些通过大量变通手段实现的近似函数式编程). 这些语言不包含支持函数式编程的结构.他们是纯面向对象的.严格非函数式的语言. 同时,纯函数语言也无法使用面向对象编程,比如Scheme.Haskell以

随机推荐