JavaScript 反应式编程(一)

原文:zh.annas-archive.org/md5/67A6EE04B94B64CB5365BD89131EE253

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

印象派画家克劳德·莫奈曾经著名地说过:“莫奈只是一双眼睛,但是多么美的一双眼睛!”今天,我们可以类似地说:“ReactJS 或者如果你愿意,“ReactJS 只是一个视图,但是多么美的一个视图!”

ReactJS 既没有意图也没有野心成为一个完整的通用 Web 框架。它甚至不包括用于 Ajax 调用的工具!相反,意图是您将使用适合应用程序不同方面的技术,并使用 ReactJS 的强大工具来进行视图和用户界面开发。

函数式反应式编程一直是一个极其高不可攀的果实,其纯数学期望对于工作来说是一个禁区。但是现在有了 ReactJS!一个没有特别深厚数学背景的资深 C++程序员——我说这话是为了挑选一类程序员的形象,他们在 Stack Overflow 上一直说他们不懂函数式反应式编程——是一个有很大机会使用 ReactJS 完成真正工作的程序员。

这本书是关于 ReactJS 的,这是一个简单而小巧的技术,尽管如此,它让庞大的团队在网页的不同组件上合作而不会互相干扰,但又没有一丝官僚主义的痕迹。再加上一些自由的仙女粉。

本书涵盖的内容

[第一章, 介绍和安装,提供了对不同编程范式的一览,每种范式都有其优势,并介绍了函数式编程、反应式编程和函数式反应式编程的三位一体。

第二章, 核心 JavaScript,涵盖了 JavaScript 的一些更好的领域,并省略了雷区,这要感谢 Douglas Crockford,即使不完全同意。就你使用的 JavaScript 部分而言,你应该在这个核心内完成大部分工作。

第三章, 反应式编程-基本理论,是对反应式理论或反应式编程的基本探索,特别是与 Facebook 的 ReactJS 用户界面框架相关。

第四章, 演示非功能性反应式编程-实时示例,证明并非所有开发都是从零开始的。大多数专业工作并非绿地。这将提供一个实时示例,将一个简单的视频游戏(最近使用 jQuery 实现)改装以利用 ReactJS(如果您正在使用 ReactJS,您可能会进行其他从 jQuery 到 ReactJS 的转换)。

第五章, 学习函数式编程-基础知识,如果你想了解函数式编程但不知道从哪里开始,这里是一个开始的地方!介绍了 map、reduce 和 filter 作为一个用不完的技巧袋。

第六章, 函数式反应式编程-基础知识,涵盖了关于函数式编程和反应式编程的内容。它将与一些明智的建议结合在一起,并为本书中剩下的实际操作工作奠定最后的基础。

第七章, 不重复造轮子-功能性反应式编程工具,包含了很多内容,甚至在一本书中,更不用说一个章节了。但这意味着有一个有趣的样本空间,其中提供了许多有趣的选择,包括从其他语言编写 ReactJS 代码而不是 JavaScript。

第八章,“使用实例演示 JavaScript 中的函数式响应式编程 - 实例演示第 I 部分”,我们看到了一个应用程序,其中包含一个用 ReactJS 从头编写的诙谐 ReactJS 组件,并展示了甜蜜的 JSX 语法糖,虽然不是必需的,但仍然可用于 ReactJS 开发。

第九章,“使用实例演示 JavaScript 中的函数式响应式编程第 II 部分 - 待办事项列表”,带我们进入了第一个真正的组件,旨在被使用而不仅仅是娱乐。我们实现了一个待办事项列表,除了“完成”之外,它还有几个标记,用于指示任务的状态、优先级和其他信息。

第十章,“使用实例演示 JavaScript 中的函数式响应式编程:实例演示第 III 部分 - 日历”,我们将构建一个日历。它旨在优雅地支持不仅是一次性事件,还有各种规则的多种重复事件。

第十一章,“使用实例演示 JavaScript 中的函数式响应式编程第 IV 部分 - 添加一个草稿本并将其整合在一起”,提供了一个带有 CKeditor 的富文本草稿本。这展示了我们如何与其他用户界面工具进行交互。然后,我们将四个组件合并到一个组合页面中,并添加持久性功能,以便我们的用户界面不会忘记它所告诉的事情。

第十二章,“一切如何契合”,回顾了本书中涵盖的内容,并探讨了在探索世界中的下一步。

附录,“Node.js 快速入门”,探讨了“狂野西部”技术的一些优点、缺点和丑闻,似乎每个人都想参与其中。

本书需要什么

需要下载一些软件,并且您需要一个至少能够提供静态内容的 Web 服务器。附录,“Node.js”,介绍了如何在 Node.js 中构建一个用于更大项目的 Web 服务器,但所有章节都可以使用最基本的方式提供静态内容的 Web 服务器。您需要一台台式电脑,几乎可以是任何可以运行 Node.js 的设备(如果您选择通过附录进行操作)。文本将在 Unix、Linux、Mac、Windows、Cygwin 等系统上运行得足够好。如果您想从移动设备上运行它,这可能是一个值得称赞的方法,但请使用标准的服务器或台式机操作系统。

然而,你真正需要的只是一个服务器或台式机、一个像 Chrome 这样的浏览器、一个 Web 服务器,以及愿意尝试新事物的心态。其他一切都在文本中提供。

这本书适合谁

本书旨在面向希望深入了解函数式响应式编程和 Facebook 的 ReactJS 的程序员。我们期望读者具有一定的编程素养,了解 JavaScript,并且对用户界面的制作有一定的了解。熟悉函数式编程也是有帮助的,但我们希望能够创建一本书,即使是在任何通用语言中具有一定(也许是轻微的)JavaScript 和 Web 开发知识的资深程序员也能够使事情顺利进行。

对于那些在前端网页开发和 JavaScript 的功能核心方面有扎实背景的人来说,他们可能会惊讶地发现使用 ReactJS 是多么容易,就像切黄油一样容易。

约定

在本书中,您会发现一些区分不同信息类型的文本样式。以下是一些示例以及它们的含义解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名会显示如下:“typeof函数返回一个包含类型描述的字符串;因此,typeof可以提供扩展类型。”

一块代码设置如下:

   var counter = (function() {
     var value = 0;
     return {
       get_value: function() {
         return value;
       },
       increment_value: function() {
         value += 1;
       }
     }
   })();

任何命令行输入或输出都以以下方式编写:

python -c "import binascii; print binascii.hexlify(open('/dev/random').read(1024))"

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:“当安装程序启动时,点击下一步,如下所示:”

注意

警告或重要说明会以这样的框出现。

提示

提示和技巧会以这样的方式出现。

第一章:介绍和安装

欢迎来到 JavaScript 中反应式(函数式)编程的美妙世界!在本书中,我们将涵盖 JavaScript 的好部分,尽管我们不会完全遵循它。我们将涵盖函数式编程、反应式编程和 ReactJS 库,并将所有这些整合到 JavaScript 的函数式反应式编程中。如果您要学习反应式编程,我们建议您认真考虑函数式反应式编程,包括尽可能多地学习函数式编程。在这种情况下,整个函数式反应式编程的总和大于其各部分。我们将把反应式编程应用到 JavaScript 用户界面开发中。用户界面是函数式反应式编程FRP)真正闪耀的领域。

本章将涵盖以下主题:

  • 主题的概述,包括:

  • 一个更简单的用户界面编程方法的讨论

  • 对编程范式的简要讨论,如函数式和反应式编程

  • 本书中各章的概述

  • 查看如何安装本书中使用的一些工具

一个概述

有很多事情可以说,但(函数式)反应式编程可能比您想象的要容易。今天,关于函数式反应式编程的大部分内容都令人生畏,就像几年前关于闭包的说明一样。

处理用户界面编程的更简单方法

多年前,当我开始学习 JavaScript 时,我选择了一个网站,从中我学到了我需要理解的一切,以执行面向对象的信息隐藏,也就是如何创建一个具有私有字段的 JavaScript 对象。我可能读了两三章,这些章节充满了理论计算机科学,还有引言的 10-15%,然后我放弃了。然后我发现使用闭包来创建一个具有私有字段的对象是多么容易,只是简单的学来用去

   var counter = (function() {
  var value = 0;
  return {
    get_value: function() {
      return value;
    },
    increment_value: function() {
      value += 1;
    }
  }
})();

现在,函数式反应式编程正处于 JavaScript 闭包几年前的状态。在开始反应式编程之前,您必须阅读的理论数量令人震惊,而且大部分文献都是博士阅读水平的。这是个坏消息。但好消息是,您不必阅读那么多。

本书的目的是提供一种类似于学来用去的方式,传达如何使用闭包来创建具有私有字段的 JavaScript 对象。理论本身并不坏,引入理论进行讨论也不是问题,但是以做一些简单事情的代价来制作一篇完整的论文的理论支持是一个问题。

我们希望这本书能让您明白,例如,使用函数式反应式编程在 JavaScript 中构建游戏用户界面比使用 jQuery 更容易。

编程范式

周围有多种编程范式,并且并非所有都是互斥的。许多编程语言都是多范式语言,支持使用多种范式,包括不仅仅是 JavaScript,还有 OCaml、PHP、Python 和 Perl 等语言。

请注意,您至少有时可以使用一种语言来支持范式,而该语言并非明确设计为支持它。面向对象编程最初并不是为像 Java 或 Ruby 这样专门用于支持面向对象编程的语言而制定的,而是作为一种工程学科的问题,最初是在早于面向对象编程的语言中使用的。

在编程范式中,我们现在有以下内容:

  • 面向方面的编程:有人建议程序员的专业发展从过程式编程转向面向对象编程,然后是面向方面的编程,最后是函数式编程。面向方面编程的一个典型例子是日志记录,它是在程序中通过天真的用法传播的一个方面。面向方面编程处理编程的横切面,如安全性、状态的诊断暴露和日志记录。

  • 声明式编程:函数式响应式编程的一个关键概念是它是声明式的,而不是命令式的。换句话说,c = a + b并不意味着取a的当前值,加上b的当前值,并将它们的总和存储在c中。相反,我们声明一个持久的关系,它的工作方式有点像电子表格中的C1 = A1 + B1。如果A1B1发生变化,C1会立即受到影响。存储在C1中的不是赋值时的A1值加上B1值的结果,而是更持久的东西,可以按需获取单个值。

  • 防御性编程:类似于防御性驾驶,防御性编码意味着编写的代码在给定有缺陷的输入时能够正确地运行。函数式响应式编程是一种在面对网络问题和非理想的现实条件时,要么能够正确运行,要么能够在恶劣条件下优雅地降级的方法。

  • 函数式编程:这里,术语函数具有数学意义,而不是编程意义。在命令式编程中,函数可以(而且通常是)操作状态。因此,init()函数可能会初始化程序最初运行所需的所有数据。函数是接受零个或多个输入并返回结果的东西。例如,f(x) = 3x+1g(x) = sin(x)h(x, y) = x’(y)(在y处的x的导数)都是数学函数;它们都不涉及任何状态数据的操作。纯函数是在数学定义下排除了如何处理状态的函数。函数式编程还允许并经常包括高阶函数,或者作用于函数的函数(在微积分中,导数或积分代表一个高阶函数,迭代积分包括一个以另一个高阶函数作为输入的高阶函数)。解决方案集中在抽象函数上的问题,这些函数操作抽象函数,往往对计算机科学类型更具吸引力,而不是真正用于商业世界的东西。这里探讨的高阶函数将是相对具体的。你不需要一直使用高阶函数,一旦掌握了核心概念,它们并不难使用。

  • 命令式编程:命令式编程是一种常见的编程方式,对于大多数首次接触命令式编程的程序员来说,它可能是最自然的工作方式。函数式响应式编程的营销提案包括了对这种基本方法的另类选择。函数式响应式编程的声明式编程、函数式编程中的纯函数(包括高阶函数)以及响应式编程的时间序列,提供了一种对命令式编程自然倾向的替代方案。

  • 信息隐藏:Steve McConnel 的《代码大全》描述了几种方法,并告诉我们哪些方法对不同的环境最为理想(例如,对于过程式编程,最适合的环境是小型项目而不是面向对象编程)。对于仅仅信息隐藏而言,他的建议是“尽可能多地使用这个”。在通用信息隐藏的发展中,一个大型项目通过在更大的区域内封装秘密来处理,而更大的秘密则通过封装子秘密来分割。过程式编程、面向对象编程和功能性编程的一个很大部分都是为了促进信息隐藏。信息隐藏是洛德米特法则背后的软件工程问题,例如,你可以在方法调用中有一个点(foo.bar()),但不能有两个点(foo.baz.bar())

  • 面向对象编程:程序被分割成对象,而不是具有单一结构。这些对象有自己的方法和字段,可能又被分割成更多的对象。这为比过程式编程更大的项目提供了一个可接受的信息隐藏水平,即使面向对象编程基本上是从过程式编程开始并在其基础上构建。

  • 模式:模式并不是好软件的配方,但在更高层次的人类抽象中,它们提供了一种谈论最佳重复解决方案的方式,以避免从头开始重新发明已经解决的问题。此外,特定的模式被提到了台面上,包括 MVC 和现在的观察者模式,尽管观察者模式通常不会在与响应式编程相关的情况下提到,但它却是一个重要的组成部分。

  • 过程式编程:过程式编程是提到的方法中最古老的之一,它旨在为早期基于goto流程控制的意大利面代码提供一些秩序。也许我们可以批评过程式编程,因为一旦面向对象编程、面向方面编程和面向对象设计模式可用,它就不再做足够多的事情。当你有工具可以从 goto 的鼠窝、指针作为数据结构的 goto 等方面进一步推进时,从过程式编程转向其他编程是正确的选择。

  • 响应式编程:假设功能性编程在很大程度上是指函数具有第一类地位,并且可以创建高阶函数(作用于其他函数的函数)。那么响应式编程在很大程度上是指时间序列(随时间具有不同值的函数)具有第一类地位。对于音乐、游戏、用户界面和其他一些用例,计算当前时刻的正确值是响应式编程的一个亮点。

  • 功能性响应式编程:功能性响应式编程是建立在功能构件上的响应式编程,其中函数和时间序列都是第一类实体。有一些有用且令人惊讶地简单的函数,可以作用于一个时间序列,从而提供另一个时间序列(这两个序列中的任何一个都可以被其他时间序列的函数所作用)。功能性响应式编程的一个主要卖点是,它提供了比直接陷入“回调地狱”更为优雅和可维护的方法。

安装所需工具

许多读者可能已经安装了 Chrome 和 Node.js,如果他们之前没有安装的话,他们可能会感到很舒适。对于那些更喜欢逐步指导的人,以下是安装适当软件的详细信息。

谷歌浏览器可以从google.com/chrome安装。请注意,对于某些 Linux 发行版,Chrome 可能无法从软件包管理器中获取。谷歌浏览器是一个明显的选择,可以考虑将其包含在发行版的软件包中,但由于许可问题,Chrome 的某些部分可能被列为非免费,这意味着就发行版维护者而言,您可以使用它,但我们不愿意将其包含在仅免费的软件包存储库中。

Node.js 可以从nodejs.org/download获取。如果您使用 Linux,最好通过软件包管理器获取。请注意,Node.js 自带其自己的软件包管理器 npm,可用于下载在 Node.js 下使用的软件包。

有用的 ReactJS 入门套件可以从facebook.github.io/react/downloads.html获取。

以下说明适用于 Windows 8.1(我更喜欢在 Mac 或 Linux 上开发,但是写作 Windows 8.1 作为通用语言)。

安装谷歌浏览器

我们将使用谷歌浏览器作为主要的参考浏览器:

  1. 要下载它,转到google.com/chrome,然后点击左下方的立即下载按钮,如下所示:安装谷歌浏览器

  2. 接下来,点击右下方的接受并安装按钮,如下面的截图所示:安装谷歌浏览器

  3. 然后,当询问是否要运行或保存安装程序时,点击运行按钮,如下所示:安装谷歌浏览器

  4. 接下来,授权 Chrome 的安装程序对系统进行更改,如下面的截图所示:安装谷歌浏览器

  5. 然后点击下一步按钮安装 Chrome,如下面的截图所示:安装谷歌浏览器

  6. 等待一分钟进行安装,然后如果愿意,将 Chrome 设置为默认浏览器:安装谷歌浏览器

就是这样!

安装 Node.js

安装 Node.js 很简单,它使得使用 JavaScript 作为唯一语言来启动 HTTP 服务变得容易。

  1. 转到nodejs.org/download安装 Node.js

  2. 点击Windows 安装程序,等待安装程序下载。然后点击窗口的左下角,如此截图所示:安装 Node.js

  3. 当安装程序启动时,点击下一步,如下所示:安装 Node.js

  4. 然后点击复选框接受协议条款,如下所示:安装 Node.js

  5. 之后,点击下一步按钮继续,如下截图所示:安装 Node.js

  6. 当询问要安装软件的位置时,点击下一步按钮,如下截图所示:安装 Node.js

  7. 然后点击下一步继续,如下一截图所示。如果需要,自定义功能:安装 Node.js

  8. 之后,点击安装按钮继续安装,如下所示:安装 Node.js

  9. 最后,点击完成按钮完成安装 Node.js,如下截图所示:安装 Node.js

  10. 授权安装程序对计算机进行更改,如下所示:安装 Node.js

安装 ReactJS 的入门套件

要安装入门套件,请执行以下步骤:

  1. 转到facebook.github.io/react/downloads.html,您将看到一个类似于以下截图的屏幕:安装 ReactJS 的入门套件

  2. 单击下载入门套件 0.12.0开始下载(这将显示在左下角),如前面的截图所示。

  3. 您将看到一个 ZIP 文件在底部下载:安装 ReactJS 的入门套件

  4. 从这里,您应该能够浏览 ZIP 文件的内容:安装 ReactJS 的入门套件

总结

在本章中,我们简要概述了编程范式,以表明函数式响应式编程可能适用的领域,并安装了基本工具。

我们将在下一章讨论 JavaScript。Node.js 的基础知识在附录中讨论。

第二章:核心 JavaScript

JavaScript 是一个好坏参半的语言。JavaScript 有一些真正糟糕的部分和一些优秀的部分。总的来说,JavaScript 是一种动态的、弱类型的、解释性的脚本语言。对整个语言的处理是重点探索好的部分,正如 Douglas Crockford 所描述的那样,因为 JavaScript 的坏部分有多糟糕:它们确实是雷区。Crockford 对 JavaScript 的现在的使用产生了深远的影响;足以让题为AngularJS:坏部分的博客文章立即、清楚、完全地传达了将会被痛苦细致地剖析的事物。

本章涵盖的主题包括:

  • 严格模式:隐式用于包括 ECMAScript 6 模块的代码

  • 变量和赋值:任何语言中编程的基本构建块之一

  • 注释:有两种风格;我们更偏向于一种,因为通过在程序员意图之外的地方开始或结束注释,很容易产生意外行为

  • 流程控制:基本的 if-then、if-then-else 和 switch 语句的简要介绍

  • 关于值和 NaN 的注释:关于真值和有毒的非数字值的注释

  • 函数:JavaScript 函数的示例,是语言中最好的部分之一

  • 关于 ECMAScript 6 的一些简要注释:多年来核心 JavaScript 的第一个真正的根本性变化

严格模式

