玩转 JavaScript 面试:何为函数式编程?

函数式编程在 JavaScript 领域着实已经成为一个热门话题。就在几年前,很多 JavaScript 程序员甚至都不知道啥是函数式编程,但是就在近三年里我看到过的每一个大型应用的代码库中都包含了函数式编程思想的大规模使用。

函数式编程(缩写为 FP)是一种通过组合纯函数来构建软件的过程,避免状态共享可变数据副作用的产生。函数式编程是一种声明式编程而不是指令式编程,应用的状态全部流经的是纯函数。与面向对象编程思想形成对比的是,其应用程序的状态通常都是与对象中的方法共享的。

函数式编程是一种编程范式,意指它是一种基于一些基本的、限定原则的软件架构的思维方式,其他编程范式的例子还包括面向对象编程和面向过程编程。

相比指令式编程或面向对象,函数式编程的代码倾向于更为简洁、可预测且更容易测试。但如果你不熟悉这种方式或与其常见的几种相关模式的话,函数式编程的代码同样可以看起来很紧凑,相关文档对于新手来说可能也较为难以理解。

如果你开始去搜索函数式编程的相关术语,你可能很快就会碰壁,大量专业术语完全可以唬住一个新手。单纯的讨论其学习曲线有点儿过于轻描淡写了,但是如果你已经从事 JavaScript 编程工作有一段时间了,那么你应该已经在你的项目中使用过很多函数式编程的思想或工具了。

别让新词汇把你吓跑。它们会比听起来更容易。

这其中最难的部分可以说就是让一堆陌生词汇充斥你的脑袋了。各种术语一脸无辜,因为在掌握它们之前你还需要了解下面这些术语的含义:

  • 纯函数
  • 函数组合
  • 避免状态共享
  • 避免状态改变
  • 避免副作用

一个纯函数定义如下:

  • 每次给定相同的输入,其输出结果总是相同的
  • 无任何副作用

纯函数中的很多特性在函数式编程中都很重要,包括引用透明度(如果表达式可以替换为其相应的值而不更改程序的行为,则该表达式称为引用透明)。

引用透明度说白了就是相同的输入总是得到相同的输出,也就是说函数中未使用任何外部状态:

function plusOne(x) {
    return x + 1;
}
复制代码

上面的例子即为引用透明度函数,我们可以用 6 来代替 plusOne(5) 的函数调用。详细解释可参考 stack overflow - What is referential transparency?

函数组合是指将两个或多个函数进行组合以便产生一个新的函数或执行某些计算的过程。比如组合函数f.g(.的意识是指由...组成)在 JavaScript 中等价于 f(g(x))。理解函数组合对于理解使用函数式编程编写软件来说是个十分重要的步骤。

状态共享

状态共享是指任何变量、对象或内存空间在一个共享的作用域中存在,或者是被用来作为对象的属性在作用域之间传递。一个共享的作用域可以包括全局作用域或者闭包作用域。在面向对象编程中,对象通常都是通过添加一个属性到其他对象中来在作用域间共享的。

状态共享的问题在于为了了解一个函数的作用,你不得不去了解函数中使用的或影响的每一个共享的变量的过往。

假定你有一个用户对象需要保存,你的saveUser()函数会向服务器上的接口发起请求。与此同时,用户又进行了更换头像的操作,调用updateAvatar()来更换头像的同时也会触发另一次saveUser()请求。在保存时,服务器返回一个规范的用户对象,该对象应该替换内存中的任何内容以便与服务器上的更改或响应其他 API 调用同步。

但是问题来了,第二次响应比第一次返回要早。所以当第一个响应(已过期)返回时,新头像被从内存中抹去了,替换回了旧头像。这就是一个争用条件的例子 —— 是与状态共享有关的一个很常见的 bug。

另一个跟状态共享有关的常见问题是更改调用函数的顺序可能会导致级联失败,因为作用于状态共享的函数与时序有关:

