章节7: 闭包和对象

达成共识

首先,确保当我们提到闭包和对象时,我们都达成了共识。 我们探讨JavaScript如何处理这两种机制的上下文,特别是普通的函数闭包(参见第2章“保持作用域”)和普通对象(键值对集合) )。

普通函数闭包例子:

  1. function outer() {
  2. var one = 1;
  3. var two = 2;
  4. return function inner(){
  5. return one + two;
  6. };
  7. }
  8. var three = outer();
  9. three(); // 3

普通对象例子:

  1. var obj = {
  2. one: 1,
  3. two: 2
  4. };
  5. function three(outer) {
  6. return outer.one + outer.two;
  7. }
  8. three( obj ); // 3

当谈论到“闭包”时,许多人会想到许多额外的部分,例如异步回调,甚至包含封装和信息隐藏的模块模式。 类似地,“对象”带来了类,“this”,原型以及大量其他方法和模式。

随着我们学习深入,我们将探讨这些重要的额外的部分,但是现在,我们先记住这里所说的“闭包”和“对象”的最简单的解释,这样可以使我们保持清晰。

相似

闭包和对象的关系可能并不明显,我们首先探讨它们的相似之处。

基于这个讨论,我先简单地确定两件事:

没有闭包的编程语言可以用对象模拟闭包。 没有对象的编程语言可以用闭包来模拟对象。

换句话说,我们可以将闭包和对象视为事物的两种不同表示。

状态

思考以下代码:

  1. function outer() {
  2. var one = 1;
  3. var two = 2;
  4. return function inner(){
  5. return one + two;
  6. };
  7. }
  8. var obj = {
  9. one: 1,
  10. two: 2
  11. };

inner()函数和对象obj作用域都包含两个状态元素:one的值为1two的值为2。在语法和机制上,这些状态的表示是不同的。但从概念上讲,它们非常相似。

事实上,将对象表示为闭包或将闭包表示为对象是相当简单的。来吧,尝试一下:

  1. var point = {
  2. x: 10,
  3. y: 12,
  4. z: 14
  5. };

你想到什么了吗?

  1. function outer() {
  2. var x = 10;
  3. var y = 12;
  4. var z = 14;
  5. return function inner(){
  6. return [x,y,z];
  7. }
  8. };
  9. var point = outer();

注意: 每次inner() 函数调用并返回一个新的数组(简称, 对象!) 。原因是JS没提供在不封装多个值的情况下“返回”多个值的功能。这在技术上并不违反对象即闭包这个问题,因为它只是暴露/传递值的实现细节;状态跟踪本身仍然是基于对象的。使用ES6+数组析构,我们可以声明性地忽略临时中间数组:var [x,y,z] = point()。从开发人员的角度来看,这些值是单独存储的,并通过闭包(而非对象)跟踪。

如果有嵌套对象怎么办?

  1. var person = {
  2. name: "Kyle Simpson",
  3. address: {
  4. street: "123 Easy St",
  5. city: "JS'ville",
  6. state: "ES"
  7. }
  8. };

同样的,可以使用嵌套闭包实现:

  1. function outer() {
  2. var name = "Kyle Simpson";
  3. return middle();
  4. // ********************
  5. function middle() {
  6. var street = "123 Easy St";
  7. var city = "JS'ville";
  8. var state = "ES";
  9. return function inner(){
  10. return [name,street,city,state];
  11. };
  12. }
  13. }
  14. var person = outer();

让我们试试从闭包转为对象:

  1. function point(x1,y1) {
  2. return function distFromPoint(x2,y2){
  3. return Math.sqrt(
  4. Math.pow( x2 - x1, 2 ) +
  5. Math.pow( y2 - y1, 2 )
  6. );
  7. };
  8. }
  9. var pointDistance = point( 1, 1 );
  10. pointDistance( 4, 5 ); // 5

distFromPoint(..)封装了x1y1, 但我们可以显式地传入值作为对象:

  1. function pointDistance(point,x2,y2) {
  2. return Math.sqrt(
  3. Math.pow( x2 - point.x1, 2 ) +
  4. Math.pow( y2 - point.y1, 2 )
  5. );
  6. };
  7. pointDistance(
  8. { x1: 1, y1: 1 },
  9. 4, // x2
  10. 5 // y2
  11. );
  12. // 5

显式的传入point对象替换隐式的闭包状态。

行为也如此!