“use strict”;作为文件或函数的第一行将导致某些可能引起无数问题的事情(例如在没有声明的情况下对变量进行赋值)以清晰的错误行号报告错误,而不是让您从可能的多种后果中去猜测线索。 “use strict”;也可以是函数的第一行,在这种情况下,函数处于严格模式。

Perl 用户将了解-w标志,可能是与该语言相关的最著名的标志,以及它的后继者 Perl 的使用警告的惯例。文档中说了一些事情,比如,打开已知错误列表,“使用警告所暗示的行为不是强制性的”。

JavaScript 的严格模式本身可能与 Perl 的使用警告的惯例不相上下,但至少可以让您养成使用的习惯。

变量和赋值

变量应该使用var关键字声明。在函数外声明的任何变量,或者在没有声明的情况下使用的任何变量,都是全局变量,全局变量是一个大问题;它们在默认 JavaScript中的位置是主要的设计缺陷之一。

当 Java 进行了一次重大的公共关系推动时,JavaScript 被命名为在 Java 的影响下运行,并且做出了某些决定,使 JavaScript 代码看起来像 Java。这些决定是不幸的。JavaScript 在形式上是一种类似 C 的语言,它与 Java 或 C#的最近共同祖先是 C,而不是 C++或 Java。JavaScript 被描述为穿着 C 的外衣的 Scheme。Lisp 是与包括 Scheme、Common Lisp、Clojure 和 ClojureScript 在内的一系列语言相关的语法,可以说,追求最佳 JavaScript 的日益功能性的重点来自 ClojureScript。在 JavaScript 中,没有单独的整数和浮点类型;数字是 64 位浮点值,在一定的长范围内使用时表现得像整数。然而,它们有时会给新程序员带来惊喜;例如,0.1 + 0.2 并不等于 0.3,出于历史原因,这也困扰着其他无数语言。

基本变量赋值看起来像 C 语言:

var x;
var y = 12 + 2;

如果变量声明但未赋值,如前面示例中的x,其值将为 undefined。

以下是等效的:

y = y + 1;
y += 1;
++y;

我们将避免在前面示例中列出的最后一个选项的使用,因为它不被认为是好的部分之一。道格拉斯·克罗克福德在其中的一个视频中讲述了一个故事,他在一个小时的辩护中对++y的使用进行了精彩的辩护,然后进行了一场漫长的调试会话,在这场调试会话中,++y的微妙之处已经咬了他一口。与前面示例中的前两个选项不同,可以为其分配一个值,并且++yy++不相同。克罗克福德随后慷慨地放弃了他先前的立场。

注释

大多数语言都支持某种形式的注释。除了对代码的解释外,它们还用于临时停用代码。JavaScript 具有与 Java 相似的语言所期望的注释;但是,Javadoc 注释并不是本地特殊的(已经制定了各种解决方案,如 JSDoc 来填补这一空白)。

JavaScript 支持 C++注释的两类。前三行只包含注释而没有可执行代码,最后一行有一行代码,然后一直到行尾都是注释:

/*
 * Multiline comments are legal.
 */

if (x) // In this case, we ...

我们将避免多行注释。星号和斜杠在正则表达式中经常出现,多行注释可能会引起问题,并且可能需要上下文或由其他人编写的代码中仍然会引起意外效果。内联注释本质上不太容易受到意外效果的影响。

流程控制

如果-然后和如果-然后-否则按照以下示例代码的描述工作,其中一个在数字非零时执行某些操作,另一个在数字非零时执行一个动作,如果为假则执行另一个动作。两者都使用了真值,其中 0(和 NaN)为假,而任何其他值为真:

if (books_remaining) {
  print_title();
}

var c = 1;
if (c) {
  c += 2;
} else {
  c -= 1;
}

关于值和 NaN 的注释

所有值都可以在布尔上下文中使用并进行真值测试。值undefinednull''0[]falseNaN(不是数字)都是假值,所有其他值都是真值

NaN特别是一个特殊情况,它的行为不像其他真实数值。NaN是有毒的;包含NaN的任何计算都将得到NaN的结果。此外,尽管NaN假值,但它不等于任何东西,包括NaN本身。检查NaN的通常方法是通过isNaN()函数。如果您发现NaN潜伏在某个意想不到的地方,您可能会为代码提供调试日志语句,指导您检测到NaN的位置;在生成NaN的地方和观察到它破坏结果的地方之间可能存在一定的距离。

函数

在函数中,默认情况下,控制从开始到结束,函数返回undefined。可选地,在结束之前可以有一个返回语句,函数将停止执行并返回相应的值。以下示例说明了一个带有返回值的函数:

var add = function(first, second) {
  return first + second;
}

console.log(add(1, 2));

// 3

前面的函数接受两个参数,尽管函数可以给出(没有错误或错误消息)少于或多于声明指定的参数。如果它们被声明为具有值,这些值将作为局部变量存在(在前面的示例中,firstsecond)。无论如何,这些参数也可以在一个类似数组的对象arguments中使用,该对象具有.length方法(数组具有.length方法,比项的最高位置大 1),但不具有数组的其他特性。在这里,我们创建一个函数,该函数可以接受任意数量的数字参数,并通过使用arguments伪数组返回它们的(算术)平均值。

var average_arbitrarily_many() {
  if (!arguments.length) {
    return 0;
  }
  var count = 0;
  var total = 0;
  for(var index = 0; index < arguments.length; index += 1) {
    total += arguments[i];
  }
  return total / arguments.length;
}

基本数据类型包括数字、字符串、布尔值、符号、对象、null 和 undefined。对象包括函数、数组、日期和正则表达式。

对象包括函数意味着函数是值,可以作为值传递。这有助于高阶函数,或者将函数作为值传递的函数。

作为高阶函数的一个例子,我们将包括一个 sort 函数,它对数组进行排序并可选择接受一个比较函数。这建立在函数定义上,实际上包含了一个函数定义在另一个函数定义中(这与其他任何地方一样合法),然后是一个 QuickSort 的实现,其中值被分为 比第一个元素小等于第一个元素比第一个元素大 进行比较,并且这三个中的第一个和最后一个被递归排序。我们在排序之前检查非空长度,以避免无限递归。作为高级函数实现的经典 QuickSort 算法如下:

var sort = function(array, comparator) {
  if (typeof comparator === 'undefined') {
    comparator = function(first, second) {
      if (first < second) {
        return -1;
      } else if (second < first) {
        return 1;
      } else {
        return 0;
      }
    }
  }
  var before = [];
  var same = [];
  var after = [];
  if (array.length) {
    same.push(array[0]);
  }
  for(var i = 1; i < array.length; ++i) {
    if (comparator(array[i], array[0]) < 0) {
      before.push(array[i]);
    } else if (comparator(array[i], array[0]) > 0) {
      after.push(array[i]);
    } else {
      same.push(array[i]);
    }
  }
  var result = [];
  if (before.length) {
    result = result.concat(sort(before, comparator));
  }
  result = result.concat(same);
  if (after.length) {
    result = result.concat(sort(after, comparator));
  }
  return result;
}

注释

有几个关于这个函数的基本特性和观察需要注意,这并不是要突破界限,而是要展示如何很好地覆盖标准基础:

  1. 我们使用 var sort = function()... 而不是允许的 function sort()...。当在函数内部使用时,这将函数存储在一个变量中,而不是在全局定义某些东西。请注意,在调试时,为函数包括一个名称可能会有所帮助,var sort = function sort()...,只能通过变量访问函数,并让调试器捕捉到第二个。例如:sort 而不是匿名地引用函数。请注意,使用 var sort = function(),变量声明被提升,而不是初始化;使用 function sort(),函数值被提升,在当前范围内任何地方都可用。

  2. 这是一种标准的检查方式,用于查看两个参数中是否只有一个被指定,即是否提供了一个数组但没有提供排序函数。如果没有,将提供一个默认函数。我们运行了几次排序的试验:

