原文:medium.com/better-prog…javascript
译者:前端小智html
阿里云最近在作活动,低至2折,有兴趣能够看看:promotion.aliyun.com/ntms/yunpar…前端
为了保证的可读性,本文采用意译而非直译。java
在长时间学习和使用面向对象编程以后,我们退一步来考虑系统复杂性。git
在作了一些研究以后,我发现了函数式编程的概念,好比不变性和纯函数。这些概念使你可以构建无反作用的函数,所以更容易维护具备其余优势的系统。github
在这篇文章中,将通大量代码示例来详细介绍函数式编程和一些相关重要概念。编程
函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,它把计算看做是对数学函数的评估,避免了状态的变化和数据的可变。数组
当咱们想要理解函数式编程时,须要知道的第一个基本概念是纯函数,但纯函数又是什么鬼?bash
我们怎么知道一个函数是不是纯函数?这里有一个很是严格的定义:dom
若是给定相同的参数,则返回相同的结果(也称为肯定性)。
它不会引发任何反作用。
若是给出相同的参数,它返回相同的结果。 想象一下,咱们想要实现一个计算圆的面积的函数。
不是纯函数会这样作,接收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
:用给定字符串中的替换替换全部匹配实例
接着实现一个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 中的一级公民,很风骚,函数也能够被看做成值并用做数据使用。
其思想是将函数视为值,并将函数做为数据传递。经过这种方式,咱们能够组合不一样的函数来建立具备新行为的新函数。
假如咱们有一个函数,它对两个值求和,而后将值加倍,以下所示:
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
函数指望一个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 上解决的一个有趣问题是Filter Array问题。 问题是过滤给定的整数数组,并仅输出小于指定值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
方法经过将函数应用于其全部元素并根据返回的值构建新集合来转换集合。
假如咱们不想过滤年龄大于 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问题是更新列表问题。咱们想要用一个数组的绝对值来更新它的值。
例如,输入[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
函数的思想是接收一个函数和一个集合,并返回经过组合这些项建立的值。
常见的的一个例子是获取订单的总金额。
假设你在一个购物网站,已经将产品一、产品二、产品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
阿里云最近在作活动,低至2折,有兴趣能够看看:promotion.aliyun.com/ntms/yunpar…
干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。
我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,便可看到福利,你懂的。
每次整理文章,通常都到2点才睡觉,一周4次左右,挺苦的,还望支持,给点鼓励