关于前端:js函数式编程讲解

47次阅读

共计 5343 个字符,预计需要花费 14 分钟才能阅读完成。

什么是函数式编程

  • 是一种编程范型,它将电脑运算视为数学上的函数计算,并且防止应用程序状态以及易变对象。
  • 函数式编程更加强调程序执行的后果而非执行的过程,提倡利用若干简略的执行单元让计算结果一直渐进,逐层推导简单的运算,而不是设计一个简单的执行过程。
  • 函数式编程的思维过程是齐全不同的,它的着眼点是 函数 ,而不是 过程,它强调的是如何通过函数的组合变换去解决问题,而不是我通过写什么样的语句去解决问题

为什么叫函数式编程

依据学术上函数的定义,函数即是一种形容汇合和汇合之间的 转换关系 ,输出通过函数都会返回 有且只有一个 输入值。函数 实际上是一个 关系,或者说是一种映射,而这种映射关系是能够组合的。

在咱们的编程世界中,咱们须要解决的其实也只有“数据”和“关系”,而关系就是函数。咱们所谓的 编程工作 也不过就是在找一种 映射关系,一旦关系找到了,问题就解决了,剩下的事件,就是让数据流过这种关系,而后转换成另一个数据。

函数式编程的特点

函数是一等公民

你能够像看待任何其余数据类型一样看待它们——把它们存在数组里,当作参数传递,赋值给变量 … 等等。应用总有返回值的表达式而不是语句

// 函数式编程 - 函数作为返回参数
const add = (x) => {return plus = (y) => {return x + y;}
};
let plus1 = add(1);
let plus2 = add(2);

console.log(plus1(1)); // 2
console.log(plus2(1)); // 3

申明式编程 (Declarative Programming)

不再批示计算机如何工作,而是指出咱们明确心愿失去的后果。与命令式不同,申明式意味着咱们要写表达式,而不是一步一步的批示。

以 SQL 为例,它就没有“先做这个,再做那个”的命令,有的只是一个指明咱们想要从数据库取什么数据的表达式。至于如何取数据则是由它本人决定的。当前数据库降级也好,SQL 引擎优化也好,基本不须要更改查问语句。

无状态和数据不可变 (Statelessness and Immutable data)

这是函数式编程的外围概念:

  • 数据不可变: 它要求你所有的数据都是不可变的,这意味着如果你想批改一个对象,那你应该创立一个新的对象用来批改,而不是批改已有的对象。
  • 无状态: 次要是强调对于一个函数,不论你何时运行,它都应该像第一次运行一样,给定雷同的输出,给出雷同的输入,齐全不依赖内部状态的变动。
// 比拟 Array 中的 slice 和 splice
let test = [1, 2, 3, 4, 5];

// slice 为纯函数,返回一个新的数组
console.log(test.slice(0, 3)); // [1, 2, 3]
console.log(test); // [1, 2, 3, 4, 5]

// splice 则会批改参数数组
console.log(test.splice(0, 3)); // [1, 2, 3]
console.log(test); // [4, 5]

函数应该纯天然,无副作用

纯函数是这样一种函数,即雷同的输出,永远会失去雷同的输入,而且没有任何可察看的副作用。

副作用是指,函数外部与内部互动,产生运算以外的其余后果。例如在函数调用的过程中,利用并批改到了内部的变量,那么就是一个有副作用的函数。

副作用可能蕴含,但不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 申请
  • 可变数据
  • 打印 /log
  • 获取用户输出
  • DOM 查问
  • 拜访零碎状态