console.log(sort([1, 3, 2, 11, 9]));
console.log(sort(['a', 'c', 'b']));
console.log(sort(['a', 'c', 'b', 1, 3, 2, 11, 9]); 

这给了我们:

[1, 2, 3, 9, 11]
["a", "b", "c"]
["a", 1, 3, 2, 11, 9, "b", "c"] 

这给了我们一个调试的机会。现在,假设我们添加以下内容:

console.log('Before: ' + before);
console.log('Same: ' + same);
console.log('After: ' + after);

在结果声明之前,我们得到:

[1, 2, 3, 9, 11]
Before:
Same: a
After: c,b
Before: b
Same: c
After: 
Before:
Same: b
After:
["a", "b", "c"]
Before:
Same: a,1,3,2,11,9
After: c,b
Before: b
Same: c
After: 
Before: 
Same: b
After:  
["a", 1, 3, 2, 11, 9, "b", "c"]

输出中说 Same: a,1,3,2,11,9 看起来可疑,一个 Same 桶应该有相同的值,因此一个合适的输出可能是 Same: 2,2,2 或者只是 Same: 3,其中 Same 列表有五个值,没有两个是相同的。这不可能是我们想要的行为。看起来整数被分类为与初始的 “a” 相同,其余部分被排序。一点调查证实了 ‘"a" < 1’ 和 ‘"a" > 1’ 都是假的,所以我们的比较器可以改进。

我们对它们的类型进行了字典排序。这在类型的字母顺序上进行了一些任意排序,然后按照类型默认的排序顺序进行排序,这可以用另一个比较函数覆盖。这是另一种可能用于对数组进行排序的许多种比较器中的一个例子:与前一个不同,这个比较器将不同种类的项目进行分段,例如按顺序排序的数字将出现在字符串之前,按顺序排序:

        var comparator = function(first, second) {
          if (typeof first < typeof second) {
            return -1;
          } else if (typeof second < typeof first) {
            return -1;
          } else if (first < second) {
            return -1;
          } else if (second < first) {
            return 1;
          } else {
            return 0;
          }
        }

typeof 函数返回一个包含类型描述的字符串;因此 typeof 可以提供一个扩展类型。使用类似于这样的比较函数,可以有意义地比较诸如包含名字、姓氏和地址的记录之类的对象。

对象可以通过花括号表示法声明。代码块和对象都可以使用花括号,但这是两个不同的东西。下面的代码及其花括号,不是一个带有要执行的语句的代码块;它定义了一个具有键和值的字典:

var sample = {
  'a': 12,
  'b': 2.5
};

除非键是保留字或包含特殊字符,如 'strange.key'(这里是一个句号),否则键周围的引号是可选的。为了有一个简单和一致的规则,JSON 格式要求在所有情况下都使用引号,特别是双引号,而不是单引号。

下面显示了一个记录具有名字、姓氏和电子邮件地址的示例,可能是通过 JSON 填充的。这个示例不是 JSON,因为它没有遵循 JSON 关于双引号引用所有键和所有字符串的规则,但它说明了一个记录数组,其中可能有其他字段,可能会更长。按距离或资历排序可能是有意义的(这里没有显示填充字段):可能有一整套可能用于记录的比较器。

var records = [{
    first_name: 'Alice',
    last_name: 'Spung',
    email: 'a.spung@yahoo.com'
  }, {
    first_name: 'Robert',
    last_name: 'Hendrickson',
    email: 'Bob.Hendrickson@gmail.com'
  }
];

请注意,尾随逗号不仅在 JavaScript 中是不合适的(在几乎任何由逗号分隔的东西的最后一个条目之后),而且它有一些奇怪和意想不到的行为,这可能极其难以调试。

JavaScript 被设计为在语句的末尾有分号,这可能是可选的。这是一个有争议的决定,与决定制作一种流行的语言,普通非程序员可以在不涉及 IT 人员的情况下使用有关,这也是 SQL 设计中涉及的因素。当适当时,我们应该始终提供分号。这样做的一个副作用是,单独一行的return将返回 undefined。这意味着以下代码将不会产生预期的效果,并且将返回 undefined:

return
  {
  x: 12
  };

提示

代码执行时的效果与它看起来的效果以及可能的意图不同,因此最好不要这样做。

为了获得期望的效果,开放的大括号应该与 return 语句在同一行,如下所示:

return {
  x: 12
};

然而,JavaScript 确实具有面向对象编程,避免了面向对象编程中的一个经典困难:必须第一次就正确地获取本体论。对象通常最好是通过工厂而不是类的实例来构造。或者道格拉斯·克罗克福德已经被缩写。原型仍然是语言的一部分,就像许多好的和坏的特性一样,但是除了涉及奇特的用例,对象通常应该由允许“比本体论驱动的类”更好的面向对象编程方法的工厂制造。我们将避免伪经典的新function(),不是因为如果你忘记了新的话它可能会破坏全局变量,而是因为它传统面向对象编程的外观并没有真正帮助太多。

提示

你应该知道 JavaScript 中一个广受尊敬的惯例,即打算与new一起使用的构造函数以大写字母开头。如果函数名以大写字母开头,那么它打算与new关键字一起使用,如果在没有new关键字的情况下调用,可能会发生奇怪的事情。

在 JavaScript 中,一些其他语言中经典面向对象编程所服务的利益有时最好通过函数式编程来推进。

循环

循环包括for循环,for in循环,while-do循环和do-while循环。for循环的工作方式与 C 语言相同:

var numbers = [1, 2, 3];
var total = 0;
for(var index = 0; index < numbers.length; ++index) {
  total += numbers[index];
}

for in循环将循环遍历对象中的所有内容。hasOwnProperty()方法可用于仅检查对象的字段。对于名为obj的对象,两个变体如下:

var counter = 0;
for(var field in obj) {
  counter += 1;
}

这将包括原型链中的任何字段(此处未解释)。为了检查对象本身的直接属性,而不是原型链中潜在的嘈杂数据,我们使用对象的hasOwnProperty()方法:

var counter = 0;
for(var field in obj) {
  if (obj.hasOwnProperty(field)) {
    counter += 1;
  }
}

顺序不被保证;如果你正在寻找任何特定的字段,值得考虑的是只迭代你想要的字段并在对象上检查它们。

看一下 ECMAScript 6

JavaScript 工具一直在蓬勃发展,一个工具被另一个工具取代,有一个极其丰富的生态系统,很少有开发人员可以自豪地广泛和深入地了解。然而,核心语言 ECMAScript 或 JavaScript 已经稳定了好几年。

ECMAScript 6,有一个介绍性的路线图可在tinyurl.com/reactjs-ecmascript-6上找到,它为核心语言引入了深刻的新变化和扩展。一般来说,这些功能增强、加深或使 JavaScript 的功能方面更加一致。可以建议 ECMAScript 6 的功能不做这种工作,比如增强的面向类的语法糖,让 Java 程序员假装 JavaScript 意味着在 Java 中编程,可能会被忽略。

ECMAScript 6 的功能是不可忽视的,在撰写本文时,它们已经开始在主流浏览器中广泛应用。如果你想扩展和提高自己作为 JavaScript 开发人员的价值,不要局限于深入挖掘丰富的 JavaScript 生态系统,无论那有多重要。学习新的 JavaScript。学习一个有更多更好部分的 ECMAScript。

总结

在这场对 JavaScript 一些更好部分的风暴式之旅中,我们涵盖了可以在我们进一步推进 JavaScript 时有所帮助的基础构建模块。这些包括变量和赋值、注释、流程控制、值、NaN 函数和 ECMAScript 6。

变量和赋值部分,我们揭示了大多数编程的一些基本构建模块,尽管函数式响应式编程的重点可能在其他地方。在注释部分,我们了解到注释在任何地方都是相同的,但这里的主要关注点是防止奇怪的意外。

流程控制部分涵盖了函数内的基本流程控制(或者可能在任何函数之外,尽管通常应该避免这样做)。

关于值和 NaN 的说明部分,我们讨论了类似于 Perl,JavaScript 认为真理是不言自明的;也就是说,如果它们为 null、零、空、非数字等,则这些东西是虚假的,而不在列表上的任何东西都是真的。

函数部分,我们看了一些包含有些复杂示例的函数。在ECMAScript 6部分,我们讨论了核心 JavaScript 语言的扩展。

这只是对亮点的简要介绍,而不是全面的介绍。如果你需要更全面的 JavaScript 基础,有多种选择可供选择。我们将在下一章继续讨论响应式编程的基本理论。

第三章:响应式编程-基本理论

响应式编程,包括稍后将讨论的函数式响应式编程,是一种可以在多范式语言中使用的编程范式,例如 JavaScript、Python、Scala 等等。它主要与命令式编程有所不同,在命令式编程中,语句通过所谓的副作用来执行某些操作,在文献中,关于函数式和响应式编程。请注意,这里的副作用并不是普通英语中的副作用,其中所有药物都有一些效果,这些效果是服用药物的目的,而其他一些效果是不受欢迎的,但为了主要的益处而被容忍。例如,苯海拉明是为了减轻空气过敏症状而服用的,而事实上,苯海拉明在某种程度上与其他一些过敏药物类似,也会引起嗜睡(或者至少曾经是这样;现在它也作为睡眠辅助剂出售)是一种副作用。这是不受欢迎的,但被人们容忍,因为他们宁愿有些疲倦,而不受频繁打喷嚏的困扰。药物的副作用很少是程序员通常会考虑的副作用的唯一事情。对于他们来说,副作用是语句的主要预期目的和效果,通常通过对程序的存储状态进行更改来实现。

响应式编程源于观察者模式,如 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 的经典著作《设计模式:可复用面向对象软件的元素》中所讨论的(这本书的作者通常被称为GoF四人帮)。在观察者模式中,有一个可观察的主题。它有一个监听器列表,并在有发布内容时通知它们所有。这比发布者/订阅者(PubSub)模式要简单一些,不需要潜在复杂的消息筛选,以确定哪些消息到达哪些订阅者,这是一个常见的特性。

响应式编程已经发展成了一种独立的生活,有点像 MVC 模式变成了流行语,但最好是与 GoF 中探讨的更广泛的背景联系在一起。响应式编程,包括 ReactJS 框架(在本书中进行了探讨),旨在避免共享可变状态并且是幂等的。这意味着,就像 RESTful 网络服务一样,无论您调用一次还是一百次,您都将从函数中获得相同的结果。Facebook 的前员工皮特·亨特(现在是 ReactJS 的代表人物)曾说过,他宁愿是可预测的,而不是正确的。如果他的代码中有错误,亨特宁愿接口每次都以相同的方式失败,而不是进行对海森巴格的复杂搜索。这些错误只在一些特殊而棘手的边缘情况下表现出来,并且在本书的后面进行了探讨。

ReactJS 被称为MVCV。也就是说,它旨在用于用户界面工作,并且几乎没有提供其他标准功能的意图。但就像画家保罗·塞尚对印象派画家克劳德·莫奈所说的,“莫奈只是一只眼睛,但是多么美的眼睛!”关于 MVC 和 ReactJS,我们可以说,“ReactJS 只是一个视图,但是多么出色的视图!”

在本章中,我们将涵盖以下主题:

  • 声明式编程

  • 对抗海森巴格

  • Flux 架构

  • 从绝望之坑到成功之坑

  • 完整的 UI 拆解和重建

  • JavaScript 作为领域特定语言DSL

  • 大咖啡符号

本书中探讨的库 ReactJS 是由 Facebook 开发的,并在不久之前开源。它受到 Facebook 关于创建一个安全易于调试的大型网站以及允许大量程序员在不必将复杂性存储在头脑中的情况下工作的一些关注的影响。引用语“简单是缺乏交错”,可以在facebook.github.io/react的视频中找到,它不是关于绝对尺度上有多少或多少东西,而是关于您需要同时操纵多少个移动部分来工作在一个系统上(有关大咖啡符号的更多反思,请参见相关部分)。

声明式编程

ReactJS 框架最大的理论优势可能是编程是声明式的,而不是命令式的。在命令式编程中,您指定需要执行哪些步骤;声明式编程是指您指定需要完成什么,而不告诉如何完成。从命令式范式转变到声明式范式可能一开始会很困难,但一旦完成转变,付出的努力就是值得的。

熟悉的声明式范例,与命令式范例相对,包括 SQL 和 HTML。如果您必须指定如何查找记录并适当地过滤它们,更不用说如何使用索引,那么 SQL 查询将会更加冗长,而如果您必须指定如何渲染图像,HTML 将会更加冗长。许多库比起从头开始自己解决问题更具有声明性。使用库,您更有可能只指定需要完成什么,而不是除此之外还要指定如何完成。ReactJS 在任何意义上都不是旨在提供更具声明性 JavaScript 的唯一库或框架,但这是它的卖点之一,还有其他更好的具体功能,可以帮助团队合作并提高生产力。再次强调,ReactJS 是从 Facebook 在管理错误和认知负荷方面的一些努力中出现的。

对 Heisenbugs 的战争

在现代物理学中,海森堡的不确定性原理大致表明,有一个绝对的理论限制,即一个粒子的位置和速度可以被了解到多好。无论实验室的测量设备有多好,当您试图过于深入地固定事物时,总会发生一些有趣的事情。

海森堡不确定性原理,口语上来说,是一种微妙的、难以捉摸的错误,很难固定下来。它们只在非常特定的条件下显现,甚至在尝试调查它们时可能甚至不会显现(请注意,这个定义与 jargon 文件在www.catb.org/jargon/html/H/heisenbug.html中更狭窄和更具体的定义略有不同,该定义指出尝试测量 heisenbug 可能会抑制其显现)。对海森堡不确定性原理进行宣战的动机源于 Facebook 自己在规模化工作和看到 heisenbug 不断出现的困扰和经验。Pete Hunt 提到的一件事,一点也不令人愉快,是 Facebook 广告系统只有两名工程师能够充分理解并且愿意修改。这是一个需要避免的例子。

相比之下,看看 Pete Hunt 的评论,他宁愿“可预测也不愿意正确”是一个声明,如果一个设计有缺陷的灯可以着火烧毁,他更愿意它立即着火烧毁,以相同的方式,每一次,而不是在月相的错误时刻发生燃烧。在第一种情况下,灯会在制造商测试时失败,问题会被注意到并得到解决,直到缺陷得到适当解决之前,灯不会被运送到公众那里。相反的 Heisenbug 情况是灯只会在恰当的条件下发出火花并着火,这意味着缺陷直到灯被运送并开始烧毁客户的家才会被发现。 “可预测”意味着“如果失败,每次都以相同的方式失败”。 “正确”意味着“成功通过测试,但我们不知道它们是否安全使用[可能它们不安全]”。现在,他最终确实关心正确,但 Facebook 在 React 周围做出的选择源于一种认识,即可预测是成为正确的手段。制造商不可以运送一些在消费者插上电源时总是会发出火花并着火的东西。然而,可预测将问题移到前台和中心,而不是偶尔出现在软件迷宫的隐蔽和难以捉摸的相互作用的结果。Flux 和 ReactJS 中的选择旨在使失败显而易见,并将其显现出来,而不是仅在软件迷宫的角落和缝隙中显现。

Facebook 对共享可变状态的战争在他们对聊天 bug 的经验中得到了体现。聊天 bug 成为用户的一个主要关注点。Facebook 的一个重要的醒悟时刻是当他们宣布一个完全无关的功能时,第一个评论是要求修复聊天;它获得了 898 个赞。此外,他们评论说这是一个比较礼貌的请求之一。问题在于未读消息的指示器在没有消息可用时可能会有一个幻影正消息计数。事情来到一个人们似乎不关心 Facebook 正在添加什么改进或新功能,而只是想让他们修复幻影消息计数的地步。他们继续调查并解决边缘情况,但幻影消息计数不断重现。

除了 ReactJS 之外,解决方案还可以在下一节讨论的 flux 模式或架构中找到。在一种情况下,不太多的人感到舒服进行更改,突然之间,更多的人感到舒服进行更改。这些事情简化了事情,以至于新开发人员通常不需要真正需要之前给予的启动时间和处理。此外,当出现错误时,经验丰富的开发人员可以合理准确地猜测系统的哪个部分是罪魁祸首,而新手开发人员在处理错误后往往会感到自信,并对系统的工作原理有一般的了解。

Flux 架构

Facebook 在与 ReactJS 相关的一种方式是宣布对 heisenbugs 宣战,这是通过对可变状态宣战来实现的。Flux 是一种架构和模式,而不是一种特定的技术,它可以与 ReactJS 一起使用(或不使用)。它有点像 MVC,相当于该方法的一个松散竞争对手,但它与简单的 MVC 变体非常不同,并且旨在具有提供单向数据流的“成功深渊”,就像这样:从动作到分发器,然后到存储,最后到视图(但有些人说这两者是如此不同,以至于在尝试确定 Flux 的哪个部分对应于 MVC 中的哪个概念挂钩方面,直接比较 Flux 和 MVC 并不是真的有帮助)。动作就像事件-它们被送入顶部漏斗。分发器通过漏斗并不仅可以传递动作,还可以确保在前一个动作完全解决之前不会再发出任何其他动作。存储与模型有相似之处,也有不同之处。它们像模型一样跟踪状态。它们不像模型,因为它们只有 getter,没有 setter,这可以阻止程序的任何部分能够更改 setter 中的任何内容。存储可以接受输入,但以一种非常受控的方式,通常存储不受任何拥有对其引用的东西的控制。视图根据从存储获取的内容显示当前输出。在某些方面,存储与模型相比具有 getter 但没有 setter。这有助于培养一种不受 setter 访问者控制的数据流。事件可以作为动作传播,但分发器充当交通警察,并确保只有在存储完全解决后才处理新动作。这大大降低了复杂性。

Flux 简化了交互,使得 Facebook 开发人员不再遇到微妙的边缘情况和不断出现的错误-聊天错误最终消失了,再也没有出现。

从绝望的深渊到成功的深渊

Louis Brandy 警告了 C++的危险,冒着引起争议的风险,他称之为第二系统效应的最大例子(tinyurl.com/reactjs-second-system),自 OS/360 项目以来。在一个模糊的XKCD风格的图形中,他说“永远不要相信一个说他懂 C++的程序员”(tinyurl.com/reactjs-cpp-valley)。

以下图表显示了 C++程序员的信心水平:

从绝望的深渊到成功的深渊

他继续说:

“程序员(特别是那些来自 C 语言)可以很快地掌握 C++并感到非常熟练。这些程序员会告诉你他们懂 C++。他们在撒谎。当程序员继续学习 C++时,他会经历这种挫折的低谷,他完全认识到了语言的复杂性。好消息是很容易区分 C++程序员在低谷之前和之后的状态(在这种情况下是面试)。只要提到 C++是一种非常庞大和复杂的语言,低谷后的人会告诉你他们对语言有 127 种不同的小挫折。低谷前的人会说,“是的,我猜。我的意思是,这只是带有类的 C 语言。”

Eric Lippert 告诉我们的内容并不仅适用于 C++程序员;它导致了比 C++更大的东西:

我经常把 C++看作是我自己的绝望之坑编程语言。不受管理的 C++使人很容易陷入陷阱。想想缓冲区溢出、内存泄漏、双重释放、分配器和解分配器不匹配、使用已释放的内存、无数种方式来破坏堆栈或堆——这些只是一些内存问题。C++经常把你扔进绝望之坑,你必须爬上质量之山。(不要与攀登疯狂悬崖混淆。那是不同的。)

现在正如我之前所说的,C#的设计不是一个减法过程。它不是“去掉愚蠢部分的 C++”。但是,不看看其他语言的问题并努力确保这些问题不会出现在 C#用户身上,那我们就太愚蠢了。我希望 C#成为一种“质量之坑”语言,一种鼓励你一开始就编写正确代码的语言。你必须非常努力才能在 C#程序中写出缓冲区溢出的错误,这是有意为之的。

我从未在 C#中编写过缓冲区溢出。我从未在 C#中写过意外地在另一个作用域中隐藏变量的错误。我从未在 C#中在函数返回后使用堆栈内存。我在 C++中做了所有这些事情,这不是因为我是个白痴,而是因为 C++使得做所有这些事情变得容易,而 C#使得这变得非常困难。使得做好事情变得容易显然是好事;考虑如何使做坏事变得困难实际上更重要。

或者,就像在 Python 邮件列表上发生的那样,一个明显的 133t hax0r 拼写的人问如何在 Python 中编写缓冲区溢出,而更资深的列表成员之一回答说:“很抱歉,但 Python 不支持该功能。”这个梗的重点是,有人询问如何找到特定类型的漏洞,却得到的答复是 Python 的语言设计中已经排除了基本类型的缺陷。正如对 C#所指出的,字符串的处理方式是合理的,没有任何天真的使用会导致缓冲区覆盖漏洞。

Eric Lippert 是 C#中的一个关键人物,他的帖子清楚地阐明了如何明智地反对 Bjarne Stroustrup 的话:

“我们思考/编程的语言与我们能够想象的问题和解决方案之间的联系非常紧密。因此,出于消除程序员错误的目的限制语言特性至少是危险的。”

今天不同意 Stroustrup 的人可能不会质疑这两个句子,但可能只会质疑第二个句子:语言与解决方案之间的联系似乎确实是真实的,但对于语言特性和成功之坑却具有相反的含义。像* Douglas Crockford *的书《JavaScript:好部分》中的决定可能会考虑到这样的因素。这样或那样的细节可能会受到质疑,但使用 JavaScript 的精选子集并完全忽略其他部分的核心思想源于寻求和改装成功之坑的事实几乎是理所当然的一旦可能性被指出。

所有这些都导致了 Rico Mariani 在某种程度上提出的一个观点,与绝望之坑的相反。成功之坑与峰顶、山峰或穿越沙漠寻找胜利的旅程形成鲜明对比。我们希望我们的客户简单地“陷入”使用我们的平台和框架的成功实践。在我们让人陷入麻烦变得容易的程度上,我们失败了。

完成 UI 拆解和重建

迪杰斯特拉的一句话,深受 ReactJS 开发者如 Pete Hunt 的喜爱,是:我们的智力更适合掌握静态关系,而我们对于可视化时间演变过程的能力相对较差。因此,我们应该尽最大努力缩短静态程序和动态过程之间的概念差距,使程序(在文本空间中展开)和过程(在时间中展开)之间的对应尽可能简单。

ReactJS 在概念上发挥了这种优势的一种方式是,将所有东西都清除并重新渲染,以便程序和过程之间的对应关系变得简单。您不需要跟踪 DOM 的当前状态和需要记住的 300 个 jQuery 更改,以便准确地从一个状态过渡到另一个状态。您只需要告诉它现在应该看起来如何。实际上,ReactJS 并不会在底层将所有东西都清除;它有相当复杂的功能,可以创建一个闪电般快速的纯 JavaScript 合成 DOM(这个合成 DOM 也使得在 Internet Explorer 8 中实现 HTML5 功能成为可能),并协调和进行尽可能快速的更改(在这种情况下,“快速”包括在非 JIT iPhone 5 上实现每秒 60 帧的更新的令人印象深刻的壮举)。然而,从概念上讲,使用 ReactJS 意味着简单地将所有东西都视为被清除并从头开始重新绘制,并信任 ReactJS 将汇集所有需要的魔法粉来进行最小限度的 DOM 更改。这是为了根据请求更新页面,可能不会丢失现有的输入或惯性滚动。

ReactJS 提供了优化钩子,以提供对渲染内容的更精细控制。这些都有很好的文档说明,但实际上很少需要使用。记住 Knuth 的话,“过早优化是万恶之源。”我自己并没有使用这个优化功能,尽管 ClojureScript 中用于 ReactJS 的 Om 绑定要快得多,因为它们只需要检查引用相等性,而不需要深度相等性,因为在 ClojureScript 中对象是不可变的,尽管我做了一个次要的用法,要求 ReactJS 放弃对 DOM 的某些部分的所有权,以便它能够与第三方功能良好地配合。在第十一章中,使用实例演示 JavaScript 中的函数响应式编程的一部分 IV – 添加一个草稿并把它全部放在一起,有一个这样的例子。ReactJS 默认情况下非常高效,而不仅仅是在您定制其默认行为方面。

JavaScript 作为一种特定领域的语言。

模板系统中的一种广泛实践是为模板提供 DSL。在通常的过程中,比如在前端使用 underscore(underscorejs.org)或在后端使用 Django 模板(djangoproject.com)时,都提供了一个精心选择但故意功能不足的模板语言。例如,在 Django 中,故意限制了功能,以便将模板交给不受信任的设计师,设计师无法做任何可能损害不希望被损害的任何内容的事情。

这也许是一个吸引人的特点,但它表明了一种有限的模板语言。如果模板需要更强大的东西,它们将束手无策,直到它们的任何要求都能在服务器端进行非标准调整。与哥德尔的不完备定理和停机问题相关的一个基本观点是,如果你把某人的手绑得足够紧,以至于这个人原则上不能造成任何伤害,你就显著地限制了这个人能做的事情。最好的结果至少需要一点信任。如果你希望人们能够做出最有用的贡献,他们可能不会能够在双手被束缚的情况下做到这一点。

在 ReactJS 中,模板的 DSL 是 JavaScript,具有其全部功能。有些人也许会觉得让设计师直接使用原始 JavaScript 很奇怪;ReactJS 的人似乎采取了给它五分钟的方法,并说设计师比他们有时被认为的更聪明,非常有能力编写非常特定类型的 JavaScript 代码。但这也意味着,如果你有一个特殊情况,你复杂的情况需要你做一些在某些特定的、故意不够强大的模板语言中被排除的事情,这是个好消息。在 ReactJS 中,你可以充分利用 JavaScript 的全部功能来处理模板。在 ReactJS 中,如果你愿意,你也可以使用一种非常有限的语言子集用于模板,Pete Hunt 和其他人似乎相信设计师足够聪明,能够处理它。但更好的消息是,当你有一个需要真正强大的困难情况时。你拥有 JavaScript 提供的全部功能,这会产生很大的不同。

Big-Coffee Notation

Steve Luscher 是 Facebook 之外的 ReactJS 大师和爱好者,后来被 Facebook 聘用,他在一段关于 React 的视频中谈到了 Big-Coffee Notation。基本观点是,我们不应该只使用大 O 符号来表示运行时复杂度(运行时随问题规模粗略增长的时间,或者偶尔其他维度,比如内存使用),而是应该有一个 Big-Coffee Notation 来表示对于可怜的开发者来说需求如何增长,他们必须将这些东西保存在自己可怜的、充满咖啡因的大脑中。

Gerald Weinberg 的经典著作《计算机编程心理学》详细阐述了一个基本观点。核心观点是程序员编程计算机不仅仅是涉及计算机的活动,也是涉及人的活动,我们最好也将其视为这样的活动。也许我们也应该了解计算机的限制,但人类这一方面并不是微不足道的。魏恩伯格可能是第一个提出这一观察的人,或者可能在他之前有人提出过,但无论哪种情况,这一观察自被吸收以来一直是严肃软件工程文献的基石。例如,在 Steve McConnell 的《代码大全:软件构建实用手册》中,这是一个核心观点。我们不会对这个想法进行全面探讨,但它是值得探索的,特别是如果你以前没有探索过的话,Big-Coffee Notation 正好属于这一领域。核心思想是,除了跟踪大 O 符号或复杂度,我们还应该关注开发者在适当需求方面如何扩展越来越复杂的问题。这些需求是指需要记住多少移动部分。

在大 O 符号表示法中,根据上下文的不同,有各种运行时复杂度,它们为运行时间在解决越来越大的问题时提供了一个上限。*O(1)*运行时对于任何用例都有一个固定的上限。*O(log n)*与某些数据结构上的单元操作相关。*O(n)*也被称为线性,指的是运行时在运行时间上有一个线性的上限。你可以保证至少有一些常数乘以项目的数量。*O(n log n)*可能是下一个重要的步骤,它与某些排序算法相关。*O(n ^ 2)被称为二次(所有先前提到的复杂度都被称为次二次,意思是比二次更快),当事情真的不能扩展到大量时,它可能被视为一个阈值复杂度。(O(n * (n – 1))也被认为是二次,并被包含在O(n ^ 2)*之下。之后还有一些较慢的多项式时间和指数时间,而不排除还有更慢的升级,比如阶乘。NP 完全性的著名问题是一个问题,即某些已知可以在指数时间内解决的 NP 完全问题是否总是可以在多项式时间内解决。

Steve Luscher 关于命令式 UI 和声明式 UI 之间的区别的演示是,如果有人要制作一个小部件来显示他们队列中未读项目的数量,命令式 UI 在两个状态之间进行一次转换,使可怜的程序员需要跟踪The Big-Coffee Notation,这意味着相对于状态数量,程序员大脑中的项目数量呈二次或外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传复杂度。如果有三个状态,就有六个转换。如果添加第四个状态,将有 12 个,或者是两倍的转换。添加第五个状态,你将看到 20 个转换。程序员理解代码的解释是二次,意味着陡峭。然而,如果你以声明方式给出 UI 代码,比如在 ReactJS 编程中,你只需描述每种可能的渲染状态一次。三种状态只需要三种描述。四种状态只需要四种描述。五种状态只需要五种描述。这只是The Big-Coffee Notation,或者是线性的。对于可怜的程序员的咖啡因大脑来说,这种不断升级的需求来跟踪代码发生的事情要少得多。就像一个快速的算法一样,运行起来要少得多。

我没有听说过 Dijkstra 的《谦逊的程序员》被 ReactJS 社区引用过,但在程序员中,知识谦卑是一种美德,并且在软件工程文献中长期以来一直被认可。它在经典著作中得到强调,比如《代码大全:软件构建的实用手册》,程序员们并不是分为大脑和小脑,而是分为知道自己有小脑的人和有小脑但不自知的人。编程的卓越部分源于对自己认知限制的认识。Dijkstra 写道:

“胜任的程序员充分意识到自己头脑的严格有限;因此,他以完全谦卑的态度对待编程任务,而且在其他方面,他像瘟疫一样避开了聪明的把戏。在一个众所周知的对话式编程语言的情况下,我从各个方面听说,一旦一个编程社区配备了它的终端,就会出现一个特定的现象,甚至已经有了一个成熟的名字:它被称为“一行代码”。它有两种不同的形式:一个程序员把一行程序放在另一个程序员的桌子上,要么他自豪地告诉它做什么,然后问“你能用更少的符号编码吗?”——好像这对概念有任何重要性一样!——要么他只是问“猜猜它是做什么的!”。从这个观察中,我们必须得出结论,这种语言作为一种工具是聪明把戏的一个开放邀请;虽然这可能是它吸引力的解释之一,即对那些喜欢展示自己有多聪明的人来说,但很抱歉,我必须把这看作是对编程语言说的最严厉的话之一……*

这个挑战,也就是面对编程任务,已经教会了我们一些经验教训,我选择在这次讲话中强调的是:

我们将做得更好的编程工作,只要我们以充分的欣赏其巨大困难的态度来对待这项任务,只要我们坚持使用适度和优雅的编程语言,只要我们尊重人类思维的固有局限,并以非常谦卑的程序员的态度来对待这项任务。

总结

我们刚刚快速浏览了一些围绕使用 ReactJS 进行响应式编程的理论。这包括声明式编程,这是 ReactJS 的卖点之一,它提供了比命令式编程更容易处理的东西。Heisenbugs 的战争是 Facebook 所做决定的一个主要关注点,包括 ReactJS。这是通过 Facebook 宣布对共享可变状态的战争来实现的。Flux 架构被 Facebook 与 ReactJS 一起使用,以避免一些恶心的 bug 类。成功的陷阱和绝望的陷阱,从他人的痛苦中学习,这种痛苦集中在与 C++编程语言的联系上,并且看看我们应该追求什么。

我们涵盖了完整的 UI 拆卸和重建,提供了一个简单的替代方案来跟踪状态以更新界面。我们还将 JavaScript 作为 DSL,看作是设计 ReactJS 的一个有意的决定,旨在给你尽可能多的权力。然后讨论了大咖啡符号与健康认识自己的限制的关系,而不是让他们摔断腿,这是可以预防的。

在我们的下一章中,我们将继续通过查看使用 ReactJS 构建的用户界面的具体案例来进行讨论。

第四章:演示非功能性反应式编程-一个实时示例

在本章中,我们将看一个实时示例,其中融合了一些反应式编程原则与 ReactJS。程序的一些部分仍然是命令式的,作为之前用 jQuery 编写的东西的一个端口,经过 HP-28S RPN 和 Unix C 的其他端口之后,但是 ReactJS 的强大仍然闪耀,即使像现实世界中的大部分代码一样,它已经经历了多次迭代。我们将简要地看一下网页的 HTML 要求,然后再看 JavaScript 中真正的内容。该网页提供了一个最初在 HP-28S 图形科学计算器上开发的视频游戏的端口,并保留了计算器的外观和感觉。

在本章中,我们将涵盖以下主题:

  • 网页的 HTML

  • 动画网页的 JavaScript

在这里,我们看到游戏,渲染在经典的 HP28S 计算器的背景下。已经采取了一些措施,使字符图形模仿了 LED 屏幕上存在的暗色和亮色:

演示非功能性反应式编程-一个实时示例

具有多个端口的游戏的历史

标题指定我们正在制作 HP28S RPN 游戏的一个端口,所以让我们来看一下我们正在实现的特定游戏的一点历史。

这个游戏有不同的实现和不同的端口,包括在 C 中的重新实现,以及使用 HTML 或 JavaScript 的几种方式。原始版本是在 HP28S 上,这是一个黑客式的科学计算器,可以有 32KB 或 512KB 的 RAM(我的有 512KB)。编程和使用(两者在 Unix/Linux shell 编程中并没有太大的不同)逆波兰表示法RPN)(en.wikipedia.org/wiki/Reverse_Polish_notation)。在计算器中有很多有趣的深度,我记得我做了两个程序。一个是一个具有谦卑的二维蹒跚醉酒算法的分形屏幕保护程序(参见tinyurl.com/reactjs-staggering-drunk),另一个是将在这里重新实现的视频游戏。

基本游戏采用 dingbat 字符图形实现,一艘太空船从左到右移动,在一个从级别到级别变得更加密集的小行星场中。主要的游戏机制旨在躲避你所经历的小行星。你也可以射击小行星。这真的是必要的,因为一些(天真地)随机绘制的级别并不一定有明确的路线可用。为了阻止游戏机制简单地射击通过每个级别,射击小行星是受到惩罚的,并且意味着更多的是最后的手段而不是旨在作为主要游戏机制的操纵。我的一个朋友评论说,这是他所知道的第一个玩家实际上因射击东西而失去分数的视频游戏。

网页的 HTML

我们以标准的 HTML5 DOCTYPE 开头:

<!DOCTYPE html>

随后,我们打开文档,指定 UTF-8 作为字符集。如果网页正确提供,字符集应该在页面下载时被指定,但这在防御性编码方面仍然可能有所帮助,这总是值得记住的事情:

<html lang="en">
  <head>
    <meta charset="utf-8" />

因此文档标题:

    <title>A video game on an HP-28S scientific
      calculator</title>

这里使用的字体是复古的 VT 系列字体,与受人尊敬的 VT100 和其他系列的 Unix 终端相关联。请注意-正如稍后将在代码中看到的那样-虽然 VT100 系列是等宽终端,但该字体并不严格是等宽字体,只是在行内显示每行空格或小行星将产生不希望的间距,因此每个字符都被绝对定位。也许另一个字体可能不会有这个问题,但 VT100 字体有一种很好的复古色彩。

请注意,我们将为大部分字符图形包括装饰符号。它们在 JavaScript 中处理。

像其他在 HTML 中使用的标签一样,字体标签是通过 HTTP/HTTPS 两个斜杠的模糊格式编写的,http:https:不指定,并且被提供为与网页中相同的格式:

  <link
    href='//fonts.googleapis.com/css?family=VT323'
     rel='stylesheet' type='text/css' />

在任何可以的地方使用内容分发网络

我们从内容分发网络CDN)加载 ReactJS,遵循史蒂夫·索德斯广泛建立的 Yslow(“我的网页为什么加载慢?”)建议。