对象和闭包不仅表示表示状态集合的方法,而且还包含函数/方法。将数据与其行为捆绑在一起有一个奇特的名称:封装。

试想一下:

  1. function person(name,age) {
  2. return happyBirthday(){
  3. age++;
  4. console.log(
  5. `Happy ${age}th Birthday, ${name}!`
  6. );
  7. }
  8. }
  9. var birthdayBoy = person( "Kyle", 36 );
  10. birthdayBoy(); // Happy 37th Birthday, Kyle!

内部函数happyBirthday() 通过闭包引入nameage 使其功能与状态保持一致。(注:保持了变量引用和不销毁)

我们可以通过将this绑定到对象来实现相同的能力:

  1. var birthdayBoy = {
  2. name: "Kyle",
  3. age: 36,
  4. happyBirthday() {
  5. this.age++;
  6. console.log(
  7. `Happy ${this.age}th Birthday, ${this.name}!`
  8. );
  9. }
  10. };
  11. birthdayBoy.happyBirthday();
  12. // Happy 37th Birthday, Kyle!

我们仍然用happyBirthday()函数来表示状态数据的封装,但是使用对象而不是闭包。 而且我们不必将对象显式传递给函数(与前面的示例一样); JavaScript的this绑定很容易创建一个隐式绑定。

另一方面分析此关系:闭包将单个函数与一组状态相关联,而对象可以保持相同状态,而持有相同状态的对象可以有任意数量的函数对该状态进行操作。

事实上,你甚至可以使用单个闭包作为接口来暴露多个方法。 思考以下使用两种方法的传统对象:

  1. var person = {
  2. firstName: "Kyle",
  3. lastName: "Simpson",
  4. first() {
  5. return this.firstName;
  6. },
  7. last() {
  8. return this.lastName;
  9. }
  10. }
  11. person.first() + " " + person.last();
  12. // Kyle Simpson

只使用闭包,可以这么做:

  1. function createPerson(firstName,lastName) {
  2. return API;
  3. // ********************
  4. function API(methodName) {
  5. switch (methodName) {
  6. case "first":
  7. return first();
  8. break;
  9. case "last":
  10. return last();
  11. break;
  12. };
  13. }
  14. function first() {
  15. return firstName;
  16. }
  17. function last() {
  18. return lastName;
  19. }
  20. }
  21. var person = createPerson( "Kyle", "Simpson" );
  22. person( "first" ) + " " + person( "last" );
  23. // Kyle Simpson

这些程序在人机工程学上看起来有点不同,事实上只是相同程序行为的不同实现而已。

(不)可变数据

许多人最初会认为闭包和对象在可变性方面表现不同,闭包能保护免受外部改变(影响),而对象不会。但事实证明,两种形式都有相同的可变行为。

正如我们在 第6章所讨论的,需要关心的,的可变性,这是值本身的一个特征,无论它在何处或如何赋值:

  1. function outer() {
  2. var x = 1;
  3. var y = [2,3];
  4. return function inner(){
  5. return [ x, y[0], y[1] ];
  6. };
  7. }
  8. var xyPublic = {
  9. x: 1,
  10. y: [2,3]
  11. };

outer()的内部变量x(基础类型)值是不可变的 - 记住,像2这样的基本类型都是不可变的。 但是,数组y(引用类型)引用的值是可变的。 xyPublic里的xy属性也是这个道理。

我们可以通过指出y本身就是一个数组来强调对象和闭包对可变性没有影响,因此我们需要进一步分解这个例子:

  1. function outer() {
  2. var x = 1;
  3. return middle();
  4. // ********************
  5. function middle() {
  6. var y0 = 2;
  7. var y1 = 3;
  8. return function inner(){
  9. return [ x, y0, y1 ];
  10. };
  11. }
  12. }
  13. var xyPublic = {
  14. x: 1,
  15. y: {
  16. 0: 2,
  17. 1: 3
  18. }
  19. };

如果您将其视为“turtles (aka, objects) all the way down”(注:不太理解作者用意),那么在最低级别上,所有状态数据都是基础类型,并且所有基础类型都是不可变值。

无论使用嵌套对象表示此状态,还是使用嵌套闭包表示此状态,值都是不可变的。

同构

“同构”这个术语在JavaScript中经常出现,通常用于指可以在服务器和浏览器中使用/共享的代码。不久前,我写了一篇博客文章,对“同构”这个词的用法进行了抨击,实际上它有一个明确而重要的含义,但却被用错地方。

以下列举这篇文章的部分节选:

同构是什么意思呢? 我们可以用数学术语,社会学或生物学来解释,同构概念是两个结构相似但不相同的东西。

在所有这些用法中,同构和相等的区别在这里:如果两个值在所有方面完全相同,则它们是相等的,但如果它们以不同的方式表示但仍具有1对1的双向性,则它们是同构的 映射关系。

换句话说,如果你可以从A映射(转换)到B然后用逆映射返回到A,那么A和B可以称为是同构的。

回顾一下第2章,我们讨论了函数的数学定义,即输入和输出之间的映射,这在学术上被称为态射。 同构是一种双射(又称双向)态射的特殊情况,它不仅要求映射必须能够在任一方向上进行,而且要求行为在任何一种形式中都是相同的。

但是,我们不要考虑数学术语,而是将同构与代码联系起来。 再次引用我的博文:

如果真有这种东西,同构JS会是什么?它可能是一组JS代码被转换成另一组JS代码,而且(重要的是)如果有需要,可以将后者转换回前者。

正如我们在前面的 “闭包作为对象” 和 “对象作为闭包” 示例中所说的那样,它们的表达可以任意替换。因此,它们彼此是同构的。

简单地说,闭包和对象是状态(及其相关功能)的同构表示。

下次当你听到有人说 “X与Y是同构” 时,他们的意思是“X和Y可以从任何一个方向转换到另一个方向,而不会丢失特性。”

深入内部结构

因此,从编写代码的角度来看可以将对象看作闭包的同构表示。但我们也观察到,闭包系统实际上可以用对象实现——而且很可能是这样的!

考虑以下代码:JS如何追踪x变量,以便inner()函数在outer()函数运行之后仍然保持它的引用?

  1. function outer() {
  2. var x = 1;
  3. return function inner(){
  4. return x;
  5. };
  6. }

我们可以想象,outer()的作用域(定义的所有变量的集合)被实现为一个带有属性的对象。所以,从概念上讲,在内存的某个地方,类似有这样的东西:

  1. scopeOfOuter = {
  2. x: 1
  3. };

然后对于inner()函数,当创建时,它得到一个(空的)作用域对象叫做scopeOfInner,它通过它的[[Prototype]]链接到scopeOfOuter对象,大概是这样的:

  1. scopeOfInner = {};
  2. Object.setPrototypeOf( scopeOfInner, scopeOfOuter );

然后,在inner()内部,当它引用词法变量x时,它实际上更像是:

  1. return scopeOfInner.x;