// 在状态共享的函数中,函数调用的顺序会导致函数调用的结果的变化
const x = {
  val: 2
};

const x1 = () => x.val += 1;

const x2 = () => x.val *= 2;

x1();
x2();

console.log(x.val); // 6

// 同样的例子,改变调用顺序
const y = {
  val: 2
};

const y1 = () => y.val += 1;

const y2 = () => y.val *= 2;

y2();
y1();

// 改变了结果
console.log(y.val); // 5
复制代码

如果我们避免状态共享,函数调用的时间和顺序就不会改变函数调用的结果。使用纯函数,给定相同的输入总是能得到相同的输出。这就使得函数调用完全独立,就可以从根本上简化改变与重构。函数中的某处改变,或是函数调用的顺序不会影响或破坏程序的其他部分。

const x = {
  val: 2
};

const x1 = x => Object.assign({}, x, { val: x.val + 1});

const x2 = x => Object.assign({}, x, { val: x.val * 2});

console.log(x1(x2(x)).val); // 5

const y = {
  val: 2
};

// 由于不存在对外部变量的依赖
// 所以我们不需要不同的函数来操作不同的变量

// 此处故意留白

// 因为函数不变,所以我们可以以任意顺序调用这些函数任意次
// 而且还不改变其他函数调用的结果
x2(y);
x1(y);

console.log(x1(x2(y)).val); // 5
复制代码

在上面的例子中,我们使用了Object.assign()方法,然后传入了一个空对象作为第一个参数来复制x的属性,而不是在原处改变x。该例中,不使用Object.assign()的话,它相当于简单的从头开始创建一个新对象,但这是 JavaScript 中创建现有状态副本而不是使用变换的常见模式,我们在第一个示例中演示了这一点。

如果你仔细的看了本例中的console.log()语句,你应该会注意到我已经提到过的一些东西:函数组合。回忆一下之前说过的知识点,函数组合看起来像这样:f(g(x))。在本例中为x1(x2()),也即x1.x2

当然了,要是你改变了组合顺序,输出也会跟着改变。执行顺序仍然很重要。f(g(x))不总是等价于g(f(x)),但是再也不用担心的一件事就是函数外部的变量,这可是件大事。在非纯函数中,不可能完全知道一个函数都做了什么,除非你知道函数使用或影响的每一个变量的整个历史。

去掉了函数调用的时序依赖,你也就完全排除了这一类 bug。

不可变性

不可变对象是指一个对象一旦被创建就不能再被修改。反之可变对象就是说对象被创建后可以修改。不可变性是函数式编程中的一个核心概念,因为如果没有这个特性,程序中的数据流就会流失、状态历史丢失,然后你的程序中就总会冒出奇怪的 bug。

在 JavaScript 中,千万不要把 const 和不变性搞混。const 绑定了一个创建后就无法再被分配的变量名。const 不创建不可变对象。使用 const 创建的变量无法再被赋值但是可以修改对象的属性。

不可变对象是完全不能被修改的。你可以深度冻结一个对象使其变成真·不可变的值。JavaScript 中有一个方法可以冻结对象的第一层:

const a = Object.freeze({
  foo: ‘Hello‘,
  bar: ‘world‘,
  baz: ‘!‘
});

a.foo = ‘Goodbye‘;// Error: Cannot assign to read only property ‘foo‘ of object Object
复制代码

这种冻结方式仅仅是浅层的不可变,例如:

const a = Object.freeze({
  foo: { greeting: ‘Hello‘ },
  bar: ‘world‘,
  baz: ‘!‘
});

a.foo.greeting = ‘Goodbye‘;

console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);// Goodbye world!
复制代码

可以看到,一个被冻结的顶层的原始属性是不可变的。但如果属性值为对象的话,该对象依然可变(包括数组等)。除非你遍历整个对象树,将其层层冻结。