注意

史蒂夫·索德斯(SteveSouders.com)最初在雅虎发现,渲染网页更快实际上并不是关于削减服务器端性能的毫秒或微秒。在影响客户端更高效方面有一个显著的低成本果实,例如,当可以从计算机缓存中以闪电般的速度加载资源时,不再一遍又一遍地从网络加载相同的资源。

有很多 JavaScript 库和框架可以从 CDN 中获取,包括 ReactJS,但几乎任何其他你想要使用的主要或次要 JavaScript 工具也都可以找到。

一些简单的样式

我们为页面添加了一些基本样式。背景图像是从haywardfamily.org/hp28s.png加载的。如果需要,你可以制作本地副本,或者如果在 HTTPS 上有问题,或者如果你在本地提供文件时 HTTP/HTTPS 模糊格式遗憾地无法工作。

p#display中的文本颜色取自 HP28S 计算器的屏幕截图:

一些简单的样式

<style type="text/css">
  body
  {
  background-image:
  url(//haywardfamily.org/hp28s.png);
  background-position: top left;
  background-repeat: no-repeat;
  height: 670px;
  width: 860px;
  }
  div#main
  {
  height: 670px;
  width: 860px;
  }
  p#display
  {
  color: #4f5c65;
  font-family: VT323, courier, sans;
  font-size: 18px;
  letter-spacing: 4px;
  left: 565px;
  top: 180px;
  position: absolute;
  }
  p#legend
  {
  background-color: rgba(0, 0, 0, .6);
  border-radius: 20px;
  color: white;
  font-family: Verdana, arial, sans;
  margin-left: 40px;
  margin-right: 90px;
  margin-top: 40px;
  padding: 10px;
  }
</style>
</head>

这是页面头部的最后一部分。

一个相当简单的页面主体

我们构建页面主体,其中包含 HP-28S 计算器的图像作为背景。我们还包括一个简短的图例和游戏在虚拟计算器屏幕上显示的空间:

<body>
  <div id="main">
    <p id="legend">
      Arrow keys to move, Space to shoot.
    </p>
    <p id="display">
    </p>
  </div>

在关闭 body 标签之前,我们加载主要脚本,它将使用 ReactJS 来为游戏添加动画:

<body>
  <script
    src="img/react.js"></script>
  <script type="text/javascript"
    src="img/hp28s.js"></script>
</body>
</html>

这是页面的 HTML 结束。现在将跟随包含真正的编程内容的 JavaScript。

动画页面的 JavaScript

我们可能简要指出,脚本是常规的 JavaScript,而不是 ReactJS 的 JSX 格式,它允许混合类似 HTML 的 XML 和 JavaScript,并被称为在脚本中放置尖括号的工具。并非所有人都会使用 JSX,但如果没有其他选择,了解这个工具也是值得的。

JSX 有很多优点,值得考虑。它被一些非 Facebook ReactJS 用户使用,但并非所有人都使用,同时也被 Facebook 使用。Facebook 一直支持 JSX,但并没有要求使用 JSX 来使用 ReactJS。开发目的上,JSX 脚本可以在网页加载后从cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js加载,并在浏览器中编译。生产目的上,它们需要在 JavaScript 中编译,这时你可以运行npm install jsx,然后从命令行运行jsx编译器,具体操作可参考www.npmjs.com/package/jsx

简短的语法说明 - 立即调用的函数表达式

在我们的脚本中,我们使用立即调用函数表达式IIFE),以便我们的局部变量,使用var关键字在函数或其依赖项的某处定义,将受到闭包内的私有保护。这将避免共享可变状态的问题(因为它具有非共享的可变状态)。共享的可变状态会使程序的稳定性取决于任何有足够访问权限来修改状态的人。由于 JavaScript 语法的怪癖,该函数被括号包裹,其中以函数开头的行被视为函数定义,因此以下语法将不起作用:

function() {
}();

解决方案是将函数放在一对括号中,然后它将正常工作:

(function()
    {
    })();

回到我们的主脚本!

变量声明和初始化

我们的主wrapper函数通过编写仅在函数中使用的状态变量,开始非反应性和命令性地运行:

function()
{
  var game_over_timeout = 2000;
  var game_over_timestamp = 0;
  var height = 4;
  var tick_started = false;
  var width = 23;
  var chance_clear;
  var game_over;
  var level;
  var rows;
  var score;
  var position;
  var row;

声明并在某些情况下初始化了这些变量后,我们继续进行游戏启动的函数。这将初始化或重新初始化变量,但不包括初始化关卡。它通过将game_over变量设置为false,将玩家放在第 1 级,设置(水平)位置在屏幕/小行星区域的左侧 23 个字符宽度的开始处,以及垂直位置在第 1 行(顶部下方的第 1 行,共 4 行),得分为 0,并且大多数空间都是清晰的(即,没有小行星,因此玩家的飞船可以安全地行驶)但是小行星的密度不断增加!这 5/6 是空间不被小行星占据的机会呈指数衰减的开始。后者是一个可以调整的参数,以影响游戏的整体难度,较低的值会使领域更难以导航。在关卡之间,在空间清晰的机会呈指数衰减;指数衰减的速率,或者该功能的其他方面也是可以修改以影响关卡之间游戏难度的。

在这里,我们可以看到当玩家几乎清除了第一关时显示的样子:

![变量声明和初始化](img/B04108_04_3.jpg)

生成的关卡大部分是空格,有随机的可能性出现小行星,但最初容纳飞船的空间和其前面的空间总是清晰的。这是为了让玩家有一些空间来做出反应,而不是自动死亡。

用于启动或重新启动游戏的函数

在声明后立即调用该函数:

    var start_game = function()
    {
      game_over = false;
      level = 1;
      position = 0;
      row = 1;
      score = 0;
      chance_clear = 5 / 6;
    }
    start_game();

创建游戏关卡的函数

get_level()函数中,构建了一个级别。空间清晰的概率经历了指数衰减(衰减后的第一个级别为 0.75,然后为 0.675,然后为 0.6075,依此类推),随着小行星的密度相应增加,然后构建了一个矩形的字符数组的数组(字符数组用于集合中的字符,这些字符经历了接近恒定的变化,而不是字符串,字符串是不可变的,即使原始实现操作了字符串)。请注意,在这里的内部表示中,事物是由字符代码表示的:a表示小行星,s表示玩家的飞船,空格表示空白,依此类推。(现在将一个数组的数组的字符存储为对其他字符的引用可能有点奇怪。在原始的遗留系统上,现在显而易见的方法尚不可用。现在可能会对其进行重构,但是本章是一个旨在类似于处理遗留代码时获得良好结果的代码章节,这个瑕疵的存在是有意的。大多数开发人员所做的工作包括与遗留功能进行接口。)最初,所有空间都有可能被小行星占据。之后,清除了飞船的初始槽和其前面的空间。这是一个例子:

var get_level = function(){
  level += 1;
  rows = [];
  result = {};
  chance_clear *= (9 / 10);
  for(var outer = 0; outer < height; ++outer)
  {
    rows.push([]);
    for(var inner = 0; inner < width; ++inner)
    {
      if (Math.random() > chance_clear)
      {
        rows[outer].push('a');
      }
      else
      {
        rows[outer].push(' ');
      }
    }
  }
  rows[1][0] = 's';
  rows[1][1] = ' ';
  return rows;
}

虽然这个函数返回行的网格,但是行的网格将被分配为将与 ReactJS 一起使用的对象的字段。ReactJS 与对象上的属性一起工作得更好,而不是与数组上的属性一起工作。

前面函数调用的结果存储在 board 变量的一个字段中,并为按键定义了一个数组。在移动结束时,最后一个按键(如果有)从keystrokes数组中取出,然后清空数组,以便飞船根据上一次按键(如果有)在回合期间输入的按键移动。所有其他按键都将被忽略:

    var board = {rows: get_level()};
    var keystrokes = [];

使用 ReactJS 类来动手实践

现在我们将直接开始与 ReactJS 进行交互。我们创建一个 ReactJS 类,使用特定字段命名的函数哈希。例如,componentDidMount()函数和字段是在 ReactJS 组件挂载时调用的函数。这意味着它基本上是在 DOM 中显示和表示的。在这种情况下,我们向文档的主体添加事件侦听器,而不是直接向 ReactJS 组件添加事件。这是因为我们想要监听按键按下/按键事件,而很难让DIV对这些事件做出响应。因此,主体添加了事件侦听器。它们将处理 ReactJS 中的事件处理程序,这些事件处理程序仍然应该像通常显示它们一样定义。请注意,一些其他类型的事件,例如一些鼠标事件(至少),将通过 ReactJS 通常的方式注册,如下所示:

    var DisplayGrid = React.createClass({
      componentDidMount: function()
      {
        document.body.addEventListener("keypress",
        this.onKeyPress);
        document.body.addEventListener("keydown",
        this.onKeyDown);
      },

ReactJS 中的组件具有属性状态。属性是一次定义的东西,不能更改。它们在 ReactJS 内部可用,并且应该被视为不可变的。状态是可变的信息。属性和状态都在 ReactJS 中可用。我们可以简要地评论说,Facebook 和 ReactJS 正确地将共享的可变状态视为引发 Heisenbugs。在这里,我们从闭包中处理所有可变状态。可变状态不是共享的,也不应该共享(换句话说,它是非共享的可变状态)。

      getDefaultProps: function()
      {
        return null;
      },
      getInitialState: function()
      {
        return board;
      },

接下来,我们定义按键和按键事件处理程序,就像它们通常被使用的那样,或者至少在 DIV 响应按键事件时通常被处理的那样。(实际上,我们将监视 body,因为与悬停或鼠标点击不同,按键相关事件不会传播到包含的 DIV。这近似于您通常如何演示 ReactJS 中的事件处理。我们正在监听的特定按键,箭头键和空格键,存在一个问题。实质上,箭头键触发按键按下事件,但不触发按键事件(大多数其他键会触发按键事件)。这种行为很愚蠢,但它已经深入到 JavaScript 中,现在基本上是不可协商的。我们委托给一个通用的事件处理程序来处理这两个事件。在这里,按键被转换为键码:左或上箭头键向上移动(或向左,从游戏的方向来看),右或下箭头键向下移动(或向右,从游戏的方向来看),空格键射击。这些分别由keystrokes数组中的uds表示。

      onKeyDown: function(eventObject)
      {
        this.onKeyPress(eventObject);
      },
      onKeyPress: function(eventObject)
      {
        if (eventObject.which === 37 ||
        eventObject.which === 38)
        {
          keystrokes.push('u');
        }
        else if (eventObject.which === 39 ||
        eventObject.which === 40)
        {
          keystrokes.push('d');
        }
        else if (eventObject.which === 32)
        {
          keystrokes.push('s');
        }
      },

在这一点上,我们创建了render()函数,这是一个核心的 ReactJS 成员来定义。这个渲染函数的作用是创建 DIV 和 SPAN,以适当的方式表示空格和符号的网格。叶节点被绝对定位。

在构建了叶 SPAN 节点和中间 DIV 后,我们构建到主 DIV 元素。

out_symbol变量是一个 UTF-8 字符,而不是 ASCII 转义;这是有一个非常具体的原因。尽管 ReactJS 有一个明确定义的转义口 dangerouslySetInnerHTML()(参见tinyurl.com/reactjs-inner-html),通常设置为抵抗 XSS(跨站脚本)攻击。因此,它的正常行为是转义尖括号和大量的和符号使用。这意味着&nbsp;将被渲染为源码中的&nbsp;,而不是一个(不换行和不折叠)空格。因此,我们使用的 dingbat 符号不像其他地方那样使用转义码(尽管这些转义码在这里作为注释留下了),而是作为 UTF-8 存储在 JavaScript 中。

如果您不确定如何输入 dingbats,您可以简单地使用其他东西。或者,您可以将注释中的转义复制到普通的简单 HTMLPOSH)文件中,然后从渲染的 POSH 页面中复制并粘贴半打符号到您的 JavaScript 源代码中。您的 JavaScript 源代码应该被视为 UTF-8。

  render: function()
  {
  var children = ['div', {}];
  for(var outer = 0; outer <
  this.state.rows.length; outer += 1)
  {
    var subchildren = ['div', null];
    for(var inner = 0; inner <
    this.state.rows[outer].length;
    inner += 1)
    {
      (var symbol =
      this.state.rows[outer][inner];)
      var out_symbol; 
      if (symbol === 'a')
        {
          // out_symbol = '&#9632;';
          out_symbol = '■';
        }
        else if (symbol === 's')
        {
          // out_symbol = '&#9658;';
          out_symbol = '►';
        }
        else if (symbol === ' ')
        {
          // out_symbol = '&nbsp;';
          out_symbol = ' ';
        }
        else if (symbol === '-')
        {
          out_symbol = '-';
        }
        else if (symbol === '*')
        {
        out_symbol = '*';
        }
        else
        {
          console.log('Missed character: '
          + symbol);
        }
        subchildren.push(
        React.createElement('span',
        {'style': {'position': 'absolute',
          'top': 18 * outer - 20), 'left':
          (12 * inner - 75)}}, out_symbol));
        }
        children.push(
        React.createElement.apply(this,
        subchildren));
      }
      return React.createElement.apply(this,
      children);
    }
  });

在前面的代码中定义的 children 和 subchildren 填充了React.createElement()的参数列表。

在内部循环中构建完毕后,我们向subchildren数组添加一个叶节点。它被指定为一个 span,内联 CSS 样式以哈希的形式传递,并且内容等于out_symbol变量。然后将其添加到children数组中。它包含屏幕的行,这些行最终构建成完整的棋盘。

在 ReactJS 中,组件是在React.createElement()中定义的,随后可以供使用。React.createElement()的常规调用方式是React.createElement( 'div', null, ...),省略号部分包含所有的子元素。我们使用apply()来调用React.createElement(),并传入所需的初始参数,然后在数组中指定参数。

