Maison  >  Article  >  interface Web  >  Qu’est-ce que la programmation fonctionnelle en JavaScript ? Introduction à la programmation fonctionnelle

Qu’est-ce que la programmation fonctionnelle en JavaScript ? Introduction à la programmation fonctionnelle

不言
不言avant
2019-04-08 09:48:282630parcourir

Le contenu de cet article porte sur ce qu'est la programmation fonctionnelle en JavaScript ? L'introduction à la programmation fonctionnelle a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer, j'espère qu'elle vous sera utile.

Chaque programmeur connaît la fonction, mais certaines personnes ne comprennent pas nécessairement le concept de programmation fonctionnelle.

L'itération des applications rend les programmes de plus en plus complexes, il est donc nécessaire pour les programmeurs de créer un code bien structuré, lisible, réutilisable et maintenable.

La programmation fonctionnelle est une bonne méthode de codage, mais cela ne signifie pas que la programmation fonctionnelle est nécessaire. Ce n’est pas parce que votre projet n’utilise pas de programmation fonctionnelle qu’il est mauvais.

Qu'est-ce que la programmation fonctionnelle (FP) ?

La programmation fonctionnelle concerne le mappage des données, tandis que la programmation impérative concerne les étapes à suivre pour résoudre les problèmes.

Le contraire de la programmation fonctionnelle est la programmation impérative.

Programmation fonctionnelleLes variables du langage ne sont pas les variables du langage de Programmation impérative, c'est-à-dire l'unité qui stocke l'état, mais les variables de l'algèbre , c'est-à-dire un Le nom de la valeur. La valeur d'une variable est immuable, ce qui signifie que plusieurs affectations à une variable ne sont pas autorisées comme dans les langages de Programmation impérative.

Fonctionnelle La programmation n'est qu'un concept (méthode de codage cohérente) et n'a pas de définition stricte. Sur la base des points de connaissances sur Internet, je résume brièvement la définition de la programmation fonctionnelle (mon résumé, certaines personnes peuvent être en désaccord avec ce point de vue).

La programmation fonctionnelle est l'application de fonctions pures, puis sépare différentes logiques en de nombreuses fonctions pures avec des fonctions indépendantes (pensée modulaire), puis les intègre ensemble pour devenir une fonction complexe.

Qu'est-ce qu'une fonction pure ?

Si l'entrée d'une fonction est déterminée, le résultat de sortie est déterminé de manière unique et n'a aucun effet secondaire, alors c'est une fonction pure.

Généralement, il s'agit d'une fonction pure si elle répond aux deux points mentionnés ci-dessus :

La même entrée doit produire la même sortie

Pendant le processus de calcul, pas d'effets secondaires

Alors, comment comprenez-vous les effets secondaires ?

Pour faire simple, les valeurs des variables sont immuables, y compris les variables externes aux fonctions et les variables internes aux fonctions.

Le soi-disant effet secondaire fait référence à l'interaction entre l'intérieur et l'extérieur de la fonction (le cas le plus typique est de modifier la valeur de la variable globale), qui produit d'autres résultats autres que les opérations.

Ici, nous expliquons l'immuabilité. L'immuabilité signifie que nous ne pouvons pas modifier la valeur d'origine de la variable. Ou la modification de la valeur de la variable d'origine ne peut pas affecter le résultat renvoyé. Ce n'est pas que les valeurs variables soient intrinsèquement immuables.

Exemple de comparaison de fonctionnalités de fonctions pures

La description théorique ci-dessus peut être difficile à comprendre pour les programmeurs qui sont nouveaux dans ce concept. Les caractéristiques des fonctions pures seront expliquées ci-dessous avec des exemples.

L'entrée est la même et la valeur de retour est la même

Fonction pure

function test(pi) {
  // 只要 pi 确定,返回结果就一定确定。
  return pi + 2;
}
test(3);

Fonction inappropriée

function test(pi) {
  // 随机数返回值不确定
  return pi + Math.random();
}

test(3);

Retour La valeur n'est pas affectée par des variables externes

Fonction incorrecte, la valeur de retour sera affectée par d'autres variables (indiquant des effets secondaires) et la valeur de retour est incertaine.

let a = 2;
function test(pi) {
  // a 的值可能中途被修改
  return pi + a;
}
a = 3;
test(3);

Fonction incorrecte, la valeur de retour est affectée par le getter d'objet et le résultat du retour est incertain.