纯函数的长处:

  1. 可缓存性。

    纯函数可能依据输出来做缓存。

  2. 可移植性/自文档化。

    • 可移植性能够意味着把函数序列化(serializing)并通过 socket 发送。也能够意味着代码可能在 web workers 中运行。
    • 纯函数是齐全自力更生的,它须要的所有货色都能轻易取得。纯函数的依赖很明确,因而更易于察看和了解

  3. 可测试性(Testable)

    纯函数让测试更加容易。咱们不须要伪造一个“实在的”领取网关,或者每一次测试之前都要配置、之后都要断言状态(assert the state)。只需简略地给函数一个输出,而后断言输入就好了。

  4. 合理性(Reasonable)

    很多人置信应用纯函数最大的益处是_援用透明性_(referential transparency)。如果一段代码能够替换成它执行所得的后果,而且是在不扭转整个程序行为的前提下替换的,那么咱们就说这段代码是援用通明的。

    因为纯函数总是可能依据雷同的输出返回雷同的输入,所以它们就可能保障总是返回同一个后果,这也就保障了援用透明性。

  1. 并行代码

    咱们能够并行运行任意纯函数。因为纯函数基本不须要访问共享的内存,而且依据其定义,纯函数也不会因副作用而进入竞争态(race condition)。

面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只须要一个香蕉,但却失去一个拿着香蕉的大猩猩 … 以及整个丛林

惰性执行(Lazy Evaluation)

函数只在须要的时候执行,不产生无意义的两头变量。从头到尾都在写函数,只有在最初的时候才通过调用 产生理论的后果。

函数式编程中有两种操作是必不可少的:柯里化(Currying) 函数组合(Compose)

柯里化

把承受多个参数的函数变换成承受一个繁多参数 (最后函数的第一个参数) 的函数,只传递给函数一部分参数来调用它,让它返回一个函数去解决剩下的参数。

函数式编程 + 柯里化,将提取成柯里化的函数局部配置好之后,可作为参数传入,简化操作流程。

// 给 list 中每个元素先加 1,再加 5,再减 1
let list = [1, 2, 3, 4, 5];

// 失常做法
let list1 = list.map((value) => {return value + 1;});
let list2 = list1.map((value) => {return value + 5;});
let list3 = list2.map((value) => {return value - 1;});
console.log(list3); // [6, 7, 8, 9, 10]

// 柯里化
const changeList = (num) => {return (data) => {return data + num}
};
let list1 = list.map(changeList(1)).map(changeList(5)).map(changeList(-1));
console.log(list1); // [6, 7, 8, 9, 10]

返回的函数就通过闭包的形式记住了传入的第一个参数

一次次地调用它切实是有点繁琐,咱们能够应用一个非凡的 curry 帮忙函数(helper function)使这类函数的定义和调用更加容易。参考 前端进阶面试题具体解答

var curry = require('lodash').curry;

var match = curry(function(what, str) {return str.match(what);
});

var replace = curry(function(what, replacement, str) {return str.replace(what, replacement);
});

var filter = curry(function(f, ary) {return ary.filter(f);
});

var map = curry(function(f, ary) {return ary.map(f);
});

下面的代码中遵循的是一种简略,同时也十分重要的模式。即策略性地把要操作的数据(String,Array)放到最初一个参数里。

你能够一次性地调用 curry 函数,也能够每次只传一个参数分屡次调用。

match(/\s+/g, "hello world");
// [' ']

match(/\s+/g)("hello world");
// [' ']

var hasSpaces = match(/\s+/g);
// function(x) {return x.match(/\s+/g) }

hasSpaces("hello world");
// [' ']

hasSpaces("spaceless");
// null

这里表明的是一种“预加载”函数的能力,通过传递一到两个参数调用函数,就能失去一个记住了这些参数的新函数。

curry 的用途十分宽泛,就像在 hasSpacesfindSpacescensored 看到的那样,只需传给函数一些参数,就能失去一个新函数。

map 简略地把参数是单个元素的函数包裹一下,就能把它转换成参数为数组的函数。

var getChildren = function(x) {return x.childNodes;};

var allTheChildren = map(getChildren);

只传给函数一部分参数通常也叫做_部分调用_(partial application),可能大量缩小样板文件代码(boilerplate code)。