滴答滴答,游戏的时钟在滴答

这关闭了render()字段和React.createElement()类定义。在源代码中,我们继续进行tick()函数的处理。它处理每一轮应该发生的事情。目前,代码以 300 毫秒(0.3 秒)的间隔调用tick(),尽管这是可以调整的,以影响游戏玩法,或者稍微重构以使游戏玩法随着级别的提高而加速。

如果游戏结束,这只能是因为飞船撞到了小行星,tick()调用中什么也不会发生:

    var tick = function()
    {
      if (game_over)
      {
        return;
      }

接下来,调用React.render(),指定要呈现的类以及要呈现到的 HTML 元素。React.render()应该至少在每次想要呈现东西的时候调用。如果你只调用一次,它将只呈现一次,这意味着如果你想要重复的更新显示,就需要重复调用它。在这里,我们在tick()方法的每次调用中调用它,要求基于前面大部分代码中定义的DisplayGrid创建一个元素,并将呈现的 HTML 放入具有显示 ID 的 DIV 中:

    React.render(
      React.createElement(DisplayGrid, {}),
      document.getElementById('display'));

在这里,我们看到玩家射击了一个小行星的屏幕。小行星爆炸成了一个星号!

滴答,滴答——游戏的时钟在滴答

如果在上一轮中,飞船射击了一个小行星(在字符符号中表示为零个或多个连字符和右侧的星号;连字符填充了射击到的小行星之前的空间,星号代表了射击命中小行星的爆炸),我们清除显示在那一轮中射击的槽:

    for(var outer = 0; outer < height; outer += 1)
    {
      for(var inner = 0; inner < width; inner += 1)
      {
        if (board.rows[outer][inner] === '-' ||
        board.rows[outer][inner] === '*')
        {
          board.rows[outer][inner] = ' ';
        }
      }
    }

做完这些之后,我们清除了指示已经射击的变量:

    var shot_taken = false;

我们清除了飞船所在的空间:

    board.rows[row][position - 1] = ' ';

在每个滴答结束时,keystrokes数组被清除,并且我们注意存储的最后一个击键。换句话说,我们关注上一个回合之后存储的最后一个击键。击键在回合之间不会累积。在回合结束时,最后一个击键是唯一获胜的击键。

keystroke数组存储的是键码,而不是确切的击键。箭头键已经被处理,左箭头或上箭头按下将存储一个u表示向上,右箭头或下箭头按下将存储一个d表示向下,空格键按下将存储一个s表示射击。如果有人输入上或下,飞船将在边界内向上或向下移动:

    if (keystrokes.length)
    {
      var move = keystrokes[keystrokes.length – 1];
      if (move === 'u')
      {
        row -= 1;
        if (row < 0)
        {
          row = 0;
        }
      }
      else if (move === 'd')
      {
        row += 1; 
        if (row > height - 1)
        {
          row = height - 1;
        }
      }

如果用户射击了一个小行星,在下一轮中,一排连字符将从飞船的前端延伸到小行星,小行星将变成一个星号,表示爆炸:

    else if (move === 's')
    {
      shot_taken = true;
      score -= 1;
      var asteroid_found = false;
      for(var index = position + 1; index <
      width && !asteroid_found; index += 1)
      {
        if (board.rows[row][index] === 'a')
        {
          board.rows[row][index] = '*';
          asteroid_found = true;
        }
        else
        {
          board.rows[row][index] = '-';
        }
      }
    }
    keystrokes = [];
  }

提示

下载示例代码

您可以从您在www.packtpub.com的帐户中下载您购买的所有 Packt Publishing 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

游戏结束

如果用户撞到了小行星,游戏结束。然后我们显示一个游戏结束屏幕,并停止进一步处理,就像这样:

游戏结束

        if (position < width)
            {
            if (rows[row][position] === 'a')
                {
                game_over = true;
                game_over_timestamp = (new
                  Date()).getTime();
                (document.getElementById(
                  'display').innerHTML =
                  '<span style="font-size: larger;">' +
                  'GAME OVER' +
                  '<br />' +
                  'SCORE: ' + score + '</span>');
                return;
                }

只要用户没有撞到小行星,游戏仍在进行,我们用飞船的标记替换行中的当前槽。然后增加玩家的(水平)位置:

            board.rows[row] = board.rows[row].slice(0,
              position).concat(['s']).concat(
              board.rows[row].slice(position + 1));
            position += 1;
            }

如果用户“掉出了屏幕的右边缘”,我们将游戏提升到下一个级别:

        else
            {
            rows = get_level();
            score += level * level;
            position = 0;
            row = 1;
            }
        }

定义了所有这些之后,我们开始游戏,如果我们还没有启动滴答声,我们会以 300 毫秒的间隔启动滴答声(这个值可以调整,以使游戏更容易或更难;它可以成为一个可配置的间隔,随着游戏的进行而加快):

    start_game();
    if (!tick_started)
        {
        setInterval(tick, 300);
        tick_started = true;
        }
    })();

总结

在本章中,我们涵盖了很多内容。早些时候,我们已经涵盖了一些理论,但是在这里,我们开始使用一些 ReactJS 来拼凑一个应用程序。以后,我们将使用一个更深入的应用程序。

涵盖的主题包括网页的 HTML。这是一个简单的 HTML 骨架,用作保存反应式 JavaScript 的架子。另一个涵盖的主题是反应式 JavaScript。这包括了 JavaScript 的混合,其中有一个清晰的示例,展示了如何为 ReactJS 编写反应式 JavaScript。

我们将在下一章继续介绍函数式编程。

第五章:学习函数式编程-基础知识

JavaScript 是一种多范式语言,对于它触及的任何范式都不完美,但它具有其主要范式的有趣特性。它是一种面向对象的语言,尽管面向对象的定义在面向对象的语言之间有所不同。有人建议,它的原型继承对于面向对象编程可能不如演示如何创建无类对象重要,而不是在开始时就把分类搞对的困难任务。面向对象的定义在具有面向对象特性的多范式语言之间也有所不同。例如,Python 动态允许向现有对象添加成员,而 Java 要求在类中定义成员。JavaScript 的面向对象特性是有用和有趣的,但特别是在过去几年里,它们一直是其他面向对象语言的程序员的挫折之源,他们被告知 JavaScript 是面向对象的,但没有足够的信息来解释 JavaScript 如何通过与其他主要语言根本不同的方法来实现面向对象。

同样,对于函数式编程,JavaScript 具有函数式编程支持,或者至少有一些支持。但是像整个 JavaScript 一样,函数式 JavaScript 并不完全符合好的部分。函数式编程语言的一个普遍特征(虽然不是普遍的)是尾调用优化,它表示只在末尾递归的递归函数在内部被转换为更常见的循环样式,速度更快,并且可以在不耗尽调用堆栈空间的情况下进行非常深的递归。这种优化计划在 ECMAScript 6 中实施,但在撰写本书时,它尚未在常见浏览器中实施,这不仅提供了较慢的性能,还限制了递归深度在大约 10,000 到 20,000 次之间。

在这个限制内可以做很多事情,但是结构化程序编写者如果他们的for循环不能实现远远超过 20,000 次的迭代,就会感到不满。这里的重点不是指定 JavaScript 不总是支持尾调用优化的最佳解决方案,而是指出这个困难目前存在,并且这是 JavaScript 不直接支持标准函数语言特性的少数几种方式之一(例如,计算从 1 加到 1,000,000 或更高的所有整数并不是特别有趣,但它在教程中作为标准示例)。

关于 JavaScript 是否应该被称为函数式语言,文献意见不一;它肯定不是像 Haskell 那样的纯函数式语言(但 OCaml 也不是)。JavaScript 被称为一种已知函数式语言 Scheme 的 C 语言版本,并且它的基本函数式特性并不是事后添加的东西。也许这反映了他的偏好,但是道格拉斯·克罗克福德在评判 JavaScript 语言的哪些部分是好主意时,从JavaScript:好的部分更好的部分,他从 ECMAScript 6 开始的偏好之一是停止使用命令式风格的forwhile循环,而是使用利用尾调用优化的递归。也许 JavaScript 具有函数式内核的最有力的主张可以在语言的哪个特性是中心的问题上看出来。有人建议,在 Java 中,中心特性是对象。在 C 中,是指针。在 JavaScript 中,是函数。

JavaScript 的函数具有一流的地位,这意味着(高阶)函数可以作用于其他函数并作为参数传递,甚至可以动态构建并作为结果返回。

在本章中,我们将涵盖:

  • 自定义排序函数

  • 映射、减少和过滤

  • 愚人的金子-改变其他人原型的行为

  • 闭包和信息隐藏

自定义排序函数-函数式 JavaScript 和一级函数的第一个示例

为了打破僵局,让我们来看看如何对 JavaScript 的数组进行排序。JavaScript 的数组有一个内置的sort()函数,至少可以说是一个合理的默认值。例如,如果我们创建一个包含π的前六位数字的数组,我们可以对其进行排序:

var digits = [3, 1, 4, 1, 5, 9];
digits.sort();
console.log(digits);

Chrome 的调试器在控制台上显示了一个数组,我们可以访问:

Array[6]
   0: 1
   1: 1
   2: 3
   3: 4
   4: 5
   5: 9
   length: 6
     __proto__: Array[0]

这很好。让我们再进一步,尝试混合整数和浮点小数(浮点数)。请注意,在 JavaScript 中,有一种数值类型,它的行为类似于整数(并保持整洁)对于在 Firefox 中介于-(253-1)或-9007199254740991 和 253-1 或 9007199254740991 之间的整数。这种数值类型也存储浮点数。它们具有更大的范围,当然,对于较小的数字,有更精细的值。为了进一步扩展范围,让我们创建一个包含整数和浮点数混合的数组:

var mixed_numbers = [3, Math.PI, 1, Math.E, 4, Math.sqrt(2), 1,
  Math.sqrt(3), 5, Math.sqrt(5), 9];

在这些数字中,Math.PI大约是 3.14,Math.E大约是 2.72,Math.sqrt(2)大约是 1.41,Math.sqrt(3)大约是 1.73,Math.sqrt(5)大约是 2.24。让我们像其他数组一样对其进行排序并记录这些值:

[1, 1, 1.4142135623730951, 1.7320508075688772, 2.23606797749979, 2.718281828459045, 3, 3.141592653589793, 4, 5, 9]

Chrome 的调试器,出于某种原因,这次表现不同,显示的是一个字符串数组,而不是一个带有向左的下钻三角形的数组。然而,数组被正确排序,所有值都按升序排列,整数和浮点值显示正确。

让我们在字符串上试一试。假设我们有以下数组:

var fruits = ['apple', 'durian', 'banana', 'canteloupe'];

当我们对其进行排序时,得到了这个:

["apple", "banana", "canteloupe", "durian"]

这是有序的,很好。让我们在数组中间添加一点内容:

var words = ['apple', 'durian', 'Alpha', 'Bravo', 'Charlie',
  'Delta', 'banana', 'canteloupe'];

我们对其进行排序,得到了以下结果:

  ["Alpha", "Bravo", "Charlie", "Delta", "apple", "banana",
    "canteloupe", "durian"]

这是什么?所有新单词都在开头,所有旧单词都在结尾!也许在它们自己之间排序,但是由大小写分隔。

这是因为字符串排序是根据 Unicode 值的字典顺序,这与 ASCII 编码相同,对于 ASCII 的一部分字符。在 ASCII 中,所有大写字母都排在所有小写字母之前。在这里,大写字母在大写字母内部被正确排序,小写字母在小写字母内部被正确排序,但这两者是分开的。如果我们希望所有的’A’都排在所有的’B’之前,我们需要更具体地说明我们想要什么。

我们可以通过提供一个比较函数来实现这一点-一个将比较两个元素并告诉Array.sort()哪个应该先的函数。让我们为这些单词制作一个不区分大小写的排序:

var case_insensitive_comparison = function(first, second) {
  if (first.toLowerCase() < second.toLowerCase()) {
    return -1;
  } else if (first.toLowerCase() > second.toLowerCase()) {
    return 1;
  } else {
    return 0;
  }
}

然后我们对数组进行排序并指定比较函数:

words.sort(case_insensitive_comparison);

当我们记录排序后的数组时,我们看到了一个不区分大小写的字母顺序:

["Alpha", "apple", "banana", "Bravo", "canteloupe", "Charlie",
  "Delta", "durian"] 

如果我们希望大写字母作为一个决定因素,并且大写字母会在其小写字母等价物之前放置,该怎么办?这是我们比较器的一个简单修改:

var mostly_case_insensitive_comparison = function(first, second) {
  if (first.toLowerCase() < second.toLowerCase()) {
    return -1;
  } else if (first.toLowerCase() > second.toLowerCase()) {
    return 1;
  } else {
    if (first < second) {
      return -1;
    } else if (second < first) {
      return 1;
    } else {
      return 0;
    }
  }
}

让我们在字符串列表的末尾添加’ALPHA’和’alpha’,然后重新排序:

["ALPHA", "Alpha", "alpha", "apple", "banana", "Bravo",
  "canteloupe", "Charlie", "Delta", "durian"]

成功了!

这可能需要,也可能不需要仅仅是字符串比较,但是如果服务器运行了数据库查询并为我们打包了 JSON 结果呢?一旦在客户端解析,结果可能是具有相同结构的对象数组。电子客户联系信息可能包括以下内容:

{
  "email": {
    "personal": "jsmith@gmail.com",
    "work": "john.smith@company.com"
  },
  "name": {
    "first": "John",
    "last": "Smith"
  },
  "skype": {
    "personal": "JohnASmith",
    "work": "JASCompany"
  }
}

这种记录结构可能不是 JavaScript 本能地推断出我们希望看到的排序方式,但这并不真正伤害我们。如果我们构建一个比较函数,它可以完全访问要比较的项目的字段或其他细节。这意味着我们可以先比较一个字段,然后再比较另一个字段,然后再比较第三个字段。这也意味着我们可以按不同的标准进行比较;在某个时候,我们可以按名称比较,而在另一个时候,我们可以按地理位置比较。如果我们的服务器存储(或查找)地址的 GPS 坐标,我们可以按离特定位置最近的人进行搜索。

这引出了数组.filter()

在函数式语言中,诸如映射、减少和过滤等功能是日常使用的基本功能。它们操作列表,在更多功能性、以列表为中心的语言中,列表可以是有限的,也可以是无限的。在这个意义上,列表可能更像 JavaScript 数组或生成器,一种函数,它不是返回单个值,而是产生零个或多个值,并且在理论上可能产生无限数量的值。与数组不同,任何给定的生成器可能永远不会耗尽,即使它实际上从不产生无限数量的值。生成器是一个很棒的功能,但在撰写本书时,它们在浏览器中的支持并不稳定,这意味着我们更可能在(有限的)数组上而不是在生成器上使用映射、减少和过滤。

但在我们放弃生成器这个话题之前,让我们给出两个生成器的例子,这两个例子都会在不久之后溢出,但它们都是在像 Haskell 这样的语言中可能被认为是一个数学序列的无限列表的例子,而不是仅包含第一个n成员的数组。我们将使用 ECMA6 提议的生成器语法来查看 2 的幂和斐波那契数的生成器,如wiki.ecmascript.org/doku.php?id=harmony:generators中所讨论的:

function* powers_of_two_generator() {
  var power = 1;
  while (true) {
    yield power;
    power *= 2;
  }
}

function* fibonacci_generator() {
  var first = 0;
  var second = 1;
  var sum;
  yield second;
  while (true) {
    sum = first + second;
    yield sum;
    first = second;
    second = sum;
  }
}

将这些示例与使用标准递归方法计算 2 的 n 次幂(这实际上并不需要,因为 JavaScript 的算术处理指数,但为了完整起见,还包括了这一点)以及计算第 n 个斐波那契数的天真实现进行对比。这两种方法都是所谓的尾递归,并且如果浏览器提供了尾调用优化,它们将受益匪浅:

function power_of_two_recursive(n) {
  if (n === 0) {
    return 1;
  } else {
    return 2 * power_of_two_recursive(n – 1);
  }
}

function fibonacci_recursive(n) {
  if (n === 0 || n === 1) {
    return 1;
  } else {
    return (fibonacci_recursive(n – 2) +
      fibonacci_recursive(n – 1));
  }
}

这两个函数都假设参数为非负整数。第二个函数的性能特征也很糟糕,尽管内存使用并不特别糟糕。然而,函数调用的次数与返回的值相当,因此计算第 100 个斐波那契数,除了整数溢出的问题,可能比宇宙的年龄还要长。正如 Donald Knuth 所说,“过早的优化是万恶之源”,但这是一个不需要过早优化的情况。

请注意函数式编程的另一个特性,称为记忆化——这意味着保留中间计算的结果,而不是反复从头开始重新生成它们,从而完全避免了性能瓶颈。考虑下面递归斐波那契函数的记忆化:

var calculated_fibonacci_numbers = [];

function fibonacci_memoized(n) {
  if (calculated_fibonacci_numbers.length > n) {
    return calculated_fibonacci_numbers[n];
  } else {
    if (n === 0 || n === 1) {
      result = 1;
    } else {
      result = (fibonacci_memoized(n – 2) +
      fibonacci_memoized(n – 1));
    }
    calculated_fibonacci_numbers[n] = result;
    return result;
  }
}

幻觉主义,映射,减少和过滤

在我小时候,我对幻术非常感兴趣,我现在还有一个幻术师的道具——里面有(或曾经有)一些东西,比如一个假拇指和一个戏法杯——还有一些幻术书。我记得的一个戏法是把一根绳子绕在大腿上,然后再绕在手上。如果一个人的腿放松,绳子看起来很紧,但如果一个人抬起腿,绳子就松了很多,从而给人一种被牢牢捆绑的印象,而实际上很容易解开一个或两个手。

我从来不擅长业余魔术表演的一面,这实际上是这门手艺的核心。资深的魔术师在指导他们的后辈或有抱负的人时,往往会说诸如“取悦观众并欺骗他们,但要知道哪个先来”。我记得很长一段时间以来,我一直认为我不懂(技术方面的)真正的魔术,因为我技术上知道如何做几个戏法,但我不知道如何去做我看到的那些事情。

后来,在我公司的派对上有一个魔术师,我因为一个不寻常的原因而着迷。他做了一些对我来说新奇的戏法,但在大约 70%或 80%的时间里,他花了很多精力利用我小时候学到的绳索戏法。而且效果非常好。他有精湛的表演技巧,而我的着迷并不是想知道他是如何技术上完成这个戏法的,而是对这样一个擅长的表演者能够利用一个孩子都能做的两个戏法来取悦观众感到惊叹。

Map,reduce 和 filter(这里,“reduce”包括右折叠和左折叠)在函数式编程中有些类似。Map 接受并将其应用于列表的所有成员。Reduce 接受一个操作,并从右侧或左侧开始,将其应用于每个成员,并得到一个中间结果。Filter 接受一个函数和一个列表,并创建一个由函数为真的项目组成的新列表。这些概念将在本章中进行进一步解释和说明。Map,reduce 和 filter 并不是特别困难的概念,但是可以从中获得很多收益。让我们来看看数组的 map,reduce 和 filter,暂时不考虑生成器和 Haskell 等语言提供的潜在无限列表。我们将向您展示如何使用 JavaScript 的数组内置版本的 map,reduce 和 filter。我们还将研究使用核心 JavaScript 来实现这些函数,不是为了让人们可以在 IE8(及更早版本)中使用这些函数,而是为了让人们了解这些功能的工作原理。

