>  기사  >  웹 프론트엔드  >  JavaScript에서 함수형 프로그래밍이란 무엇입니까? 함수형 프로그래밍 소개

JavaScript에서 함수형 프로그래밍이란 무엇입니까? 함수형 프로그래밍 소개

不言
不言앞으로
2019-04-08 09:48:282628검색

이 글의 내용은 JavaScript에서 함수형 프로그래밍이란 무엇인가에 관한 것입니다. 함수형 프로그래밍에 대한 소개는 참고할만한 가치가 있습니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

모든 프로그래머는 함수를 알고 있지만 일부 사람들은 함수형 프로그래밍의 개념을 모를 수도 있습니다.

애플리케이션의 반복은 프로그램을 점점 더 복잡하게 만들기 때문에 프로그래머는 잘 구조화되고, 읽기 쉽고, 재사용 및 유지 관리가 가능한 코드를 만드는 것이 필요합니다.

함수형 프로그래밍은 좋은 코딩 방법이지만 이것이 함수형 프로그래밍이 필요하다는 의미는 아닙니다. 프로젝트가 함수형 프로그래밍을 사용하지 않는다고 해서 프로젝트가 나쁘다는 의미는 아닙니다.

함수형 프로그래밍(FP)이란 무엇인가요?

함수형 프로그래밍은 데이터 매핑에 관심이 있는 반면, 명령형 프로그래밍은 문제 해결 단계에 관심이 있습니다.

함수형 프로그래밍의 반대말은 명령형 프로그래밍입니다.

함수형 프로그래밍언어의 변수는 명령형 프로그래밍언어의 변수, 즉 상태를 저장하는 단위가 아니지만 대수학의 변수, 즉 값의 이름입니다. 변수의 값은 불변입니다. 즉, 명령형 프로그래밍 언어처럼 변수에 값을 여러 번 할당할 수 없습니다.

Functional프로그래밍은 단지 개념(일관된 코딩 방법)일 뿐이며 엄격한 정의가 없습니다. 인터넷의 지식 포인트를 바탕으로 함수형 프로그래밍의 정의를 간략하게 요약합니다(제 요약이지만 일부 사람들은 이 견해에 동의하지 않을 수 있습니다).

함수형 프로그래밍은 순수 함수를 적용한 후 서로 다른 논리를 독립적인 함수(모듈식 사고)를 갖는 여러 순수 함수로 분리한 다음 이들을 통합하여 복잡한 함수를 만드는 것입니다.

순수함수란 무엇인가요?

함수의 입력이 결정되고 출력 결과가 고유하게 결정되고 부작용이 없으면 순수 함수입니다.

일반적으로 순수 함수는 위에서 언급한 두 가지 사항을 충족합니다.

동일한 입력은 동일한 출력을 생성해야 합니다.

계산 과정에서는 부작용이 없습니다

그렇다면 부작용을 어떻게 이해합니까?

간단히 말하면, 함수 외부 변수와 함수 내부 변수를 포함하여 변수의 값은 불변입니다.

소위 부작용은 함수 내부와 외부 사이의 상호 작용(가장 일반적인 경우는 전역 변수의 값을 수정하는 경우)을 말하며 연산 이외의 결과를 생성합니다.

여기서는 불변성을 설명합니다. 불변성은 원래 변수 값을 변경할 수 없음을 의미합니다. 또는 원래 변수 값의 변경은 반환된 결과에 영향을 미칠 수 없습니다. 변수 값이 본질적으로 불변인 것은 아닙니다.

순수 함수 특성 비교 예

위의 이론적 설명은 이 개념을 처음 접하는 프로그래머에게는 이해하기 어려울 수 있습니다. 다음은 예제를 통해 순수함수의 특징을 하나씩 설명하겠습니다.

입력값도 같고 반환값도 동일

순수함수

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

불순함수

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

test(3);

반환값은 외부변수의 영향을 받지 않습니다

불순함수, 반환값은 영향을 받습니다 다른 변수(부작용을 나타냄)에 의해 반환 값이 불확실합니다.

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

부적절한 함수로 인해 반환값이 객체 getter의 영향을 받으며 반환 결과가 불확실합니다.

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

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

고유한 매개변수와 결정된 반환 값을 가진 순수 함수입니다.

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

입력 값을 변경할 수 없습니다

불순한 함수로, 이 함수는 외부 personInfo의 값을 변경했습니다(부작용 발생).

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

순수한 함수로, 이 함수는 외부 변수에 영향을 주지 않습니다.

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

질문이 있으신가요? personInfo 객체는 참조 유형이므로 비동기 작업 중에 personInfo가 변경되면 출력 결과가 불확실할 수 있습니다.

함수에 비동기 작업이 있는 경우 이 문제가 존재하며 외부에서(아마도 깊은 복사를 통해) personInfo를 다시 변경할 수 없도록 보장해야 합니다.

그러나 이 간단한 함수에는 결과가 반환될 때까지 reverseName 함수가 실행되는 순간 p 값이 이미 결정됩니다.

personInfo가 중간에 변경되지 않도록 하려면 다음 비동기 작업이 필요합니다.

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

personInfo 수정이 비동기 작업에 영향을 미치지 않도록 다음 방법으로 수정합니다.

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

아직도 단점이 있습니다. 즉, 외부 personInfo 객체는 여전히 변경되지만 이전에 실행되었던 실행 기능에는 영향을 미치지 않습니다. 다시 run 함수를 실행하면 입력이 바뀌었고, 당연히 출력도 바뀌었습니다.

매개변수와 반환값은 어떤 유형이든 가능합니다

그런 다음 함수를 반환하는 것도 가능합니다.

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

한 가지만 시도하세요

물론 이는 실제 적용 시나리오에 따라 다릅니다. 다음은 간단한 예입니다.

두 가지 일을 함께 하세요(좋은 생각은 아닙니다):

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

无参数风格缺点?

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

위 내용은 JavaScript에서 함수형 프로그래밍이란 무엇입니까? 함수형 프로그래밍 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제