JavaScript 权威指南第七版(GPT 重译)(七)(4)

简介: JavaScript 权威指南第七版(GPT 重译)(七)

JavaScript 权威指南第七版(GPT 重译)(七)(3)https://developer.aliyun.com/article/1485475

17.6 使用 Babel 进行转译

Babel 是一个工具,将使用现代语言特性编写的 JavaScript 编译成不使用这些现代语言特性的 JavaScript。由于它将 JavaScript 编译成 JavaScript,因此有时称为“转译器”。Babel 的创建是为了让 web 开发人员能够使用 ES6 及更高版本的新语言特性,同时仍针对只支持 ES5 的 web 浏览器。

诸如**指数运算符和箭头函数之类的语言特性可以相对容易地转换为Math.pow()function表达式。其他语言特性,如class关键字,需要进行更复杂的转换,而且一般来说,Babel 输出的代码并不是为了人类可读性。然而,像打包工具一样,Babel 可以生成源映射,将转换后的代码位置映射回原始源位置,这在处理转换后的代码时非常有帮助。

浏览器供应商正在更好地跟上 JavaScript 语言的演变,今天几乎不需要编译掉箭头函数和类声明。当你想要使用最新功能,如数字文字中的下划线分隔符时,Babel 仍然可以帮助。

像本章描述的大多数其他工具一样,你可以使用 npm 安装 Babel,并使用 npx 运行它。Babel 读取一个 .babelrc 配置文件,告诉它如何转换你的 JavaScript 代码。Babel 定义了“预设”,你可以根据想要使用的语言扩展和你想要多么积极地转换标准语言特性来选择。Babel 的一个有趣的预设是用于通过缩小来进行代码压缩(去除注释和空格,重命名变量等)。

如果你使用 Babel 和一个代码捆绑工具,你可以设置代码捆绑器在构建捆绑包时自动运行 Babel 来处理你的 JavaScript 文件。如果是这样,这可能是一个方便的选项,因为它简化了生成可运行代码的过程。例如,Webpack 支持一个“babel-loader”模块,你可以安装并配置它在捆绑时运行 Babel 来处理每个 JavaScript 模块。

即使今天对核心 JavaScript 语言的转换需求较少,Babel 仍然常用于支持语言的非标准扩展,我们将在接下来的章节中描述其中的两个语言扩展。

17.7 JSX:JavaScript 中的标记表达式

JSX 是核心 JavaScript 的扩展,使用类似 HTML 的语法来定义元素树。JSX 与 React 框架最为密切相关,用于 Web 上的用户界面。在 React 中,使用 JSX 定义的元素树最终会被渲染成 HTML 在 Web 浏览器中。即使你没有计划自己使用 React,但由于其流行,你可能会看到使用 JSX 的代码。本节将解释你需要了解的内容以理解它。(本节关于 JSX 语言扩展,不是关于 React,仅解释 React 的部分内容以提供 JSX 语法的上下文。)

你可以将 JSX 元素视为一种新类型的 JavaScript 表达式语法。JavaScript 字符串字面量用引号界定,正则表达式字面量用斜杠界定。同样,JSX 表达式字面量用尖括号界定。这是一个非常简单的例子:

let line = <hr/>;

如果你使用 JSX,你将需要使用 Babel(或类似工具)将 JSX 表达式编译成常规 JavaScript。转换足够简单,以至于一些开发人员选择在不使用 JSX 的情况下使用 React。Babel 将此赋值语句中的 JSX 表达式转换为简单的函数调用:

let line = React.createElement("hr", null);

JSX 语法类似 HTML,并且像 HTML 元素一样,React 元素可以具有以下属性:

let image = <img src="logo.png" alt="The JSX logo" hidden/>;

当一个元素有一个或多个属性时,它们成为传递给createElement()的第二个参数的对象的属性:

let image = React.createElement("img", {
              src: "logo.png",
              alt: "The JSX logo",
              hidden: true
            });

像 HTML 元素一样,JSX 元素可以具有字符串和其他元素作为子元素。就像 JavaScript 的算术运算符可以用于编写任意复杂度的算术表达式一样,JSX 元素也可以任意深度地嵌套以创建元素树:

let sidebar = (
  <div className="sidebar">
    <h1>Title</h1>
    <hr/>
    <p>This is the sidebar content</p>
  </div>
);

常规 JavaScript 函数调用表达式也可以任意深度地嵌套,这些嵌套的 JSX 表达式转换为一组嵌套的createElement()调用。当一个 JSX 元素有子元素时,这些子元素(通常是字符串和其他 JSX 元素)作为第三个及后续参数传递:

let sidebar = React.createElement(
    "div", { className: "sidebar"},  // This outer call creates a <div>
    React.createElement("h1", null,  // This is the first child of the <div/>
                        "Title"),    // and its own first child.
    React.createElement("hr", null), // The second child of the <div/>.
    React.createElement("p", null,   // And the third child.
                        "This is the sidebar content"));