在很多函数式编程语言中都又比较特殊的不可变数据结构,称之为查找树数据结构,这种数据结构是可以有效的进行深度冻结的。

查找树通过结构共享来共享内存空间的引用,其在对象被复制后依然是不变的,从而节省了内存,使得某类操作的性能有显著的提升。

例如,你可以在一个对象树的根节点使用身份对照来进行比较。如果身份相同,如果身份相同,那你就不用去遍历整颗树来对比差异了。

在 JavaScript 中有一些比较优秀的利用树的类库,比如 Immutable.jsMori

这俩库我都用过,我更倾向于在需要很多不可变状态的大型项目中使用 Immutable.js

副作用

副作用就是指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。副作用的函数不仅仅只是返回了一个值,而且还做了其他的事情:

  • 改变了外部对象或变量属性(全局变量或父函数作用域链中的变量)
  • 在控制台中有输出打印
  • 向屏幕中写了东西
  • 向文件中写了东西
  • 向网络中写了东西
  • 触发了外部过程
  • 调用了其他有副作用的函数

副作用在函数式编程中大多数时候都是要避免的,这样才能使得程序的作用一目了然,也更容易被测试。

Haskell 等其他编程语言总是从纯函数中使用 Monads 将副作用独立并封装。有关 Monads 内容太多了,大家可以去了解一下。

但你现在就需要了解的是,副作用行为需要从你的软件中独立出来,这样你的软件就更易扩展、重构、debug、测试和维护。

这也是大多数前端框架鼓励用户单独的管理状态和组件渲染、解耦模块。

通过高阶函数提高复用性

函数式编程倾向于复用一系列函数工具来处理数据。面向对象编程则倾向于将方法和数据放在对象中,这些合并起来的方法只能用来操作那些被设计好的数据,经常还是包含在特定组件实例中的。

在函数式编程中,任何类型的数据都是一样的地位,同一个 map() 函数可以遍历对象、字符串、数字或任何类型的数据,因为它接收一个函数作为参数,而这个函数参数可以恰当的处理给定的数据类型。函数式编程通过高阶函数来实现这种特性。

JavaScript 秉承函数是一等公民的观点,允许我们把函数当数据对待 —— 把函数赋值给变量、将函数传给其他函数、让函数返回函数等...

高阶函数就是指任何可以接收函数作为参数的函数、或者返回一个函数的函数,或者两者同时。高阶函数经常被用于:

  • 抽象或独立的动作、回调函数的异步流控制、promises,、monads 等等...
  • 创建可以处理各种数据类型的实用工具函数
  • 使用函数的部分参数或以复用目的或函数组合创建的柯里化函数
  • 接收一组函数作为参数然后返回其中的一些作为组合

容器、函子、列表、流

函子就是一种可以被映射的东西。换句话说,它就是一个有接口的容器,该接口可以被用来apply到函数内部的一个值(这句翻译太奇怪了,功力不够。原文 it’s a container which has an interface which can be used to apply a function to the values inside it.)。

前面我们知道了相同的 map()函数可以在多种数据类型上执行。它通过提升映射操作以使用函子 API 来实现。关键的流控制操作可以通过 map() 函数利用该接口使用。如果是 Array.prototype.map() 的话,容器就是个数组,其他数据结构可以作为函子,只要它们提供了 map() API。

让我们来看一下 Array.prototype.map() 是如何允许从映射函数中抽象数据类型使得 map() 函数在任何数据类型上可用的。我们创建一个 double() 函数来映射传入的参数乘 2 的操作:

const double = n => n * 2;
const doubleMap = numbers => numbers.map(double);
console.log(doubleMap([2, 3, 4])); // [ 4, 6, 8 ]
复制代码

要是我们想对一个游戏中的目标进行操作,让其得分数翻倍呢?只需要在 double() 函数中传入 map() 的值上稍作改动即可:

const double = n => n.points * 2;

const doubleMap = numbers => numbers.map(double);