在警告了愚人金之后,我们将探讨一种有点非函数式风格的实现。它们使用for循环,在纯函数式语言中,首选的解决方案可能是尾递归实现。选择这种方式的理由是为了以一种对 JavaScript 的管道操作效果最佳的方式提供函数式特性支持,并且不会在(非尾调用优化的)JavaScript 递归在极少数情况下达到极限时失败。

愚人金 - 扩展 Array.prototype

需要注意的是,一个吸引人的解决方案,而且可以很容易地实现,是扩展(这里)Array.prototype或其他对象的原型,包括Object.prototype。不要这样做。

扩展Array.prototype及其相关内容会破坏其他人软件的平衡;这就好像在没有看到其他人的代码的情况下重写其他人的代码。扩展基本原型的最佳用例可能是填充(使用可用功能重新实现当前环境中不可用的功能),但即使在这种情况下,如果存在竞争的填充,也只有一个可以胜出。现在,你的填充不太可能像主要浏览器制造商一样对错误兼容性进行测试。这为微妙的错误留下了空间。在我们的情况下,为了支持稀疏矩阵,我们忽略了未定义的条目,但不是 null。我认为在这种情况下这是合理的,但远非唯一可能的智者(或不那么聪明的人)会如何处理这个问题。JavaScript 有两个 null 值,nullundefined,对于这两个不同的 null 值应该如何处理,可能会有不止一个观点。如果对我们有意义的语义并不是对其他人明显的语义,我们想要打开滑动的 heisenbugs 之门吗?

有一个简单而好的替代方案:编写自己的函数,最好是在闭包内定义的匿名函数,并存储在一个变量中。如果需要,这些函数可以检查是否存在浏览器的内置函数,比如Array.prototype.map(),如果找到,则可以回退到内置函数。它几乎可以完成通过扩展Array.prototype实现的任何工作。但它展现了良好的习惯,不会给其他人带来困扰。

注意

JavaScript 中的匿名函数一词并不排除存储在命名变量中的函数。它只是意味着它们是在没有函数名称的情况下定义的。换句话说,它们是这样定义的:function()var foo = function()或其他替代方法,但不是在函数关键字和开括号之间有一个名称,即function bar()。通常,我们将使用匿名函数,无论它们是否存储在变量中,但是有一个与调试相关的原因,即使我们从不使用它,也可能会给函数命名:调试器的堆栈跟踪可能会更详细地提到函数,即使从未使用过这些名称。出于这个目的,写var quux = function quux()是有意义的。

关于我们可能在私下开发的事情,有一点需要注意:令人惊讶的是,许多 Unix 实用程序最初是作为解决不同人的本地问题的私人黑客行为而诞生的。像野火一样传播的东西通常不是被设计成像野火一样传播的东西,比如 Web、JavaScript 和 5.0 版本之前的 PHP。在它们的第一个版本中,它们做了一些特定的事情,并让人们努力以更完整的方式运行。

HTTP 的无状态性是一个精心选择的特性,但在那个时候,大部分的 Web 编程都在尝试支持无状态 HTTP 变得非常痛苦的用例。5 MB 的 HTML5 键值存储和 4096 字节的 cookie 上限可能存在差异,但它们在提供适当的有状态行为的钩子时都提供了更或多或少优雅的容纳:Web 是建立起来的,不是为了使动态内容成为今天所有 Web 内容的主要部分。JavaScript 有它的优点和缺点,它的缺点可能是任何一个非常成功的语言中最糟糕的,但它之所以广受成功和名声显赫并不是因为它的优点或缺点。它成功是因为它在 Web 迅速传播的时候被包含在浏览器中。JavaScript 和 Web 都有人试图修复它们的限制和缺点,以便在它们迅速传播后做好多种事情,而实际上它们只擅长做一件事。

“这只是一个角落里的东西,我们不需要考虑维护或互操作性”这种心态非常非常危险。也许现在你的软件不会因为微妙地重新定义对象或数组的行为而破坏任何东西,但永远不会吗?甚至在未来的任何决定中?即使有人意识到,在解决 X 的同时,你为 Y 创造了一个可以节省大量工作的强大引擎?客户端 JavaScript 是一些最快成为开源的代码(毕竟,即使是关心保持专有内容的律师也知道你的整个系统可以交付给从 Web 登录到系统的任何人),假设某种标准行为的重新定义只是未来的保证是危险的。

对前面问题的一个简短回答是:不要重新定义其他人构建的任何东西,包括重新定义Object.prototypeArray.prototypeFunction.prototype等部分。尽可能选择自己的实现,但不要(强制性地)为每个人安装它。

避免全局污染

最好的做法也是尽量减少对全局命名空间的侵入。你添加的全局变量越多,就越容易与其他工具发生冲突。当雅虎宣布 YUI 时,基本礼貌上,他们只使用了一个全局变量——YUI。有一个完整的库可用,但你不会在浏览器的全局命名空间中看到一页页的项目;每次调用 YUI().use()或其他内容都完全包含在 YUI 对全局命名空间的一个侵入中。jQuery 使用的全局命名空间比他们宣传的要多一点,但原则上,他们只要求我们使用jQuery$。此外,他们试图使第二个变量完全可协商,因为jQuery承认其他框架需要$,而jQuery旨在与其他框架友好相处。

然而,你实际上可以走得更远,通过立即调用的函数表达式,包括 ReactJS Web 应用程序在第八章中探讨的,在 JavaScript 中演示函数式响应式编程 - 实时示例到第十一章,在 JavaScript 中演示函数式响应式编程 - 实时示例第四部分 - 添加一个草稿并把它全部放在一起。你可以在不触及全局变量的情况下实现相当多的功能。也许库应该有一些全局变量作为公共面向其他人的接口,但完全可以制作一个不触及全局变量的 Web 应用程序。

map、reduce 和 filter 工具箱 - map

地图接受一个数组和一个函数,并返回一个将该函数应用于其所有元素的新数组。例如,让我们创建一个包含 1 到 10 的数字的数组,并使用地图创建一个新数组,其中包含它们的平方。(请注意,JavaScript 在数组选项修改数组原地、返回修改后的数组或两者都做方面有些不一致。数组的map()reduce()filter()函数都创建一个新数组,保持原始数组不变和不受影响。)

var one_to_ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var squares = one_to_ten.map(function(x) {return x * x});

squares变量现在包含[1, 4, 9, 16, 25, 36, 49, 84, 81, 100]

map()的一个实现可能如下所示:

var map = function(arr, func) {
  var result = [];
  for(var index = 0; index < arr.length; ++index) {
    if (typeof arr[index] !== 'undefined') {
      result[index] = func(arr[index]);
    }
  }
  return result;
};

减少函数

减少函数的作用是,它接受一个操作,并逐步将其应用于数组中的元素。你可能在学校学过无限(和有限)级数,也许有这样的符号:

减少函数

在这种操作中,大写希腊字母 sigma(Σ,大致相当于希腊字母中的“S”)用于求和,较少地使用大写希腊字母 pi(Π,大致相当于希腊字母中的“P”)用于乘积。它们都反复将算术运算符应用于一系列有限(或无限)的数字,并且它们按照与reduce()相同的基本原理工作。

这种类型的符号所说的是,“对于这一系列数字,如果我们将它们相加,也就是说,用加法函数减少它们,取第一个数字和第二个数字,计算它们的和,然后将其添加到第三个数字,依此类推,我们会得到什么?”

如果我们希望将数组的内容相加,我们可以使用加法函数来减少它(作为一个小的实现细节,我们不使用裸的+运算符,因为我们不能直接将运算符作为常规函数传递)。

var sum = one_to_ten.reduce(function(x, y) {return x + y});
// returns 55

在学校教授的有限和无限级数通常是和;我们也可以使用其他级数。例如,如果我们想计算 10!,我们可以通过乘法而不是加法来减少我们提供的函数的项:

var factorial = one_to_ten.reduce(function(x, y) {return x * y});
// returns 3628800

减少不需要是数学性质的;这只是给我们一个快速演示它的方法。我们还可以使用 reduce 来连接字符串数组,其中+运算符用于字符串连接而不是数值相加:

var message1 = ['H', 'e', 'l', 'l', 'o', ',', ' ',
  'w', 'o', 'r', 'l', 'd', '!'].reduce(function(x, y)
  {return x + y});
var message2 = ['Hello', ', ', 'world', '!'].reduce(
  function(x, y) {return x + y});
// Both invocations return, 'Hello, world!'

然而,JavaScript 内置函数没有为我们解决的一个基本困难。有时我们需要做出选择,并进一步指定我们真正想要的是什么。数值相加、乘法和字符串连接都是可结合的,这基本上意味着你可以在任何地方放括号,遵循括号的标准规则,得到相同的答案。在数值相加中,以下是等价的:

1 + (2 + (3 + 4))
((1 + 2) + 3) + 4

这两个计算都得到10,如果我们乘以而不是相加,两者都给出24的乘积。但是,如果我们对非常略微不同的值使用指数运算符会发生什么:

Math.pow(2, Math.pow(3, 4))
Math.pow(Math.pow(2, 3), 4)

这与我们刚刚看到的代码是相同类型的东西,尽管显然使用了一个更丑陋的命名空间函数而不是中缀运算符。在非 JavaScript 符号中,使用一个脱字符(^),我们得到以下伪 JavaScript,它重新陈述了前面的计算:

2 ^ (3 ^ 4)
(2 ^ 3) ^ 4

如果我们使用console.log()和刚刚看到的Math.pow()计算,我们会得到这个:

2.4178516392292583e+24
4096

这里有一个细微的差别。一个结果是一个四位整数;另一个用科学计数法表示,比四位数多得多。那么,有多少东西真的像指数运算的特殊情况?

这个问题的答案有点棘手,部分原因是我欺骗性地构造了这个问题,以说明一个危险的误解。指数,括号的添加方式很重要,可能更像一般情况。存在一些情况,括号的位置并不重要,它们甚至更常见地成为reduce()的候选项,但在一般情况下,我们不应该假设这两者是等价的。我们将给出fold_left()fold_right()函数;这不是唯一的两个选项(如果它们都不是你想要的,你可以手动操作),但它们分别计算数组一到十的和如下:

((((((((1 + 2) + 3) + 4) + 5) + 6) + 7) + 8) + 9) + 10
1 + (2 + (3 + (4 + (5 + (6 + (7 + (8 + (9 + 10))))))))

两者并不一定有优劣之分,但差异可能很重要。JavaScript 内置的reduce()函数是一个左折叠,从左到右开始,如前两个表达式中所示(这可能是一个合理的默认值)。

如果我们定义fold_left()fold_right(),它可能看起来像下面这样。我在这里使用了缩写,因为全拼看起来太接近保留字;例如,array 不会与 Array 冲突,但它们之间有令人困惑的相似之处(如果变量命名为 function,也会有类似的冲突):

function fold_left(arr, fun) {
  var accumulator;
  for(var index = 0; index < arr.length; ++index) {
    if (typeof arr[index] !== 'undefined') {
      if (typeof accumulator === 'undefined') {
        accumulator = arr[index];
      } else {
        accumulator = fun(accumulator, arr[index]);
      }
    }
  }
  return accumulator;
}

function fold_right(arr, fun) {
  var accumulator;
  for(var index = arr.length - 1; index >= 0; --index) {
    if (typeof arr[index] !== 'undefined') {
      if (typeof accumulator === 'undefined') {
        accumulator = arr[index];
      } else {
        accumulator = fun(arr[index], accumulator);
      }
    }
  }
  return accumulator;
}

最后一个核心工具——过滤器

过滤器通过数组筛选出符合某些标准的值。例如,我们可以过滤出仅为正值的内容,如下所示:

var positive_and_negative = [-4, -3, -2, -1, 0, 1, 2, 3, 4];
var positive_only = positive_and_negative.filter(
  function(x) {return x > 0;});

positive_and_negative过滤器,在此运行后,如声明的那样;positive_only的数组值为[1, 2, 3, 4]

过滤对于缩小数组的内容很有用。如果我们有一个数组,如前面的一个,包含了 Smith 先生的联系信息,我们可以访问字段,缩小到可能符合我们兴趣的内容。我们可以声明我们只想要一个州,或者需要一个特定电话区号的电话号码,或者对函数能够告诉的任何内容陈述其他标准。如果我们的记录包括 GPS 坐标,我们可以过滤内容,只包括特定中心点半径内的结果。我们只需要一个函数,对于我们想要包括的记录返回 true,对于我们不想要的记录返回 false。

JavaScript 中信息隐藏的概述

一般来说——意味着任何编程语言、客户端、服务器端、非网络、移动应用程序,几乎任何其他东西,甚至可能是微控制器(现在可以运行精简版的 Python)——都有不同的方法论,通常都有各自的优势和劣势。Steve McConnell 的《代码大全:软件构建的实用手册》(tinyurl.com/reactjs-code-complete)讨论了不同的方法论,比如,面向对象编程的优势在于比过程式编程更适合大型项目。对于大多数方法论,他的建议是它们都有各自的优势、劣势和适用范围,在 X 和 Y 条件下,你应该考虑方法论 Z。但有一个例外——信息隐藏。他对何时使用信息隐藏的简单建议是“尽可能多地使用”。

程序化或结构化编程可能很容易被忽视,使用其优势并不是在突破界限。但是假设我们看看它最初发布时的情况,它的函数/过程,if-then-else,for/while循环和过程体不对外开放。现在,如果我们将其与直接的汇编或机器代码进行比较,使用了 Dijkstra 风格的 goto,甚至不假装模拟结构化控制流,那么我们就会明白,程序化或结构化编程真的是非常惊人的。此外,今天这显而易见的事实是因为它在很大程度上取得了成功,使每个人受益。流程图曾经是理解复杂系统的必备救生工具,现在已经成为一种新奇物品。它们出现在杯子上,在 XKCD 漫画中展示了如何提供出色的技术支持,或者在其他幽默的用途中,因为它们不再需要提供任何一种路线图来帮助一些人——找到他们穿过意大利面的方法。

现在,一个大型系统可能比旧的、流程图导航的程序所能容纳的要复杂得多,也要大得多,但是程序化编程已经有效地驱逐了那个幽灵。此外,软件工程范式中的新迭代,如面向对象编程,已经减少了理解大型系统的难度。在这两种情况下,很大一部分好处是一种推进信息隐藏的实用方式。通过结构化编程,您可以在源代码中导航,而无需跟踪汇编或机器语言呈现跳转(即 goto)的每个点。结构化和面向对象编程在历史上都允许开发人员前所未有地将更多的程序视为封闭的黑匣子,您只需要打开和检查其中的一小部分。它们提供了信息隐藏。

JavaScript 中信息隐藏的概述

前面的流程图是来自xkcd.com/627/的新奇流程图。我从未见过有人谈论的程序的真正流程图。

在经典的 Java 中,信息隐藏的标准教科书例子可能是这样的:

class ObjectWithPrivateMember {
  private int counter;
  public ObjectWithPrivateMember() {
    counter = 0;
  }
  public void increment() {
    counter += 1;
  }
  public void decrement() {
    counter -= 1;
  }
  public void get_counter() {
    return counter;
  }
}

与生产中的 Java 类的一个好例子不同,这个类有一个私有成员和四个公共成员。通常的目标是尽可能隐藏有趣的重要工作,并向世界展示一个简化的外观。这就是 Java 面向对象编程提供信息隐藏的方式,尽管面向对象语言之间在处理对象的方式和方法上有重要的区别,但这是 Java 的一大优势。对象方法不仅应该按过程方式编写——作为具有定义输入和输出的黑匣子——而且它们还封装在对象中,多个较小的黑匣子可以被包含在一个较大的黑匣子下。在这种情况下,我们看到了信息隐藏的另一个好处:我们在很大程度上受到外部保护,并且可以自由地进行任何内部更改,而不会破坏外部使用,只要我们保持相同的行为。假设我们决定保留计数器何时更改以及更改为何值的日志。这至少是另一个私有字段和对代表公共接口的方法的内部更改,但我们可以进行这些更改而不改变任何访问这个类的任何类的任何细节。现在假设我们想要更多的日志记录,我们希望我们的日志记录完整的堆栈跟踪。在内部,我们需要使用诸如Thread.currentThread.getStackTrace()这样的东西,但在外部,没有人需要知道或关心我们的重构。在一个更大的类中,我们可能会发现一个瓶颈,通过切换到另一个等效算法可以显著改进。由于 Java 面向对象编程提供信息隐藏的方式,我们可以进行大量的更改而不会打扰其他人,他们可以使用我们的工作而不必为除了我们的公共接口之外的任何事情而烦恼。

使用 JavaScript 闭包进行信息隐藏

我们需要进一步观察信息隐藏的模式。在 Java 中,你会很快学到信息隐藏的语言特性,并且会被鼓励使用诸如“特权的吝啬是伪装下的善意”这样的安全最大化原则,以便在声明事物为非公开时出错。在 JavaScript 中,可能会有一些未来保留的关键词,比如publicprivateprotected(Douglas Crockford 建议这些关键词可能是为了让 Java 程序员更容易理解 JavaScript 而牺牲了 JavaScript 更好的一面),但现在并没有同样明显的机制。对象的所有成员,无论是函数还是其他,都是公开的。JSON——另一种像野火一样传播开来的东西,但没有人因其简单而诅咒——没有提供任何标记任何东西为非公开的机制。

然而,在一些函数式语言的特性中有一种叫做闭包的技术,可以创建私有字段。这并不是一种简单存在于语言中的创建信息隐藏的技术,但它允许我们创建包含非公开信息的对象。为了移植前面的例子的功能,包括非公开状态,我们可以有如下所示的东西:

var factory_for_objects_with_private_member = function() {
  var counter = 0;
  return {
    'increment': function() {
      counter += 1;
    },
    'decrement': function() {
      counter -= 1;
    },
    'get_count': function() {
      return counter;
    }
  }
};

这里给出的例子建议了我们如何扩展它。一个更复杂的例子可能有更多的var函数存储字段和函数,并返回一个将公共接口暴露出来的字典。但我们不要简单地跳到那一步;在这条路上有一些有趣的事情。

在函数式语言中,函数可以包含其他函数。事实上,鉴于 JavaScript 的语法——其中函数是第一类实体,可以存储在变量中,作为参数传递等等——如果函数不能嵌套甚至两层深,那将是令人惊讶的。因此,以下语法是合法的:

var a = 1;
var inner = function() {
  var b = 2;
  return a + b;
}

但同样的东西可以合法地包装在一个函数中,如下所示:

var outer = function() {
  var a = 1;
  var inner = function() {
    var b = 2;
    return a + b;
  }
}

这是功能语言的基本特性,包括 JavaScript 的传统,内部函数可以访问外部函数的变量;因此abinner函数同样可用。

现在如果我们将outer函数改为返回inner函数会发生什么?然后我们有以下内容:

var outer = function() {
  var a = 1;
  var inner = function() {
    var b = 2;
    return a + b;
  }
  return inner;
}
var result = outer();