React.createElement()返回的值是 React 用于在浏览器窗口中呈现输出的普通 JavaScript 对象。由于本节是关于 JSX 语法而不是关于 React,我们不会详细介绍返回的元素对象或呈现过程。值得注意的是,你可以配置 Babel 将 JSX 元素编译为调用不同函数的调用,因此如果你认为 JSX 语法是表达其他类型嵌套数据结构的有用方式,你可以将其用于自己的非 React 用途。

JSX 语法的一个重要特点是你可以在 JSX 表达式中嵌入常规 JavaScript 表达式。在 JSX 表达式中,花括号内的文本被解释为普通 JavaScript。这些嵌套表达式允许作为属性值和子元素。例如:

function sidebar(className, title, content, drawLine=true) {
  return (
    <div className={className}>
      <h1>{title}</h1>
      { drawLine && <hr/> }
      <p>{content}</p>
    </div>
  );
}

sidebar()函数返回一个 JSX 元素。它接受四个参数,这些参数在 JSX 元素中使用。花括号语法可能会让你想起使用${}在字符串中包含 JavaScript 表达式的模板字面量。由于我们知道 JSX 表达式编译为函数调用,因此包含任意 JavaScript 表达式并不奇怪,因为函数调用也可以用任意表达式编写。Babel 将此示例代码转换为以下内容:

function sidebar(className, title, content, drawLine=true) {
  return React.createElement("div", { className: className },
                             React.createElement("h1", null, title),
                             drawLine && React.createElement("hr", null),
                             React.createElement("p", null, content));
}

这段代码易于阅读和理解:花括号消失了,生成的代码以自然的方式将传入的函数参数传递给React.createElement()。请注意我们在这里使用drawLine参数和短路&&运算符的巧妙技巧。如果你只用三个参数调用sidebar(),那么drawLine默认为true,并且外部createElement()调用的第四个参数是


元素。但如果将false作为第四个参数传递给sidebar(),那么外部createElement()调用的第四个参数将计算为false,并且永远不会创建


元素。这种使用 &&运算符的习惯用法在 JSX 中是一种常见的习语,根据某些其他表达式的值有条件地包含或排除子元素。(这种习惯用法在 React 中有效,因为 React 简单地忽略 falsenull的子元素,并且不为它们生成任何输出。)

当你在 JSX 表达式中使用 JavaScript 表达式时,你不仅限于前面示例中的字符串和布尔值等简单值。任何 JavaScript 值都是允许的。事实上,在 React 编程中使用对象、数组和函数是非常常见的。例如,考虑以下函数:

// Given an array of strings and a callback function return a JSX element
// representing an HTML <ul> list with an array of <li> elements as its child.
function list(items, callback) {
  return (
    <ul style={ {padding:10, border:"solid red 4px"} }>
      {items.map((item,index) => {
        <li onClick={() => callback(index)} key={index}>{item}</li>
      })}
    </ul>
  );
}

此函数将对象字面量用作

    元素上 style属性的值。(请注意,这里需要双大括号。)
      元素只有一个子元素,但该子元素的值是一个数组。子数组是通过在输入数组上使用 map()函数创建
    • 元素数组而创建的数组。(这在 React 中有效,因为 React 库在渲染时会展平元素的子元素。具有一个数组子元素的元素与该元素的每个数组元素作为子元素相同。)最后,请注意每个嵌套的
    • 元素都有一个onClick事件处理程序属性,其值是一个箭头函数。JSX 代码编译为以下纯 JavaScript 代码(我已使用 Prettier 格式化):
function list(items, callback) {
  return React.createElement(
    "ul",
    { style: { padding: 10, border: "solid red 4px" } },
    items.map((item, index) =>
      React.createElement(
        "li",
        { onClick: () => callback(index), key: index },
        item
      )
    )
  );
}

JSX 中对象表达式的另一个用途是使用对象扩展运算符(§6.10.4)一次指定多个属性。假设你发现自己编写了许多重复一组常见属性的 JSX 表达式。你可以通过将属性定义为对象的属性并将它们“扩展到”你的 JSX 元素中来简化表达式:

let hebrew = { lang: "he", dir: "rtl" }; // Specify language and direction
let shalom = <span className="emphasis" {...hebrew}>שלום</span>;

Babel 将其编译为使用_extends()函数(此处省略)将className属性与hebrew对象中包含的属性组合在一起:

let shalom = React.createElement("span",
                                 _extends({className: "emphasis"}, hebrew),
                                 "\u05E9\u05DC\u05D5\u05DD");

最后,还有一个 JSX 的重要特性我们还没有涉及。正如你所见,所有 JSX 元素在开角括号后立即以标识符开头。如果此标识符的第一个字母是小写(就像在这里的所有示例中一样),那么该标识符将作为字符串传递给createElement()。但如果标识符的第一个字母是大写,则将其视为实际标识符,并将该标识符的 JavaScript 值作为createElement()的第一个参数传递。这意味着 JSX 表达式编译为将全局 Math 对象传递给React.createElement()的 JavaScript 代码。

对于 React 来说,将非字符串值作为createElement()的第一个参数传递的能力使得创建组件成为可能。组件是一种编写简单 JSX 表达式(使用大写组件名称)来表示更复杂表达式(使用小写 HTML 标签名称)的方式。