console.log(doubleMap([
  { name: ‘ball‘, points: 2 },
  { name: ‘coin‘, points: 3 },
  { name: ‘candy‘, points: 4}
])); // [ 4, 6, 8 ]
复制代码

使用如函子/高阶函数的概念来使用原生工具函数来操作不同的数据类型在函数式编程中很重要。类似的概念被应用在 all sorts of different ways。

列表在时间上的延续即为流。

你现在只需要知道数组和函数不是容器和值在容器中应用的唯一方式。比如说,一个数组就是一组数据。列表在时间上的延续即为流 -- 因此你可以使用同类工具函数来处理进来的事件流 —— 在日后实践函数式编程中你会对此有所体会。

声明式编程 & 指令式编程

函数式编程是一种声明式编程范式,程序的逻辑在表达时没有明确的描述流控制。

指令式编程用一行行代码来描述特定步骤来达到预期结果。而根本不在乎流控制是啥?

声明式编程抽象了流控制过程,用代码来描述数据流该怎么做,如何去获得抽象的方式。

下面的例子中给出了指令式编程映射数组中数字并返回将值乘 2 返回新数组:

const doubleMap = numbers => {
  const doubled = [];
  for (let i = 0; i < numbers.length; i++) {
    doubled.push(numbers[i] * 2);
  }
  return doubled;
};

console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
复制代码

声明式编程做同样的事,但是使用函数工具 Array.prototype.map() 抽象了流控制的方式,允许你对数据流做更清晰的表达:

const doubleMap = numbers => numbers.map(n => n * 2);

console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
复制代码

指令式编程常使用语句,语句即一段执行某个动作的代码,包括forifswitchthrow 等等。

声明式编程的代码更多依赖的是表达式,表达式是一段有返回值的代码。表达式的例子如下:

2 * 2
doubleMap([2, 3, 4])
Math.max(4, 3, 2)
复制代码

你会在代码中经常看见一个表达式被赋给一个变量、从函数中返回一个表达式或是被传入一个函数。

结论

本文要点:

  • 使用纯函数而不是共享状态或者有副作用的函数
  • 发扬不可变性而不是可变数据
  • 使用函数组合而不是指令式的流控制
  • 很多原生、可复用的工具函数可以通过高阶函数应用到很多数据类型上,而不是只能处理指定数据
  • 声明式编程而不是指令式编程(要知道做什么,而不是如何做)
  • 表达式和语句
  • 容器 & 高阶函数对比 特设多态

原文地址:https://www.cnblogs.com/dashjunih/p/10988104.html

时间: 2024-10-08 18:54:14

玩转 JavaScript 面试:何为函数式编程?的相关文章

JS函数式编程【译】4.在Javascript中实现函数式编程的技术

?? Functional Programming in Javascript 主目录上一章 建立函数式编程环境 第四章 在Javascript中实现函数式编程的技术 扶好你的帽子,我们现在要真正进入函数式的思想了. 这章我们继续下面的内容: 把所有的核心概念放到一个集中的范式里 探索函数式编程之美 一步步跟踪函数式模式相互交织的逻辑 我们将贯穿整章建立一个简单的应用做一些很酷的事情 你可能已经注意到,在上一章我们介绍Javascript的函数式库的时候引入了一些概念, 而不是在第二章<函数式编

javascript - Underscore 与 函数式编程

<Javascript函数式编程 PDF> # csdn下载地址http://download.csdn.net/detail/tssxm/9713727 Underscore # githubhttps://github.com/jashkenas/underscore # 中文官方网站http://www.css88.com/doc/underscore/ # CDN<script src="https://cdn.bootcss.com/underscore.js/1.8

Javascript 中的函数式编程

本文和大家分享的主要是javascript中函数式编程相关内容,一起来看看吧,希望对大家学习javascript有所帮助. 函数式编程(functional programming)或称函数程序设计,又称泛函编程,是一种编程范型,比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程. 函数式编程,近年来一直被炒得火热,国内外的开发者好像都在议论和提倡这种编程范式.在众多的函数式语言中,Jav

