【译】JavaScript 中的函数式编程道理

原文:
Functional Programming Principles in Javascript

作者:TK

译者:博轩

经由很长一段时候的进修和面向对象编程的事变,我退后一步,最先思索体系的复杂性。

“复杂性是任何使软件难以明白或修正的东西。” – John Outerhout

做了一些研讨,我发现了函数式编程观点,如稳定性和纯函数。 这些观点使你能够构建无副作用的功用,而函数式编程的一些长处,也使得体系变得越发轻易保护。

在这篇文章中,我将经由过程 JavaScript 中的大批代码示例向您细致引见函数式编程和一些主要观点。

什么是函数式编程?

维基百科:Functional programming

函数式编程是一种编程范式,一种构建盘算机顺序结构和元素的体式格局,将盘算视为数学函数的评价并防止转变状况和可变数据 — 维基百科

纯函数

当我们想要明白函数式编程时,我们学到的第一个基本观点是纯函数。 那末我们如何晓得函数是不是地道呢? 这是一个异常严厉的纯度定义:

  • 假如给出雷同的参数,它返回雷同的效果(它也称为肯定性
  • 它不会引起任何可视察到的副作用

假如给出雷同的参数,它返回雷同的效果

我们想要完成一个盘算圆的面积的函数。 不纯的函数将吸收半径:radius 作为参数,然后盘算 radius * radius * PI :

const PI = 3.14;

const calculateArea = (radius) => radius * radius * PI;

calculateArea(10); // returns 314

为何这是一个不纯的功用? 仅仅由于它运用的是未作为参数通报给函数的全局对象。

设想一下,数学家以为 PI 值实际上是 42, 而且转变了全局对象的值。

不纯的函数如今将致使 10 * 10 * 42 = 4200 .关于雷同的参数(radius= 10),我们取得差别的效果。

我们来处置惩罚它吧!

const PI = 3.14;

const calculateArea = (radius, pi) => radius * radius * pi;

calculateArea(10, PI); // returns 314

如今我们将 PI 的值作为参数通报给函数。 所以如今我们只是接见通报给函数的参数。 没有外部对象(参数)

  • 关于参数 radius = 10PI = 3.14,我们将一直具有雷同的效果:314
  • 关于参数 radius = 10PI = 42,我们将一直具有雷同的效果:4200

读取文件 (Node.js)

假如我们的函数读取外部文件,它也不是纯函数 – 文件的内容能够变动:

const fs = require('fs');
const charactersCounter = (text) => `Character count: ${text.length}`;

function analyzeFile(filepath) {
    let fileContent = fs.readFileSync(filepath);
    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 。 然后运用不纯的函数吸收该值并从新为 counter 赋值,使其值增添 1

注重:在函数式编程中不勉励可变性。

上面的例子中,我们修正了全局对象。 然则我们如何才能让函数变得纯洁呢? 只需返回增添 1 的值。

let counter = 1;

const increaseCounter = (value) => value + 1;

increaseCounter(counter); // 2
console.log(counter); // 1

能够看到我们的纯函数 increaseCounter 返回 2 ,然则 counter 还坚持之前的值。该函数会使返回的数字递增,而且不更转变量的值。

假如我们遵照这两个简朴的划定规矩,就会使我们的顺序越发轻易明白。每一个功用都是伶仃的,没法影响到我们的体系。

纯函数是稳固,一致而且可展望的。给定雷同的参数,纯函数将一直返回雷同的效果。我们不须要斟酌,雷同的参数会发作差别的效果,由于它永久不会发作。

纯函数的优点

轻易测试

纯函数的代码越发轻易测试。我们不须要模仿任何实行的上下文。我们能够运用差别的上下文对纯函数举行单元测试:

  • 给定参数 A -> 希冀函数返回 B
  • 给定参数 C -> 希冀函数返回 D

一个简朴的例子,函数吸收一个数字鸠合,并希冀数字鸠合每一个元素递增。

let list = [1, 2, 3, 4, 5];

const incrementNumbers = (list) => list.map(number => number + 1);

我们吸收到数字数组,运用 map 递增每一个数字,并返回一个新的递增数字列表。

incrementNumbers(list); // [2, 3, 4, 5, 6]

关于输入 [1, 2, 3, 4, 5],预期输出将是 [2, 3, 4, 5, 6]

稳定性

跟着时候的推移稳定,或没法转变

当数据具有不可变性时,它的状况在竖立以后,就不能转变了。你不能去变动一个不可变的对象,然则你能够运用新值去竖立一个新的对象。

JavaScript 中,我们常运用 for 轮回。下面这个 for 轮回有一些可变的变量。

var values = [1, 2, 3, 4, 5];
var sumOfValues = 0;

for (var i = 0; i < values.length; i++) {
  sumOfValues += values[i];
}

sumOfValues // 15

关于每次迭代,我们都在转变变量 isumOfValues 的状况。然则我们要如何处置惩罚迭代中的可变性?运用递归

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 函数吸收数值向量。 该函数挪用本身,直到我们将列表清空。 关于每一个“迭代”,我们会将该值添加到总累加器。

运用递归,我们能够坚持变量的不可变性。 列表和累加器变量不会变动,会坚持雷同的值。

注重:我们能够运用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"

他已完成了!(It’s implemented!

这里我们运用敕令式编程,正确的申明我们想要在 函数完成的过程当中(slugify)每一步要做什么:起首是转换成小写,然后移除无用的空格,末了用连字符替代盈余的空格。

然则,在这个过程当中,函数转变了输入的参数。

我们能够经由过程实行函数组合或函数链来处置惩罚这类变异。 换句话说,函数的效果将用作下一个函数的输入,而不修正原始输入字符串。

let string = " I will be a url slug   ";

function slugify(string) {
  return string.toLowerCase()
    .trim()
    .split(" ")
    .join("-");
}

slugify(string); // i-will-be-a-url-slug

这里我们:

  • toLowerCase:将字符串转换为悉数小写
  • trim:从字符串的两头删除空格
  • splitjoin :用给定字符串中的替代替代一切婚配实例

我们将一切这四个功用结合起来,就能够完成 slugify 的功用了。

参考通明度

维基百科:Referential transparency

假如表达式能够替代为其响应的值而稳定动顺序的行动,则该表达式称为援用通明。这请求表达式是地道的,也就是说雷同输入的表达式值必需雷同,而且其评价必需没有副作用。– 维基百科

让我们完成一个盘算平方的要领:

const square = (n) => n * n;

在给定雷同输入的情况下,此纯函数将一直具有雷同的输出。

square(2); // 4
square(2); // 4
square(2); // 4
// ...

2 通报给 square 要领将一直返回 4。所以,如今我们能够运用 4 来替代 square(2)。我们的函数是援用通明的。

基本上,假如函数对统一输入一直发作雷同的效果,则援用通明

pure functions +
immutable data =
referential transparency

纯函数 + 不可变数据 = 参照通明度

有了这个观点,我们能够做一件很 cool 的事变,就是使这个函数具有影象(memoize)。
设想一下我们具有如许一个函数:

const sum = (a, b) => a + b;

我们用这些参数挪用它:

sum(3, sum(5, 8));

sum(5, 8) 即是 13。这个函数老是返回 13。因而,我们能够如许做:

sum(3, 13);

这个表达式老是会返回 16 。我们能够用一个数值常量替代全部表达式,并记着它。

这里引荐一篇
淘宝FED关于
memoize 的文章:
机能优化:memoization

函数是一等国民

函数作为一等国民,意味着函数也能够视为值处置惩罚,并当作数据来运用。

函数作为一等国民有以下特征:

  • 能够当作常量,或许变量来援用
  • 将函数当作参数通报给其他函数
  • 将函数作为其他函数的返回值

我们的主意是函数视为值并将它们作为参数通报。 如许我们就能够组合差别的函数来竖立具有新行动的新函数。

设想一下,我们有一个函数能够将两个值相加,然后将该值更加:

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,并用它来处置惩罚 ab 。 我们通报了 sumsubtraction 函数以运用 doubleOperator 函数举行组兼并竖立一个新行动。

高阶函数

维基百科:Higher-order function

当我们议论高阶函数时,通常是指一个函数同时具有:

  • 将一个或多个函数作为参数,或
  • 返回一个函数作为效果

我们上面完成的 doubleOperator 函数是一个高阶函数,由于它将一个运算符函数作为参数并运用它。

您能够已听说过 filtermapreduce 。 我们来看看这些。

Filter

给定一个鸠合,我们愿望依据属性举行过滤。filter 函数须要 true 或许 false 值来肯定元素是不是应当包括在效果鸠合中。基本上,假如回调表达式返回的是 truefilter 函数返回的效果会包括该元素。不然,就不会包括该元素。

一个简朴的例子是当我们有一个整数鸠应时,我们只想要过滤偶数。

敕令式编程

运用 JavaScript 来完成时,须要以下操纵:

  • 竖立一个空数组 evenNumbers
  • 迭代数字数组
  • 将偶数推到 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 高阶函数来吸收 even 函数,并返回偶数列表:

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的值。

针对此题目,敕令式JavaScript处置惩罚方案以下:

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]

我们的函数会做以下的事变 – 迭代鸠合,将鸠合当前项与 x 举行比较,假如它相符前提,则将此元素推送到 resultArray

声明式编程

但我们想要一种更具声明性的要领来处置惩罚这个题目,并运用过滤器高阶函数。

声明式 JavaScript 处置惩罚方案将是如许的:

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 起首看起来有点新鲜,但很轻易明白。

this 将作为第二个参数传给 filter 要领。在这个示例中,3x)代表 this

如许的操纵也能够用于鸠合。 设想一下,我们有一个人物鸠合,包括了 nameage 属性。

let people = [
  { name: "TK", age: 26 },
  { name: "Kaio", age: 10 },
  { name: "Kazumi", age: 30 }
];

我们愿望仅过滤指定岁数值的人,在此示例中,岁数凌驾18岁的人。

const olderThan18 = person => person.age > 18;
const overAge = people => people.filter(olderThan18);
overAge(people); // [{ name: 'TK', age: 26 }, { name: 'Kazumi', age: 30 }]

代码择要:

  • 我们有一份人员名单(姓名和岁数)。
  • 我们有一个函数 oldThan18。在这类情况下,关于 people 数组中的每一个人,我们想要接见岁数并检察它是不是凌驾 18 岁。
  • 我们依据此功用过滤一切人。

Map

map 的观点是转换一个鸠合。

map 要领会将鸠合传入函数,并依据返回的值构建新鸠合。

让我们运用适才的 people 鸠合。我们如今不想过滤岁数了。我们只想取得一个列表,元素就像:TK is 26 years old。所以末了的字符串多是 :name is:age years old 个中 :name:agepeople 鸠合中每一个元素的属性。

下面是运用敕令式 JavaScript 编码的示例:

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']

下面是运用声明式 JavaScript 编码的示例:

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']

要做的事变是将给定数组转换为新数组。

另一个风趣的 Hacker Rank 题目是更新列表题目。 我们只想用它们的绝对值更新给定数组的值。

比方,输入 [1,2,3-4,5] 须要输出为 [1,2,3,4,5]-4 的绝对值是 4

一种简朴的处置惩罚方案是将每一个鸠合的值举行当场更新 (in-place)。

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]