在 React 中定义一个新组件的最简单方法是编写一个以“props 对象”作为参数的函数,并返回一个 JSX 表达式。props 对象只是一个表示属性值的 JavaScript 对象,就像作为createElement()的第二个参数传递的对象一样。例如,这里是我们sidebar()函数的另一种写法:

function Sidebar(props) {
  return (
    <div>
      <h1>{props.title}</h1>
      { props.drawLine && <hr/> }
      <p>{props.content}</p>
    </div>
  );
}

这个新的Sidebar()函数与之前的sidebar()函数非常相似。但这个函数以大写字母开头的名称,并接受一个对象参数而不是单独的参数。这使它成为一个 React 组件,并意味着它可以在 JSX 表达式中替代 HTML 标签名称使用:

let sidebar = <Sidebar title="Something snappy" content="Something wise"/>;

这个元素编译如下:

let sidebar = React.createElement(Sidebar, {
  title: "Something snappy",
  content: "Something wise"
});

这是一个简单的 JSX 表达式,但当 React 渲染它时,它会将第二个参数(Props 对象)传递给第一个参数(Sidebar()函数),并将该函数返回的 JSX 表达式替换为表达式的位置。

17.8 使用 Flow 进行类型检查

Flow是一种语言扩展,允许您在 JavaScript 代码中添加类型信息,并用于检查您的 JavaScript 代码(包括带注释和不带注释的代码)中的类型错误。要使用 Flow,您开始使用 Flow 语言扩展编写代码以添加类型注解。然后运行 Flow 工具分析您的代码并报告类型错误。一旦您修复了错误并准备运行代码,您可以使用 Babel(可能作为代码捆绑过程的一部分自动执行)来剥离代码中的 Flow 类型注解。(Flow 语言扩展的一个好处是,Flow 没有必须编译或转换的新语法。您使用 Flow 语言扩展向代码添加注解,而 Babel 只需剥离这些注解以将您的代码返回到标准 JavaScript。)

使用 Flow 需要承诺,但我发现对于中大型项目来说,额外的努力是值得的。为代码添加类型注解,每次编辑代码时运行 Flow,以及修复它报告的类型错误都需要额外的时间。但作为回报,Flow 将强制执行良好的编码纪律,并不允许你采取可能导致错误的捷径。当我在使用 Flow 的项目上工作时,我对它在我的代码中发现的错误数量感到印象深刻。在这些问题变成错误之前修复这些问题是一种很棒的感觉,并让我对我的代码正确性更有信心。

当我第一次开始使用 Flow 时,我发现有时很难理解它为什么会抱怨我的代码。然而,通过一些实践,我开始理解它的错误消息,并发现通常很容易对我的代码进行微小更改,使其更安全并满足 Flow 的要求。¹ 如果你仍然觉得自己在学习 JavaScript 本身,我不建议使用 Flow。但一旦你对这门语言有信心,将 Flow 添加到你的 JavaScript 项目中将推动你将编程技能提升到下一个水平。这也是为什么我将这本书的最后一节专门用于 Flow 教程的原因:因为了解 JavaScript 类型系统提供了另一种编程水平或风格的一瞥。

本节是一个教程,不打算全面涵盖 Flow。如果您决定尝试 Flow,几乎肯定会花时间阅读https://flow.org上的文档。另一方面,您不需要在掌握 Flow 类型系统之前就能开始在项目中实际使用它:这里描述的 Flow 的简单用法将带您走很远。

17.8.1 安装和运行 Flow

像本章中描述的其他工具一样,您可以使用包管理器安装 Flow 类型检查工具,使用类似npm install -g flow-binnpm install --save-dev flow-bin的命令。如果使用-g全局安装工具,那么可以使用flow运行它。如果在项目中使用--save-dev本地安装它,那么可以使用npx flow运行它。在使用 Flow 进行类型检查之前,首次在项目的根目录中运行flow --init以创建.flowconfig配置文件。您可能永远不需要向此文件添加任何内容,但 Flow 需要知道您的项目根目录在哪里。

运行 Flow 时,它会找到项目中的所有 JavaScript 源代码,但只会为已通过在文件顶部添加// @flow注释而“选择加入”类型检查的文件报告类型错误。这种选择加入的行为很重要,因为这意味着您可以为现有项目采用 Flow,然后逐个文件地开始转换代码,而不会受到尚未转换的文件上的错误和警告的困扰。

即使您只是通过// @flow注释选择加入,Flow 也可能能够找到代码中的错误。即使您不使用 Flow 语言扩展并且不向代码添加任何类型注释,Flow 类型检查工具仍然可以推断程序中的值,并在您不一致地使用它们时提醒您。

考虑以下 Flow 错误消息:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ variableReassignment.js:6:3
Cannot assign 1 to i.r because:
 • property r is missing in number [1].
     2│ let i = { r: 0, i: 1 };    // The complex number 0+1i
 [1] 3│ for(i = 0; i < 10; i++) {  // Oops! The loop variable overwrites i
     4│     console.log(i);
     5│ }
     6│ i.r = 1;                   // Flow detects the error here