const obj = Object.create(
  {},
  {
    bar: {
      get: function() {
        return Math.random();
      },
    },
  }
);

function test(obj) {
  // obj.a 的值是随机数
  return obj.a;
}
test(obj);

Fonction pure, avec des paramètres uniques et une valeur de retour déterminée.

function test(pi) {
  // 只要 pi 确定,返回结果就一定确定。
  return pi + 2;
}
test(3);

La valeur d'entrée ne peut pas être modifiée

Fonction inappropriée, cette fonction a modifié la valeur de la personne externe (produisant des effets secondaires).

const personInfo = { firstName: 'shannan', lastName: 'xian' };

function revereName(p) {
  p.lastName = p.lastName
    .split('')
    .reverse()
    .join('');
  p.firstName = p.firstName
    .split('')
    .reverse()
    .join('');
  return `${p.firstName} ${p.lastName}`;
}
revereName(personInfo);
console.log(personInfo);
// 输出 { firstName: 'nannahs',lastName: 'naix' }
// personInfo 被修改了

Fonction pure, cette fonction n'affecte aucune variable externe.

const personInfo = { firstName: 'shannan', lastName: 'xian' };

function reverseName(p) {
  const lastName = p.lastName
    .split('')
    .reverse()
    .join('');
  const firstName = p.firstName
    .split('')
    .reverse()
    .join('');
  return `${firstName} ${lastName}`;
}
revereName(personInfo);
console.log(personInfo);
// 输出 { firstName: 'shannan',lastName: 'xian' }
// personInfo 还是原值

Alors, avez-vous des questions ? L'objet personInfo est un type référence Lors d'un fonctionnement asynchrone, si personInfo est modifié à mi-chemin, le résultat de la sortie peut être incertain.

Si la fonction a des opérations asynchrones, ce problème existe, et il faut en effet s'assurer que personInfo ne peut pas être à nouveau modifié par l'extérieur (via une copie profonde).

Cependant, il n'y a pas d'opération asynchrone dans cette fonction simple. La valeur de p est déjà déterminée au moment de l'exécution de la fonction reverseName, jusqu'à ce que le résultat soit renvoyé.

L'opération asynchrone suivante est requise pour garantir que personInfo ne sera pas modifiée à mi-chemin :

async function reverseName(p) {
  await new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, 1000);
  });
  const lastName = p.lastName
    .split('')
    .reverse()
    .join('');
  const firstName = p.firstName
    .split('')
    .reverse()
    .join('');
  return `${firstName} ${lastName}`;
}

const personInfo = { firstName: 'shannan', lastName: 'xian' };

async function run() {
  const newName = await reverseName(personInfo);
  console.log(newName);
}

run();
personInfo.firstName = 'test';
// 输出为 tset naix,因为异步操作的中途 firstName 被改变了

Modifiez-la avec la méthode suivante pour garantir que la modification de personInfo n'affectera pas l'opération asynchrone :

// 这个才是纯函数
async function reverseName(p) {
  // 浅层拷贝,这个对象并不复杂
  const newP = { ...p };
  await new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, 1000);
  });
  const lastName = newP.lastName
    .split('')
    .reverse()
    .join('');
  const firstName = newP.firstName
    .split('')
    .reverse()
    .join('');
  return `${firstName} ${lastName}`;
}

const personInfo = { firstName: 'shannan', lastName: 'xian' };

// run 不是纯函数
async function run() {
  const newName = await reverseName(personInfo);
  console.log(newName);
}

// 当然小先运行 run,然后再去改 personInfo 对象。
run();
personInfo.firstName = 'test';
// 输出为 nannahs naix

Cela présente toujours un inconvénient, c'est-à-dire que l'objet personInfo externe sera toujours modifié, mais cela n'affectera pas la fonction d'exécution qui a été exécutée auparavant. Si vous exécutez à nouveau la fonction run, les entrées ont changé, et bien sûr la sortie a également changé.

Les paramètres et la valeur de retour peuvent être de n'importe quel type

Ensuite, les fonctions de retour sont également possibles.

function addX(y) {
  return function(x) {
    return x + y;
  };
}

Essayez de faire une seule chose

Bien sûr, cela dépend du scénario d'application réel. Voici un exemple simple.

Faire deux choses ensemble (pas une bonne idée) :