Wow,鹅妹子嘤!

Reduce

reduce 函数的观点是,吸收一个函数和一个鸠合,然后组合他们来竖立返回值。

一个罕见的例子是取得定单的总金额。设想一下,你正在一个购物网站购物。你增添了 Product 1Product 2Product 3Product 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 ,我们能够竖立一个用来处置惩罚累加的函数,并将其作为参数传给 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 = (cart) => cart.reduce(sumAmount, 0);

getTotalAmount(shoppingCart); // 120

这里我们有 shoppingCartsumAmount函数吸收当前的 currentTotalAmount ,对一切定单举行累加。

getTotalAmount 函数会吸收 sumAmount 函数 从 0 最先累加购物车的值。

取得总金额的另一种要领是组合运用 mapreduce。 那是什么意思? 我们能够运用 mapshoppingCart 转换为 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 函数吸收产物对象并仅返回金额值。 所以我们这里有 [10,30,20,60] 。 然后,经由过程 reduce 累加一切金额。Nice~

我们看了每一个高阶函数的事变道理。 我想向您展现一个示例,申明如何在一个简朴的示例中组合一切三个函数。

照样购物车,设想一下在我们的定单中有一个产物列表:

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 }
]

我们想要购物车中一切图书的总金额。 就那末简朴, 须要如何编写算法?

  • 运用 filter 函数过滤书本范例
  • 运用 map 函数将购物车转换为数目的鸠合
  • 运用 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

Done!

    原文作者:joking_zhang
    原文地址: https://segmentfault.com/a/1190000019052188
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