在这种情况下,我们声明变量i并将一个对象赋给它。然后我们再次使用i作为循环变量,覆盖了对象。Flow 注意到这一点,并在我们尝试像仍然保存对象一样使用i时标记错误。(一个简单的修复方法是写for(let i = 0;使循环变量局部于循环。)

这是 Flow 即使没有类型注释也能检测到的另一个错误:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ size.js:3:14
Cannot get x.length because property length is missing in Number [1].
     1│ // @flow
     2│ function size(x) {
     3│     return x.length;
     4│ }
 [1] 5│ let s = size(1000);

Flow 看到size()函数接受一个参数。它不知道该参数的类型,但可以看到该参数应具有length属性。当看到使用数字参数调用此size()函数时,它会正确地标记此为错误,因为数字没有length属性。

17.8.2 使用类型注释

当声明 JavaScript 变量时,可以在变量名称后面加上冒号和类型来添加 Flow 类型注释:

let message: string = "Hello world";
let flag: boolean = false;
let n: number = 42;

即使您没有为这些变量添加注释,Flow 也会知道这些变量的类型:它可以看到您为每个变量分配的值,并跟踪它们。但是,如果添加了类型注释,Flow 既知道变量的类型,又知道您已表达了该变量应始终为该类型的意图。因此,如果使用类型注释,如果将不同类型的值分配给该变量,Flow 将标记错误。对于变量,类型注释也特别有用,如果您倾向于在函数使用之前在函数顶部声明所有变量。

函数参数的类型注释与变量的注释类似:在函数参数名称后面跟着冒号和类型名称。在注释函数时,通常还会为函数的返回类型添加注释。这在函数体的右括号和左花括号之间。返回空值的函数使用 Flow 类型void

在前面的示例中,我们定义了一个期望具有length属性的参数的size()函数。下面是如何将该函数更改为明确指定它期望一个字符串参数并返回一个数字。请注意,即使在这种情况下函数可以正常工作,Flow 现在也会标记错误,如果我们将数组传递给函数:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ size2.js:5:18
Cannot call size with array literal bound to s because array literal [1]
is incompatible with string [2].
 [2] 2│ function size(s: string): number {
     3│     return s.length;
     4│ }
 [1] 5│ console.log(size([1,2,3]));

使用箭头函数的类型注解也是可能的,尽管这可能会将这个通常简洁的语法变得更冗长:

const size = (s: string): number => s.length;

关于 Flow 的一个重要事项是,JavaScript 值null具有 Flow 类型null,JavaScript 值undefined具有 Flow 类型void。但这两个值都不是任何其他类型的成员(除非你明确添加它)。如果你声明一个函数参数为字符串,那么它必须是一个字符串,传递null、传递undefined或省略参数(基本上与传递undefined相同)都是错误的:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ size3.js:3:18
Cannot call size with null bound to s because null [1] is incompatible
with string [2].
     1│ // @flow
 [2] 2│ const size = (s: string): number => s.length;
 [1] 3│ console.log(size(null));

如果你想允许nullundefined作为变量或函数参数的合法值,只需在类型前加上问号。例如,使用?string?number代替stringnumber。如果我们将size()函数更改为期望类型为?string的参数,那么当我们将null传递给函数时,Flow 不会抱怨。但现在它有其他事情要抱怨:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ size4.js:3:14
Cannot get s.length because property length is missing in null or
undefined [1].
     1│ // @flow
 [1] 2│ function size(s: ?string): number {
     3│     return s.length;
     4│ }
     5│ console.log(size(null));

Flow 在这里告诉我们的是,在我们的代码中,写s.length是不安全的,因为此处的s可能是nullundefined,而这些值没有length属性。这就是 Flow 确保我们不会偷懒的地方。如果一个值可能是null,Flow 会坚持要求我们在执行任何依赖于该值不是null的操作之前检查该情况。

在这种情况下,我们可以通过更改函数主体来解决问题如下:

function size(s: ?string): number {
    // At this point in the code, s could be a string or null or undefined.
    if (s === null || s === undefined) {
        // In this block, Flow knows that s is null or undefined.
        return -1;
    } else {
        // And in this block, Flow knows that s is a string.
        return s.length;
    }
}

当函数首次调用时,参数可以有多种类型。但通过添加类型检查代码,我们在代码中创建了一个块,Flow 可以确定参数是一个字符串。当我们在该块内使用s.length时,Flow 不会抱怨。请注意,Flow 不要求你编写冗长的代码。如果我们只是用return s ? s.length : -1;替换size()函数的主体,Flow 也会满意。

Flow 语法允许在任何类型规范之前加上问号,以指示除了指定的类型外,nullundefined也是允许的。问号也可以出现在参数名后,以指示参数本身是可选的。因此,如果我们将参数s的声明从s: ?string更改为s? : string,那意味着可以用没有参数调用size()(或值为undefined,这与省略它相同),但如果我们用除undefined之外的参数调用它,那么该参数必须是一个字符串。在这种情况下,null不是合法值。

到目前为止,我们已经讨论了原始类型stringnumberbooleannullvoid,并演示了如何在变量声明、函数参数和函数返回值中使用它们。接下来的小节描述了 Flow 支持的一些更复杂的类型。

17.8.3 类型类

除了 Flow 了解的原始类型外,它还了解所有 JavaScript 的内置类,并允许你使用类名作为类型。例如,以下函数使用类型注解指示应使用一个 Date 对象和一个 RegExp 对象调用它:

// @flow
// Return true if the ISO representation of the specified date
// matches the specified pattern, or false otherwise.
// E.g: const isTodayChristmas = dateMatches(new Date(), /^\d{4}-12-25T/);
export function dateMatches(d: Date, p: RegExp): boolean {
    return p.test(d.toISOString());
}

如果你使用class关键字定义自己的类,那些类会自动成为有效的 Flow 类型。然而,为了使其工作,Flow 确实要求你在类中使用类型注解。特别是,类的每个属性必须声明其类型。这里是一个简单的复数类示例,演示了这一点:

// @flow
export default class Complex {
    // Flow requires an extended class syntax that includes type annotations
    // for each of the properties used by the class.
    i: number;
    r: number;
    static i: Complex;
    constructor(r: number, i:number) {
        // Any properties initialized by the constructor must have Flow type
        // annotations above.
        this.r = r;
        this.i = i;
    }
    add(that: Complex) {
        return new Complex(this.r + that.r, this.i + that.i);
    }
}
// This assignment would not be allowed by Flow if there was not a
// type annotation for i inside the class.
Complex.i = new Complex(0,1);

17.8.4 对象类型

描述对象的 Flow 类型看起来很像对象字面量,只是属性值被属性类型替换。例如,这里是一个期望具有数字 xy 属性的对象的函数:

// @flow
// Given an object with numeric x and y properties, return the
// distance from the origin to the point (x,y) as a number.
export default function distance(point: {x:number, y:number}): number {
    return Math.hypot(point.x, point.y);
}

在这段代码中,文本 {x:number, y:number} 是一个 Flow 类型,就像 stringDate 一样。与任何类型一样,你可以在前面加上问号来表示 nullundefined 也应该被允许。

在对象类型中,你可以在任何属性名称后面加上问号,表示该属性是可选的,可以省略。例如,你可以这样写一个表示 2D 或 3D 点的对象类型:

{x: number, y: number, z?: number}

如果在对象类型中未标记属性为可选,则该属性是必需的,如果实际值中缺少适当的属性,Flow 将报告错误。然而,通常情况下,Flow 容忍额外的属性。如果你向上面的 distance() 函数传递一个具有 w 属性的对象,Flow 不会抱怨。

如果你希望 Flow 严格执行对象除了在其类型中明确声明的属性之外没有其他属性,你可以通过在花括号中添加竖线来声明精确对象类型

{| x: number, y: number |}

JavaScript 的对象有时被用作字典或字符串值映射。当以这种方式使用对象时,属性名称事先不知道,也不能在 Flow 类型中声明。如果你以这种方式使用对象,你仍然可以使用 Flow 来描述数据结构。假设你有一个对象,其中属性是世界主要城市的名称,这些属性的值是指定这些城市地理位置的对象。你可以这样声明这个数据结构:

// @flow
const cityLocations : {[string]: {longitude:number, latitude:number}} = {
    "Seattle": { longitude: 47.6062, latitude: -122.3321 },
    // TODO: if there are any other important cities, add them here.
};
export default cityLocations;

17.8.5 类型别名

对象可以有许多属性,描述这样一个对象的 Flow 类型将会很长且难以输入。即使相对较短的对象类型也可能令人困惑,因为它们看起来非常像对象字面量。一旦我们超越了像 number?string 这样的简单类型,为我们的 Flow 类型定义名称通常是有用的。事实上,Flow 使用 type 关键字来做到这一点。在 type 关键字后面跟上标识符、等号和 Flow 类型。一旦你这样做了,该标识符将成为该类型的别名。例如,这里是我们如何使用显式定义的 Point 类型重写上一节中的 distance() 函数:

// @flow
export type Point = {
    x: number,
    y: number
};
// Given a Point object return its distance from the origin
export default function distance(point: Point): number {
    return Math.hypot(point.x, point.y);
}

请注意,此代码导出了 distance() 函数,并且还导出了 Point 类型。其他模块可以使用 import type Point from './distance.js' 如果他们想使用该类型定义。但请记住,import type 是一个 Flow 语言扩展,而不是真正的 JavaScript 导入指令。类型导入和导出被 Flow 类型检查器使用,但像所有其他 Flow 语言扩展一样,在代码运行之前它们都会被剥离。

最后,值得注意的是,与其定义一个代表点的 Flow 对象类型的名称,可能更简单和更清晰的是只定义一个 Point 类并将该类用作类型。

17.8.6 数组类型

描述数组的 Flow 类型是一个复合类型,还包括数组元素的类型。例如,这里是一个期望数字数组的函数,以及如果尝试使用具有非数字元素的数组调用该函数时 Flow 报告的错误:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ average.js:8:16
Cannot call average with array literal bound to data because string [1]
is incompatible with number [2] in array element.
 [2]  2│ function average(data: Array<number>) {
      3│     let sum = 0;
      4│     for(let x of data) sum += x;
      5│     return sum/data.length;
      6│ }
      7│
 [1]  8│ average([1, 2, "three"]);

表示数组的 Flow 类型是 Array,后面跟着尖括号中的元素类型。你也可以通过在元素类型后面加上开放和关闭方括号来表示数组类型。因此,在这个例子中,我们可以写成 number[] 而不是 Array。我更喜欢尖括号表示法,因为,正如我们将看到的,还有其他使用这种尖括号语法的 Flow 类型。

所示的 Array 类型语法适用于具有任意数量元素的数组,所有元素都具有相同的类型。Flow 有一种不同的语法来描述元组的类型:一个具有固定数量元素的数组,每个元素可能具有不同的类型。要表示元组的类型,只需写出每个元素的类型,用逗号分隔,然后将它们都括在方括号中。

例如,一个返回 HTTP 状态码和消息的函数可能如下所示:

function getStatus():[number, string] {
    return [getStatusCode(), getStatusMessage()];
}

返回元组的函数在不使用解构赋值的情况下很难处理:

let [code, message] = getStatus();

解构赋值,再加上 Flow 的类型别名功能,使得元组易于处理,以至于你可能会考虑它们作为简单数据类型的替代方案:

// @flow
export type Color = [number, number, number, number];  // [r, g, b, opacity]
function gray(level: number): Color {
    return [level, level, level, 1];
}
function fade([r,g,b,a]: Color, factor: number): Color {
    return [r, g, b, a/factor];
}
let [r, g, b, a] = fade(gray(75), 3);

现在我们有了一种表达数组类型的方法,让我们回到之前的size()函数,并修改它以接受一个数组参数而不是一个字符串参数。我们希望函数能够接受任意长度的数组,因此元组类型不合适。但我们也不希望将函数限制为仅适用于所有元素类型相同的数组。解决方案是类型Array

// @flow
function size(s: Array<mixed>): number {
    return s.length;
}
console.log(size([1,true,"three"]));

元素类型mixed表示数组的元素可以是任何类型。如果我们的函数实际上对数组进行索引并尝试使用其中的任何元素,Flow 将坚持要求我们使用typeof检查或其他测试来确定元素的类型,然后再执行任何不安全的操作。(如果你愿意放弃类型检查,也可以使用any代替mixed:它允许你对数组的值做任何想做的事情,而不必确保这些值是你期望的类型。)

17.8.7 其他参数化类型

我们已经看到,当您将一个值注释为Array时,Flow 要求您还必须在尖括号内指定数组元素的类型。这个额外的类型被称为类型参数,而 Array 并不是唯一一个被参数化的 JavaScript 类。

JavaScript 的 Set 类是一个元素集合,就像数组一样,你不能单独使用Set作为一种类型,而是必须在尖括号内包含一个类型参数来指定集合中包含的值的类型。(尽管如果集合可能包含多种类型的值,你可以使用mixedany。)以下是一个示例:

// @flow
// Return a set of numbers with members that are exactly twice those
// of the input set of numbers.
function double(s: Set<number>): Set<number> {
    let doubled: Set<number> = new Set();
    for(let n of s) doubled.add(n * 2);
    return doubled;
}
console.log(double(new Set([1,2,3])));  // Prints "Set {2, 4, 6}"

Map 是另一种参数化类型。在这种情况下,必须指定两个类型参数;键的类型和值的类型:

// @flow
import type { Color } from "./Color.js";
let colorNames: Map<string, Color> = new Map([
    ["red", [1, 0, 0, 1]],
    ["green", [0, 1, 0, 1]],
    ["blue", [0, 0, 1, 1]]
]);

Flow 还允许您为自己的类定义类型参数。以下代码定义了一个 Result 类,但使用一个 Error 类型和一个 Value 类型对该类进行参数化。我们在代码中使用占位符EV来表示这些类型参数。当这个类的用户声明一个 Result 类型的变量时,他们将指定实际类型来替换EV。变量声明可能如下所示:

let result: Result<TypeError, Set<string>>;

下面是参数化类的定义方式:

// @flow
// This class represents the result of an operation that can either
// throw an error of type E or a value of type V.
export class Result<E, V> {
    error: ?E;
    value: ?V;
    constructor(error: ?E, value: ?V) {
        this.error = error;
        this.value = value;
    }
    threw(): ?E { return this.error; }
    returned(): ?V { return this.value; }
    get():V {
        if (this.error) {
            throw this.error;
        } else if (this.value === null || this.value === undefined) {
            throw new TypeError("Error and value must not both be null");
        } else {
            return this.value;
        }
    }
}

甚至可以为函数定义类型参数:

// @flow
// Combine the elements of two arrays into an array of pairs
function zip<A,B>(a:Array<A>, b:Array<B>): Array<[?A,?B]> {
    let result:Array<[?A,?B]> = [];
    let len = Math.max(a.length, b.length);
    for(let i = 0; i < len; i++) {
        result.push([a[i], b[i]]);
    }
    return result;
}
// Create the array [[1,'a'], [2,'b'], [3,'c'], [4,undefined]]
let pairs: Array<[?number,?string]> = zip([1,2,3,4], ['a','b','c'])

17.8.8 只读类型

Flow 定义了一些特殊的参数化“实用类型”,其名称以$开头。这些类型中的大多数都有我们这里不打算涵盖的高级用例。但其中两个在实践中非常有用。如果你有一个对象类型 T,并想要创建该类型的只读版本,只需编写$ReadOnly。类似地,您可以编写$ReadOnlyArray来描述一个具有类型 T 的只读数组。

使用这些类型的原因不是因为它们可以提供任何对象或数组不能被修改的保证(如果你想要真正的只读对象,请参见 §14.2 中的 Object.freeze()),而是因为它可以帮助你捕捉由无意修改引起的错误。如果你编写一个接受对象或数组参数并且不改变对象的任何属性或数组的元素的函数,那么你可以用 Flow 的只读类型注释函数参数。如果你这样做,那么如果你忘记并意外修改输入值,Flow 将报告错误。以下是两个示例:

// @flow
type Point = {x:number, y:number};
// This function takes a Point object but promises not to modify it
function distance(p: $ReadOnly<Point>): number {
    return Math.hypot(p.x, p.y);
}
let p: Point = {x:3, y:4};
distance(p)  // => 5
// This function takes an array of numbers that it will not modify
function average(data: $ReadOnlyArray<number>): number {
    let sum = 0;
    for(let i = 0; i < data.length; i++) sum += data[i];
    return sum/data.length;
}
let data: Array<number> = [1,2,3,4,5];
average(data) // => 3

17.8.9 函数类型

我们已经看到如何添加类型注释来指定函数参数和返回类型的类型。但是当函数的一个参数本身是一个函数时,我们需要能够指定该函数参数的类型。

要使用 Flow 表达函数的类型,需要写出每个参数的类型,用逗号分隔,将它们括在括号中,然后跟上一个箭头和函数的返回类型。

这里是一个期望传递回调函数的示例函数。请注意我们为回调函数的类型定义了一个类型别名:

// @flow
// The type of the callback function used in fetchText() below
export type FetchTextCallback = (?Error, ?number, ?string) => void;
export default function fetchText(url: string, callback: FetchTextCallback) {
    let status = null;
    fetch(url)
        .then(response => {
            status = response.status;
            return response.text()
        })
        .then(body => {
            callback(null, status, body);
        })
        .catch(error => {
            callback(error, status, null);
        });
}

17.8.10 Union 类型

让我们再次回到 size() 函数。创建一个什么都不做,只返回数组长度的函数并没有太多意义。数组有一个完全好用的 length 属性。但如果 size() 能够接受任何类型的集合对象(数组或 Set 或 Map)并返回集合中元素的数量,那么它可能会有用。在常规的未类型化 JavaScript 中,编写一个这样的 size() 函数很容易。但是在 Flow 中,我们需要一种方式来表达一个允许数组、Set 和 Map 的类型,但不允许任何其他类型值。

Flow 将这种类型称为 Union 类型,并允许你通过简单列出所需类型并用竖线字符分隔它们来表达它们:

// @flow
function size(collection: Array<mixed>|Set<mixed>|Map<mixed,mixed>): number {
    if (Array.isArray(collection)) {
        return collection.length;
    } else {
        return collection.size;
    }
}
size([1,true,"three"]) + size(new Set([true,false])) // => 5

Union 类型可以用“或”这个词来阅读——“一个数组或一个 Set 或一个 Map”——因此,这种 Flow 语法使用与 JavaScript 的 OR 运算符相同的竖线字符是有意的。

我们之前看到在类型前面加一个问号允许 nullundefined 值。现在你可以看到,? 前缀只是一个为类型添加 |null|void 后缀的快捷方式。

一般来说,当你用 Union 类型注释一个值时,Flow 不会允许你使用该值,直到你进行足够的测试以确定实际值的类型。在我们刚刚看过的 size() 示例中,我们需要明确检查参数是否为数组,然后再尝试访问参数的 length 属性。请注意,我们不必区分 Set 参数和 Map 参数,然而:这两个类都定义了 size 属性,因此只要参数不是数组,else 子句中的代码就是安全的。

17.8.11 枚举类型和辨别联合

Flow 允许你使用原始字面量作为只包含一个单一值的类型。如果你写 let x:3;,那么 Flow 将不允许你给该变量赋值除了 3 之外的任何值。定义只有一个成员的类型通常不太有用,但字面量类型的联合可能会有用。你可能可以想象出这些类型的用途,例如:

type Answer = "yes" | "no";
type Digit = 0|1|2|3|4|5|6|7|8|9;

如果你使用由字面量组成的类型,你需要理解只有字面值是允许的:

let a: Answer = "Yes".toLowerCase(); // Error: can't assign string to Answer
let d: Digit = 3+4;                  // Error: can't assign number to Digit

当 Flow 检查你的类型时,它实际上并不执行计算:它只检查计算的类型。Flow 知道 toLowerCase() 返回一个字符串,+ 运算符在数字上返回一个数字。尽管我们知道这两个计算返回的值都在类型内,但 Flow 无法知道这一点,并在这两行上标记错误。

AnswerDigit这样的字面类型的联合类型是枚举类型的一个例子。枚举类型的一个典型用例是表示扑克牌的花色:

type Suit = "Clubs" | "Diamonds" | "Hearts" | "Spades";

更相关的例子可能是 HTTP 状态码:

type HTTPStatus =
    | 200    // OK
    | 304    // Not Modified
    | 403    // Forbidden
    | 404;   // Not Found

新手程序员经常听到的建议之一是避免在代码中使用字面量,而是定义符号常量来代表这些值。这样做的一个实际原因是避免拼写错误的问题:如果你拼错了一个字符串字面量,比如“Diamonds”,JavaScript 可能不会抱怨,但你的代码可能无法正常工作。另一方面,如果你拼错了一个标识符,JavaScript 很可能会抛出一个你会注意到的错误。然而,在使用 Flow 时,这个建议并不总是适用。如果你用类型 Suit 注释一个变量,然后尝试将一个拼写错误的 suit 赋给它,Flow 会提醒你错误。

字面类型的另一个重要用途是创建辨别联合体。当你使用联合类型(由实际不同类型组成,而不是字面量)时,通常需要编写代码来区分可能的类型。在前一节中,我们编写了一个函数,它可以接受一个数组、一个 Set 或一个 Map 作为其参数,并且必须编写代码来区分数组输入和 Set 或 Map 输入。如果你想创建一个对象类型的联合体,可以通过在每个单独的对象类型中使用字面类型来使这些类型易于区分。

举个例子来说明。假设你正在 Node 中使用工作线程(§16.11),并且正在使用postMessage()和“message”事件在主线程和工作线程之间发送基于对象的消息。工作线程可能想要向主线程发送多种类型的消息,但我们希望编写一个描述所有可能消息的 Flow 联合类型。考虑以下代码:

// @flow
// The worker sends a message of this type when it is done
// reticulating the splines we sent it.
export type ResultMessage = {
    messageType: "result",
    result: Array<ReticulatedSpline>, // Assume this type is defined elsewhere.
};
// The worker sends a message of this type if its code failed with an exception.
export type ErrorMessage = {
    messageType: "error",
    error: Error,
};
// The worker sends a message of this type to report usage statistics.
export type StatisticsMessage = {
    messageType: "stats",
    splinesReticulated: number,
    splinesPerSecond: number
};
// When we receive a message from the worker it will be a WorkerMessage.
export type WorkerMessage = ResultMessage | ErrorMessage | StatisticsMessage;
// The main thread will have an event handler function that is passed
// a WorkerMessage. But because we've carefully defined each of the
// message types to have a messageType property with a literal type,
// the event handler can easily discriminate among the possible messages:
function handleMessageFromReticulator(message: WorkerMessage) {
    if (message.messageType === "result") {
        // Only ResultMessage has a messageType property with this value
        // so Flow knows that it is safe to use message.result here.
        // And Flow will complain if you try to use any other property.
        console.log(message.result);
    } else if (message.messageType === "error") {
        // Only ErrorMessage has a messageType property with value "error"
        // so knows that it is safe to use message.error here.
        throw message.error;
    } else if (message.messageType === "stats") {
        // Only StatisticsMessage has a messageType property with value "stats"
        // so knows that it is safe to use message.splinesPerSecond here.
        console.info(message.splinesPerSecond);
    }
}

17.9 总结

JavaScript 是当今世界上使用最广泛的编程语言。它是一种活跃的语言,不断发展和改进,周围有着繁荣的库、工具和扩展生态系统。本章介绍了其中一些工具和扩展,但还有许多其他内容需要了解。JavaScript 生态系统蓬勃发展,因为 JavaScript 开发者社区活跃而充满活力,同行们通过博客文章、视频和会议演讲分享他们的知识。当你结束阅读这本书,加入这个社区时,你会发现有很多信息源可以让你与 JavaScript 保持联系并继续学习。

祝一切顺利,David Flanagan,2020 年 3 月

¹ 如果你有 Java 编程经验,可能在第一次编写使用类型参数的通用 API 时会遇到类似的情况。我发现学习 Flow 的过程与 2004 年 Java 添加泛型时经历的过程非常相似。

相关文章
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(3)
JavaScript 权威指南第七版(GPT 重译)(七)
33 0
|
前端开发 JavaScript Unix
JavaScript 权威指南第七版(GPT 重译)(七)(2)
JavaScript 权威指南第七版(GPT 重译)(七)
42 0
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
60 0
|
13天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
93 2
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
13天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
55 4
|
13天前
|
XML 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(2)
JavaScript 权威指南第七版(GPT 重译)(六)
60 4
JavaScript 权威指南第七版(GPT 重译)(六)(2)
|
13天前
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(六)(1)
JavaScript 权威指南第七版(GPT 重译)(六)
28 3
JavaScript 权威指南第七版(GPT 重译)(六)(1)
|
13天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(五)(4)
JavaScript 权威指南第七版(GPT 重译)(五)
39 9
|
13天前
|
前端开发 JavaScript 程序员
JavaScript 权威指南第七版(GPT 重译)(五)(3)
JavaScript 权威指南第七版(GPT 重译)(五)
36 8
|
13天前
|
JSON 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(五)(2)
JavaScript 权威指南第七版(GPT 重译)(五)
36 5