当函数执行完成时,它的变量已经超出了范围。JavaScript 现在具有函数范围的var变量,并且正在过程中获得块范围的let变量,但是在outer函数中以任何方式声明的变量已不再可用。然而,有趣的事情发生了;inner函数在outer函数结束后仍然存在,但以一种逻辑一致的方式。inner函数应该并且确实可以访问它的执行上下文。我们有了一个闭包。

这种现象可以用于信息隐藏,信息隐藏很重要。然而,可以争论的是这里最有趣的不是它可以包含非公共变量,潜在地包括函数,而是只要有东西可以访问它,执行上下文作为一个整体就会被保留下来。这留下了一个有趣的领域可以探索。一个 StackOverflow 成员曾评论说,“对象是穷人的闭包”,对象和闭包都有有趣的可能性,超出了关于如何使用它们的特性进行信息隐藏的 FAQ 条目。即使是《代码大全》,它可能会强烈支持信息隐藏,也从未说过,“尽可能使用信息隐藏,但不要使用其他东西。”

也许责怪功能语言纯粹主义者说,“JavaScript 必须等到它成为 20 年历史才能实现尾调用优化,而不是惩罚标准的函数式编程使用递归——就像美国法律下的一个新生儿成长为成年人一样。”然而,不管功能程序员对 JavaScript 可能感到不满的其他方面如何,JavaScript 从一开始就足够正确地实现了闭包,以至于保留执行上下文的闭包在 JavaScript 中一直是一个重要的特性。而 20 年后,它们仍然是大多数浏览器中的主要,可能是唯一的信息隐藏资源。

摘要

在本章中,我们涉及了 JavaScript 与功能编程的一些注解。我们主要关注了三个主题。自定义排序函数提供了一个简单而有用的窥视,我们如何将一个辅助函数传递给一个高阶函数,以获得比默认更有用的行为。Map、reduce 和 filter 是与数组相关的功能编程的三个主要工具。通过闭包和信息隐藏,我们看了一种在负责任的软件开发中提供一些核心兴趣的功能方式。

JavaScript 是一种具有功能根源和一些功能语言优势的多范式语言,尽管功能语言纯粹主义者将 JavaScript 与命令式多范式语言一起归类可能是不常见的。JavaScript 没有像一些功能语言那样对赋值或纯不可变数据结构进行永久绑定。

所有语言都有好坏不同的地方,但 JavaScript 将优秀的部分和糟糕的部分如此明显地结合在一起,以至于 Crockford 的《JavaScrpt: The Good Parts》和《The Better Parts》的基本方法在优秀的开发人员中没有受到严肃质疑(我想知道为什么还没有人将 Kernigan 和 Ritchie 的《C 程序设计语言》第二版销售为《C++: The Good Parts》)。认为默认将事物倾倒在全局对象上是开发 Web 应用的好主意,这种观点可能会引起争议,甚至令人讨厌。这也适用于 JavaScript 的功能方面。JavaScript 是第一种主流语言,允许使用匿名函数或 lambda,这在函数式编程中已经成为基本要素,大约自 LISP 出现 50 多年以来。即使现在连 Java 也加入了这一潮流,但它在主流语言中的存在是受 JavaScript 的影响。JavaScript 从一开始就有闭包。就一些糟糕的地方而言,JavaScript 似乎花了几十年的时间才应用尾调用优化,以及使用尾递归而不受惩罚,而不是使用 for 和while循环来构建迭代工作的函数式编程风格。

函数式编程是一个有趣的话题,你可以无限地探索(也就是说,函数式编程的有趣方面的列表是一个无限的列表,尽管在具体情况下,人们只能从列表的左边取出有限数量的项目)。在不试图解决 JavaScript 是否应该被视为函数式语言的问题的情况下,最好将 JavaScript 理解为与函数式编程根源相关,并且学习在函数式语言/范式中更好地编程应该是在 JavaScript 中更好编程的基础。JavaScript 可能会载入历史,不仅作为 Web 的语言,也许是程序员必须了解的最关键的语言,还是功能性语言的优点不再被视为(如 Scheme)“你永远不会使用的最好的语言”的桥梁语言。也许,功能性语言的优势会被视为严肃、主流的多范式语言构建的不可妥协的部分。

让我们继续看看函数式响应式编程。

第六章:函数式响应式编程-基础知识

我可能从这里有点尴尬地提到,我有数学硕士学位和许多数学奖项,但我发现与基本函数式编程相关的一些数学概念有点难以理解。一个人不会阻止在相关数学和计算机科学领域有扎实基础的人去攻克,例如,在维基百科关于函数式响应式编程中链接的基础性函数式响应式编程论文的完整数学严谨性。然而,这里的意图略有不同:从函数式响应式编程中学到对于没有或不记得那些开创性作品所依赖的数学水平的专业开发人员有用的东西。

StackOverflow 的评论反复问道,“你能不能以不假设计算数学博士的方式解释它?”这里的意图不是提供那些数学文章的全部内容,而是提供一个对于真正的专业软件开发人员有用的、实用的子集,他们不会梦见 Scheme 或 Haskell。

在本章中,我们将涵盖:

  • 计算机传统的记忆之旅

  • 函数式响应式编程

  • 如果你只学到一件事……

  • 了解有关函数式编程的知识

  • 前端 Web 开发的未来

让我们深入了解。这段充满传统的记忆之旅可能会相当漫长,但在任何意义上都不会枯燥无味。

计算机传统的记忆之旅

有一个非常侮辱性的检查表一直在流传,适用于宠物(或其他)编程语言。其中一个侮辱是,“程序员不应该需要理解范畴论来编写Hello, World!”这反映了部分湿后耳朵的初级程序员在他们提出最好的编程语言时不断犯的错误的烦恼。在这方面,它可能与愚蠢的事情清单一样,愚蠢的事情清单是从无数冒险电影中发生的愚蠢事情中学到的。作者从无数电影反派的错误中吸取教训,宣称“射击对我的敌人来说并不是太好”,“除非绝对必要,我不会包括自毁机制……”这些侮辱来自于一次又一次看到相同错误的挫败感。

还有其他一些显示编程智慧和智慧的观点。例如,宠物或玩具语言的数量级比成功的语言要多得多,成功可以是在学术计算机科学领域或商业信息技术领域。任何语言发展中被广泛认可的转折点是,当它在一个可以通过使用它来编写自己的编译器的水平上工作时。

这并不是一个不重要的观点;当 Java 首次宣布时,宣称 Java 编译器是用 Java 本身编写的,这意味着能够运行 Java 运行时环境的系统应该能够编译用 Java 编写的软件。在这方面,一个常见的侮辱某人对自己热情洋溢的宠物语言的标准问题是,“除了自己的编译器,它还用来写过其他东西吗?”计算机智慧和传统的这一特定节点已经融入了检查表:其中两个条目是,“用这种语言编写的最重要的程序是它自己的编译器”,然后,更具侮辱性的是,“用这种语言编写的最重要的程序甚至不是它自己的编译器”。

但是,“程序员不应该需要理解范畴论来编写Hello, World!”的哲学反对意见并不是为了听起来侮辱而凭空捏造的。这是由 1978 年 Kernigan 和 Ritchie 经典著作《C 程序设计语言》第一版开始的传统,在深入研究复杂性之前开发的第一个程序是一个最小的 C 程序,思想是“让我们在尝试行走之前先爬”,打印出Hello, World!

main()
{
  printf("hello, world.\n");
}

介绍新编程语言的人普遍遵循使用Hello, World!作为他们的第一个示例程序的传统。这是“程序员不需要理解范畴论就能编写Hello, World!”的一端。那么光谱的另一端是什么?

在学术数学世界中,无论是纯数学还是应用数学,数学作为成功的标志已经变得非常专业化(就像任何经历了足够工作的领域一样)。有评论说,能够理解数学会议上提出的 50 篇论文中的 13 篇以上的数学家是非常罕见的。数学已经变得足够专业化,以至于大多数数学博士,无论多么有能力,都无法理解大多数其他数学博士的工作。在这种情况下,希望能够理解所有数学就像希望能够说出所有人类语言一样:有点天真。数学博士项目的目的也许不是让你发展到能够理解数学学科的整个广度,而是深入理解某个狭窄领域,以至于在你的博士学位完成时,你比世界上任何其他人更深入地理解了这个高度专注的领域。

有两种例外,连接所有数学的学科,但从完全相反的方向。一方面是逻辑和数学基础,它研究了所有其他数学领域所基础的基石。现在有一些关于逻辑属于数学还是哲学的问题,人们也听说过有人被要求决定他们想成为逻辑学家还是数学家。但撇开这些问题,可以说逻辑通过挖掘其他数学领域所依赖的基石与所有数学领域相连接。

然后还有另一种选择:范畴论。一个芭比娃娃曾经说过,“数学很难”,但数学界很清楚这一点,不需要芭比的帮助。阿尔伯特·爱因斯坦说过,“不要担心你在数学上的困难。我可以向你保证,我的困难更大。”但数学分支范畴论尤其难以理解。

如果逻辑可以研究数学伟大建筑的基石,范畴论则着眼于已经建成的城市,并探索贯穿各种数学领域的建筑主题和相似之处。范畴论是一门学科,有点像比较文学学科,从业者被期望能够应对不只一种语言的比较文学。可以说范畴论是整个数学领域中最困难的地方。你需要做一些大多数数学博士从未学过的事情。

此外,也许是一种致敬,我的导师是一位范畴论家,他能够有效地指导一个关于点集拓扑这个鲜为人知的分支的论文,尽管他在点集拓扑方面并没有显示出特别的专长。因此,说一个使用你的语言的程序员需要理解范畴论才能编写*Hello, world!*是相当刻薄的侮辱。

那么这与函数式或函数式反应式编程有什么关系呢?很高兴你问!

在维基百科文章中链接的函数式响应式编程的资源中,Haskell(或者构建在其上的东西)是主导语言。还有一些其他的语言,比如 Scheme 的一个方言,但人们似乎总是回到 Haskell。有很多 Haskell 的资源;其中最受尊敬的之一是《学习 Haskell 为了更好的》tinyurl.com/reactjs-learn-haskell。它在第九章中神秘地出现了一个*Hello, World!*程序,而不是第一章。为什么是第九章?嗯,正如解释的那样,输入和输出是建立在单子之上的。但是真的需要这么多的解释才能理解单子吗?是的;单子是建立在应用函子的概念之上的,而应用函子是建立在函子的基本概念之上的,这让人想起在数学研究生阶段遇到的某个名字,但并没有真正理解。让我们去维基百科的函子页面看看。维基百科以清晰易读而闻名。维基百科页面中有一些内容。其中一点是,语言对于维基百科来说实在是太过神秘了。另一点是,函子实际上是从范畴论中获得的东西:

在数学中,函子是范畴之间的一种映射,适用于范畴论。函子可以被看作是范畴之间的同态。在小范畴的范畴中,函子可以被更普遍地看作是态射。

函子最初是在代数拓扑学中考虑的,在那里,代数对象(如基本群)与拓扑空间相关联,代数同态与连续映射相关联。如今,函子在现代数学中被广泛应用于各种范畴。因此,函子通常适用于数学中范畴论可以进行抽象的领域。

Hello, World!的高级先决条件!

如果你想挑战自己,可以阅读维基百科关于函子的文章。但如果你发现自己只是匆匆浏览,因为大部分内容都超出了你的理解范围,那么你不用担心:可能很多数学博士也会以同样的方式匆匆浏览,出于同样的原因。

在这一点上还有更多可以说的,但我将限制自己在评论本章意图之后再做一点补充。这导致了本章的核心困难。这是一本关于信息技术而不是计算机科学的文本,虽然可以向计算机科学家请教,但这本文本的撰写意图是从一个程序员的角度向另一个程序员撰写。在 Haskell 中,目标相当于基于看到,模仿的基础上说,“这是一个纯函数的例子;那是一个输入和输出单子的例子。尽量让你的程序的大部分工作都来自纯部分,并将输入和输出限制在尽可能小的隔离空间中。”

有一点可以用 Haskell 和 Python 进行对比,再次以 XKCD 为例——注意 Python 中给出的一切都是简单的第一个例子。第一次接触 Python 会让人感觉再次爱上编程。同样的情况也完全适用于 ReactJS,就像再次发现网络一样:

Hello, World!的高级先决条件!

Python 和 Haskell 在至少一个方面相似:它们都允许快速软件开发。Haskell 拥有与 Python 所期望的类似功能:一名本科生花了几个月的时间,在 Haskell 中实现了 Quake 3 引擎的大部分功能。Haskell 可能还有其他优势,比如其非常可靠的类型系统,而且一旦某些东西编译完成,它就已经有很大的可能性可以工作。然而,这里追求的问题是,“它能让程序员高效生产吗?”这个屏幕截图来自一个本科生项目中在几个月内实现的 Quake 3 级别。Haskell 有一些 Python 没有的东西:比如非常可靠的类型系统。然而,Haskell 和 Python 至少在这一点上是相似的:在熟练开发者的手中,它们都允许生产力和开发速度,这需要在看到之前才能相信。

Hello, World!的高级先决条件

但是,有经验的程序员可能会尝试 Python 并发现自己能够游刃有余,但如果他们无法处理数学,他们在使用 Haskell 时就不会有相同的体验。Haskell 为那些能够处理大量计算数学的人提供了快速开发的超能力。Python 为更广泛的程序员群体提供了这样的能力,无论他们是否具有丰富的数学背景。

这一章是一个尝试,无论对错,都是为了解释事情,以便信息技术工作者,而不仅仅是计算机科学专业人员,可以像有经验的程序员一样,用函数式响应式编程和 ReactJS 获得良好的 Python 体验,而不是导致许多开发人员继续发表令人沮丧的评论的有经验的程序员的糟糕的 Haskell 体验,这些评论在一段时间后变得非常悲伤,比如“如果我懂得更多数学,我可能会理解这个”。

我们写这本书的目的是为了使程序员在这个领域有用,而不仅仅是懂很多数学的计算机科学学生。但至少暗示了一种更简单的方法,以及有经验的程序员说“如果我懂得更多数学,我可能会理解这个”。直接从页面tinyurl.com/reactjs-learn-monads获取。现在,以下是学习单子的一些步骤:

  1. 获得计算机科学博士学位。

  2. 把它丢掉,因为你在这一部分不需要它!

(也许普通开发人员终究可以从(响应式)函数式编程中获益!)

区分函数式响应式编程的特点

作为函数式响应式编程的领军人物之一,也可以说是函数式响应式编程的鼻祖之一,Conal Elliott 回顾了术语“函数式响应式编程”,一个领军人物对于名称的反思可能会非常有趣。Elliott 对术语“函数式”表示了保留意见,他认为这个词现在意味着很多东西,因此意义不大,并对术语没有包含的一个词表示遗憾:时间。他提出了一个另类的名称,即指示性连续时间编程,即使我们在这里使用更标准的术语“函数式响应式编程”,这也是重要的。通过“指示性”,我们的意思是,正如我们之前讨论 ReactJS 时所讨论的,你只需指定需要完成的任务,而不是每一步如何完成它。连续时间不仅仅意味着应该这样称呼它,而且连续时间是如此重要,以至于它应该被纳入现在所谓的函数式响应式编程的名称中。

连续时间元素出现在这些来源中,对一些人来说可能会感到惊讶,因为计算机只能离散地测量时间,但这种区别是概念模型中的区别,而不是实现中观察到的特征。这种比较类似于函数式语言中存在的无限列表,人们可以从列表中取出多少或多少而不会用完预先计算的条目,或者更明显地是栅格图形(GIF、JPEG、PNG)与矢量图形(SVG、一些 PDF)之间的区别,栅格图形有一定数量的像素表示,而矢量图形可以根据经典广告执行人员的说法进行渲染,公司标志在信头上高一英寸时看起来和在公司总部的八英尺高处一样好看。

连续时间意味着时间的处理方式类似于 SVG 或其他矢量图形,而不是栅格 GIF/JPEG/PNG,后者存储在固定分辨率上,没有多余的像素。对于函数式响应式编程的建议之一是,连续时间事件和可能是连续值行为或事件流具有第一类实体的地位,作为定义特征的一部分(尽管有人可能指出,也许不是唯一的特征)是函数是第一类实体,可以作为参数传递,就像 JavaScript 和其他语言中的匿名函数一样。

ReactJS 与这个有什么关系可能并不立即明显;我看过十多个 ReactJS 视频,通常是来自 Facebook 开发人员。强调了指称语义,这是一个正式术语,用来描述只需要完成什么,而不是如何完成每一步。还有关于虚拟 DOM 的持续讨论,这相当于“如果你想了解更多,你可以学习,但你只需要告诉系统如何render(),然后相信系统会完成其余的工作。”但事实上,连续时间语义是内置在 ReactJS 的基本工作原理中的。开发人员的责任之一是编写一个render()方法,指定在调用时页面上应该显示什么(也许还要适当地调用render()render()不会自己运行)。

这并不具备连续时间的所有特性;一个教学视频暗示了一个系统,不仅可以实时工作,还可以允许类似 VCR 的“倒带”和“快进”功能来逐步通过时间,Pete Hunt 的一个 ReactJS 视频暗示了 Facebook 可能通过 ReactJS 技术接收一个错误报告,并能够逐个细节地重放发生错误之前的情况,而没有书面描述错误的情况,只是“粗话”。然而,最突出的用例是假定连续时间,并且开发人员有责任编写一个render()函数,可以在调用时正确地指定要渲染的内容,并且(顺便)适当地调用该函数。

如果你只学到一件事…

理查德·P·费曼的经典“费曼讲义”被认为是对技术主题进行清晰解释的典范,他以一个非常简单的问题开篇:如果科学的其他一切都被遗忘,只有一句话的信息幸存下来,那么理想情况下会是什么?费曼给出了一个简洁的答案,实际上表达了很多内容:

*"如果在某场灾难中,所有的科学知识都被摧毁,只有一句话传给下一代生物,哪句话会包含最多信息?我相信那就是原子假设,即**一切事物都是由原子组成的——小颗粒在永恒的运动中移动,当它们相距一点时相互吸引,但当它们被挤压到一起时则相互排斥。*在这一句话中,你会看到,关于世界的大量信息,只要稍加想象和思考。"

这在费曼讲座中作为一个跳板,可以让我们对我们所知道的物理学说很多。

函数式响应式编程的最大学习要点可以用一句话概括,这是与函数式编程本身相关的一堂课,函数式响应式编程进一步完善了这一点:尽可能多地编写纯函数,数学风格的,尽可能少地编写或遵循配方

配方会说一些诸如“将烤箱预热至 350°F。在一个大碗中混合叶子、酥油和盐。用羊皮纸在两个大烤盘上铺一层。将叶子均匀分布在每个托盘上的一层中…”现在这并不是在挖苦家政和烹饪的人。(纯粹的功能性烹饪方法永远不会产生任何可食用的东西,如果你想完成任何事情,这是一个小缺点。)配方同样可以在许多许多 YouTube 视频中找到,详细说明如何更换,例如 2004 年福特 Escort 上的破损雨刷,它们也支持传统的黑客编写的 How-to,尽管它们在今天不像早期那样突出,这并不是因为黑客社区意识到使用 How-to 不是解决困难的适当方式,而是因为几乎所有事物的通用性都得到了足够的改善,以至于你不需要一个在烧录 CD 时提到月相的 How-to;How-to 很少可能是唯一的选择(这实际上是它们最好的用例)。