scopeOfInner没有x属性,但它是[[Prototype]]-链接到scopeOfOuter,它确实有x属性。通过原型委托访问scopeOfOuter.x`将返回“1”值。

通过这种方式,我们可以看到为什么outer() 的作用域(通过闭包)在完成之后仍然保留:因为scopeOfInner对象与scopeOfOuter对象相链接,从而保持该对象及其属性的活动和状态。

这些都是概念性的。我并不是说JS引擎使用对象和原型。但它完全有可能以类似的方式工作。

实际上,许多语言都通过对象实现闭包。其他语言使用闭包实现对象。但我们会让读者发挥他们的想象力来理解它是如何运作的。

两个观点

闭包和对象是等价的?不完全是。我打赌它们比您在开始本章之前所认为的更相似,但是它们仍然有重要的区别。

这些差异不应被视为缺点或反对使用的理由;这是错误的观点。它们应该被看作是使其中一个或另一个更适合(和可读!)用于给定任务的特性和优势。

结构可变性

从概念上讲,闭包的结构不是可变的。

换句话说,您永远不能向闭包添加或删除状态。闭包是声明变量的一个特性(在作者/编译时固定),并且不敏感于任何运行时条件——当然,假设您使用严格模式和/或避免使用eval(..)之类的欺骗自己不会出错的方式!

注: JS引擎可以在技术上剔除一个闭包,以剔除其范围内不再使用的任何变量,但这是一个高级优化,对开发人员来说是透明的。无论引擎实际上是否执行这些优化,我认为对于开发人员来说,假设闭包是针对范围而不是针对变量的是最安全的。如果你不想让它作用域影响,就不要对他修改!

然而,对象在默认情况下是相当可变的。您可以自由地从对象中添加或删除(delete)属性/索引,只要该对象没有被冻结(Object.freeze(..))。

根据程序中的运行时条件,可以跟踪更多(或更少)的状态,这可能是代码的一个优势。

例如,让我们想象一下在游戏中跟踪按键事件。几乎可以肯定的是,您将考虑使用数组来完成以下操作:

  1. function trackEvent(evt,keypresses = []) {
  2. return [ ...keypresses, evt ];
  3. }
  4. var keypresses = trackEvent( newEvent1 );
  5. keypresses = trackEvent( newEvent2, keypresses );

注:你注意到为什么我没有直接将push(..)推到keypresses中了吗?因为在FP中,我们通常希望将数组视为不可变的数据结构,可以重新创建并添加到其中,但不能直接更改。我们用副作用的影响来换取明确的重新分配(稍后会详细介绍)。

虽然我们没有改变数组的结构,但如果我们愿意,我们可以。稍后会详细介绍。

但是数组并不是跟踪不断增长的evt对象“列表”的唯一方法。我们可以使用闭包:

  1. function trackEvent(evt,keypresses = () => []) {
  2. return function newKeypresses() {
  3. return [ ...keypresses(), evt ];
  4. };
  5. }
  6. var keypresses = trackEvent( newEvent1 );
  7. keypresses = trackEvent( newEvent2, keypresses );

你看到这里发生了什么吗?

每次我们向“列表”添加一个新事件时,我们都会创建一个新的闭包,它围绕着现有的keypresses()函数,该函数捕获当前的evt。当我们调用keypresses()函数时,它将依次调用所有嵌套函数,为所有单独关闭的evt对象构建一个中间数组。闭包是跟踪所有状态的机制;您看到的数组只是需要从函数返回多个值的方法的实现细节。

那么哪一个更适合我们的任务呢?毫无疑问,数组方法可能更合适。闭包的结构不变性意味着我们唯一的选择是在它周围封装更多的闭包。对象默认是可扩展的,因此我们可以根据需要增长数组。

那么哪一个更适合我们的任务呢?毫无疑问,数组方法可能更合适。闭包的结构不变性意味着我们唯一的选择是在它周围封装更多的闭包。对象默认是可扩展的,因此我们可以根据需要增长数组。

顺便说一下,尽管我将这种结构可变性表示为闭包和对象之间的明显区别,但是我们将对象用作不可变值的方式实际上非常相似。

为数组的每一个添加创建一个新数组就是将数组视为结构上不可变的,这在概念上与闭包是对称的,闭包的设计本身就是结构上不可变的。

私有

在分析闭包和对象时,您首先想到的一个区别可能是闭包通过嵌套的词法范围提供了状态的“私有”,而对象则将所有内容公开为公共属性。这种隐私有一个奇特的名字:信息隐藏。

考虑词法闭包隐藏:

  1. function outer() {
  2. var x = 1;
  3. return function inner(){
  4. return x;
  5. };
  6. }
  7. var xHidden = outer();
  8. xHidden(); // 1

现在同样的状态在公共中:

  1. var xPublic = {
  2. x: 1
  3. };
  4. xPublic.x; // 1

在一般软件工程原则方面有一些明显的差异——考虑抽象、带有公共api和私有api的模块模式,等等——但是让我们将讨论限制在FP的视角;毕竟,这是一本关于函数式编程的书!

可见性

隐藏信息的能力似乎是状态跟踪的一个理想特性,但我认为FPer可能持相反的观点。

将状态管理为对象上的公共属性的优点之一是,枚举(并迭代)状态中的所有数据更容易。假设您希望处理每个按键事件(来自前面的示例),并使用以下实用程序将其保存到数据库:

  1. function recordKeypress(keypressEvt) {
  2. // 数据库操作
  3. DB.store( "keypress-events", keypressEvt );
  4. }

如果您已经有了一个数组——只是一个对象,它具有公共数字命名的属性——那么使用内置的JS数组实用程序forEach(..)就非常简单了

  1. keypresses.forEach( recordKeypress );

但是,如果按键列表隐藏在闭包中,则必须在闭包的公共API上公开一个实用程序,该实用程序具有访问隐藏数据的特权。

例如,我们可以给我们的闭包的keypresses例子自己的forEach,就像内置数组有:

  1. function trackEvent(
  2. evt,
  3. keypresses = {
  4. list() { return []; },
  5. forEach() {}
  6. }
  7. ) {
  8. return {
  9. list() {
  10. return [ ...keypresses.list(), evt ];
  11. },
  12. forEach(fn) {
  13. keypresses.forEach( fn );
  14. fn( evt );
  15. }
  16. };
  17. }
  18. // ..
  19. keypresses.list(); // [ evt, evt, .. ]
  20. keypresses.forEach( recordKeypress );

对象状态数据的可见性使它的使用更加直观,而闭包模糊了状态,使我们更加努力地处理它。

变更控制

如果词法变量x隐藏在闭包中,那么唯一可以自由重新分配它的代码也在闭包中;从外部修改x是不可能的。

正如我们在第6章中所看到的,仅这一事实就通过减少读者必须考虑的预测任何给定变量行为来提高代码的可读性。

词法重新分配的局部相似性是我不认为const这个特性有帮助的一个重要原因。范围(以及闭包)通常应该非常小,这意味着只有几行代码可以影响重新分配。在上面的outer()中,我们可以快速检查是否有一行代码重新分配了x,因此它实际上是一个常量。

例如,这种确保证对我们对函数纯度的有强大的贡献。

另一方面,xPublic.x是一个公共属性,程序中任何引用xPublic的部分在默认情况下都可以重新分配xPublic.x到另一个值。这需要考虑更多的代码行!

这就是为什么在第6章中Object.freeze(..)看作是一种快速的方法,它使对象的所有属性都是只读的(“writable: false”),因此不能不可预知地重新分配它们。

不幸的是,Object.freeze(..)既是全有或全无的,也是不可逆转的。

使用闭包,您可以修改一些代码,而程序的其他部分则受到限制。冻结对象时,代码的任何部分都无法重新分配。此外,一旦一个对象被冻结,它就不能被解冻,因此在程序执行期间属性将保持只读。

在允许重新分配但限制其表现的地方,闭包比对象更方便、更灵活。在不需要重新分配的地方,冻结的对象比在函数中重复const声明要方便得多。

许多函数编程者采取强硬立场在重新分配:它不应该使用。他们倾向于使用const将所有闭包变量设置为只读,并使用 Object.freeze(..)或完整的不可变数据结构来防止属性重新分配。此外,他们将尽可能减少显式声明/跟踪的变量和属性的数量,更喜欢值传递——函数链、作为参数传递的return值,等等——而不是中间值存储。

这本书是关于JavaScript中的“轻量函数”编程的,这是我与函数编程核心人群的分歧之一。

我认为变量重新分配是非常有用的,如果使用得当,它的明确性是非常可读的。根据我的经验,如果可以插入一个debugger或断点,或者跟踪一个watch表达式,调试就会容易得多。

克隆状态

正如我们在第6章中了解到的,防止副作用侵蚀代码可预测性的最好方法之一是确保将所有状态值都视为不可变的,而不管它们实际上是否不可变(冻结)。

如果您没有使用专门构建的库来提供复杂的不可变数据结构,那么最简单的方法就足够了:每次更改之前复制对象/数组。

数组很容易被浅克隆——只需使用...数组扩展:

  1. var a = [ 1, 2, 3 ];
  2. var b = [...a];
  3. b.push( 4 );
  4. a; // [1,2,3]
  5. b; // [1,2,3,4]

对象也可以相对容易地浅克隆:

  1. var o = {
  2. x: 1,
  3. y: 2
  4. };
  5. // ES2018+可以使用对象扩展:
  6. var p = { ...o };
  7. p.y = 3;
  8. // 在 ES6/ES2015+:
  9. var p = Object.assign( {}, o );
  10. p.y = 3;

如果对象/数组中的值本身是非基础值(对象/数组),则要进行深度克隆,必须手动遍历每一层以克隆每个嵌套对象。否则,您将拥有对这些子对象的共享引用的副本,这可能会在程序逻辑中造成混乱。

您是否注意到,之所以可以进行克隆,是因为所有这些状态值都是可见的,因此很容易复制?那么包在闭包中的一组状态呢?你如何克隆那个状态?

这要乏味得多。本质上,您必须做一些类似于前面的自定义forEachAPI方法的事情:在闭包的每个层中提供一个函数,该函数具有提取/复制隐藏值的特权,并在此过程中创建新的等效闭包。

尽管这在理论上是可能的——这是读者的另一个练习!对于任何实际的程序来说,实现它的实际意义都远远小于它的实际意义。

对象在表示我们需要能够克隆的状态方面具有明显的优势。

性能

从实现的角度来看,对象可能比闭包更受欢迎的一个原因是,在JavaScript中,对象在内存甚至计算方面通常更轻。

但是要注意这是一个通用的断言:您可以对对象执行许多操作,这些操作将消除忽略闭包并转向基于对象的状态跟踪所带来的性能收益。

让我们考虑一个同时具有这两种实现的场景。首先,封闭式实现:

  1. function StudentRecord(name,major,gpa) {
  2. return function printStudent(){
  3. return `${name}, Major: ${major}, GPA: ${gpa.toFixed(1)}`;
  4. };
  5. }
  6. var student = StudentRecord( "Kyle Simpson", "CS", 4 );
  7. // 然后
  8. student();
  9. // Kyle Simpson, Major: CS, GPA: 4.0

内部函数printStudent()关闭三个变量:name, major, 和 gpa。无论我们在何处传递对该函数的引用,它都会保持这种状态——在本例中,我们将其称为student()

现在对于对象(和“this”)方法:

  1. function StudentRecord(){
  2. return `${this.name}, Major: ${this.major}, \
  3. GPA: ${this.gpa.toFixed(1)}`;
  4. }
  5. var student = StudentRecord.bind( {
  6. name: "Kyle Simpson",
  7. major: "CS",
  8. gpa: 4
  9. } );
  10. // 然后
  11. student();
  12. // Kyle Simpson, Major: CS, GPA: 4.0

student()函数——在技术上称为“绑定函数”——有一个硬绑定的this引用,指向我们传入的对象文本,这样以后任何对student()的调用都会将该对象用于它的this,从而能够访问它的封装状态。

这两种实现都有相同的结果:具有保留状态的函数。但是性能呢?会有什么不同?

注意:准确而有效地判断JS代码片段的性能是一件非常危险的事情。我们不会在这里讨论所有的细节,但我强烈建议您阅读《您不知道JS: Async & Performance》,特别是第6章,“基准测试和调优”,以获得更多的细节。

如果您正在编写一个用它的函数创建状态对的库——第一个代码片段中对StudentRecord(..)的调用,或者第二个代码片段中对StudentRecord.bind(..)的调用——您可能最关心这两个函数的性能。检查代码,我们可以看到前者每次都必须创建一个新的函数表达式。第二种方法使用bind(..),其含义并不明显。

考虑bind(..)在内部做什么的一种方法是,它在函数上创建一个闭包,如下所示:

  1. function bind(orinFn,thisObj) {
  2. return function boundFn(...args) {
  3. return origFn.apply( thisObj, args );
  4. };
  5. }
  6. var student = bind( StudentRecord, { name: "Kyle.." } );

这样,看起来我们场景的两种实现都创建了一个闭包,所以性能可能差不多。

然而,内置的bind(..)实际上并不需要创建一个闭包来完成该任务。它只是创建一个函数,并手动将其内部this设置为指定的对象。这可能比我们自己封闭更有效。

我们在这里讨论的性能节省对于单个操作来说是微不足道的。但是,如果您的库的关键路径是这样做数百次、数千次或更多次,那么节省的时间就会很快增加。许多库——Bluebird库就是一个这样的例子——最终都通过删除闭包和使用对象来优化,这就是所谓的优化。

在库用例之外,状态与其函数的配对通常只在应用程序的关键路径中发生相对较少的情况。相比之下,通常使用函数+状态方式(在两个代码段中调用student())更为常见。

如果代码中的某些特定情况就是这种情况,那么您可能应该更关心后者的性能问题。

绑定函数过去的性能一般都很差,但最近JS引擎对其进行了高度优化。如果您在几年前对这些变化进行基准测试,完全有可能使用最新的引擎重复相同的测试,得到不同的结果。

现在,绑定函数的性能至少可能与等价的闭包函数一样好,甚至更好。这是另一个有利于对象而不是闭包的标记。

我只想重申:这些性能观察并不是绝对的,对于给定的场景,确定什么是最好的是非常复杂的。不要随意地应用你从别人那里听到的,甚至是你在其他早期项目中看到的。仔细检查对象或闭包是否适合该任务。

总结

这一章的真理是不能写出来的。人们必须阅读这一章才能找到它的真理。


在这里,我想要创造一些禅宗智慧,让自己变得更聪明。但是你应该对这一章的信息做一个适当的总结。

对象和闭包是同构的,这意味着它们可以在一定程度上互换使用,以表示程序中的状态和行为。

表示为闭包有一定的好处,比如粒度更改控制和自动隐私。表示为对象还有其他好处,比如更容易克隆状态。

批判性思维的函数编程者应该能够构想出程序中任意一段状态和行为,并选择最适合当前任务的表示。