JavaScript10bet: 之函数式编程

来源:http://www.chinese-glasses.com 作者:Web前端 人气:118 发布时间:2020-05-06
摘要:时间: 2019-09-11阅读: 128标签: 编程 时间: 2019-04-07阅读: 234标签: 函数 百科定义:函数化编程:又称泛函数编程,是一种编程泛式,它将电脑运算视为数学上的函数计算,并且避免使用程序

时间: 2019-09-11阅读: 128标签: 编程

时间: 2019-04-07阅读: 234标签: 函数

百科定义:函数化编程: 又称泛函数编程,是一种编程泛式,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。简单理解,以函数为单元,对复杂逻辑进行拆分,将复杂逻辑转化为多个简单函数逻辑,同时通过对函数进行层层调用,来达到最终目的。特点: 函数可以作为参数传入,也可以作为返回值返回。

是个程序员都知道函数,但是有些人不一定清楚函数式编程的概念。应用的迭代使程序变得越来越复杂,那么程序员很有必要创造一个结构良好、可读性好、重用性高和可维护性高的代码。

  1. 纯函数 非纯函数

函数式编程就是一个良好的代码方式,但是这不代表函数式编程是必须的。你的项目没用到函数式编程,不代表项目不好。

定义: 输入一个x产生输出一个唯一y值特点:1. 输入相同的值时,输出也是一样的,不受外部环境影响2. 运行时,无副作用,不对外部环境产生影响

什么是函数式编程(FP)?函数式编程关心数据的映射,命令式编程关心解决问题的步骤。

Array.prototype.slice;function add(a) { return a+1}add(1)add(1) 

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

那么非纯函数则是正好相反:易受外部环境影响,提高了系统复杂性。

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

 var arr = [1, 3, 5, 6]; arr.splice(0,1) == [1] arr.splice(0,1) == [3] var b = 34; function add(a) { return a + b; }

10bet,函数式编程只是一个概念(一致编码方式),并没有严格的定义。本人根据网上的知识点,简单的总结一下函数式编程的定义(本人总结,或许有人会不同意这个观点)。

纯函数是函数编程的基础,那么如何将非纯函数转化为纯函数呢?

函数式编程就是纯函数的应用,然后把不同的逻辑分离为许多独立功能的纯函数(模块化思想),然后再整合在一起,变成复杂的功能。

  1. 函数柯里化(curry)

什么是纯函数?一个函数如果输入确定,那么输出结果是唯一确定的,并且没有副作用,那么它就是纯函数。

简单的定义就是:将一个低阶函数转化为高阶函数的过程被称之为柯里化。这样说未免有点不太清楚,就是将一个需要传入多个参数的函数转化为多个只需要传入一个参数的函数。

一般符合上面提到的两点就算纯函数:

function (arg1, arg2, arg3) == function(arg1)(arg2)(arg3) function add(a, b){ return a+b }add(1,2)function add(a) { return function(b){ return a+b; }}add(1)(2)

相同的输入必定产生相同的输出在计算的过程中,不会产生副作用

这样一看是不是就清楚的多了,其中有提到一个概念高阶函数,那么什么是高阶函数呢?高阶函数: "Higher-order function",js中的函数都是指向某个变量的,那么同样可以指向某个函数,同理,也可以将某个函数作为返回值,返回。

那怎么理解副作用呢?

所谓的高阶函数,就是可以接受一个函数为参数或返回一个函数的函数。

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

var arr = [1,2,34]arr.map(function(item, index){ return item*2}) function add(a, b, fn){ return fn(a) + fn(b)}

所谓副作用,指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。

  1. 声明式语句 命令式语句

这里说明一下不可变不可变指的是我们不能改变原来的变量值。或者原来变量值的改变,不能影响到返回结果。不是变量值本来就是不可变。

顾名思义,命令式语句是,一步一步的指令,告诉你要怎么做,而声明式语句,则是只需要说要什么,具体怎么做,你自己看着办

纯函数特性对比例子

//命令式:var arr = [1, 2, 4, 5],result = [];for(var i = 0; i arr.length; i++) { result.push(arr[i])} //声明式:var result = arr.map(function(item){return item})//从这两个例子,就可以清楚的看到命令式语句和声明式语句的区别了

上面的理论描述对于刚接触这个概念的程序员,或许不好理解。下面会通过纯函数的特点一一举例说明。

通过上面的内容,已经对js函数化编程有了一个较为基础的浅显的认识,至于更深层次的认识,就需要看看其他大佬的文章了。

输入相同返回值相同

纯函数

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);

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

结果可缓存

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

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: '', });}

本文由10bet发布于Web前端,转载请注明出处:JavaScript10bet: 之函数式编程

关键词:

最火资讯