JavaScript系列:函数式编程(开篇)

前言: 上一篇介绍了 函数回调,高阶函数以及函数柯里化等高级函数应用,同时,因为正在学习JavaScript·函数式编程,想整理一下函数式编程中,对于我们日常比较有用的部分. 为什么函数式编程很重要? 学习过C++,java这些面向对象编程语言,我们大概都知道面向对象编程就是把目标问题分成几个部分,实现各部分的功能,再组合成一个更强大的对象.虽说JavaScript不是面向对象编程语言(虽然ES6已经出现了Class类这种对象,在此暂且不说),但它却是一种完完全全支持函数式编程的语言,利用函数式

给 JavaScript 开发者讲讲函数式编程

本文译自:Functional Programming for JavaScript People 和大多数人一样,我在几个月前听到了很多关于函数式编程的东西,不过并没有更深入的了解.于我而言,可能只是一个流行词罢了.从那时起,我开始更深地了解函数式编程并且我觉得应该为那些总能听到它但不知道究竟是什么的新人做一点事情. 谈及函数式编程,你可能会想到它们:Haskell 和 Lisp,以及很多关于哪个更好的讨论.尽管它们都是函数式语言,不过的确有很大的不同,可以说各有各的卖点.在文章的结尾处,我希

用函数式编程技术编写优美的 JavaScript

用函数式编程技术编写优美的 JavaScript_ibm作者: 字体:[增加 减小] 类型:转载函数式编程语言在学术领域已经存在相当长一段时间了,但是从历史上看,它们没有丰富的工具和库可供使用.随着 .NET 平台上的 Haskell 的出现,函数式编程变得更加流行.一些传统的编程语言,例如 C++ 和 JavaScript,引入了由函数式编程提供的一些构造和特性.在许多情况下,JavaScript 的重复代码导致了一些拙劣的编码.如果使用函数式编程,就可以避免这些问题.此外,可以利用函数式编程

JS函数式编程 3.1 Javascript的函数式库

?? Functional Programming in Javascript 主目录第三章 建立函数式编程环境 Javascript的函数式库 据说所有的函数式程序员都会写自己的函数库,函数式Javascript程序员也不例外. 随着如今开源代码分享平台如GitHab.Bower和NPM的涌现,对这些函数库进行分享.变得及补充变得越来越容易. 现在已经有很多Javascript的函数式变成苦,从小巧的工具集到庞大的模块库都有. 每一个库都宣扬着自己的函数式编程风格.从一本正经的数学风格到灵活松

JavaScript中函数式编程中文翻译

原著由 Dan Mantyla 编写 近几年来,随着 Haskell.Scala.Clojure 等学院派原生支持函数式编程的偏门语言越来越受到关注,同时主流的 Java.JavaScript.Python 甚至 C++都陆续支持函数式编程.特别值得一提的是,在 nodejs 出现后,JavaScript 成为第一种从前端到后台的全栈语言,而且 JavaScript 支持多范式编程.应用函数式编程的最大挑战就是思维模式的改变———从传统面向对象的范式变为函数式编程范式. <JavaScript

JavaScript函数式编程(1):基本思想

1 函数式编程简介 函数式编程是和传统命令式编程区分的一种编程思想,"在函数式编程语言中,函数是第一类的对象,也就是说,函数 不依赖于任何其他的对象而可以独立存在,而在面向对象的语言中,函数 ( 方法 ) 是依附于对象的,属于对象的一部分.这一点决定了函数在函数式语言中的一些特别的性质,比如作为传出 / 传入参数,作为一个普通的变量等.[1]" 函数式编程思想的源头可以追溯到 20 世纪 30 年代,数学家阿隆左 . 丘奇在进行一项关于问题的可计算性的研究,也就是后来的 lambda