我对 Haskell 经历了哪些理论上的扭曲(即:需要经历)以最小程度地妥协其功能状态来包含输入和输出有些担忧。但同样,即使纯粹的函数式 JavaScript 可能存在与否,我们最好还是尽量增加我们的软件部分是纯函数式的,并最小化通过指定如何做事来完成工作的部分。这里的函数不应该像结构化编程中那样意味着“返回一个值的子例程”。一个函数不是在做某事并在完成时返回一些有趣的东西。它更多地具有数学上的意义,“接受零个或多个参数,并且不多不少地返回一个基于该值得到的值。”

计算机人员使用的基本数学中的纯函数的例子包括算术函数,如加法、减法、乘法、除法、指数、阶乘(例如,4 的阶乘是 432*1),斐波那契数,三角函数,如正弦和余弦,双曲函数,积分,导数,欧几里得除法来计算两个正整数之间的最大公约数,等等。毫无例外,这些函数接受零个或更多个(或者,对于这些情况,一个或更多个)输入,并从中计算出一些东西,而不做任何外部更改;它们没有更新数据库或输出东西到控制台。它们只是接受它们的输入,并确定性地计算出一个输出,不多不少。这就是纯函数的本质。

长词是“一个半英尺长的词”。其中一些词在各处流传,包括在关于 ReactJS 和函数式响应式编程的视频中,比如幂等和引用透明度。但是与纯函数相关的含义是简单而直接的。

幂等函数是指无论调用一次还是一百次,都会返回相同结果的函数。在数学中,例如加法和阶乘,总是给出相同的结果。RESTful 网络服务提供了一个较少数学的幂等性的例子:请求相同的 URL 意味着每次都会得到相同的 HTML 或其他数据。获取静态内容是幂等的;从 CDN 获取的库的版本应该无论谁、在哪里或何时请求,都会得到相同的下载。

缓存,比如使用 Steve Souder 的经典远期Expires头部来实现 Yslow,是一种非常有用的方法,特别是在下载之间存在幂等性的情况下。(如果下载是幂等的,无论是下载新副本还是从浏览器缓存中提供副本,文档都是相同的。)动态内容,无论是老式的 CGI 脚本还是动态的 Django 应用程序,都不是幂等的。如果一个页面上甚至在 HTML 注释中写着“此页面在某个时间下载”,那就不是幂等的。Web 最初是设计为幂等的;后来人们开始意识到动态内容可能非常有用,并开始研究如何克服 HTTP 的无状态、幂等设计。

引用透明度这八个音节的词意味着函数调用可以等效地替换为它返回的值。因为 4!等于 24,所以在你的代码中包含 4!和只包含 24 应该是等效的。如果你有一个值的余弦,使用cos()的结果的存储值或重新计算应该是等效的。破坏引用透明度的不纯行为是每次调用cos()都记录一个字符串,这是一个经典的副作用的例子。

提示

“副作用”这个术语是不幸的,可能是有意使用的措辞;在医学背景下,所有药物都会产生多种效果,其中一些是服药的目的,而另一些则被容忍为必要的副作用。在医学上,副作用是指药物的效果是被容忍的,但不是服药的目的。在程序中记录消息是一种副作用,有点像说服用止痛药并随后减轻身体疼痛是一种副作用:这正是服药的全部目的,而不是药物可能产生的其他效果,这样称呼它是一种副作用有点奇怪。

上面是一些基本数学函数的示例,也许这很容易,因为在某些数学领域,一切都是纯函数,可能是由纯函数构建的,而且环境排除了不纯函数或副作用。人们也可以举多项式作为由纯函数构建的纯函数的例子,如果你有能力使用它,这是一种非常好的方法,但如果习惯于用信息性假设来构建一切,这种方法会感到陌生和困惑。对于以命令为基础的程序员来说,命令式函数是短期内最容易的方法,但长期来看更难。对于以函数为基础的程序员来说,函数式编程在短期和长期内都很容易。但是,要在实际信息技术中看到这些问题的例子,我们不需要看得比 ReactJS 更远。

Facebook 的长期痛苦学习基本上导致了一个认识,即摆脱困境的方法是通过幂等性和引用透明度,这就是 ReactJS 的编写目的。

学会你能学到的东西!

正统精神传统中的一位智者将许多事情归纳为 55 条格言(tinyurl.com/reactjs-55-maxims),其中第二条是,“祈祷应该是你能做到的,而不是你认为你必须做到的”,这些话对于大部分编程也同样适用。这里有一个建议,但不会让非数学家感到畏惧。尽可能按照你能做到的方式去遵循这个建议,而不是你认为你必须这样做。尽可能多地学习函数式编程。尽可能纯粹地使用 JavaScript 进行函数式编程。

我现在已经理解了函子,而在我读数学研究生时却未能做到。从理论角度来看,我还没有完全理解应用函子和单子的概念,但尽可能地编写纯函数,并尽量少地使用输入和输出单子的想法似乎是可行的,这比追溯单子的概念起源要容易得多。这属于尽可能地使用函数式编程,而不是你认为必须使用的范畴。

函数式响应式编程的维基百科文章链接到了该领域的九部重要作品,如果你想解决一个良好的数学难题,所有这些作品都值得一搏。数学符号可能像维基百科上的函子文章一样密集。

但是,如果我们看看编程语言,这里有一个线索。文献中提到了几种有趣的可能性,都是函数式的:一种 Scheme 方言,DDD 和 Elm(它是一种独立的语言,编译成自己的 JavaScript / HTML / CSS,与 DDD 相比)。但是,函数式响应式编程作者最感兴趣的似乎远远是 Haskell。这给了我们一个免费的线索,至少在其起源中,Haskell 是几乎所有关于函数式响应式编程的重要论文的重心。任何语言,包括 Haskell,都有缺陷,但简单地忽略函数式响应式编程的重要作品倾向于 Haskell 是愚蠢的。

函数式响应式编程是建立在函数式编程的基础上的响应式编程。在使用 ReactJS 开发 JavaScript 时,一些方面已经为我们处理了。我们只需要声明性地指定 UI 在渲染时应该是什么样子,ReactJS 将处理所有必要的编译,因此声明性的render()方法将被转换为 DOM 上的优化的命令。但至少乍一看,如果你想理解函数式响应式编程,学习一种与 Haskell 紧密相关的技术是有意义的,只有在你穿上 Haskell 的鞋走了一英里之后,然后知道它们是否会让你不舒服,才写下你对 Haskell 的“独立宣言”。

《Learn You a Haskell for Great Good》受到了批评,但这本书被故意选为教授一门一流的函数式语言的优秀教材。指出 Haskell 的教材在允许读者看到传统的“Hello, world!”程序之前,涵盖了八章的理论和一些范畴论概念,这比挑剔一个一般的介绍更有说服力,并且会引起一个明显的反应,但是有更好的例子没有这个问题。一个更专注于实际应用于现实世界信息技术需求的伴随教材是《Real World Haskell》(book.realworldhaskell.org/read/)。这些并不是唯一的教材,但至少提供了一个很好的搭配和一个起点,并经常一起推荐。

更重要的是,不要试图匆匆翻阅这两本书,并期望经过一天甚至一个月的学习,就能比你多年使用的任何喜爱的语言更容易地在 Haskell 中完成工作。相反,玩耍,尝试这些东西。把 Glasgow Haskell Compiler 当作你圣诞节收到的一套漂亮的虚拟乐高。《Learn You a Haskell for Great Good》从未深入探讨如何编写 Web 服务器,这正是该书的优势所在。它建立了只有对你有利的核心优势,并应该让你更有能力欣赏和利用 JavaScript 中函数式编程的机会。G.K.切斯特顿说:

理解一切都是一种负担。诗人只渴望升华和扩张,一个可以展现自己的世界。诗人只希望把头伸进天堂。是逻辑学家试图把天堂放进他的脑袋。而他的脑袋却会崩裂。试着把你的头伸进天堂,而不是立刻把天堂放进你的头脑。如果你现在是一个熟练的程序员,也许是在命令范式中,那么很有可能在学校时,当你探索事物时,你试图用编程把头伸进天堂。你写游戏;你玩耍,获得了以后在专业工作中会用到的基础。如果你想学习 Haskell,不要死记硬背。重新变成一个小孩,玩耍。阅读《Learn You a Haskell for Great Good》,它故意避免了如何在最后期限前快速完成某事,直到你真正掌握了基础才去阅读《Real World Haskell》,请不要把《Real World Haskell》当作“抄近路”的理由,试图在最后期限内发布商业风格的功能。

在他关于《更好的部分》的南美演讲中,道格拉斯·克罗克福德在描述良好的 JavaScript 时,给了越来越强的函数式编程重点。我看过的早期克罗克福德的视频,当时只有《好部分》,甚至没有《更好的部分》的迹象,似乎将 JavaScript 的更好部分与其函数式一面联系在一起。但《更好的部分》更明确地表示,JavaScript 和谐的改进之一是你可以应用尾递归,并使用函数式的流程控制风格,使一些流程控制,如循环几乎或完全不必要。

即使不考虑函数式响应式编程,更好的 JavaScript 似乎越来越意味着函数式 JavaScript。这是一件非常好的事情。正如前面提到的,Scheme 被称为“你永远不会使用的最好的语言”,计算机科学家们一直选择基于计算机科学使用价值的一组通常的函数式语言,这是一个必须离开的小天堂,才能进入专业编程。

JavaScript 改变了这一点,不仅仅是通过使匿名函数成为主流。特别是在与 ReactJS 一起使用时,JavaScript 为主流软件开发提供了享受函数式编程优点的最大机会。只要你明白自己在做什么,你就能越多地以函数式范式编写 JavaScript。

函数式编程,无论是响应式还是其他方式,如果你在学校接触了更数学化的函数式编程,可能会更容易理解。但是可以教会程序员如何在 Haskell 中写出“Hello, world!”,同时将范畴论中最难理解的数学知识放在视线之外,隐藏在引擎盖下。

函数式编程的计算数学基础应该像高级语言中的机器或汇编语言一样:存在于引擎盖下,使语言功能成为可能,但在最小程度上是不透明的抽象,只是工作,无论一个人是否是一个能够在引擎盖下进行调整的机械师。

在某种意义上,对于已经离开学校一段时间的资深程序员来说,需要的是学习函数式编程的“好部分”。在这种情况下,好部分可能因程序员的函数式编程舒适水平而异。标准是“做你能做的,而不是你认为你必须做的”。理解声明式/指示性编程与命令式编程之间的差异,可能有些困难,但并不是太难。尽可能编写纯函数,并隔离必须具有副作用的代码,即使你试图避免它们,这是一种思维转变,但并不是太棘手。

例如,在 Haskell 中学习函子实际上比范畴论要容易一些,即使维基百科页面没有反映出这一点。大多数程序员不应该花太长时间编写一个主要是纯函数,输入和输出由单子的最小隔离处理的第一个 Haskell 程序。但是使用单子等功能要比理解使用纯函数并逐步构建单子的扭曲步骤容易得多。

而且值得重申:如果函数式(反应式)编程适合主流使用,那么从函数到单子的重度数学理论不应该像 C 程序员被迫使用汇编语言或软件生成的机器指令一样被强加给普通的专业开发人员。有人说,“C 是一种将汇编语言的强大与使用汇编语言的便利结合在一起的语言”,但 C 从来没有强迫大多数程序员微观管理编译器如何渲染 C 源代码。

现在,许多主流语言,特别是多范式语言,已经融入了一些函数式编程的优势。尽管如此,有人可能会建议,JavaScript 是所有主流语言中直接提供最佳函数式编程优势的语言。不一定是计算机科学家喜欢的 Haskell 或 Lisp/Scheme 等语言中最好的函数式编程;很难找到一个主流编程工作,管理层会允许使用 Haskell 或 Scheme 的解决方案。但在主流语言中,JavaScript 仍然是最有吸引力的。计算机科学家长期以来一直喜欢函数式编程,至少有一位在学校学习数学的程序员评论说,“函数式编程是我见过的第一个有意义的编程范式。”对于几乎所有计算机科学家都喜欢的卓越性,JavaScript 不仅仅是浏览器执行的语言,尽管这很重要。它还为在雇主的招聘要求中经常遇到的语言提供了最佳的函数式编程机会。

JavaScript 作为新的裸金属

Douglas Crockford 在前面提到的《更好的部分》中试图表明程序员和其他人一样情感用事。他以一种对于库恩学者来说并不奇怪的方式支持了这一观点:软件工程的基本改进是通过坚持早期方法的程序员的消退而取得的。他举了六个或者更多的“需要一代人”的例子:“需要一代人”才意识到高级语言是一个好主意,或者所有编程语言语句中的 F 炸弹,goto 语句,不是一个好主意。尽管 Crockford 举了几个例子,但他的努力似乎并不打算包括所有重要的例子:尽管我并不完全确定日期,但似乎在 20 世纪 60 年代,Smalltalk 意识到引用比指针更好(指针被称为“数据结构的 goto”),到 20 世纪 90 年代,像 Java 这样的主流语言用引用取代了指针,大约需要一代人的时间。

Crockford 对程序员以自我表达的自由来进行裸金属编程或者经常使用 goto 语句来处理流程控制做了一些评论。但说到底,包括匿名函数在主流语言中使用需要两代人的时间,JavaScript 是第一个,软件开发的改进并不是因为现有的程序员接受了更好的方法而生根。

它们生根是因为新程序员接受了更好的方法,而大多数年长、不信服的程序员则逐渐消失。(即使他们可能学会了。但在某种意义上,通过接受新变化来避免过时是一种选择。只是很多人会说,“20 岁时对我来说够好,40 岁时也够好了。”并不是每个人都被铁定的决定所控制:只是在程序员中存在一个“默认设置”,随着时间推移而变得不再合适。)

JavaScript 是网络上的通用语言,即使你不喜欢它,因为它不是你最喜欢的语言(确实,为什么它会像 Perl、Python、Java 或 C++一样呢?),它已经存在并且可能是最重要的语言,而且将在相当长的一段时间内保持这种地位。但在这种情况下,“需要一代人”的可能是意识到在非 JavaScript 语言中进行网络编程是一个好主意。

Alan Perlis 说过,“当一个编程语言的程序需要关注无关紧要的事情时,它就是低级的”,如果要在 JavaScript 中进行良好的编程,就需要避开许多语言的陷阱,而这些原因对于一般检查来说并不明显,JavaScript 需要关注无关紧要的事情:JavaScript 是低级的。

在新的网络开发中,ReactJS 视频中的一个令人鼓舞的迹象不仅是使用了另一种非 JavaScript 语言或语法糖 CoffeeScript,而且它的引入是平稳和随意的,完全没有道歉、辩护或解释。他们使用 CoffeeScript 的事实本身就很重要,而且他们这样做时没有任何防御性的痕迹更加重要。现在 CoffeeScript 可能不是所有可以或应该被编译成 JavaScript 的语言中的全部或最终选择。但看到除了 JavaScript“裸金属”之外的东西是令人鼓舞的。

这并不意味着“裸金属”编程没有用武之地。业余游戏开发者或来自大型公司的程序员,试图从裸金属中挤出最后一丝性能,无论是为独立应用程序和游戏,都会合理地希望挤出用户计算机的最后一丝性能,无论是在应用程序编程的“裸金属”上还是在网络上的 JavaScript 上。但就像通常情况下不会用 C 或汇编语言编写 Web 应用程序一样(即使在 CGI 脚本是交付动态内容的主要手段时也不会这样),对于大多数 Web 编程的用途来说,一部好的智能手机(稍微老一点的 iPhone 5 大约比网络刚刚出现时的顶级计算机快 100 倍)真的足够快,可以运行通过编译其他语言生成的 JavaScript 代码。而且,当人们了解 JavaScript 是如何开发的时,它就更加令人印象深刻。这是一种在 10 天内设计的计算机语言,通常人们不仅会对此表示印象深刻,还会说:“伙计,放松点!如果你继续滥用兴奋剂,你会害死自己!”

使用高级语言是可取的基本原因是程序员们学会忽略这一点,以便在 JavaScript 中完成任何工作。道格拉斯·克罗克福德的《精粹》,以及 JavaScript 既有宝藏又有地雷的想法,以及良好地避开地雷的一部分,已经深深扎根,以至于对于大多数阅读本书的程序员来说,这本书的简要摘要可能是完全多余的。

这本身就是一个严肃的理由,如果有选择的话,要考虑替代 JavaScript“裸金属”编程。事实上,对于前端 Web 开发,有许多替代 JavaScript“裸金属”的选择。tinyurl.com/reactjs-compiled-javascript列出了许多其他语言,包括旨在在某些方面提供增强 JavaScript 的家族和朋友,以及将其他语言编译为 JavaScript 的编译器,包括(通常)Basic、C/C++、C#/F#/.NET、Erlang、Go、Haskell、Java/JVM、Lisp、OCAML、Pascal、PHP、Python、Ruby、Scheme、Smalltalk 和 SQL 的多个选项。显然,并非每个编译器和实现都特别好,但就像其他任何严肃的计算机语言一样,JavaScript 是图灵完备的,不仅在理论上可以将其他完整的语言编译为 JavaScript 以及“裸金属”,而且在实践中也是可能的,并且是有充分意义的。JavaScript 可能会成为最重要的编译目标,甚至超越 x86_64 机器代码。或者也可能不会,但 JavaScript 的可取性和能力意味着编写编译为 JavaScript 的语言的现象——意味着大多数其他语言——在可预见的未来可能会不断增长。

总结

这一章可能是对早期尝试的一种尝试,而早期尝试通常不会成功。网上有许多重要文件,但它们假设你不仅可以编程,还可以处理大多数专业开发人员无法处理的特定类型的数学,也许甚至从未达到过熟练程度。这里的目标不是提供另一个高度数学化的解释,而是产生一份对大多数前端开发人员有用的文件,也许在一个不那么高尚的层面上,自然会考虑命令式解决方案。这里的目标是朝着更加功能化和不那么命令式的编程方向发展,但也要产生一份适合专业程序员实际具有的数学技能水平的文件,而不是某个权威可能希望他们具有的数学技能水平。因此,先决条件并不是假设程序员在被允许编写“Hello, world!”之前必须理解范畴论本身。

在本章中,我们回顾了计算机传说的记忆。这次回顾包括了对严厉的计算机清单、简单的“Hello, world!”程序和范畴论的探讨,以及函数式响应式编程首选语言 Haskell 可能会要求你在写“Hello, world!”时使用范畴论。这是一个重大问题。

我们还研究了函数式响应式编程的特点,包括时间的处理方式。本章还涵盖了对问题“如果你只能从函数式响应式编程中学到一件事,最好学什么?”的认真回答。

本章还涵盖了学习纯函数式开发的内容,而不是你认为必须学习的内容。试图学习太多函数式编程很容易让自己陷入瘫痪,而且老练的命令式程序员学习函数式编程(需要改变看待世界的方式)并不是件容易的事。这是一种尝试,提供了一种理智的方法来从函数式编程中获益,而不至于完全迷失在与函数式编程的无尽挣扎中。

我们还讨论了 Web 开发的未来,JavaScript 被视为新的“裸金属”。

在我们的下一章中,我们将探讨支持函数式响应式编程的工具。让我们开始吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值