- 原文地址:A Quick Introduction to Functional Javascript
- 原文做者:Angelos Chalaris
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:Zheng7426
- 校对者:AmyFoxFN
函数式编程是目前最热门的趋势之一,有不少好的论点解释了人们为何想在代码中使用它。我并不打算在这里详细介绍全部函数式编程的概念和想法,而是会尽力给你演示在平常状况下和 JavaScript 打交道的时候如何用上这种编程。javascript
函数式编程是一种编程范例,它将计算机运算视为数学上的函数计算,而且避免了状态的改变和易变的数据。前端
在深刻接触 JavaScript 的函数式编程范例以前,我们得先知道什么是高阶函数、它的用途以及这个定义自己究竟有什么含义。高阶函数既能够把函数当成参数来接收,也能够做为把函数做为结果输出。你须要记住 函数其实也是一种值,也就是说你能够像传递变量同样去传递函数。java
因此呢,在 JavaScript 里你能够这么作:android
// 建立函数
function f(x){
return x * x;
}
// 调用该函数
f(5); // 25
// 建立匿名函数
// 并赋值一个变量
var g = function(x){
return x * x;
}
// 传递函数
var h = g;
// And use it
h(5); // 25
复制代码
一旦使用上面这个技巧,你的代码更容易被重复利用,同时功能也更增强大。我们都经历过这样的状况:想要把一个函数传到另外一个函数里去执行任务,但须要写一些额外的代码来实现这一点,对吧?使用函数式编程的话,你将再也不须要写额外的代码,而且可使你的代码变得很干净、易于理解。ios
有一点要注意,正确的泛函代码的特色是没有反作用,也就是说函数应该只依赖于它们的参数输入,而且不该以任何方式影响到外界环境。这个特色有重要的含义,举个例子:若是传递进函数的参数相同,那么输出的结果也老是相同的;若是一个被调用的函数所输出的结果并无被用到,那么这个结果即便被删掉也不会影响别的代码。git
Array.prototype
应该是你学习 JavaScript 函数式编程的第一步,它涵盖了不少数组转化的实用方法,这些方法在现代网页应用里至关的常见。github
先来看看这个叫 Array.prototype.sort()
的方法会很不错,由于这个转化挺直白的。顾名思义,咱能够用这个方法来给数组排序。.sort()
只接收一个参数(即一个用于比较两个元素的函数)。若是第一个元素在第二个元素的前面,结果返回的是负值。反之,则返回正值。面试
排序听起来很是简单,然而当你须要给比通常数字数组复杂得多的数组排序时,可能就不那么简单了。在下面这个例子里,咱们有一个对象的数组,里面存的是以磅(lbs)或千克(kg)为单位的体重,我们须要对这些人的体重进行升序排列。代码看起来会是这样:编程
// 我们这个比较函数的定义
var sortByWeight = function(x,y){
var xW = x.measurement == "kg" ? x.weight : x.weight * 0.453592;
var yW = y.measurement == "kg" ? y.weight : y.weight * 0.453592;
return xW > yW ? 1 : -1;
}
// 两组数据有细微差异
// 要根据体重来对它们进行排序
var firstList = [
{ name: "John", weight: 220, measurement: "lbs" },
{ name: "Kate", weight: 58, measurement: "kg" },
{ name: "Mike", weight: 137, measurement: "lbs" },
{ name: "Sophie", weight: 66, measurement: "kg" },
];
var secondList = [
{ name: "Margaret", weight: 161, measurement: "lbs", age: 51 },
{ name: "Bill", weight: 76, measurement: "kg", age: 62 },
{ name: "Jonathan", weight: 72, measurement: "kg", age: 43 },
{ name: "Richard", weight: 74, measurement: "kg", age: 29 },
];
// 用开头定义的函数
// 对两组数据进行排序
firstList.sort(sortByWeight); // Kate, Mike, Sophie, John
secondList.sort(sortByWeight); // Jonathan, Margaret, Richard, Bill
复制代码
在上面的例子里,你能够很清楚地观察到使用高阶函数带来的好处:节省了空间、时间,也让你的代码更能被读懂、更容易被重复利用。若是你不打算用 .sort()
来写的话,你得另外写两个循环并重复大部分的逻辑。坦率来讲,那样将致使更冗长、臃肿且不易理解的代码。后端
一般你对数组的操做也不单只是排序而已。就个人经验而言,根据属性来过滤一个数组很常见,并且没有什么方法比 Array.prototype.filter()
更加合适。过滤数组并不困难,由于你只需将一个函数做为参数,对于那些须要被过滤掉的元素,该函数会返回 false
。反之,该函数会返回 true
。很简单,不是吗?我们来看看实例:
// 一群人的数组
var myFriends = [
{ name: "John", gender: "male" },
{ name: "Kate", gender: "female" },
{ name: "Mike", gender: "male" },
{ name: "Sophie", gender: "female" },
{ name: "Richard", gender: "male" },
{ name: "Keith", gender: "male" }
];
// 基于性别的简易过滤器
var isMale = function(x){
return x.gender == "male";
}
myFriends.filter(isMale); // John, Mike, Richard, Keith
复制代码
虽然 .filter()
会返回数组中全部符合条件的元素,你也能够用 Array.prototype.find()
提取数组中第一个符合条件的元素,或是用 Array.prototype.findIndex()
来提取数组中第一个匹配到的元素索引。同理,你可使用 Array.prototype.some()
来测试是否至少有一个元素符合条件,抑或是用 Array.prototype.every()
来检查是否全部的元素都符合条件。这些方法在某些应用中能够变得至关有用,因此我们来看一个囊括了这几种方法的例子:
// 一组关于分数的数组
// 不是每一项都标注了人名
var highScores = [
{score: 237, name: "Jim"},
{score: 108, name: "Kit"},
{score: 91, name: "Rob"},
{score: 0},
{score: 0}
];
// 这些简单且能重复使用的函数
// 是用来查看每一项是否有名字
// 以及分数是否为正数
var hasName = function(x){
return typeof x['name'] !== 'undefined';
}
var hasNotName = function(x){
return !hasName(x);
}
var nonZeroHighScore = function(x){
return x.score != 0;
}
// 填充空白的名字,直到全部空白的名字都有“---”
while (!highScores.every(hasName)){
var highScore = highScores.find(hasNotName);
highScore.name = "---";
var highScoreIndex = highScores.findIndex(hasNotName);
highScores[highScoreIndex] = highScore;
}
// 检查非零的分数是否存在
// 并在 console 里输出
if (highScores.some(nonZeroHighScore))
console.log(highScores.filter(nonZeroHighScore));
else
console.log("No non-zero high scores!");
复制代码
到这一步,你应该会有些融会贯通的感受了。上面的例子清楚地体现出高阶函数是如何使你避免了大量重复且难以理解的代码。这个例子虽然简单,但你也能看出代码的简洁之处,与你在未使用函数式编程范例时所编写的内容造成鲜明对比。
先撇开上面例子里复杂的逻辑,我们有的时候只想要将数组转化成另外一个数组,且无需对数组里的数据作那么多的改变。这个时候 Array.prototype.map()
就派上用场了,咱们能够用这个方法来转化数组中的对象。.map()
和以前例子所用到的方法并不相同,区别在于其做为参数的高阶函数会返回一个对象,能够是任何你想写的对象。让我用一个简单的例子来演示一下:
// 一个有 4 个对象的数组
var myFriends = [
{ name: "John", surname: "Smith", age: 52},
{ name: "Sarah", surname: "Smith", age: 49},
{ name: "Michael", surname: "Jones", age: 46},
{ name: "Garry", surname: "Thomas", age: 48}
];
// 一个简单的函数
// 用来把名和姓放在一块儿
var fullName = function(x){
return x.name + " " + x.surname;
}
myFriends.map(fullName);
// 应输出
// ["John Smith", "Sarah Smith", "Michael Jones", "Garry Thomas"]
复制代码
从上面这个例子能够看出,一旦对数组使用了 .map()
方法,很容易就能获得一个仅包含我们所需属性的数组。在这个例子里,咱只想要对象中 name
和 surname
这两行字符串,因此才使用简单的 mapping(译者注:即便用 map 方法) 来利用原来包含不少对象的数组上建立了另外一个只包含字符串的数组。Mapping 这种方式可能比你想象的还要经常使用,它在每一个网页开发者的口袋中能够成为很强大的工具。因此说,这整篇文章里你若是别的没记住的话,不要紧,但千万要记住如何使用 .map()
。
最后还有一点很是值得你注意,那就是常规目的数组转化中的 Array.prototype.reduce()
。.reduce()
与上面提到的全部方法都有所不一样,由于它的参数不只仅是一个高阶函数,还包含一个累加器。一开始听起来可能有些使人困惑,因此先看一个例子来帮助你理解 .reduce()
背后的基础概念吧:
// 关于不一样公司支出的数组
var oldExpenses = [
{ company: "BigCompany Co.", value: 1200.10},
{ company: "Pineapple Inc.", value: 3107.02},
{ company: "Office Supplies Inc.", value: 266.97}
];
var newExpenses = [
{ company: "Office Supplies Inc.", value: 108.11},
{ company: "Megasoft Co.", value: 1208.99}
];
// 简单的求和函数
var sumValues = function(sum, x){
return sum + x.value;
}
// 将第一个数组降为几个数值之和
var oldExpensesSum = oldExpenses.reduce(sumValues, 0.0);
// 将第二个数组降为几个数值之和
console.log(newExpenses.reduce(sumValues, oldExpensesSum)); // 5891.19
复制代码
对于任何曾经把数组中的值求和的人来讲,理解上面这个例子应该不会特别困难。一开始我们定义了一个可重复使用的高阶函数,用于把数组中的 value 都加起来。以后,我们用这个函数来给第一个数组中的支出数值求和,并把求出来的值当成初始值,而不是从零开始地去累加第二个数组中的支出数值。因此最后得出的是两个数组的支出数值总和。
固然了,.reduce()
能够作的事情远不止在数组中求和而已。大多数别的方法解决不了的复杂转化,均可以使用 .reduce()
与一个数组或对象的累加器来轻松解决。一个实用的例子是转化一个有不少篇文章的数组,每一篇文章有一个标题和一些标签。原来的数组会被转化成标签的数组,每一项中有使用该标签的文章数目以及这些文章的标题构成的数组。我们来看看代码:
// 一个带有标签的文章的数组
var articles = [
{title: "Introduction to Javascript Scope", tags: [ "Javascript", "Variables", "Scope"]},
{title: "Javascript Closures", tags: [ "Javascript", "Variables", "Closures"]},
{title: "A Guide to PWAs", tags: [ "Javascript", "PWA"]},
{title: "Javascript Functional Programming Examples", tags: [ "Javascript", "Functional", "Function"]},
{title: "Why Javascript Closures are Important", tags: [ "Javascript", "Variables", "Closures"]},
];
// 一个可以将文章数组降为标签数组的函数
//
var tagView = function(accumulator, x){
// 针对文章的标签数组(原数组)里的每个标签
x.tags.forEach(function(currentTag){
// 写一个函数看看标签是否匹配
var findCurrentTag = function(y) { return y.tag == currentTag; };
// 检查是否该标签已经出如今累积器数组
if (accumulator.some(findCurrentTag)){
// 找到标签并得到索引
var existingTag = accumulator.find(findCurrentTag);
var existingTagIndex = accumulator.findIndex(findCurrentTag);
// 更新使用该标签的文章数目,以及文章标题的列表
accumulator[existingTagIndex].count += 1;
accumulator[existingTagIndex].articles.push(x.title);
}
// 不然就在累积器数组中增添标签
else {
accumulator.push({tag: currentTag, count: 1, articles: [x.title]});
}
});
// 返回累积器数组
return accumulator;
}
// 转化原数组
articles.reduce(tagView,[]);
// 输出:
/*
[
{tag: "Javascript", count: 5, articles: [
"Introduction to Javascript Scope",
"Javascript Closures",
"A Guide to PWAs",
"Javascript Functional Programming Examples",
"Why Javascript Closures are Important"
]},
{tag: "Variables", count: 3, articles: [
"Introduction to Javascript Scope",
"Javascript Closures",
"Why Javascript Closures are Important"
]},
{tag: "Scope", count: 1, articles: [
"Introduction to Javascript Scope"
]},
{tag: "Closures", count: 2, articles: [
"Javascript Closures",
"Why Javascript Closures are Important"
]},
{tag: "PWA", count: 1, articles: [
"A Guide to PWAs"
]},
{tag: "Functional", count: 1, articles: [
"Javascript Functional Programming Examples"
]},
{tag: "Function", count: 1, articles: [
"Javascript Functional Programming Examples"
]}
]
*/
复制代码
上面这个例子可能看起来会有些小复杂,因此须要一步一步来研究。首先呢,咱想要的最终结果是一个数组,因此累加器的初始值就成了[]
。而后,咱想要数组中的每个对象都包含标签名、使用该标签的文章数目以及文章标题的列表。不但如此,每个标签在数组中只能出现一次,因此咱必须用 .some()
、.find()
和 .findIndex()
来检查标签是否存在,以后将现有标签的对象进行转化,而不是另加一个新的对象。
棘手的地方在于,咱不能定义一个函数来检查每一个标签是否都存在(不然须要 7 个不一样的函数)。因此我们才在当前标签的循环里定义高阶函数,这样一来就能够再次使用高阶函数,避免重写代码。对了,其实这也能够经过 Currying 来完成,但我不会在本文中解释这个技巧。
当我们在累加器数组中获取标签的对象以后,只须要把使用该标签的文章数目递增,而且将当前标签下的文章添加到其文章数组中就好了。最后,我们返回累加器,大功告成。仔细阅读的话会发现代码不但很是简短,并且很容易理解。相同状况下,非函数式编程的代码将会看起来很是使人困惑,并且明显会更冗杂。
函数式编程做为目前最热门的趋势之一,是有其充分缘由的。它使我们在写出更清晰、更精简和更“吝啬”代码的同时,没必要去担忧反作用和状态的改变。JavaScript 的 [Array.prototype](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype)
方法在许多平常状况下很是实用,而且让我们在对数组进行简单和复杂的转化,也没必要去写太多重复的代码。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。