JavaScript函数式编程究竟是什么?
摘要: 理解函数式编程。
- 作者:前端小智
- 原文:JS中函数式编程基本原理简介
Fundebug经授权转载,版权归原作者所有。
在长时间学习和使用面向对象编程之后,咱们退一步来考虑系统复杂性。
在做了一些研究之后,我发现了函数式编程的概念,比如不变性和纯函数。这些概念使你能够构建无副作用的函数,因此更容易维护具有其他优点的系统。
在这篇文章中,将通大量代码示例来详细介绍函数式编程和一些相关重要概念。
什么是函数式编程
函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,它把计算看作是对数学函数的评估,避免了状态的变化和数据的可变。
纯函数
当我们想要理解函数式编程时,需要知道的第一个基本概念是纯函数,但纯函数又是什么鬼?
咱们怎么知道一个函数是否是纯函数?这里有一个非常严格的定义:
- 如果给定相同的参数,则返回相同的结果(也称为确定性)。
- 它不会引起任何副作用。
如果给定相同的参数,则得到相同的结果
如果给出相同的参数,它返回相同的结果。 想象一下,我们想要实现一个计算圆的面积的函数。
不是纯函数会这样做,接收radius 作为参数,然后计算radius * radius * PI:
let PI = 3.14;
const calculateArea = (radius) => radius * radius * PI;
calculateArea(10); // returns 314.0
为什么这是一个不纯函数?原因很简单,因为它使用了一个没有作为参数传递给函数的全局对象。
现在,想象一些数学家认为圆周率的值实际上是42并且修改了全局对象的值。
不纯函数得到10 * 10 * 42 = 4200。对于相同的参数(radius = 10),我们得到了不同的结果。
修复它:
let PI = 3.14;
const calculateArea = (radius, pi) => radius * radius * pi;
calculateArea(10, PI); // returns 314.0
现在把 PI 的值作为参数传递给函数,这样就没有外部对象引入。
- 对于参数
radius = 10和PI = 3.14,始终都会得到相同的结果:314.0。 - 对于
radius = 10和PI = 42,总是得到相同的结果:4200
读取文件
下面函数读取外部文件,它不是纯函数,文件的内容随时可能都不一样。
const charactersCounter = (text) => `Character count: ${text.length}`;
function analyzeFile(filename) {
let fileContent = open(filename);
return charactersCounter(fileContent);
}
随机数生成
任何依赖于随机数生成器的函数都不能是纯函数。
function yearEndEvaluation() {
if (Math.random() > 0.5) {
return "You get a raise!";
} else {
return "Better luck next year!";
}
}
无明显副作用
纯函数不会引起任何可观察到的副作用。可见副作用的例子包括修改全局对象或通过引用传递的参数。
现在,咱们要实现一个函数,该接收一个整数并返对该整数进行加1操作且返回。
let counter = 1;
function increaseCounter(value) {
counter = value + 1;
}
increaseCounter(counter);
console.log(counter); // 2
该非纯函数接收该值并重新分配counter,使其值增加1。
函数式编程不鼓励可变性。我们修改全局对象,但是要怎么做才能让它变得纯函数呢?只需返回增加1的值。
let counter = 1;
const increaseCounter = (value) => value + 1;
increaseCounter(counter); // 2
console.log(counter); // 1
纯函数increaseCounter返回2,但是counter值仍然是相同的。函数返回递增的值,而不改变变量的值。
如果我们遵循这两条简单的规则,就会更容易理解我们的程序。现在每个函数都是孤立的,不能影响系统的其他部分。
纯函数是稳定的、一致的和可预测的。给定相同的参数,纯函数总是返回相同的结果。
咱们不需要考虑相同参数有不同结果的情况,因为它永远不会发生。
纯函数的好处
纯函数代码肯定更容易测试,不需要 mock 任何东西,因此,我们可以使用不同的上下文对纯函数进行单元测试:
- 给定一个参数
A,期望函数返回值B - 给定一个参数
C,期望函数返回值D
一个简单的例子是接收一组数字,并对每个数进行加 1 这种沙雕的操作。
let list = [1, 2, 3, 4, 5];
const incrementNumbers = (list) => list.map(number => number + 1);
接收numbers数组,使用map递增每个数字,并返回一个新的递增数字列表。
incrementNumbers(list); // [2, 3, 4, 5, 6]
对于输入[1,2,3,4,5],预期输出是[2,3,4,5,6]。
不可变性
尽管时间变或者不变,纯函数大佬都是不变的。
当数据是不可变的时,它的状态在创建后不能更改。
咱们不能更改不可变对象,如果非要来硬的,刚需要深拷贝一个副本,然后操作这个副本。
在JS中,我们通常使用for循环,for的每次遍历 i是个可变变量。
var values = [1, 2, 3, 4, 5];
var sumOfValues = 0;
for (var i = 0; i < values.length; i++) {
sumOfValues += values[i];
}
sumOfValues // 15
对于每次遍历,都在更改i和sumOfValue状态,但是我们如何在遍历中处理可变性呢? 答案就是使用递归。
let list = [1, 2, 3, 4, 5];
let accumulator = 0;
function sum(list, accumulator) {
if (list.length == 0) {
return accumulator;
}
return sum(list.slice(1), accumulator + list[0]);
}
sum(list, accumulator); // 15
list; // [1, 2, 3, 4, 5]
accumulator; // 0
上面代码有个 sum 函数,它接收一个数值向量。函数调用自身,直到 list为空退出递归。对于每次“遍历”,我们将把值添加到总accumulator中。
使用递归,咱们保持变量不变。不会更改list和accumulator变量。它保持相同的值。
观察:我们可以使用reduce来实现这个功能。这个在接下的高阶函数内容中讨论。
构建对象的最终状态也很常见。假设我们有一个字符串,想把这个字符串转换成url slug。
在Ruby的面向对象编程中,咱们可以创建一个类 UrlSlugify,这个类有一个slugify方法来将字符串输入转换为url slug。
class UrlSlugify
attr_reader :text
def initialize(text)
@text = text
end
def slugify!
text.downcase!
text.strip!
text.gsub!(' ', '-')
end
end
UrlSlugify.new(' I will be a url slug ').slugify! # "i-will-be-a-url-slug"
上面使用的有命令式编程方式,首先用小写字母表示我们想在每个slugify进程中做什么,然后删除无用的空格,最后用连字符替换剩余的空格。
这种方式在整个过程中改变了输入状态,显然不符合纯函数的概念。
这边可以通过函数组合或函数链来来优化。换句话说,函数的结果将用作下一个函数的输入,而不修改原始输入字符串。
const string = " I will be a url slug ";
const slugify = string =>
string
.toLowerCase()
.trim()
.split(" ")
.join("-");
slugify(string); // i-will-be-a-url-slug
上述代码主要做了这几件事:
toLowerCase:将字符串转换为所有小写字母。- trim:删除字符串两端的空白。
split和join:用给定字符串中的替换替换所有匹配实例
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
引用透明性
接着实现一个square 函数:
const square = (n) => n * n;
给定相同的输入,这个纯函数总是有相同的输出。
square(2); // 4
square(2); // 4
square(2); // 4
// ...
将2作为square函数的参数传递始终会返回4。这样咱们可以把square(2)换成4,我们的函数就是引用透明的。
基本上,如果一个函数对于相同的输入始终产生相同的结果,那么它可以看作透明的。
有了这个概念,咱们可以做的一件很酷的事情就是记住这个函数。假设有这样的函数
const sum = (a, b) => a + b;
用这些参数来调用它
sum(3, sum(5, 8));
sum(5, 8) 总等于13,所以可以做些骚操作:
sum(3, 13);
这个表达式总是得到16,咱们可以用一个数值常数替换整个表达式,并把它记下来。
函数是 JS 中的一级公民
函数作为 JS 中的一级公民,很风骚,函数也可以被看作成值并用作数据使用。
- 从常量和变量中引用它。
- 将其作为参数传递给其他函数。
- 作为其他函数的结果返回它。
其思想是将函数视为值,并将函数作为数据传递。通过这种方式,我们可以组合不同的函数来创建具有新行为的新函数。
假如我们有一个函数,它对两个值求和,然后将值加倍,如下所示:
const doubleSum = (a, b) => (a + b) * 2;
对应两个值求差,然后将值加倍:
const doubleSubtraction = (a, b) => (a - b) * 2;
这些函数具有相似的逻辑,但区别在于运算符的功能。 如果我们可以将函数视为值并将它们作为参数传递,我们可以构建一个接收运算符函数并在函数内部使用它的函数。
const sum = (a, b) => a + b;
const subtraction = (a, b) => a - b;
const doubleOperator = (f, a, b) => f(a, b) * 2;
doubleOperator(sum, 3, 1); // 8
doubleOperator(subtraction, 3, 1); // 4
f参数并用它来处理a和b, 这里传递了sum函数和subtraction并使用doubleOperator函数进行组合并创建新行为。
高阶函数
当我们讨论高阶函数时,通常包括以下几点:
- 将一个或多个函数作为参数
- 返回一个函数作为结果
上面实现的doubleOperator函数是一个高阶函数,因为它将一个运算符函数作为参数并使用它。
我们经常用的filter、map和reduce都是高阶函数,Look see see。
Filter
对于给定的集合,我们希望根据属性进行筛选。filter函数期望一个true或false值来决定元素是否应该包含在结果集合中。
如果回调表达式为真,过滤器函数将在结果集合中包含元素,否则,它不会。
一个简单的例子是,当我们有一个整数集合,我们只想要偶数。
命令式
使用命令式方式来获取数组中所有的偶数,通常会这样做:
- 创建一个空数组
evenNumbers - 遍历数组
numbers - 将偶数 push 到
evenNumbers数组中
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var evenNumbers = [];
for (var i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 == 0) {
evenNumbers.push(numbers[i]);
}
}
console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10]
我们还可以使用filter高阶函数来接收偶函数并返回一个偶数列表:
const even = n => n % 2 == 0;
const listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
listOfNumbers.filter(even); // [0, 2, 4, 6, 8, 10]
我在 [Hacker Rank FP][2] 上解决的一个有趣问题是[Filter Array问题][3]。 问题是过滤给定的整数数组,并仅输出小于指定值X的那些值。
命令式做法通常是这样的:
var filterArray = function(x, coll) {
var resultArray = [];
for (var i = 0; i < coll.length; i++) {
if (coll[i] < x) {
resultArray.push(coll[i]);
}
}
return resultArray;
}
console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0])); // (3) [2, 1, 0]
声明式方式
对于上面的总是,我们更想要一种更声明性的方法来解决这个问题,如下所示:
function smaller(number) {
return number < this;
}
function filterArray(x, listOfNumbers) {
return listOfNumbers.filter(smaller, x);
}
let numbers = [10, 9, 8, 2, 7, 5, 1, 3, 0];
filterArray(3, numbers); // [2, 1, 0]
在smaller的函数中使用 this,一开始看起来有点奇怪,但是很容易理解。
filter函数中的第二个参数表示上面 this, 也就是 x 值。
我们也可以用map方法做到这一点。想象一下,有一组信息
let people = [
{ name: "TK", age: 26 },
{ name: "Kaio", age: 10 },
{ name: "Kazumi", age: 30 }
]
我们希望过滤 age 大于 21 岁的人,用 filter 方式
const olderThan21 = person => person.age > 21;
const overAge = people => people.filter(olderThan21);
overAge(people); // [{ name: 'TK', age: 26 }, { name: 'Kazumi', age: 30 }]
map
map函数的主要思路是转换集合。
map方法通过将函数应用于其所有元素并根据返回的值构建新集合来转换集合。
假如我们不想过滤年龄大于 21 的人,我们想做的是显示类似这样的:TK is 26 years old.
使用命令式,我们通常会这样做:
var people = [
{ name: "TK", age: 26 },
{ name: "Kaio", age: 10 },
{ name: "Kazumi", age: 30 }
];
var peopleSentences = [];
for (var i = 0; i < people.length; i++) {
var sentence = people[i].name + " is " + people[i].age + " years old";
peopleSentences.push(sentence);
}
console.log(peopleSentences); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']
声明式会这样做:
const makeSentence = (person) => `${person.name} is ${person.age} years old`;
const peopleSentences = (people) => people.map(makeSentence);
peopleSentences(people);
// ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']
整个思想是将一个给定的数组转换成一个新的数组。
另一个有趣的HackerRank问题是[更新列表问题][3]。我们想要用一个数组的绝对值来更新它的值。
例如,输入[1,2,3,- 4,5]需要输出为[1,2,3,4,5],-4的绝对值是4。
一个简单的解决方案是每个集合中值的就地更新,很危险的作法
var values = [1, 2, 3, -4, 5];
for (var i = 0; i < values.length; i++) {
values[i] = Math.abs(values[i]);
}
console.log(values); // [1, 2, 3, 4, 5]
我们使用Math.abs函数将值转换为其绝对值并进行就地更新。
这种方式不是最做解。
首先,前端我们学习了不变性,知道不可变性让函数更加一致和可预测,咱们的想法是建立一个具有所有绝对值的新集合。
其次,为什么不在这里使用map来“转换”所有数据
我的第一个想法是测试Math.abs函数只处理一个值。
Math.abs(-1); // 1
Math.abs(1); // 1
Math.abs(-2); // 2
Math.abs(2); // 2
我们想把每个值转换成一个正值(绝对值)。
现在知道如何对一个值执行绝对值操作,可以使用此函数作为参数传递给map函数。
还记得高阶函数可以接收函数作为参数并使用它吗? 是的,map函数可以做到这一点
let values = [1, 2, 3, -4, 5];
const updateListMap = (values) => values.map(Math.abs);
updateListMap(values); // [1, 2, 3, 4, 5]
Reduce
reduce函数的思想是接收一个函数和一个集合,并返回通过组合这些项创建的值。
常见的的一个例子是获取订单的总金额。
假设你在一个购物网站,已经将产品1、产品2、产品3和产品4添加到购物车(订单)中。现在,我们要计算购物车的总数量:
以命令式的方式,就是便利订单列表并将每个产品金额与总金额相加。
var orders = [
{ productTitle: "Product 1", amount: 10 },
{ productTitle: "Product 2", amount: 30 },
{ productTitle: "Product 3", amount: 20 },
{ productTitle: "Product 4", amount: 60 }
];
var totalAmount = 0;
for (var i = 0; i < orders.length; i++) {
totalAmount += orders[i].amount;
}
console.log(totalAmount); // 120
使用reduce,我们可以构建一个函数来处理量计算sum并将其作为参数传递给reduce函数。
let shoppingCart = [
{ productTitle: "Product 1", amount: 10 },
{ productTitle: "Product 2", amount: 30 },
{ productTitle: "Product 3", amount: 20 },
{ productTitle: "Product 4", amount: 60 }
];
const sumAmount = (currentTotalAmount, order) => currentTotalAmount + order.amount;
const getTotalAmount = (shoppingCart) => shoppingCart.reduce(sumAmount, 0);
getTotalAmount(shoppingCart); // 120
这里有shoppingCart,接收当前currentTotalAmount的函数sumAmount,以及对它们求和的order对象。
咱们也可以使用map将shoppingCart转换为一个amount集合,然后使用reduce函数和sumAmount函数。
const getAmount = (order) => order.amount;
const sumAmount = (acc, amount) => acc + amount;
function getTotalAmount(shoppingCart) {
return shoppingCart
.map(getAmount)
.reduce(sumAmount, 0);
}
getTotalAmount(shoppingCart); // 120
getAmount接收product对象并只返回amount值,即[10,30,20,60],然后,reduce通过相加将所有项组合起来。
三个函数的示例
看了每个高阶函数的工作原理。这里为你展示一个示例,说明如何在一个简单的示例中组合这三个函数。
说到购物车,假设我们的订单中有这个产品列表
let shoppingCart = [
{ productTitle: "Functional Programming", type: "books", amount: 10 },
{ productTitle: "Kindle", type: "eletronics", amount: 30 },
{ productTitle: "Shoes", type: "fashion", amount: 20 },
{ productTitle: "Clean Code", type: "books", amount: 60 }
]
假如相要想要购物车里类型为 books的总数,通常会这样做:
- 过滤 type 为 books的
- 使用
map将购物车转换为amount集合。 - 用
reduce将所有项加起来。
let shoppingCart = [
{ productTitle: "Functional Programming", type: "books", amount: 10 },
{ productTitle: "Kindle", type: "eletronics", amount: 30 },
{ productTitle: "Shoes", type: "fashion", amount: 20 },
{ productTitle: "Clean Code", type: "books", amount: 60 }
]
const byBooks = (order) => order.type == "books";
const getAmount = (order) => order.amount;
const sumAmount = (acc, amount) => acc + amount;
function getTotalAmount(shoppingCart) {
return shoppingCart
.filter(byBooks)
.map(getAmount)
.reduce(sumAmount, 0);
}
getTotalAmount(shoppingCart); // 70
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
关于Fundebug
Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了20亿+错误事件,付费客户有阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费试用!
JavaScript函数式编程究竟是什么?的更多相关文章
- 转:JavaScript函数式编程(三)
转:JavaScript函数式编程(三) 作者: Stark伟 这是完结篇了. 在第二篇文章里,我们介绍了 Maybe.Either.IO 等几种常见的 Functor,或许很多看完第二篇文章的人都会 ...
- 转: JavaScript函数式编程(二)
转: JavaScript函数式编程(二) 作者: Stark伟 上一篇文章里我们提到了纯函数的概念,所谓的纯函数就是,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环 ...
- 转:JavaScript函数式编程(一)
转:JavaScript函数式编程(一) 一.引言 说到函数式编程,大家可能第一印象都是学院派的那些晦涩难懂的代码,充满了一大堆抽象的不知所云的符号,似乎只有大学里的计算机教授才会使用这些东西.在曾经 ...
- JavaScript 函数式编程读书笔记2
概述 这是我读<javascript函数式编程>的读书笔记,供以后开发时参考,相信对其他人也有用. 说明:虽然本书是基于underscore.js库写的,但是其中的理念和思考方式都讲的很好 ...
- JavaScript 函数式编程读书笔记1
概述 这是我读<javascript函数式编程>的读书笔记,供以后开发时参考,相信对其他人也有用. 说明:虽然本书是基于underscore.js库写的,但是其中的理念和思考方式都讲的很好 ...
- 一文带你了解JavaScript函数式编程
摘要: 函数式编程入门. 作者:浪里行舟 Fundebug经授权转载,版权归原作者所有. 前言 函数式编程在前端已经成为了一个非常热门的话题.在最近几年里,我们看到非常多的应用程序代码库里大量使用着函 ...
- javascript函数式编程和链式优化
1.函数式编程理解 函数式编程可以理解为,以函数作为主要载体的编程方式,用函数去拆解.抽象一般的表达式 与命令式相比,这样做的好处在哪?主要有以下几点: (1)语义更加清晰 (2)可复用性更高 (3) ...
- JavaScript函数式编程(纯函数、柯里化以及组合函数)
JavaScript函数式编程(纯函数.柯里化以及组合函数) 前言 函数式编程(Functional Programming),又称为泛函编程,是一种编程范式.早在很久以前就提出了函数式编程这个概念了 ...
- javascript 函数式编程
编程范式 编程范式是一个由思考问题以及实现问题愿景的工具组成的框架.很多现代语言都是聚范式(或者说多重范式): 他们支持很多不同的编程范式,比如面向对象,元程序设计,泛函,面向过程,等等. 函数式编程 ...
随机推荐
- Linux—服务器之间传输文件
https://www.jb51.net/article/82608.htm https://blog.csdn.net/taian1665/article/details/86492400 http ...
- Vim基础配置
vim 个性化设置 安装插件管理器Vundle: 创建目录: mkdir -p ~/.vim/bundle 下载文件: git clone https://github.com/VundleVim/V ...
- (办公)vue下载excel,后台用post方法
后台方法的参数必须是@RequestBody修饰的. 前台关键代码: axios ( { method : 'post', url : api.exportPlayTime , // 请求地址 dat ...
- 2. Linux文件与目录管理
一.目录与路径 1. 相对路径与绝对路径 绝对路径:路径写法[一定由根目录 / 写起],如:/usr/share/doc 相对路径:路径写法[不由 / 写起], /usr/share/doc 要到 / ...
- gn gen ninja
- MySQL学习笔记1——DDL
DDL 1.数据库 *查看所有数据库:SHOW DATABASES;*切换(选择要操作的)数据库:USE 数据库名;*创建数据库:CREATE DATABASES [IF NOT EXISTS] my ...
- C语言前置知识汇编基础--总线【地址总线,控制总线,数据总线】练习题
寻址就是用一个数字表示一个字节存储单元的地址,而不是表示这个存储单元中的内容,所以不用乘以8. 1. 1个CPU的寻址能力为8KB,那么它的地址总线宽度是多少? 8*1024=8192 共有8 ...
- R语言- 实验报告 - 利用R语言脚本与Java相互调用
一. 实训内容 利用R语言对Java项目程序进行调用,本实验包括利用R语言对java的.java文件进行编译和执行输出. 在Java中调用R语言程序.本实验通过eclipse编写Java程序的方式,调 ...
- LeetCode 234:回文链表 Palindrome Linked List
请判断一个链表是否为回文链表. Given a singly linked list, determine if it is a palindrome. 示例 1: 输入: 1->2 输出: ...
- SQL Server 判断各种对象是否存在和sysobjects的关系
一.判断表是否存在 object_id():获取表的ID,其中N表示Unicode类型.可以支持不同语种的对象名 ) drop table [dbo].[表名] 二.判断要创建的存储过程名是否存在 ) ...