Javascript中什么是函数式编程?外文翻译资料
2023-01-28 11:50:25
Javascript中什么是函数式编程?Eric Elliott
函数式编程在 JavaScript 界已经成为了一个非常热门的话题。而仅在几年之前,还几乎没有 JavaScript 程序员了解函数式编程是什么,但在最近三年里,我看到非常多的应用程序代码库里大量使用着函数式编程思想。
函数式编程 (通常简称为 FP)是指通过复合纯函数来构建软件的过程,它避免了共享的状态(share state)、易变的数据(mutable data)、以及副作用(side-effects)。函数式编程是声明式而不是命令式,并且应用程序状态通过纯函数流转。对比面向对象编程,后者的应用程序状态通常是共享并共用于对象方法。
函数式编程是一种编程范式意味着它是一种软件构建的思维方式,有着自己的理论基础和界定法则。其他编程范式的例子包括面向对象编程和过程式编程。
与命令式或面向对象代码相比,函数式代码倾向于更简洁、更可预测以及更易于测试 —— 但是如果你对它以及与它相关的常见模式不熟悉,读函数式代码会让你觉得信息量太大,而且相关文献对于初学者来说往往难以理解。
如果你开始 google 函数式编程的术语,你很可能一下子碰壁,那些学术术语对新人来说着实有点吓人。它有一个非常陡峭的学习曲线。但是如果你已经用 JavaScript 写了一段时间的代码,你很可能不知不觉中在你的软件里已经使用了很多函数式编程原理和功能。
不要让那些新名词把你吓跑。实际上它比你所听说的要简单很多。
最难的部分是记住那些以前不熟悉的词汇。在这些名词定义中蕴含了许多思想,你只有理解了它们,才能够开始掌握函数式编程真正的意义:
纯函数(Pure functions)
函数复合(Function composition)
避免共享状态(Avoid shared state)
避免改变状态(Avoid mutating state)
避免副作用(Avoid side effects)
换句话说,如果你想要了解函数式编程在实际中的意义,你需要从理解那些核心概念开始。
一个纯函数是这样的一个函数:
给它同样的输入,总是返回同样的结果,并且没有副作用
纯函数有着许多对函数式编程而言非常重要的属性,包括引用透明(你可以将一个函数调用替换成它的结果值,而不会对程序的运行造成影响)。获取更多细节,可以阅读“什么是纯函数”。
函数复合是结合两个或多个函数,从而产生一个新函数或进行某些计算的过程。例如,复合操作 f·g(点号意思是对两者执行复合运算)在 JavaScript 中相当于执行 f(g(x))。理解函数复合是理解软件如何用函数式编程模型来构建的很重要的一步。获取更多细节,可以阅读“什么是函数组合”。
共享状态
共享状态 的意思是任意变量、对象或者内存空间存在于共享作用域下,或者作为对象的属性在各个作用域之间被传递。共享作用域包括全局作用域和闭包作用域。通常,在面向对象编程中,对象以添加属性到其他对象上的方式在作用域之间共享。
举个例子,一个电脑游戏可能会控制一个游戏对象(game object),它上面有角色(characters)和游戏道具(items),这些数据作为属性存储在游戏对象之上。而函数式编程避免共享状态 —— 与前者不同地,它依赖于不可变数据结构和纯粹的计算过程来从已存在的数据中派生出新的数据。要获取更多关于软件开发如何使用函数式编程处理应用程序状态的详细内容,可以阅读“10 Tips for Better Redux Architecture”。
共享状态的问题是为了理解函数的作用,你需要了解那个函数所用到的全部共享变量的变化历史。
想象你有一个 user 对象需要保存。你的 saveUser() 函数向服务器 API 发起一个请求。此时,用户改变了他们的头像,通过 updateAvatar() 并触发了另一次 saveUser() 请求。在保存动作执行后,服务器返回一个更新的 user 对象,客户端要将这个对象替换内存中的对象,以保持与服务器同步。
不幸地是,第二次请求有可能比第一次请求更早返回,所以当第一次请求(现在已经过时了)返回时,新的头像又从内存中丢失了,被替换回旧的头像。这是一个同步竞争的例子,是一个非常常见的共享状态 bug。
共享状态的另一个常见问题是改变函数调用次序可能导致一连串的错误,因为函数操作共享数据是依时序的:
//使用共享数据,函数调用的次序会改变函数调用的结果
const x = { val: 2 };
const x1 = () =gt; x.val = 1;
const x2 = () =gt; x.val *= 2; x1(); x2();
console.log(x.val); // 6 //下面的例子与上面的相同,除了hellip;hellip;
const y = { val: 2 };
const y1 = () =gt; y.val = 1;
const y2 = () =gt; y.val *= 2; // ...函数的调用次序颠倒了一下...
y2();
y1(); // ... 这改变了结果值:
console.log(y.val); // 5
如果你避免共享状态,函数的调用时序不同就不会改变函数的调用结果。使用纯函数,给定同样的输入,你将总是能得到同样的输出。这使得函数调用完全独立于其他函数调用,可以从根本上简化变更和重构。改变函数内容,或者改变函数调用的时序不会波及和破坏程序的其他部分。
const x = { val: 2 };
const x1 = x =gt; Object.assign({}, x, { val: x.val 1});
const x2 = x =gt; 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() 并传入一个空的 object 作为第一个参数来拷贝 x 的属性,以防止 x 在函数内部被改变。在这个例子里,它等价由于重新创建一个对象,而这是一种 JavaScript 里的通用模式, 用来拷贝已存在状态而不是使用引用,从而避免像我们第一个例子里产生的问题。
如果你仔细看例子里的 console.log() 语句,你会发现我前面已经提到过的概念:函数复合。回顾一下,函数复合看起来像是这样:f(g(x))。在这个例子里,我们的 f() 和 g() 是 x1() 和 x2(),所以复合是 x1·x2。
当然,如果你改变复合的顺序,输出将改变。操作的顺序仍然很重要。f(g(x)) 并不总是等价于 g(f(x)),但是,有一件事情发生了改变,那就是函数外部的变量不会被修改 —— 原本函数修改外部变量是一个大问题。要是函数不纯,我们如果不了解函数使用或操作的每个变量的完整历史,就不可能完全理解它做了什么。
移除函数时序依赖,你就完全消除了一大类潜在的 bug。
不可变性
一个不可变的(immutable)对象是指一个对象不会在它创建之后被改变。对应地,一个可变的(mutable)对象是指任何在创建之后可以被改变的对象。
不可变性是函数式编程的一个核心概念,因为没有它,你的程序中的数据流是有损的。状态历史被抛弃而奇怪的 bug 可能会在你的软件中产生。关于更多不变性的意义,阅读 “The Dao of Immutability.”。
在 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}`);
如你所见,被冻结的 object 的顶层基本属性不能被改变,但是如果有一个属性本身也是 object(包括数组等),它依然可以被改变 —— 因此甚至被冻结的对象也不是不可变的,除非你遍历整个对象树并冻结每一个对象属性。
在许多函数式编程语言中,有特殊的不可变数据结构,被称为 trie 数据结构(trie 的发音为 tree),这一结构有效地深冻结 —— 意味任何属性无论它的对象层级如何都不能被改变。
当一个对象被拷贝给一个操作符时,tries 使用结构共享来共用不可变对象的引用内存地址,这减少内存占用,而且能够显著地改善一些类型的操作的性能。
例如,你可以使用 ID 来比较对象,如果两个对象的根 ID 相同,你不需要继续遍历比较整个对象树来寻找差异。
有一些 JavaScript 的库使用了 tries,包括 Immutable.js 和 Mori。
我体验了这两个库,最终决定在需要大量不可变状态大的型项目中使用 Immutable.js。想要了解这一部分的更多内容,请移步 “10 Tips for Better Redux Architecture”。
副作用(Side Effects)
副作用是指除了函数返回值以外,任何在函数调用之外观察到的应用程序状态改变。副作用包括:
改变了任何外部变量或对象属性(例如,全局变量,或者一个在父级函数作用域链上的变量)
写日志
在屏幕输出
写文件
发网络请求
触发任何外部进程
调用另一个有副作用的函数
在函数式编程中,副作用被尽可能避免,这使得程序的作用更容易理解,也使得程序更容易被测试。
Haskell 以及其他函数式编程语言经常从纯函数中隔离和封装副作用,使用 monads 技巧。Mondas 这个话题要深入下去可以写一本书,所以我们先放一放。
你现在需要做的是要从你的软件中隔离副作用行为。如果你让副作用与你的程序逻辑分离,你的软件将会变得更易于扩展、重构、调试、测试和维护。
这也是为什么大部分前端框架鼓励我们分开管理状态和组件渲染,采用松耦合的模型。
通过高阶函数提升可重用性
函数式编程倾向于复用一组通用的函数功能来处理数据。面向对象编程倾向于把方法和数据集中到对象上。那些被集中的方法只能用来操作
剩余内容已隐藏,支付完成后下载完整资料
Javascript中什么是函数式编程?Eric Elliott
函数式编程在 JavaScript 界已经成为了一个非常热门的话题。而仅在几年之前,还几乎没有 JavaScript 程序员了解函数式编程是什么,但在最近三年里,我看到非常多的应用程序代码库里大量使用着函数式编程思想。
函数式编程 (通常简称为 FP)是指通过复合纯函数来构建软件的过程,它避免了共享的状态(share state)、易变的数据(mutable data)、以及副作用(side-effects)。函数式编程是声明式而不是命令式,并且应用程序状态通过纯函数流转。对比面向对象编程,后者的应用程序状态通常是共享并共用于对象方法。
函数式编程是一种编程范式意味着它是一种软件构建的思维方式,有着自己的理论基础和界定法则。其他编程范式的例子包括面向对象编程和过程式编程。
与命令式或面向对象代码相比,函数式代码倾向于更简洁、更可预测以及更易于测试 —— 但是如果你对它以及与它相关的常见模式不熟悉,读函数式代码会让你觉得信息量太大,而且相关文献对于初学者来说往往难以理解。
如果你开始 google 函数式编程的术语,你很可能一下子碰壁,那些学术术语对新人来说着实有点吓人。它有一个非常陡峭的学习曲线。但是如果你已经用 JavaScript 写了一段时间的代码,你很可能不知不觉中在你的软件里已经使用了很多函数式编程原理和功能。
不要让那些新名词把你吓跑。实际上它比你所听说的要简单很多。
最难的部分是记住那些以前不熟悉的词汇。在这些名词定义中蕴含了许多思想,你只有理解了它们,才能够开始掌握函数式编程真正的意义:
纯函数(Pure functions)
函数复合(Function composition)
避免共享状态(Avoid shared state)
避免改变状态(Avoid mutating state)
避免副作用(Avoid side effects)
换句话说,如果你想要了解函数式编程在实际中的意义,你需要从理解那些核心概念开始。
一个纯函数是这样的一个函数:
给它同样的输入,总是返回同样的结果,并且没有副作用
纯函数有着许多对函数式编程而言非常重要的属性,包括引用透明(你可以将一个函数调用替换成它的结果值,而不会对程序的运行造成影响)。获取更多细节,可以阅读“什么是纯函数”。
函数复合是结合两个或多个函数,从而产生一个新函数或进行某些计算的过程。例如,复合操作 f·g(点号意思是对两者执行复合运算)在 JavaScript 中相当于执行 f(g(x))。理解函数复合是理解软件如何用函数式编程模型来构建的很重要的一步。获取更多细节,可以阅读“什么是函数组合”。
共享状态
共享状态 的意思是任意变量、对象或者内存空间存在于共享作用域下,或者作为对象的属性在各个作用域之间被传递。共享作用域包括全局作用域和闭包作用域。通常,在面向对象编程中,对象以添加属性到其他对象上的方式在作用域之间共享。
举个例子,一个电脑游戏可能会控制一个游戏对象(game object),它上面有角色(characters)和游戏道具(items),这些数据作为属性存储在游戏对象之上。而函数式编程避免共享状态 —— 与前者不同地,它依赖于不可变数据结构和纯粹的计算过程来从已存在的数据中派生出新的数据。要获取更多关于软件开发如何使用函数式编程处理应用程序状态的详细内容,可以阅读“10 Tips for Better Redux Architecture”。
共享状态的问题是为了理解函数的作用,你需要了解那个函数所用到的全部共享变量的变化历史。
想象你有一个 user 对象需要保存。你的 saveUser() 函数向服务器 API 发起一个请求。此时,用户改变了他们的头像,通过 updateAvatar() 并触发了另一次 saveUser() 请求。在保存动作执行后,服务器返回一个更新的 user 对象,客户端要将这个对象替换内存中的对象,以保持与服务器同步。
不幸地是,第二次请求有可能比第一次请求更早返回,所以当第一次请求(现在已经过时了)返回时,新的头像又从内存中丢失了,被替换回旧的头像。这是一个同步竞争的例子,是一个非常常见的共享状态 bug。
共享状态的另一个常见问题是改变函数调用次序可能导致一连串的错误,因为函数操作共享数据是依时序的:
//使用共享数据,函数调用的次序会改变函数调用的结果
const x = { val: 2 };
const x1 = () =gt; x.val = 1;
const x2 = () =gt; x.val *= 2; x1(); x2();
console.log(x.val); // 6 //下面的例子与上面的相同,除了hellip;hellip;
const y = { val: 2 };
const y1 = () =gt; y.val = 1;
const y2 = () =gt; y.val *= 2; // ...函数的调用次序颠倒了一下...
y2();
y1(); // ... 这改变了结果值:
console.log(y.val); // 5
如果你避免共享状态,函数的调用时序不同就不会改变函数的调用结果。使用纯函数,给定同样的输入,你将总是能得到同样的输出。这使得函数调用完全独立于其他函数调用,可以从根本上简化变更和重构。改变函数内容,或者改变函数调用的时序不会波及和破坏程序的其他部分。
const x = { val: 2 };
const x1 = x =gt; Object.assign({}, x, { val: x.val 1});
const x2 = x =gt; 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() 并传入一个空的 object 作为第一个参数来拷贝 x 的属性,以防止 x 在函数内部被改变。在这个例子里,它等价由于重新创建一个对象,而这是一种 JavaScript 里的通用模式, 用来拷贝已存在状态而不是使用引用,从而避免像我们第一个例子里产生的问题。
如果你仔细看例子里的 console.log() 语句,你会发现我前面已经提到过的概念:函数复合。回顾一下,函数复合看起来像是这样:f(g(x))。在这个例子里,我们的 f() 和 g() 是 x1() 和 x2(),所以复合是 x1·x2。
当然,如果你改变复合的顺序,输出将改变。操作的顺序仍然很重要。f(g(x)) 并不总是等价于 g(f(x)),但是,有一件事情发生了改变,那就是函数外部的变量不会被修改 —— 原本函数修改外部变量是一个大问题。要是函数不纯,我们如果不了解函数使用或操作的每个变量的完整历史,就不可能完全理解它做了什么。
移除函数时序依赖,你就完全消除了一大类潜在的 bug。
不可变性
一个不可变的(immutable)对象是指一个对象不会在它创建之后被改变。对应地,一个可变的(mutable)对象是指任何在创建之后可以被改变的对象。
不可变性是函数式编程的一个核心概念,因为没有它,你的程序中的数据流是有损的。状态历史被抛弃而奇怪的 bug 可能会在你的软件中产生。关于更多不变性的意义,阅读 “The Dao of Immutability.”。
在 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}`);
如你所见,被冻结的 object 的顶层基本属性不能被改变,但是如果有一个属性本身也是 object(包括数组等),它依然可以被改变 —— 因此甚至被冻结的对象也不是不可变的,除非你遍历整个对象树并冻结每一个对象属性。
在许多函数式编程语言中,有特殊的不可变数据结构,被称为 trie 数据结构(trie 的发音为 tree),这一结构有效地深冻结 —— 意味任何属性无论它的对象层级如何都不能被改变。
当一个对象被拷贝给一个操作符时,tries 使用结构共享来共用不可变对象的引用内存地址,这减少内存占用,而且能够显著地改善一些类型的操作的性能。
例如,你可以使用 ID 来比较对象,如果两个对象的根 ID 相同,你不需要继续遍历比较整个对象树来寻找差异。
有一些 JavaScript 的库使用了 tries,包括 Immutable.js 和 Mori。
我体验了这两个库,最终决定在需要大量不可变状态大的型项目中使用 Immutable.js。想要了解这一部分的更多内容,请移步 “10 Tips for Better Redux Architecture”。
副作用(Side Effects)
副作用是指除了函数返回值以外,任何在函数调用之外观察到的应用程序状态改变。副作用包括:
改变了任何外部变量或对象属性(例如,全局变量,或者一个在父级函数作用域链上的变量)
写日志
在屏幕输出
写文件
发网络请求
触发任何外部进程
调用另一个有副作用的函数
在函数式编程中,副作用被尽可能避免,这使得程序的作用更容易理解,也使得程序更容易被测试。
Haskell 以及其他函数式编程语言经常从纯函数中隔离和封装副作用,使用 monads 技巧。Mondas 这个话题要深入下去可以写一本书,所以我们先放一放。
你现在需要做的是要从你的软件中隔离副作用行为。如果你让副作用与你的程序逻辑分离,你的软件将会变得更易于扩展、重构、调试、测试和维护。
这也是为什么大部分前端框架鼓励我们分开管理状态和组件渲染,采用松耦合的模型。
通过高阶函数提升可重用性
函数式编程倾向于复用一组通用的函数功能来处理数据。面向对象编程倾向于把方法和数据集中到对象上。那些被集中的方法只能用来操作
剩余内容已隐藏,支付完成后下载完整资料
资料编号:[141478],资料为PDF文档或Word文档,PDF文档可免费转换为Word