当咱们议论_纯函数_的时候,咱们说它们承受一个输出返回一个输入。curry 函数所做的正是这样:每传递一个参数调用函数,就返回一个新函数解决残余的参数。这就是一个输出对应一个输入啊。哪怕输入是另一个函数,它也是纯函数。

函数组合

函数组合的目标是将多个函数组合成一个函数。

const compose = (f, g) => {return (x) => {return f(g(x));
  };
};

compose 的定义中,g 将先于 f 执行,因而就创立了一个从右到左的数据流。组合的概念间接来自于数学课本,从右向左执行更加可能反映数学上的含意。

所有的组合都有一个个性

// 结合律(associativity)var associative = compose(f, compose(g, h)) == compose(compose(f, g), h);
// true

所以,如果咱们想把字符串变为大写(假如headreversetoUpperCase 函数存在),能够这么写:

compose(toUpperCase, compose(head, reverse));

// 或者
compose(compose(toUpperCase, head), reverse);

结合律的一大益处是任何一个函数分组都能够被拆开来,而后再以它们本人的组合形式打包在一起。对于如何组合,并没有规范的答案——咱们只是以本人喜爱的形式搭乐高积木罢了。

pointfree

pointfree 模式指的是,函数毋庸提及将要操作的数据是什么样的。一等公民的函数、柯里化(curry)以及组合合作起来十分有助于实现这种模式。

// 非 pointfree,因为提到了数据:word
var snakeCase = function (word) {return word.toLowerCase().replace(/\s+/ig, '_');
};

// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

利用 curry,咱们可能做到让每个函数都先接收数据,而后操作数据,最初再把数据传递到下一个函数那里去。另外留神在 pointfree 版本中,不须要 word 参数就能构造函数;而在非 pointfree 的版本中,必须要有 word 能力进行所有操作。pointfree 模式可能帮忙咱们缩小不必要的命名,让代码放弃简洁和通用。

debug

如果在 debug 组合的时候遇到了艰难,那么能够应用上面这个实用的,然而不纯的 trace 函数来追踪代码的执行状况。

var trace = curry(function(tag, x){console.log(tag, x);
  return x;
});

劣势

  1. 更好的治理状态。因为它的主旨是无状态,或者说更少的状态。而平时 DOM 的开发中,因为 DOM 的视觉出现依靠于状态变动,所以不可避免的产生了十分多的状态,而且不同组件可能还相互依赖。以 FP 来编程,能最大化的缩小这些未知、优化代码、缩小出错状况。
  2. 更简略的复用。极其的 FP 代码应该是每一行代码都是一个函数,当然咱们不须要这么极其。咱们尽量的把过程逻辑以更纯的函数来实现,固定输出 -> 固定输入,没有其余内部变量影响,并且无副作用。这样代码复用时,齐全不须要思考它的外部实现和内部影响。
  3. 更优雅的组合。往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。参考下面第二点,更强的复用性,带来更弱小的组合性。
  4. 隐性益处。缩小代码量,进步维护性。

毛病

  1. 性能:函数式编程相往往会对一个办法进行适度包装,从而产生上下文切换的性能开销。同时,在 JS 这种非函数式语言中,函数式的形式必然会比间接写语句指令慢(引擎会针对很多指令做特地优化)。
  2. 资源占用:在 JS 中为了实现对象状态的不可变,往往会创立新的对象,因而,它对垃圾回收(Garbage Collection)所产生的压力远远超过其余编程形式。这在某些场合会产生非常重大的问题。
  3. 递归陷阱:在函数式编程中,为了实现迭代,通常会采纳递归操作,为了缩小递归的性能开销,咱们往往会把递归写成尾递归模式,以便让解析器进行优化。然而家喻户晓,JS 是不反对尾递归优化的.
  4. 代码不易读。特地相熟 FP 的人可能会感觉这段代码高深莫测。而不相熟的人,遇到写的艰涩的代码,看懂代码,得脑子里先演算半小时。

正文完
 0