Home  >  Article  >  Web Front-end  >  What is functional programming in JavaScript? Introduction to functional programming

What is functional programming in JavaScript? Introduction to functional programming

不言
不言forward
2019-04-08 09:48:282620browse

The content of this article is about what is functional programming in JavaScript? The introduction to functional programming has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.

Every programmer knows function, but some people may not necessarily understand the concept of functional programming.

The iteration of applications makes the program more and more complex, so it is necessary for programmers to create a code that is well structured, readable, reusable and maintainable.

Functional programming is a good coding method, but this does not mean that functional programming is necessary. Just because your project doesn't use functional programming doesn't mean the project is bad.

What is functional programming (FP)?

Functional programming cares about the mapping of data, while imperative programming cares about the steps to solve problems.

The opposite of functional programming is Imperative programming.

Functional programmingThe variables in the language are not Imperative programmingThe variables in the language, that is, the unit that stores the state, but the variables in algebra, that is, a The name of the value. The value of a variable is immutable, which means that multiple assignments to a variable are not allowed like in Imperative programming languages.

FunctionalProgramming is just a concept (consistent coding method) and has no strict definition. Based on the knowledge points on the Internet, I briefly summarize the definition of functional programming (my summary, some people may disagree with this view).

Functional programming is the application of pure functions, and then separates different logic into many pure functions with independent functions (modular thinking), and then integrates them together to become complex Function.

What is a pure function?

If the input of a function is determined, then the output result is uniquely determined and has no side effects, then it is a pure function.

Generally, it is a pure function if it meets the two points mentioned above:

The same input must produce the same output

In the process of calculation, no Side effects

So how do you understand side effects?

Simply put, the value of a variable is immutable, including variables external to the function and variables internal to the function.

The so-called side effects refers to the interaction between the inside and the outside of the function (the most typical case is to modify the value of the global variable), which produces other results other than operations.

Here is a description of immutability. Immutability means that we cannot change the original variable value. Or the change of the original variable value cannot affect the returned result. It's not that variable values ​​are inherently immutable.

Pure function feature comparison example

The above theoretical description may be difficult to understand for programmers who are new to this concept. The following will illustrate the characteristics of pure functions one by one with examples.

The input is the same and the return value is the same

Pure function

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

Improper function

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

test(3);

The return value is not affected by external Influence of variables

Is an impure function, the return value will be affected by other variables (indicating side effects), and the return value is uncertain.

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

Improper function, the return value is affected by the object getter, and the return result is uncertain.

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

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

Pure function, with unique parameters and determined return value.

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

The input value cannot be changed

Impure function, this function has changed the value of the outer personInfo (producing side effects).

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 被修改了

Pure function, this function does not affect any external variables.

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 还是原值

So do you have any questions? The personInfo object is a reference type. During asynchronous operation, if personInfo is changed midway, the output result may be uncertain.

If the function has asynchronous operations, this problem does exist, and it should be ensured that personInfo cannot be changed again by the outside (perhaps through deep copy).

However, there is no asynchronous operation in this simple function. The value of p is already determined at the moment the reverseName function is run, until the result is returned.

The following asynchronous operation is required to ensure that personInfo will not be changed midway:

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 被改变了

Modify it to the following method to ensure that personInfo's midway modification will not affect the asynchronous operation:

// 这个才是纯函数
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

This still has a disadvantage, that is, the external personInfo object will still be changed, but it will not affect the run function that has been run before. If you run the run function again, the inputs have changed, and of course the output has also changed.

The parameters and return value can be of any type

Then the return function is also possible.

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

Try to do only one thing

Of course, this depends on the actual application scenario. Here is a simple example.

Do two things together (not a good practice):

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 等构造函数的便捷性进行一些过滤操作。

无参数风格缺点?

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

The above is the detailed content of What is functional programming in JavaScript? Introduction to functional programming. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:segmentfault.com. If there is any infringement, please contact admin@php.cn delete