function getFilteredTasks(tasks) {
  let filteredTasks = [];
  for (let i = 0; i < tasks.length; i++) {
    let task = tasks[i];
    if (task.type === &#39;RE&#39; && !task.completed) {
      filteredTasks.push({ ...task, userName: task.user.name });
    }
  }
  return filteredTasks;
}
const filteredTasks = getFilteredTasks(tasks);

getFilteredTasks 也是纯函数,但是下面的纯函数更好。

两件事分开做(推荐的做法):

function isPriorityTask(task) {
  return task.type === &#39;RE&#39; && !task.completed;
}
function toTaskView(task) {
  return { ...task, userName: task.user.name };
}
let filteredTasks = tasks.filter(isPriorityTask).map(toTaskView);

isPriorityTask 和 toTaskView 就是纯函数,而且都只做了一件事,也可以单独反复使用。

结果可缓存

根据纯函数的定义,只要输入确定,那么输出结果就一定确定。我们就可以针对纯函数返回结果进行缓存(缓存代理设计模式)。

const personInfo = { firstName: &#39;shannan&#39;, lastName: &#39;xian&#39; };

function reverseName(firstName, lastName) {
  const newLastName = lastName
    .split(&#39;&#39;)
    .reverse()
    .join(&#39;&#39;);
  const newFirstName = firstName
    .split(&#39;&#39;)
    .reverse()
    .join(&#39;&#39;);
  console.log(&#39;在 proxyReverseName 中,相同的输入,我只运行了一次&#39;);
  return `${newFirstName} ${newLastName}`;
}

const proxyReverseName = (function() {
  const cache = {};
  return (firstName, lastName) => {
    const name = firstName + lastName;
    if (!cache[name]) {
      cache[name] = reverseName(firstName, lastName);
    }
    return cache[name];
  };
})();

函数式编程有什么优点?

实施函数式编程的思想,我们应该尽量让我们的函数有以下的优点:

更容易理解

更容易重复使用

更容易测试

更容易维护

更容易重构

更容易优化

更容易推理

函数式编程有什么缺点?

性能可能相对来说较差

函数式编程可能会牺牲时间复杂度来换取了可读性和维护性。但是呢,这个对用户来说这个性能十分微小,有些场景甚至可忽略不计。前端一般场景不存在非常大的数据量计算,所以你尽可放心的使用函数式编程。看下上面提到个的例子(数据量要稍微大一点才好对比):

首先我们先赋值 10 万条数据:

const tasks = [];
for (let i = 0; i < 100000; i++) {
  tasks.push({
    user: {
      name: &#39;one&#39;,
    },
    type: &#39;RE&#39;,
  });
  tasks.push({
    user: {
      name: &#39;two&#39;,
    },
    type: &#39;&#39;,
  });
}

两件事一起做,代码可读性不够好,理论上时间复杂度为 o(n),不考虑 push 的复杂度

(function() {
  function getFilteredTasks(tasks) {
    let filteredTasks = [];
    for (let i = 0; i < tasks.length; i++) {
      let task = tasks[i];
      if (task.type === &#39;RE&#39; && !task.completed) {
        filteredTasks.push({ ...task, userName: task.user.name });
      }
    }
    return filteredTasks;
  }

  const timeConsumings = [];

  for (let k = 0; k < 100; k++) {
    const beginTime = +new Date();
    getFilteredTasks(tasks);
    const endTime = +new Date();

    timeConsumings.push(endTime - beginTime);
  }

  const averageTimeConsuming =
    timeConsumings.reduce((all, current) => {
      return all + current;
    }) / timeConsumings.length;

  console.log(`第一种风格平均耗时:${averageTimeConsuming} 毫秒`);
})();

两件事分开做,代码可读性相对好,理论上时间复杂度接近 o(2n)

(function() {
  function isPriorityTask(task) {
    return task.type === 'RE' && !task.completed;
  }
  function toTaskView(task) {
    return { ...task, userName: task.user.name };
  }

  const timeConsumings = [];

  for (let k = 0; k < 100; k++) {
    const beginTime = +new Date();
    tasks.filter(isPriorityTask).map(toTaskView);
    const endTime = +new Date();

    timeConsumings.push(endTime - beginTime);
  }

  const averageTimeConsuming =
    timeConsumings.reduce((all, current) => {
      return all + current;
    }) / timeConsumings.length;

  console.log(`第二种风格平均耗时:${averageTimeConsuming} 毫秒`);
})();

上面的例子多次运行得出耗时平均值,在数据较少和较多的情况下,发现两者平均值并没有多大差别。10 万条数据,运行 100 次取耗时平均值,第二种风格平均多耗时 15 毫秒左右,相当于 10 万条数据多耗时 1.5 秒,1 万条数多据耗时 150 毫秒(150 毫秒用户基本感知不到)。

虽然理论上时间复杂度多了一倍,但是在数据不庞大的情况下(会有个临界线的),这个性能相差其实并不大,完全可以牺牲浏览器用户的这点性能换取可读和可维护性。

很可能被过度使用

过度使用反而是项目维护性变差。有些人可能写着写着,就变成别人看不懂的代码,自己觉得挺高大上的,但是你确定别人能快速的看懂不? 适当的使用才是合理的。

应用场景

概念是概念,实际应用却是五花八门,没有实际应用,记住了也是死记硬背。这里总结一些常用的函数式编程应用场景。

简单使用

有时候很多人都用到了函数式的编程思想(最简单的用法),但是没有意识到而已。下面的列子就是最简单的应用,这个不用怎么说明,根据上面的纯函数特点,都应该看的明白。

function sum(a, b) {
  return a + b;
}

立即执行的匿名函数

匿名函数经常用于隔离内外部变量(变量不可变)。

const personInfo = { firstName: 'shannan', lastName: 'xian' };

function reverseName(firstName, lastName) {
  const newLastName = lastName
    .split('')
    .reverse()
    .join('');
  const newFirstName = firstName
    .split('')
    .reverse()
    .join('');
  console.log('在 proxyReverseName 中,相同的输入,我只运行了一次');
  return `${newFirstName} ${newLastName}`;
}

// 匿名函数
const proxyReverseName = (function() {
  const cache = {};
  return (firstName, lastName) => {
    const name = firstName + lastName;
    if (!cache[name]) {
      cache[name] = reverseName(firstName, lastName);
    }
    return cache[name];
  };
})();

JavaScript 的一些 API

如数组的 forEach、map、reduce、filter 等函数的思想就是函数式编程思想(返回新数组),我们并不需要使用 for 来处理。

const arr = [1, 2, '', false];
const newArr = arr.filter(Boolean);
// 相当于 const newArr = arr.filter(value => Boolean(value))

递归

递归也是一直常用的编程方式,可以代替 while 来处理一些逻辑,这样的可读性和上手度都比 while 简单。

如下二叉树所有节点求和例子:

const tree = {
  value: 0,
  left: {
    value: 1,
    left: {
      value: 3,
    },
  },
  right: {
    value: 2,
    right: {
      value: 4,
    },
  },
};

while 的计算方式:

function sum(tree) {
  let sumValue = 0;
  // 使用列队方式处理,使用栈也可以,处理顺序不一样
  const stack = [tree];

  while (stack.length !== 0) {
    const currentTree = stack.shift();
    sumValue += currentTree.value;

    if (currentTree.left) {
      stack.push(currentTree.left);
    }

    if (currentTree.right) {
      stack.push(currentTree.right);
    }
  }

  return sumValue;
}

递归的计算方式:

function sum(tree) {
  let sumValue = 0;

  if (tree && tree.value !== undefined) {
    sumValue += tree.value;

    if (tree.left) {
      sumValue += sum(tree.left);
    }
    if (tree.right) {
      sumValue += sum(tree.right);
    }
  }

  return sumValue;
}

递归会比 while 代码量少,而且可读性更好,更容易理解。

链式编程

如果接触过 jquery,我们最熟悉的莫过于 jq 的链式便利了。现在 ES6 的数组操作也支持链式操作:

const arr = [1, 2, '', false];
const newArr = arr.filter(Boolean).map(String);
// 输出 "1", "2"]

或者我们自定义链式,加减乘除的链式运算:

function createOperation() {
  let theLastValue = 0;
  const plusTwoArguments = (a, b) => a + b;
  const multiplyTwoArguments = (a, b) => a * b;

  return {
    plus(...args) {
      theLastValue += args.reduce(plusTwoArguments);
      return this;
    },
    subtract(...args) {
      theLastValue -= args.reduce(plusTwoArguments);
      return this;
    },
    multiply(...args) {
      theLastValue *= args.reduce(multiplyTwoArguments);
      return this;
    },
    pide(...args) {
      theLastValue /= args.reduce(multiplyTwoArguments);
      return this;
    },
    valueOf() {
      const returnValue = theLastValue;
      // 获取值的时候需要重置
      theLastValue = 0;
      return returnValue;
    },
  };
}
const operaton = createOperation();
const result = operation
  .plus(1, 2, 3)
  .subtract(1, 3)
  .multiply(1, 2, 10)
  .pide(10, 5)
  .valueOf();
console.log(result);

当然上面的例子不完全都是函数式编程,因为 valueOf 的返回值就不确定。

高阶函数

高阶函数(Higher Order Function),按照维基百科上面的定义,至少满足下列一个条件的函数

函数作为参数传入

返回值为一个函数

简单的例子:

function add(a, b, fn) {
  return fn(a) + fn(b);
}
function fn(a) {
  return a * a;
}
add(2, 3, fn); // 13

还有一些我们平时常用高阶的方法,如 map、reduce、filter、sort,以及现在常用的 redux 中的 connect 等高阶组件也是高阶函数。

柯里化(闭包)

柯里化(Currying),又称部分求值(Partial Evaluation),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

柯里化的作用以下优点:

参数复用

提前返回

延迟计算/运行

缓存计算值

柯里化实质就是闭包。其实上面的立即执行匿名函数的例子就用到了柯里化。

// 柯里化之前
function add(x, y) {
  return x + y;
}

add(1, 2); // 3

// 柯里化之后
function addX(y) {
  return function(x) {
    return x + y;
  };
}

addX(2)(1); // 3

高阶组件

这是组件化流行后的一个新概念,目前经常用到。ES6 语法中 class 只是个语法糖,实际上还是函数。

一个简单例子:

class ComponentOne extends React.Component {
  render() {
    return <h1>title</h1>;
  }
}

function HocComponent(Component) {
  Component.shouldComponentUpdate = function(nextProps, nextState) {
    if (this.props.id === nextProps.id) {
      return false;
    }
    return true;
  };
  return Component;
}

export default HocComponent(ComponentOne);

深入理解高阶组件请看这里。

无参数风格(Point-free)

其实上面的一些例子已经使用了无参数风格。无参数风格不是没参数,只是省略了多余参数的那一步。看下面的一些例子就很容易理解了。

范例一:

const arr = [1, 2, '', false];
const newArr = arr.filter(Boolean).map(String);
// 有参数的用法如下:
// arr.filter(value => Boolean(value)).map(value => String(value));

范例二:

const tasks = [];
for (let i = 0; i < 1000; i++) {
  tasks.push({
    user: {
      name: &#39;one&#39;,
    },
    type: &#39;RE&#39;,
  });
  tasks.push({
    user: {
      name: &#39;two&#39;,
    },
    type: &#39;&#39;,
  });
}
function isPriorityTask(task) {
  return task.type === &#39;RE&#39; && !task.completed;
}
function toTaskView(task) {
  return { ...task, userName: task.user.name };
}
tasks.filter(isPriorityTask).map(toTaskView);

范例三:

// 比如,现成的函数如下:
var toUpperCase = function(str) {
  return str.toUpperCase();
};
var split = function(str) {
  return str.split(&#39;&#39;);
};
var reverse = function(arr) {
  return arr.reverse();
};
var join = function(arr) {
  return arr.join(&#39;&#39;);
};

// 现要由现成的函数定义一个 point-free 函数toUpperCaseAndReverse
var toUpperCaseAndReverse = _.flowRight(
  join,
  reverse,
  split,
  toUpperCase
); // 自右向左流动执行
// toUpperCaseAndReverse是一个point-free函数,它定义时并无可识别参数。只是在其子函数中操纵参数。flowRight 是引入了 lodash 库的组合函数,相当于 compose 组合函数
console.log(toUpperCaseAndReverse(&#39;abcd&#39;)); // => DCBA

无参数风格优点?

参风格的好处就是不需要费心思去给它的参数进行命名,把一些现成的函数按需组合起来使用。更容易理解、代码简小,同时分离的回调函数,是可以复用的。如果使用了原生 js 如数组,还可以利用 Boolean 等构造函数的便捷性进行一些过滤操作。

无参数风格缺点?

缺点就是需要熟悉无参数风格,刚接触不可能就可以用得得心应手的。对于一些新手,可能第一时间理解起来没那没快。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer