JavaScript 之函数式编程

同步宣布于 https://github.com/xianshanna…

是个递次员都晓得函数,然则有些人不肯定清晰函数式编程的看法。

运用的迭代使递次变得愈来愈庞杂,那末递次员很有必要制造一个组织优越、可读性好、重用性高和可保护性高的代码。

函数式编程就是一个优越的代码体式格局,然则这不代表函数式编程是必需的。你的项目没用到函数式编程,不代表项目不好。

什么是函数式编程(FP)?

函数式编程体贴数据的映照,敕令式编程体贴解决题目的步骤。

函数式编程的对立面就是敕令式编程

函数式编程语言中的变量也不是
敕令式编程语言中的变量,即存储状况的单位,而是代数中的变量,即一个值的称号。 变量的值是不可变的(immutable),也就是说不允许像
敕令式编程语言中那样屡次给一个变量赋值。

函数式编程只是一个看法(一致编码体式格局),并没有严厉的定义。本人依据网上的知识点,简朴的总结一下函数式编程的定义(本人总结,也许有人会差异意这个看法)。

函数式编程就是纯函数的运用,然后把差异的逻辑星散为很多自力功用的纯函数(模块化头脑),然后再整合在一同,变成庞杂的功用。

什么是纯函数?

一个函数假如输入肯定,那末输出效果是唯一肯定的,而且没有副作用,那末它就是纯函数。

平常相符上面提到的两点就算纯函数:

  • 雷同的输入肯定发生雷同的输出
  • 在盘算的过程当中,不会发生副作用

那怎样邃晓副作用呢?

简朴的说就是变量的值不可变,包含函数外部变量和函数内部变量。

所谓
副作用,指的是函数内部与外部互动(最典范的状况,就是修正全局变量的值),发生运算之外的其他效果。

这里申明一下不可变不可变指的是我们不能转变原本的变量值。或许原本变量值的转变,不能影响到返回效果。不是变量值原本就是不可变。

纯函数特征对照例子

上面的理论形貌关于刚打仗这个看法的递次员,也许不好邃晓。下面会经由过程纯函数的特性逐一举例申明。

输入雷同返回值雷同

纯函数

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 函数。假如再次运转 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 === 'RE' && !task.completed) {
      filteredTasks.push({ ...task, userName: task.user.name });
    }
  }
  return filteredTasks;
}
const filteredTasks = getFilteredTasks(tasks);

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

两件事离开做(引荐的做法):

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

isPriorityTasktoTaskView 就是纯函数,而且都只做了一件事,也能够零丁重复运用。

效果可缓存

依据纯函数的定义,只需输入肯定,那末输出效果就肯定肯定。我们就能够针对纯函数返回效果举行缓存(缓存代办设想形式)。

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];
  };
})();

函数式编程有什么长处?

实行函数式编程的头脑,我们应当只管让我们的函数有以下的长处:

  • 更轻易邃晓
  • 更轻易重复运用
  • 更轻易测试
  • 更轻易保护
  • 更轻易重构
  • 更轻易优化
  • 更轻易推理

函数式编程有什么瑕玷?

  • 机能能够相对来讲较差

    函数式编程能够会捐躯时候庞杂度来调换了可读性和保护性。然则呢,这个对用户来讲这个机能非常细小,有些场景以至可忽略不计。前端平常场景不存在非常大的数据量盘算,所以你尽可宁神的运用函数式编程。看下上面提到个的例子(数据量要轻微大一点才好对照):

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

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

    两件事一同做,代码可读性不够好,理论上时候庞杂度为 o(n),不斟酌 push 的庞杂度

    (function() {
      function getFilteredTasks(tasks) {
        let filteredTasks = [];
        for (let i = 0; i < tasks.length; i++) {
          let task = tasks[i];
          if (task.type === 'RE' && !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;
    },
    divide(...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)
  .divide(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: 'one',
    },
    type: 'RE',
  });
  tasks.push({
    user: {
      name: 'two',
    },
    type: '',
  });
}
function isPriorityTask(task) {
  return task.type === 'RE' && !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('');
};
var reverse = function(arr) {
  return arr.reverse();
};
var join = function(arr) {
  return arr.join('');
};

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

无参数作风长处?

参作风的优点就是不须要费心机去给它的参数举行定名,把一些现成的函数按需组合起来运用。更轻易邃晓、代码简小,同时星散的回调函数,是能够复用的。假如运用了原生 js 如数组,还能够应用 Boolean 等组织函数的便利性举行一些过滤支配。

无参数作风瑕玷?

瑕玷就是须要熟习无参数作风,刚打仗不能够就能够用得随心所欲的。关于一些新手,能够第一时候邃晓起来没那没快。

参考文章

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