目录

JavaScript 开发者学习 Dart

本指南旨在帮助您在学习 Dart 时利用您现有的 JavaScript 编程知识。它展示了两种语言的关键相似点和不同点,并介绍了 JavaScript 中不支持的 Dart 概念。作为 JavaScript 开发人员,您会发现 Dart 非常熟悉,因为这两种语言共享许多概念。

与 JavaScript 一样,Dart 也运行在事件循环上,因此两种语言都以类似的方式执行代码。例如,异步概念(如 Dart 的 future 和 JavaScript 的 promise)以及 async/await 语法都非常相似。

Dart 是强类型语言,而 JavaScript 不是。如果您使用过 TypeScript 或 Flow,这将简化您学习 Dart 的过程。如果您主要使用的是纯 JavaScript,那么这可能需要更多调整。通过强类型系统,Dart 在编译之前就能捕获许多 JavaScript 代码中可能存在的错误。

Dart 默认启用空安全功能。JavaScript 不支持空安全。作为 JavaScript 开发人员,您可能需要一段时间才能学会如何编写空安全的代码,但其好处是可以更好地防止空引用异常,这些异常甚至在编译 Dart 代码之前就能被检测到。(从而避免了在对 JavaScript 变量执行操作时,当该变量为空时发生的令人头疼的 TypeError 错误。)

约定和代码风格检查

#

JavaScript 和 Dart 都有代码风格检查工具来强制执行标准约定。虽然 JavaScript 提供了许多工具、标准和配置,但 Dart 有一套官方的布局和样式约定以及一个代码风格检查器来简化合规性。Dart 分析器不仅可以检查代码风格,还可以提供更多分析功能。要自定义项目的代码风格检查规则,请遵循 自定义静态分析 说明。

Dart 提供了 dart fix 来查找和修复错误。

Dart 还提供了一个代码格式化程序,类似于 JavaScript 的 Prettier 等工具。要在任何 Dart 项目中格式化代码,请在命令行上运行 dart format 。Dart 和 Flutter 的 IDE 插件也提供了此功能。

Dart 支持逗号分隔的集合、参数或参数列表的尾随逗号。添加尾随逗号后,格式化程序会将每个列表项放在单独一行。如果您认为稍后可能会在列表中添加更多项,请添加尾随逗号。避免仅仅为了格式化的便利而添加尾随逗号。

JavaScript 仅支持列表和映射字面量中的尾随逗号。

内置类型

#

JavaScript 和 Dart 都将其数据分类为 类型 。每个变量都有一个关联的类型。该类型决定了变量可以存储的值的种类以及可以对这些值执行的操作。Dart 与 JavaScript 的不同之处在于它为每个表达式和变量分配静态类型。静态类型预测变量的值或表达式的值的运行时类型。这意味着 Dart 应用程序具有健全的静态类型。

JavaScript 提供了 numstringboolean 原生类型以及 null 值,以及 数组Map 类型。

Dart 支持以下内置类型:

  • 数字 (numintdouble)
  • 字符串 (String)
  • 布尔值 (bool)
  • 列表 (List ,也称为数组)
  • 集合 (Set)
  • 映射 (Map)
  • 符号 (Symbol)
  • null 值 (Null)

要了解更多信息,请查看 Dart 语言教程 中的 内置类型

Dart 中所有非 Null 类型都是 Object 的子类型。所有值也是对象。Dart 不像 JavaScript 那样使用“原生类型”。相反,Dart 将数字、布尔值和 null 值进行规范化或 标准化 。这意味着只有一个数值为 1int 值存在。

例如: 对于数字类型的相同值, == 运算符和 identical() 方法返回 true 。请查看以下代码中显示的示例:

dart
var a = 2;
var b = 1 + 1;

print(a == b); // 打印 true
print(identical(a, b)); // 打印 true;只有一个“2”对象存在

原生类型

#

本节介绍 Dart 如何表示 JavaScript 中的原生类型。

数字

#

Dart 有三种数据类型用于保存数字:

num
等效于 JavaScript 中的通用数字类型。
int
没有小数部分的数值。
double
任何 64 位(双精度)浮点数。

Dart API 将所有这些类型都包含为类。 intdouble 类型都共享 num 作为其父类:

num 是 Object 的子类,int 和 double 都是 num 的子类

由于 Dart 将数字视为对象,因此数字可以将其自身的实用程序函数作为对象方法公开。您不需要使用其他对象来将函数应用于数字。

例如,将 double 四舍五入为整数:

js
let rounded = Math.round(2.5);
dart
var rounded = 2.5.round();

字符串

#

Dart 中的字符串与 JavaScript 中的字符串类似。要编写字符串字面量,请将其用单引号 (') 或双引号 (") 括起来。大多数 Dart 开发人员使用单引号,但语言本身没有强制规定。如果您不想转义字符串中的单引号,请使用双引号。

dart
var a = 'This is a string.';
转义特殊字符
#

要在字符串中包含具有其他含义的字符,例如用于字符串插值的 $ ,您必须转义该字符。Dart 中转义特殊字符的方式与 JavaScript 和大多数其他语言类似。要转义特殊字符,请在该字符前面加上反斜杠字符 (\)。

以下代码显示了一些示例。

dart
final singleQuotes = 'I\'m learning Dart'; // I'm learning Dart
final doubleQuotes = "Escaping the \" character"; // Escaping the " character
final dollarEscape = 'The price is \$3.14.'; // The price is $3.14.
final backslashEscape = 'The Dart string escape character is \\.';
final unicode = '\u{1F60E}'; // 😎,Unicode 标量 U+1F60E
字符串插值
#

JavaScript 支持模板字面量。它们使用反引号 (`) 字符分隔符,原因如下:

  • 允许多行字符串
  • 使用嵌入式表达式插入字符串
  • 创建称为标记模板的特殊结构

在 Dart 中,您不需要将字符串用反引号括起来即可连接字符串或在字符串字面量中使用插值。

要了解更多信息,请查看 Dart 语言教程中的 字符串

与 JavaScript 模板字面量一样,您可以使用 ${<expression>} 语法将表达式插入字符串字面量中。Dart 使用此语法,并且允许您在表达式使用单个标识符时省略花括号。

dart
var food = 'bread';
var str = 'I eat $food'; // I eat bread
var str = 'I eat ${food}'; // I eat bread

字符串连接和多行声明

#

在 JavaScript 中,您可以使用模板字面量定义多行字符串。Dart 有两种定义多行字符串的方法。

  1. 使用隐式字符串连接: Dart 连接任何相邻的字符串字面量,即使它们跨越多行:
    dart
    final s1 = 'String '
        'concatenation'
        " even works over line breaks.";
  2. 使用多行字符串字面量: 当在一串字符串的两侧使用三个引号(单引号或双引号)时,该字面量可以跨越多行。
    dart
    final s2 = '''
    You can create
    multiline strings like this one.
    ''';
    
    final s3 = """
    This is also a
    multiline string.""";

相等性

#

当两个字符串包含相同的代码单元序列时,Dart 认为它们相等。要确定两个字符串是否具有相同的序列,请使用等于运算符 (==)。

dart
final s1 = 'String '
    'concatenation'
    " works even over line breaks.";
assert(s1 ==
    'String concatenation works even over '
        'line breaks.');

布尔值

#

Dart 和 Javascript 中的布尔值都表示二元条件。这两个值表示值或表达式是 true 还是 false 。您可以使用字面量 truefalse 返回这些值,或者使用 x < 5y == null 等表达式生成它们。

js
let isBananaPeeled = false;
dart
var isBananaPeeled = false;

变量

#

Dart 中的变量与 JavaScript 中的变量类似,但有两个例外:

  1. 每个变量都有一个类型。
  2. Dart 在块级范围内作用域所有变量,就像 JavaScript 中的 letconst 变量一样。

Dart 变量可以通过以下两种方式之一获取其类型:

  1. 声明:在声明中写入的类型。
  2. 推断:用于初始化变量的表达式。根据 约定 ,当分析器可以推断类型时,请使用 varfinal
js
// 同时声明和初始化变量
let name = "bob";
dart
// 声明具有特定类型的变量
// 当您不提供初始值时
String name;
// 同时声明和初始化变量
// 并且 Dart 推断
// 类型
var name = 'bob';

变量只能接受其类型的值。

dart
var name = 'bob';
name = 5; // 禁止,因为 `name` 的类型为 `String` 。

如果您不提供初始值或显式类型,Dart 会将变量的类型推断为通配类型 dynamic

与 JavaScript 变量一样,您可以将任何值赋给使用 dynamic 类型的 Dart 变量。

js
// 声明一个变量
let name;
// 初始化变量
name = "bob";
dart
// 声明一个没有类型或赋值的变量
// Dart 推断为 'dynamic' 类型
var name;
// 初始化变量,类型保持为 `dynamic`
name = 'bob';
name = 5; // 允许,因为 `name` 的类型为 `dynamic` 。

final 和 const

#

JavaScript 和 Dart 都使用变量修饰符。两者都使用 const ,但在 const 的工作方式上有所不同。JavaScript 使用 const 的地方,Dart 使用 final

当您向 Dart 变量添加 final 或向 JavaScript 变量添加 const 时,您必须在其他代码读取其值之前初始化该变量。初始化后,您无法更改这些变量的引用。

当 Dart 使用 const 时,它指的是在编译时创建的特殊值。Dart 使用有限的表达式来创建这些不可变的值。这些表达式 不能 有副作用。 在此条件下,编译器就可以预测常量变量或表达式的精确值,而不仅仅是其静态类型。

dart
final String name;
// 在此处无法读取 name,未初始化。
if (useNickname) {
  name = "Bob";
} else {
  name = "Robert";
}
print(name); // 在此处已正确初始化。

在 Dart 中, 常量变量必须包含常量值 。非常量变量可以包含常量值,您也可以将其标记为 const

dart
var foo = const [];
  // foo 不是常量,但它指向的值是常量。
  // 您可以将 foo 重新赋值为不同的列表值,
  // 但其当前的列表值不能更改。

const baz = []; // 等效于 `const []`

同样,类也可以拥有自己的 const 构造函数,以生成不可变的实例。

您不能修改 JavaScript 或 Dart 中的 const 变量。JavaScript 允许您修改 const 对象的字段,但 Dart 不允许。

要了解更多信息,请参阅 部分。

空安全

#

与 JavaScript 不同,Dart 支持空安全。在 Dart 中,所有类型默认情况下都是不可空的。这对 Dart 开发人员来说很有益,因为 Dart 在编写代码时就会捕获空引用异常,而不是在运行时。

可空类型与不可空类型

#

以下代码示例中没有任何变量可以为 null

dart
// 在空安全的 Dart 中,这些变量永远不可能为 null。
var i = 42; // 推断为 int。
String name = getFileName();
final b = Foo(); // Foo() 调用构造函数

要指示变量可能具有值 null ,请在其类型声明中添加 ?

dart
int? aNullableInt = null;

对于任何其他类型声明(例如函数声明)也是如此:

dart
String? returnsNullable() {
  return random.nextDouble() < 0.5
    ? 'Sometimes null!'
    : null;
}

String returnsNonNullable() {
  return 'Never null!';
}

空感知运算符

#

Dart 支持多个运算符来处理可空性。与 JavaScript 一样,Dart 支持空赋值运算符 (??=)、空合并运算符 (??) 和可选链运算符 (?.)。这些运算符的工作方式与 JavaScript 相同。

! 运算符

#

在可空变量或表达式可能不为空的情况下,您可以使用 (!) 运算符告诉编译器抑制任何编译时错误。将此运算符放在表达式的后面。

不要将其与 Dart 的非 (!) 运算符混淆,后者使用相同的符号,但放置在表达式的前面。

dart
int? a = 5;

int b = a; // 不允许。
int b = a!; // 允许。

在运行时,如果 a 恰好为 null ,则会发生运行时错误。

?. 运算符一样,在访问对象的属性或方法时使用 ! 运算符:

dart
myObject!.someProperty;
myObject!.someMethod();

如果 myObject 在运行时为 null ,则会发生运行时错误。

函数

#

虽然 Dart 的函数的工作方式与其在 JavaScript 中的对应函数非常相似,但它们确实有一些附加功能,以及在声明它们时的一些细微的语法差异。与 JavaScript 类似,您几乎可以在任何地方声明函数,无论是在顶层、作为类字段还是在局部作用域中。

js
// 在顶层
function multiply(a, b) {
  return a * b;
}

// 作为类字段
class Multiplier {
  multiply(a, b) {
    return a * b;
  }
}

// 在局部作用域中
function main() {
  function multiply(a, b) {
    return a * b;
  }

  console.log(multiply(3, 4));
}
dart
// 在顶层
int multiply(a, b) {
  return a * b;
}

// 作为类字段
class Multiplier {
  multiply(a, b) {
    return a * b;
  }
}

// 在局部作用域中
main() {
  multiply(a, b) {
    return a * b;
  }

  print(multiply(3, 4));
}

箭头语法

#

Dart 和 JavaScript 都支持箭头语法 (=>),但在支持方式上有所不同。在 Dart 中,只有当函数包含单个表达式或 return 语句时,才能使用箭头语法。

例如,以下 isNoble 函数是等效的:

dart
bool isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}
dart
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

参数

#

在 JavaScript 中,所有参数 都可以 是位置参数。默认情况下,Dart 要求 您将所有参数作为参数传递给函数。

dart
int multiply(int a, int b) {
  return a * b;
}

main() {
  multiply(3, 5); // 有效。所有参数都已提供。
  multiply(3); // 无效。必须提供所有参数。
}

这可以在两种情况下改变:

  1. 位置参数被标记为可选。
  2. 参数已命名且未标记为必需。

要定义可选位置参数,请在任何必需位置参数之后用方括号将它们括起来。您不能在可选参数之后跟上必需参数。

由于空安全,可选位置参数必须具有默认值或被标记为可空。要了解更多信息,请参阅前面关于 空安全 的部分。

以下代码包含一个有效示例和两个无效示例,这些示例定义了可选位置参数的函数。

dart
// 有效: `b` 的默认值为 5。 `c` 被标记为可空。
multiply(int a, [int b = 5, int? c]) {
  ...
}
// 无效:必需位置参数位于可选参数之后。
multiply(int a, [int b = 5], int c) {
  ...
}
// 无效:可选位置参数既没有默认值,也没有被标记为可空。
multiply(int a, [int b, int c]) {
  ...
}

以下示例显示了如何调用具有可选参数的函数:

dart
multiply(int a, [int b = 5, int? c]) {
  ...
}

main() {
  // 所有这些都是有效的函数调用。
  multiply(3);
  multiply(3, 5);
  multiply(3, 5, 7);
}

Dart 支持 命名参数 。这些参数不必按定义的顺序提供,这与位置参数不同。您可以通过名称来引用它们。默认情况下,这些参数是可选的,除非它们被标记为必需。命名参数通过用大括号将它们括起来来定义。您可以将命名参数与必需位置参数结合使用——在这种情况下,命名参数始终位于位置参数之后。当使用命名参数调用函数时,请通过在传递的值前面加上参数的名称(用冒号分隔)来传递值。例如, f(namedParameter: 5)

同样,对于空安全,未标记为必需的命名参数需要具有默认值或被标记为可空。

以下代码定义了一个具有命名参数的函数:

dart
// 有效:
// - `a` 已被标记为必需
// - `b` 的默认值为 5
// - `c` 被标记为可空
// - 命名参数位于位置参数之后
multiply(bool x, {required int a, int b = 5, int? c}) {
  ...
}

以下示例调用一个具有命名参数的函数:

dart
// 所有这些都是有效的函数调用。
// 除了提供必需的位置参数之外:
multiply(false, a: 3); // 只提供必需的命名参数
multiply(false, a: 3, b: 9); // 重写 `b` 的默认值
multiply(false, c: 9, a: 3, b: 2); // 提供所有命名参数,顺序不限

一等函数

#

JavaScript 和 Dart 将函数视为一等公民。这意味着 Dart 将函数视为任何其他对象。例如,以下代码显示了如何将函数作为参数传递给另一个函数:

dart
void printElement(int element) {
  print(element);
}

var list = [1, 2, 3];

// 将 printElement 作为参数传递。
list.forEach(printElement);

匿名函数

#

JavaScript 和 Dart 都支持 匿名函数 ,即没有名称的函数。与命名函数一样,您可以像传递任何其他值一样传递匿名函数。例如,将匿名函数存储在变量中,将其作为参数传递给另一个函数,或从另一个函数返回它们。

JavaScript 有两种声明匿名函数的方法:

  1. 使用标准函数表达式
  2. 使用箭头语法

同样,Dart 也提供了两种声明匿名函数的方法。两者都以类似于 JavaScript 箭头表达式的方式工作。Dart 的匿名函数不支持常规函数表达式附带的额外功能。例如,JavaScript 支持函数表达式充当构造函数,或创建对 this 的自定义绑定。

要了解更多信息,请参阅 部分。

js
// 一个常规函数表达式
// 赋值给一个变量
let funcExpr = function(a, b) {
  return a * b;
}
// 同一个匿名函数
// 表示为箭头
// 带花括号的函数。
let arrowFuncExpr = (a, b) => {
  return a * b;
}
// 一个箭头函数,只有一个
// return 语句作为
// 其内容不需要
// 块。
let arrowFuncExpr2 = (a, b) => a * b;
dart
// 将匿名函数赋值
// 给一个变量。
var blockFunc =
  optionalCallback ?? (int a, int b) {
    return a * b;
};

// 对于只有一个 return 语句的表达式,
// 可以使用箭头语法:
var singleFunc = (int a, int b) => a * b;

与 JavaScript 一样,您可以将匿名函数传递给其他函数。开发人员在使用数组和列表的 map 函数时经常传递匿名函数:

js
// 返回 [4, 5, 6]
[1, 2, 3].map(e => e + 3);

// 返回 [5, 7, 9]
[1, 2, 3].map(e => {
  e *= 2;
  return e + 3;
});
dart
// 返回 [4, 5, 6]
[1, 2, 3].map((e) => e + 3).toList();

// 返回 [5, 7, 9]
var list2 = [1, 2, 3].map((e) {
  e *= 2;
  return e + 3;
}).toList();

生成器函数

#

两种语言都支持 生成器函数 。这些函数返回一个计算得出的可迭代项集合,以避免不必要的工作。

要在 Dart 中编写生成器函数,请在函数参数之后添加 sync* 关键字,并返回一个 Iterable 。使用 yield 关键字将项目添加到最终的可迭代对象中,或者使用 yield* 添加整个项目集。

以下示例显示了如何编写基本的生成器函数:

js
function* naturalsTo(n) {
  let k = 0;
  while (k < n) {
    yield k++;
  }
}

// 返回 [0, 1, 2, 3, 4]
for (let value of naturalsTo(5)) {
  console.log(value);
}
dart
Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) {
    yield k++;
  }
}

// 返回一个包含 [0, 1, 2, 3, 4] 的可迭代对象
print(naturalsTo(5).toList());
js
function* doubleNaturalsTo(n) {
  let k = 0;
  while (k < n) {
    yield* [k, k];
    k++;
  }
}

// 返回 [0, 0, 1, 1, 2, 2]
for (let value of doubleNaturalsTo(3)) {
  console.log(value);
}
dart
Iterable<int> doubleNaturalsTo(int n) sync* {
  int k = 0;
  while (k < n) {
    yield* [k, k];
    k++;
  }
}

// 返回一个包含 [0, 0, 1, 1, 2, 2] 的可迭代对象
print(doubleNaturalsTo(3));

您还可以定义异步生成器函数,这些函数返回流而不是可迭代对象。在即将到来的 异步 部分了解更多信息。

语句

#

本节描述了 JavaScript 和 Dart 之间的语句差异。

控制流 (if/else、for、while、switch)

#

大多数控制语句的工作方式与其 JavaScript 对应物类似。有些语句对 集合 有额外的用途。

迭代

#

虽然 JavaScript 和 Dart 都有 for-in 循环,但它们的行为有所不同。

JavaScript 的 for-in 循环迭代对象的属性。要迭代 JavaScript 可迭代对象的元素,必须使用 for-ofArray.forEach() 。Dart 的 for-in 循环的工作方式类似于 JavaScript 的 for-of

以下示例显示了如何迭代集合并打印出每个元素:

js
for (const element of list) {
  console.log(element);
}
dart
for (final element in list) {
  print(element);
}

switch

#

switch 语句中使用 continue 时,可以将其与放在 case 上的标签结合使用:

dart
switch (testEnum) {
  case TestEnum.A:
    print('A');
    continue b;
  b:
  case TestEnum.B:
    print('B');
    break;
}

运算符

#

Dart 和 JavaScript 都包含预定义的运算符。两种语言都不支持添加新的运算符。Dart 支持使用 operator 关键字重载一些现有的运算符。例如:

dart
class Vector {
  final double x;
  final double y;
  final double z;
  Vector(this.x, this.y, this.z);
  Vector operator +(Vector other) => Vector(
    x + other.x, 
    y + other.y,
    z + other.z,
  );
  Vector operator *(double scalar) => Vector(
    x * scalar,
    y * scalar,
    z * scalar,
  );
}

算术运算符

#

两种语言的相等和关系运算符几乎相同,如下表所示:

含义JavaScript 运算符Dart 运算符
++
--
一元减(也称为否定)-expr-expr
**
//
返回整数结果的除法~/
获取整数除法的余数(模)%%
x = x + 1 (表达式值为 x + 1++x++x
x = x + 1 (表达式值为 xx++x++
x = x - 1 (表达式值为 x - 1--x--x
x = x - 1 (表达式值为 xx--x--

例如:

dart
assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // 结果为 double
assert(5 ~/ 2 == 2); // 结果为 int
assert(5 % 2 == 1); // 余数

a = 0;
b = ++a; // 在 b 获取其值之前递增 a。
assert(a == b); // 1 == 1

a = 0;
b = a++; // 在 b 获取其值之后递增 a。
assert(a != b); // 1 != 0

a = 0;
b = --a; // 在 b 获取其值之前递减 a。
assert(a == b); // -1 == -1

a = 0;
b = a--; // 在 b 获取其值之后递减 a。
assert(a != b); // -1 != 0

您可能已经注意到,Dart 还包含一个 ~/ 运算符(称为 截断除法运算符 ),它除以一个 double 并输出一个向下取整的整数:

dart
assert(25 == 50.4 ~/ 2);
assert(25 == 50.6 ~/ 2);
assert(25 == 51.6 ~/ 2);

相等和关系运算符

#

两种语言的相等和关系运算符的工作方式相同:

含义JavaScript 运算符Dart 运算符
严格相等=====
抽象相等==
严格不相等!==!=
抽象不相等!=
大于>>
小于<<
大于或等于>=>=
小于或等于<=<=

==!= JavaScript 运算符没有等效项。

例如:

dart
assert(2 == 2);
assert(2 != 3);
assert(3 > 2);
assert(2 < 3);
assert(3 >= 3);
assert(2 <= 3);

类型测试运算符

#

两种语言的测试运算符的实现略有不同:

含义JavaScript 运算符Dart 运算符
类型转换x as T
如果对象具有指定的类型则为真x instanceof Tx is T
如果对象缺少指定的类型则为真!(x instanceof T)x is! T

如果 obj 实现由 T 指定的接口,则 obj is T 的结果为真。例如, obj is Object? 始终为真。

使用类型转换运算符 (as) 可确保值具有特定类型。如果您 知道 对象将具有该类型,则编译器可以使用它。

例如:

dart
(person as Employee).employeeNumber = 4204583;

如果您 不知道 对象是否为 T 类型,则使用 is T 在使用对象之前检查其类型。

在 Dart 中,局部变量的类型会在 if 语句的作用域内更新。实例变量并非如此。

dart
if (person is Employee) {
   person.employeeNumber = 4204583;
}

逻辑运算符

#

您可以使用逻辑运算符反转或组合布尔表达式。两种语言的逻辑运算符相同。

含义JavaScript 运算符Dart 运算符
反转下一个表达式(将 false 更改为 true,反之亦然)!x!x
逻辑或||||
逻辑与&&&&

JavaScript 允许使用任何值来代替布尔值。然后它将这些值转换为 truefalse 。JavaScript 认为空字符串和数字 0 是“假值”。Dart 允许在条件和逻辑运算符的操作数中使用 bool 值。

例如:

dart
if (!done && (col == 0 || col == 3)) {
  // ...执行某些操作...
}

位运算和移位运算符

#

您可以使用位运算和移位运算符与整数一起操作数字的各个位。两种语言的运算符几乎相同,如下表所示:

含义JavaScript 运算符Dart 运算符
位与&&
位或||
位异或^^
一元位补码(0 变成 1;1 变成 0)~expr~expr
左移<<<<
右移>>>>
无符号右移>>>>>>

例如:

dart
final value = 0x22;
final bitmask = 0x0f;

assert((value & bitmask) == 0x02); // AND
assert((value & ~bitmask) == 0x20); // AND NOT
assert((value | bitmask) == 0x2f); // OR
assert((value ^ bitmask) == 0x2d); // XOR
assert((value << 4) == 0x220); // 左移
assert((value >> 4) == 0x02); // 右移
assert((-value >> 4) == -0x03); // 右移
assert((value >>> 4) == 0x02); // 无符号右移
assert((-value >>> 4) > 0); // 无符号右移

条件运算符

#

Dart 和 JavaScript 都包含一个条件运算符 (?:) 用于评估表达式。一些开发人员将其称为三元运算符,因为它接受三个操作数。由于 Dart 有另一个接受三个操作数的运算符 ([]=),因此将此运算符 (?:) 称为条件运算符。此运算符适用于表达式,就像 if-else 适用于语句一样。

js
let visibility = isPublic ? "public" : "private";
dart
final visibility = isPublic ? 'public' : 'private';

赋值运算符

#

使用 (=) 运算符赋值。

dart
// 将值赋给 a
a = value;

此运算符还有一个空感知变体 (??=)。

要了解更多信息,请参阅 空赋值 运算符部分。

JavaScript 和 Dart 包含计算并为表达式中的变量赋值新值的运算符。这些赋值运算符使用右侧值和变量初始值作为操作数。

下表列出了这些赋值运算符:

运算符说明
=赋值
+=加法赋值
-=减法赋值
*=乘法赋值
/=除法赋值
~/=截断除法赋值
%=余数(模)赋值
>>>=无符号右移赋值
^=位异或赋值
<<=左移赋值
>>=右移赋值
&=位与赋值
|=位或赋值

JavaScript 不支持 ~/= 赋值运算符。

dart
var a = 5;
a *= 2; // 将 a 乘以 2 并将结果赋值回 a。
print(a); // a 现在为 10。

级联 (.. 运算符)

#

Dart 允许您在一个对象上链接多个方法调用、属性赋值或两者兼而有之。Dart 将此称为 级联 ,并使用级联语法 (..) 来执行此操作。

JavaScript 缺少此语法。

以下示例显示了使用级联语法在一个新构造的对象上链接多个方法:

dart
var animal = Animal() // 设置多个属性和方法
  ..name = "Bob"
  ..age = 5
  ..feed()
  ..walk();

print(animal.name); // "Bob"
print(animal.age); // 5

要使第一个级联语法为空感知,请将其编写为 ?..

dart
var result = maybePerson
    ?..employment = employer
    ..salary = salary;

如果 maybePerson 值为 null ,则 Dart 会忽略整个级联。

集合

#

本节介绍 Dart 中的一些集合类型,并将它们与 JavaScript 中的类似类型进行比较。

列表

#

Dart 的列表字面量与 JavaScript 数组的编写方式相同。Dart 将列表括在方括号中,并用逗号分隔值。

dart
// 初始化列表并指定完整类型
final List<String> list1 = <String>['one', 'two', 'three'];

// 使用简写类型初始化列表
final list2 = <String>['one', 'two', 'three'];

// Dart 也可以推断类型
final list3 = ['one', 'two', 'three'];

以下代码示例概述了可以在 Dart List 上执行的基本操作。以下示例显示了如何使用索引运算符从 List 中检索值。

dart
final fruits = <String>['apple', 'orange', 'pear'];
final fruit = fruits[1];

使用 add 方法将值添加到 List 的末尾。使用 addAll 方法添加另一个 List

dart
final fruits = <String>['apple', 'orange', 'pear'];
fruits.add('peach');
fruits.addAll(['kiwi', 'mango']);

使用 insert 方法在特定位置插入值。使用 insertAll 方法在特定位置插入另一个 List

dart
final fruits = <String>['apple', 'orange', 'pear'];
fruits.insert(0, 'peach');
fruits.insertAll(0, ['kiwi', 'mango']);

结合索引和赋值运算符更新 List 中的值:

dart
final fruits = <String>['apple', 'orange', 'pear'];
fruits[2] = 'peach';

使用以下方法之一从 List 中删除项目:

dart
final fruits = <String>['apple', 'orange', 'pear'];
// 从列表中删除值 'pear'。
fruits.remove('pear');
// 从列表中删除最后一个元素。
fruits.removeLast();
// 从列表中删除位置 1 处的元素。
fruits.removeAt(1);
// 从列表中删除位置大于或等于 start (1) 且小于 end (3) 的元素。
fruits.removeRange(1, 3);
// 从列表中删除所有与给定谓词匹配的元素。
fruits.removeWhere((fruit) => fruit.contains('p'));

使用 length 获取 List 中值的个数:

dart
final fruits = <String>['apple', 'orange', 'pear'];
assert(fruits.length == 3);

使用 isEmpty 检查 List 是否为空:

dart
var fruits = [];
assert(fruits.isEmpty);

使用 isNotEmpty 检查 List 是否不为空:

dart
final fruits = <String>['apple', 'orange', 'pear'];
assert(fruits.isNotEmpty);

filled

#

Dart 的 List 类包含一种方法,可以使用该方法创建每个项目都具有相同值的列表。此 filled 构造函数创建一个大小为 n 的固定长度列表,其中包含一个默认值。以下示例创建了一个包含 3 个项目的列表:

dart
final list1 = List.filled(3, 'a'); // 创建:['a', 'a', 'a']
  • 默认情况下,您无法添加或删除此列表中的元素。要允许此列表添加或删除元素,请在参数列表的末尾添加 , growable: true
  • 您可以使用其索引值访问和更新此列表中的元素。

generate

#

Dart List 类包含一种方法,可以使用该方法创建具有递增值的列表。此 generate 构造函数创建一个大小为 n 的固定长度列表,并使用模板来构建元素值。此模板将索引作为参数。

dart
// 创建:['a0', 'a1', 'a2']
final list1 = List.generate(3, (index) => 'a$index');

集合

#

与 JavaScript 不同,Dart 支持使用字面量定义 Set 。Dart 定义集合的方式与列表相同,但是使用花括号而不是方括号。集合是无序集合,只包含唯一项。Dart 使用哈希码强制执行这些项的唯一性,这意味着对象需要哈希值才能存储在 Set 中。

以下代码片段显示了如何初始化 Set

dart
final abc = {'a', 'b', 'c'};

创建空集合的语法乍一看可能令人困惑,因为指定空花括号 ({}) 会导致创建空 Map 。要创建空 Set ,请在 {} 声明前加上类型参数或将 {} 赋值给 Set 类型的变量:

dart
final names = <String>{};
// Set<String> names = {}; // 这也行得通。
// final names = {}; // 创建一个空映射,而不是集合。

以下示例概述了可以在 Dart Set 上执行的基本操作。

使用 add 方法将值添加到 Set 。使用 addAll 方法添加多个值:

dart
final fruits = {'apple', 'orange', 'pear'};
fruits.add('peach');
fruits.addAll(['kiwi', 'mango']);

Set 中使用以下方法之一从集合中删除内容:

dart
final fruits = {'apple', 'orange', 'pear'};
// 从集合中删除值 'pear'。
fruits.remove('pear');
// 从集合中删除提供的列表中的所有元素。
fruits.removeAll(['orange', 'apple']);
// 从列表中删除所有与给定谓词匹配的元素。
fruits.removeWhere((fruit) => fruit.contains('p'));

使用 length 获取 Set 中值的个数:

dart
final fruits = {'apple', 'orange', 'pear'};
assert(fruits.length == 3);

使用 isEmpty 检查 Set 是否为空:

dart
var fruits = <String>{};
assert(fruits.isEmpty);

使用 isNotEmpty 检查 Set 是否不为空:

dart
final fruits = {'apple', 'orange', 'pear'};
assert(fruits.isNotEmpty);

映射

#

Dart 中的 Map 类型类似于 JavaScript 中的 Map 类型。这两种类型都将键与值关联起来。如果所有键都具有相同的类型,则键可以是任何对象类型。此规则也适用于值。每个键最多出现一次,但您可以多次使用相同的值。

Dart 基于哈希表构建字典。这意味着键必须是可哈希的。每个 Dart 对象都包含一个哈希值。

考虑以下使用字面量创建的简单 Map 示例:

dart
final gifts = {
  'first': 'partridge',
  'second': 'turtle doves',
  'fifth': 'golden rings'
};

final nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

以下代码示例概述了可以在 Dart Map 上执行的基本操作。以下示例显示了如何使用索引运算符从 Map 中检索值。

dart
final gifts = {'first': 'partridge'};
final gift = gifts['first'];

使用 containsKey 方法检查 Map 是否包含键。

dart
final gifts = {'first': 'partridge'};
assert(gifts.containsKey('fifth'));

使用索引赋值运算符 ([]=) 添加或更新 Map 中的条目。如果 Map 还不包含键,则 Dart 会添加该条目。如果键存在,则 Dart 会更新其值。

dart
final gifts = {'first': 'partridge'};
gifts['second'] = 'turtle'; // 添加
gifts['second'] = 'turtle doves'; // 更新

使用 addAll 方法添加另一个 Map 。使用 addEntries 方法向 Map 添加其他条目。

dart
final gifts = {'first': 'partridge'};
gifts['second'] = 'turtle doves';
gifts.addAll({
  'second': 'turtle doves',
  'fifth': 'golden rings',
});
gifts.addEntries([
  MapEntry('second', 'turtle doves'),
  MapEntry('fifth', 'golden rings'),
]);

使用 remove 方法从 Map 中删除条目。使用 removeWhere 方法删除满足给定测试的所有条目。

dart
final gifts = {'first': 'partridge'};
gifts.remove('first');
gifts.removeWhere((key, value) => value == 'partridge');

使用 length 获取 Map 中键值对的个数。

dart
final gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);

使用 isEmpty 检查 Map 是否为空。

dart
final gifts = {};
assert(gifts.isEmpty);

使用 isNotEmpty 检查 Map 是否不为空。

dart
final gifts = {'first': 'partridge'};
assert(gifts.isNotEmpty);

不可修改

#

纯 JavaScript 不支持不变性。Dart 提供多种方法可以使数组、集合或字典等集合不可变。

  • 如果集合是编译时常量并且不应修改,请使用 const 关键字:
    const fruits = <String>{'apple', 'orange', 'pear'};
  • Set 赋值给 final 字段,这意味着 Set 本身不必是编译时常量。这确保了字段不能被另一个 Set 覆盖,但它仍然允许修改 Set 的大小或内容:
    final fruits = <String>{'apple', 'orange', 'pear'};
  • 使用 unmodifiable 构造函数创建集合类型的最终版本(如下例所示)。这将创建一个大小和内容都不能更改的集合:
dart
final _set = Set<String>.unmodifiable(['a', 'b', 'c']);
final _list = List<String>.unmodifiable(['a', 'b', 'c']);
final _map = Map<String, String>.unmodifiable({'foo': 'bar'});

展开运算符

#

与 JavaScript 一样,Dart 支持使用展开运算符 (...) 和空感知展开运算符 (...?) 将列表嵌入另一个列表中。

dart
var list1 = [1, 2, 3];
var list2 = [0, ...list1]; // [0, 1, 2, 3]
// 当要插入的列表可能为 null 时:
list1 = null;
var list2 = [0, ...?list1]; // [0]

这也适用于集合和映射:

dart
// 使用映射的展开运算符
var map1 = {'foo': 'bar', 'key': 'value'};
var map2 = {'foo': 'baz', ...map1}; // {foo: bar, key: value}
// 使用集合的展开运算符
var set1 = {'foo', 'bar'};
var set2 = {'foo', 'baz', ...set1}; // {foo, baz, bar}

集合 if/for

#

在 Dart 中, forif 关键字在集合方面具有附加功能。

集合 if 语句仅在满足指定条件时才包含列表字面量中的项目:

dart
var nav = [
  'Home',
  'Furniture',
  'Plants',
  if (promoActive) 'Outlet',
];

它对映射和集合的工作方式类似。

集合 for 语句允许将多个项目映射到另一个列表中:

dart
var listOfInts = [1, 2, 3];
var listOfStrings = [
  '#0',
  for (var i in listOfInts) '#$i',
]; // [#0, #1, #2, #3]

这也同样适用于映射和集合。

异步

#

与 JavaScript 一样,Dart 虚拟机 (VM) 运行单个事件循环来处理所有 Dart 代码。这意味着此处也适用类似的异步规则。所有代码都是同步运行的,但是您可以根据使用异步工具的方式来改变处理顺序。以下是一些这样的构造以及它们与 JavaScript 对应项的关系。

Futures

#

Future 是 Dart 版本的 JavaScript Promise 。两者都是异步操作的 结果 ,该操作将在稍后某个时间点解析。

Dart 或 Dart 包中的函数可能会返回 Future ,而不是它们表示的值,因为该值可能要到以后才能获得。

以下示例显示了处理 future 的方式与在 JavaScript 中处理 promise 的方式相同。

js
const httpResponseBody = func();

httpResponseBody.then(value => {
  console.log(
    `Promise resolved to a value: ${value}`
  );
});
dart
Future<String> httpResponseBody = func();

httpResponseBody.then((String value) {
  print('Future resolved to a value: $value');
});

同样,future 也可能像 promise 一样失败。捕获错误的工作方式也相同:

js
httpResponseBody
  .then(...)
  .catch(err => {
    console.log(
      "Promise encountered an error before resolving."
    );
  });
dart
httpResponseBody
  .then(...)
  .catchError((err) {
    print(
      'Future encountered an error before resolving.'
    );
  });

您还可以创建 future。要创建 Future ,请定义并调用 async 函数。当您有一个需要作为 Future 的值时,请像以下示例一样转换函数。

dart
String str = 'String Value';
Future<String> strFuture = Future<String>.value(str);

Async/Await

#

如果您熟悉 JavaScript 中的 promise,那么您可能也熟悉 async / await 语法。此语法在 Dart 中是相同的:函数标记为 asyncasync 函数总是返回一个 Future 。如果函数返回一个 String 并标记为 async ,则它返回 Future<String> 。如果它不返回任何内容,但它是 async ,则它返回 Future<void>

以下示例显示了如何编写 async 函数:

js
// 返回一个字符串的 Promise,
// 因为该方法是异步的
async fetchString() {
  // 通常在此处会执行其他异步操作。
  return "String Value";
}
dart
// 返回一个字符串的 future,
// 因为该方法是异步的
Future<String> fetchString() async {
  // 通常在此处会执行其他异步操作。
  return 'String Value';
}

调用此 async 函数的方式如下:

dart
Future<String> stringFuture = fetchString();
stringFuture.then((String str) {
  print(str); // 'String Value'
});

使用 await 关键字获取 future 的值。与 JavaScript 一样,这消除了在 Future 上调用 then 来获取其值的需要,并且允许您以更类似于同步的方式编写异步代码。与 JavaScript 一样,只有在 async 上下文中(例如另一个 async 函数)才能等待 future。

以下示例显示了如何等待 future 的值:

dart
// 我们只能在 async 上下文中等待 future。
Future<void> asyncFunction() async {
  var str = await fetchString();
  print(str); // 'String Value'
}

要了解有关 Futureasync / await 语法的更多信息,请参阅 异步编程 教程。

#

Dart 异步工具箱中的另一个工具是 Stream 。虽然 JavaScript 有自己的流概念,但 Dart 的流更类似于 Observable ,如常用的 rxjs 库中一样。如果您碰巧熟悉这个库,那么 Dart 的流应该会让您感觉很熟悉。

对于不熟悉这些概念的人: Stream 基本上就像 Future ,但是随着时间的推移会散布多个值,就像事件总线一样。您的代码可以监听流,并且它可以完成或达到失败状态。

监听

#

要监听流,请调用其 listen 方法并提供回调方法。每当流发出值时,Dart 都会调用此方法:

dart
Stream<int> stream = ...
stream.listen((int value) {
  print('A value has been emitted: $value');
});

listen 方法包含用于处理错误或流完成时的可选回调:

dart
stream.listen(
  (int value) { ... },
  onError: (err) {
    print('Stream encountered an error! $err');
  },
  onDone: () {
    print('Stream completed!');
  },
);

listen 方法返回 StreamSubscription 的一个实例,您可以使用它来停止监听流:

dart
StreamSubscription subscription = stream.listen(...);
subscription.cancel();

这不是监听流的唯一方法。与 Futureasync / await 语法类似,您可以在 async 上下文中将流与 for-in 循环结合使用。 for 循环为每个发出的项目调用回调方法,并在流完成或出错时结束:

dart
Future<int> sumStream(Stream<int> stream) async {
  var sum = 0;
  await for (final value in stream) {
    sum += value;
  }
  return sum;
}

当以这种方式监听流时发生错误时,该错误将在包含 await 关键字的行中重新抛出。您可以使用 try-catch 语句处理此错误:

dart
try {
  await for (final value in stream) { ... }
} catch (err) {
  print('Stream encountered an error! $err');
}

创建流

#

Future 一样,您可以通过多种不同的方式创建流。 Stream 类具有实用程序构造函数,用于从 FutureIterable 创建流,或者用于创建以定时间隔发出值的流。要了解更多信息,请参阅 Stream API 页面。

StreamController
#

实用程序类 StreamController 可以创建和控制流。它的 stream 属性公开了它控制的流。它的方法提供了向该流添加事件的方法。

例如, add 方法可以发出新项目,而 close 方法可以完成流。

以下示例显示了流控制器的基本用法:

dart
var listeners = 0;
StreamController<int>? controller;
controller = StreamController<int>(
  onListen: () {
    // 每当流获得新的监听器时,都会发出一个新值。
    controller!.add(listeners++);
    // 在第五个监听器之后关闭流。
    if (listeners > 5) controller.close();
  }
);
// 获取流控制器的流
var stream = controller.stream;
// 监听流
stream.listen((int value) {
  print('$value');
});
异步生成器
#

异步生成器函数可以创建流。这些函数类似于同步生成器函数,但是使用 async* 关键字并返回 Stream

在异步生成器函数中, yield 关键字将给定值发送到流。但是, yield* 关键字与流一起使用,而不是其他可迭代对象。这允许将来自其他流的事件发送到此流。在以下示例中,该函数在新建的已生成的流完成之后继续。

dart
Stream<int> asynchronousNaturalsTo(int n) async* {
  var k = 0;
  while (k < n) yield k++;
}

Stream<int> stream = asynchronousNaturalsTo(5);

// 依次打印 0 1 2 3 4 中的每一个。
stream.forEach(print(value));

异步编程 文档中了解有关 future、流和其他异步功能的更多信息。

#

从表面上看,Dart 中的类类似于 JavaScript 中的类,尽管 JavaScript 类在技术上更像是原型周围的包装器。在 Dart 中,类是语言的标准特性。本节介绍在 Dart 中定义和使用类以及它们与 JavaScript 的区别。

"this" 上下文

#

Dart 中的 this 关键字比 JavaScript 中的更简单明了。在 Dart 中,您不能将函数绑定到 this ,并且 this 从不依赖于执行上下文(如在 JavaScript 中那样)。在 Dart 中, this 仅在类中使用,并且始终引用当前实例。

构造函数

#

本节讨论 Dart 中的构造函数与 JavaScript 中的构造函数有何不同。

标准构造函数

#

标准类构造函数与 JavaScript 构造函数非常相似。在 Dart 中, constructor 关键字被完整的类名替换,并且所有参数都必须显式声明类型。在 Dart 中,曾经需要 new 关键字来创建类实例,但现在是可选的,并且不再推荐使用。

dart
class Point {
  final double x;
  final double y;

  Point(double x, double y) : this.x = x, this.y = y { }
}

// 创建 Point 类的新的实例
Point p = Point(3, 5);

初始化列表

#

使用初始化列表编写构造函数。将初始化列表插入到构造函数的参数和主体之间。

dart
class Point {
  ...
  Point.fromJson(Map<String, double> json)
      : x = json['x']!,
        y = json['y']! {
    print('In Point.fromJson(): ($x, $y)');
  }
  ...
}

构造函数参数

#

在构造函数中编写分配类字段的代码可能感觉像是在创建样板代码,因此 Dart 有些语法糖,称为 初始化参数 ,使这更容易:

dart
class Point {
  double x;
  double y;

  // 用于在构造函数主体运行之前设置 x 和 y 的语法糖。
  Point(this.x, this.y);
}

// 创建 Point 类的新的实例
Point p = Point(3, 5);

与函数类似,构造函数可以选择使用位置参数或命名参数:

dart
class Point {
  ...
  // 带有一个可选的位置参数
  Point(this.x, [this.y = 5]);
  // 带有命名参数
  Point({ required this.y, this.x = 5 });
  // 带有位置参数和命名参数
  Point(int x, int y, { boolean multiply }) {
    ...
  }
  ...
}

命名构造函数

#

与 JavaScript 不同,Dart 允许类具有多个构造函数,方法是允许您为它们命名。您可以选择只带有一个未命名的构造函数,任何其他构造函数都必须命名:

dart
const double xOrigin = 0;
const double yOrigin = 0;

class Point {
  double x = 0;
  double y = 0;

  Point(this.x, this.y);

  // 命名构造函数
  Point.origin()
      : x = xOrigin,
        y = yOrigin;
}

常量构造函数

#

要启用不可变的类实例,请使用 const 构造函数。具有 const 构造函数的类只能具有 final 实例变量。

dart
class ImmutablePoint {
  final double x, y;

  const ImmutablePoint(this.x, this.y);
}

构造函数重定向

#

您可以从其他构造函数调用构造函数,以防止代码重复或为参数添加其他默认值:

dart
class Point {
  double x, y;

  // 此类的主要构造函数。
  Point(this.x, this.y);

  // 委托给主构造函数。
  Point.alongXAxis(double x) : this(x, 0);
}

工厂构造函数

#

当您不需要创建新的类实例时,可以使用工厂构造函数。一个例子是在返回缓存的实例时:

dart
class Logger {
  static final Map<String, Logger> _cache =
      <String, Logger>{};
 
  final String name;
 
  // 工厂构造函数,返回缓存的副本,
  // 或在尚未可用时创建一个新的副本。
  factory Logger(String name) {
    return _cache.putIfAbsent(
        name, () => _cache[name] ??= Logger._internal(name);
  }

  // 仅供内部使用的私有构造函数
  Logger._internal(this.name);
}

方法

#

在 Dart 和 JavaScript 中,方法都是为对象提供行为的函数。

js
function doSomething() { // 这是一个函数
  // 实现..
}

class Example {
  doSomething() { // 这是一个方法
    // 实现..
  }
}
dart
void doSomething() { // 这是一个函数
 // 实现..
}

class Example {
 void doSomething() { // 这是一个方法
   // 实现..
 }
}

扩展类

#

Dart 允许类扩展另一个类,方式与 JavaScript 相同。

dart
class Animal {
  int eyes;
 
  Animal(this.eyes);
 
  makeNoise() {
    print('???');
  }
}

class Cat extends Animal {
  Cat(): super(2);

  @override
  makeNoise() {
    print('Meow');
  }
}
Animal animal = Cat();
print(animal.eyes); // 2
animal.makeNoise(); // Meow

当覆盖父类的方法时,请使用 @override 注解。虽然此注解是可选的,但它表明覆盖是故意的。如果该方法实际上没有覆盖超类方法,则 Dart 分析器会显示警告。

可以使用 super 关键字调用正在覆盖的父方法:

dart
class Cat extends Animal {
  ...
  @override
  makeNoise() {
    print('Meow');
    super.makeNoise();
  }
}
Animal animal = Cat();
animal.makeNoise(); // Meow
                    // ???

类作为接口

#

与 JavaScript 一样,Dart 没有单独的接口定义。但是,与 JavaScript 不同的是,所有类定义都同时作为接口;您可以使用 implements 关键字将类实现为接口。

当类实现为接口时,

它的公共 API 必须由新类实现。与 extends 不同,它的方法和字段实现不会与新类共享。虽然一个类只能扩展一个类,但是您可以同时实现多个接口,即使实现类已经扩展了另一个类也是如此。

dart
class Consumer {
  consume() {
    print('Eating food...');
  }
}
class Cat implements Consumer {
  consume() {
    print('Eating mice...');
  }
}
Consumer consumer = Cat();
consumer.consume(); // Eating mice

实现接口时,不能调用 super 方法,因为方法体没有继承:

dart
class Cat implements Consumer {
  @override
  consume() {
    print('Eating mice...');
    super.consume(); 
    // 无效。超类 `Object` 没有 `consume` 方法。
  }
}

抽象类和方法

#

为了确保类只能被扩展或实现其接口,但不允许构造任何实例,请将其标记为 abstract

标记为 abstract 的类可以具有抽象方法,这些方法不需要主体,而是在扩展类或实现其接口时需要实现:

dart
abstract class Consumer {
  consume();
}
// 扩展完整类
class Dog extends Consumer {
  consume() {
    print('Eating cookies...');
  }
}
// 只实现接口
class Cat implements Consumer {
  consume() {
    print('Eating mice...');
  }
}
Consumer consumer;
consumer = Dog();
consumer.consume(); // Eating cookies...
consumer = Cat();
consumer.consume(); // Eating mice...

Mixin

#

Mixin 用于在类之间共享功能。您可以使用 mixin 的字段和方法,将其功能用作类的一部分。一个类可以使用多个 mixin。当多个类共享相同的功能时,这很有帮助,无需相互继承或共享共同的祖先。

使用 with 关键字向类添加一个或多个用逗号分隔的 mixin。

JavaScript 没有等效的关键字。JavaScript 可以使用 Object.assign 在实例化后将其他对象合并到现有对象中。

以下示例显示了 JavaScript 和 Dart 如何实现类似的行为:

js
class Animal {}

// 定义 mixin
class Flyer {
  fly = () => console.log('Flaps wings');
}
class Walker {
  walk = () => console.log('Walks on legs');
}
 
class Bat extends Animal {}
class Goose extends Animal {}
class Dog extends Animal {}

// 使用正确的功能组合类实例。
const bat =
  Object.assign(
    new Bat(),
    new Flyer()
    );
const goose =
  Object.assign(
    new Goose(),
    new Flyer(),
    new Walker()
    );
const dog =
  Object.assign(
    new Dog(),
    new Walker()
    );

// 正确的调用
bat.fly();
goose.fly();
goose.walk();
dog.walk();
// 错误的调用
bat.walk(); // `bat` 缺少 `walk` 方法
dog.fly(); // `dog` 缺少 `fly` 方法
dart
abstract class Animal {}

// 定义 mixin
class Flyer {
  fly() => print('Flaps wings');
}
class Walker {
  walk() => print('Walks on legs');
}
 
class Bat extends Animal with Flyer {}
class Goose extends Animal with Flyer, Walker {}
class Dog extends Animal with Walker {}

// 正确的调用
Bat().fly();
Goose().fly();
Goose().walk();
Dog().walk();
// 错误的调用
Bat().walk(); // 没有使用 Walker mixin
Dog().fly(); // 没有使用 Flyer mixin

或者,您可以将 class 关键字替换为 mixin ,以防止将 mixin 用作常规类:

dart
mixin Walker {
  walk() => print('Walks legs');
}
// 不可能,因为 Walker 不再是类。
class Bat extends Walker {}

由于您可以使用多个 mixin,因此当它们在同一个类上使用时,它们之间可能具有重叠的方法或字段。它们甚至可能与使用它们的类或该类的超类重叠。将它们添加到类的顺序很重要。

举个例子:

dart
class Bird extends Animal with Consumer, Flyer {

当在一个 Bird 实例上调用方法时,Dart 从它自己的类 Bird 开始,它优先于其他实现。如果 Bird 没有实现,则检查 Flyer ,然后检查 Consumer ,直到找到实现为止。最后检查父类 Animal

扩展

#

当受影响的类可编辑时,扩展类、实现接口或使用 mixin 都可以工作。但是,有时扩展已经存在的类或另一个库或 Dart SDK 的一部分很有用。

在这些情况下,Dart 提供了为现有类编写扩展的功能。

例如,以下对 Dart SDK 中 String 类的扩展允许解析整数:

dart
extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
}

为了使扩展可用,它必须存在于同一个文件中,或者必须导入其文件。

使用方法如下:

dart
import 'string_apis.dart'; // 导入扩展所在的的文件
var age = '42'.parseInt(); // 使用扩展方法。

Getter 和 Setter

#

Dart 中的 getter 和 setter 的工作方式与其 JavaScript 对应物完全相同:

js
class Person {
  _age = 0;

  get age() {
    return this._age;
  }

  set age(value) {
    if (value < 0) {
      throw new Error(
        'age cannot be negative'
        );
    }
    this._age = value;
  }
}

var person = new Person();
person.age = 10;
console.log(person.age);
dart
class Person {
  int _age = 0;
 
  int get age {
    return _age;
  }
 
  set age(int value) {
    if (value < 0) {
      throw ArgumentError(
        'Age cannot be negative'
        );
    }
    _age = value;
  }
}

void main() {
  var person = Person();
  person.age = 10;
  print(person.age);
}

公有和私有成员

#

与 JavaScript 一样,Dart 没有访问修饰符关键字:默认情况下,所有类成员都是公有的。

JavaScript 将在 EcmaScript 标准的下一个实用版本中包含私有类成员。因此,一段时间以来,各种浏览器和运行时环境中都提供了对此的实现。

要在 JavaScript 中使类成员私有,请在其名称前加上井号(或哈希)符号 (#)。

js
class Animal {
  eyes; // 公有字段
  #paws; // 私有字段

  #printEyes() { // 私有方法
    print(this.eyes);
  }

  printPaws() { // 公有方法
    print(this.#paws);
  }
}

要在 Dart 中使类成员私有,请在其名称前加上下划线 (_)。

dart
class Animal {
  int eyes; // 公有字段
  int _paws; // 私有字段

  void _printEyes() { // 私有方法
    print(this.eyes);
  }

  void printPaws() { // 公有方法
    print(this._paws);
  }
}

JavaScript 使用哈希作为约定。Dart 的编译器强制使用下划线来实现此功能。

Dart 将私有成员私有化为库,而不是类。这意味着您可以从同一库中的代码访问私有成员。默认情况下,Dart 将对同一文件中的代码访问私有类成员的权限限制为同一文件。要将库的作用域扩展到一个文件之外,请添加 part 指令。如果可能的话, 避免使用 part 。将 part 的使用保留给代码生成器。

延迟变量

#

要指示 Dart 在稍后某个时间点初始化类字段,请将 late 关键字赋值给这些类字段。这些类字段将保持不可为空。当不需要立即观察或访问变量并且可以稍后初始化时,请执行此操作。这与将字段标记为可空不同。

  • (不可为空)延迟字段以后不能赋值为 null。

  • (不可为空)在初始化之前访问延迟字段会引发运行时错误。应避免这种情况。

dart
class PetOwner {
  final String name;
  late final Pet _pet;
  PetOwner(this.name, String petName) {
    // 循环对象图,在所有者存在之前无法设置 _pet。
    _pet = Pet(petName, this);
  }
  Pet get pet => _pet;
}
class Pet {
  final String name;
  final PetOwner owner;
  Pet(this.name, this.owner);
}

仅当不清楚的代码导致编译器无法确定代码是否初始化了变量时,才对局部变量使用 late

dart
doSomething(int n, bool capture) {
  late List<Foo> captures;
  if (capture) captures = [];
  for (var i = 0; i < n; i++) {
    var foo = something(i);
    if (capture) captures.add(foo);
  }
}

在前面的示例中,编译器不知道如果 capture 为真是否要赋值 captures 。使用 late 将正常的“已赋值”检查延迟到运行时。

泛型

#

虽然 JavaScript 不提供泛型,但 Dart 提供泛型来提高类型安全性并减少代码重复。

泛型方法

#

您可以将泛型应用于方法。要在方法名称后定义泛型类型参数,请将其放在尖括号 < > 之间。然后,您可以将此类型在方法中用作返回类型或在方法的参数中:

dart
Map<Object?, Object?> _cache = {};
T cache<T>(T value) => (_cache[value] ??= value) as T;

通过用逗号分隔它们来定义多个泛型类型:

dart
// 定义一个具有多个泛型的的方法。
T transform<T, Q>(T param1, Q param2) {
   ...
}
// 使用显式定义的类型调用该方法。
transform<int, String>(5, 'string value');
// 当分析器可以推断类型时,类型是可选的。
transform(5, 'string value');

泛型类

#

泛型也可以应用于类。您可以包含调用构造函数时要使用的类型。这允许您根据特定类型定制可重用类。

在以下示例中, Cache 类缓存特定类型:

dart
class Cache<T> {
  T getByKey(String key) {}
  void setByKey(String key, T value) {}
}
// 为字符串创建缓存
var stringCache = Cache<String>(); // stringCache 的类型为 Cache<String>
stringCache.setByKey('Foo', 'Bar'); // 有效,设置字符串值。
stringCache.setByKey('Baz', 5); // 无效,int 类型与泛型不匹配。

如果您省略类型声明,则运行时类型将变为 Cache<dynamic> ,并且对 setByKey 的两次调用都是有效的。

限制泛型

#

您可以使用泛型来限制代码使用 extends 来使用一系列类型。这确保您的类是用扩展特定类型的泛型类型实例化的:

dart
class NumberManager<T extends num> {
   ...
}
// 有效。
var manager = NumberManager<int>();
var manager = NumberManager<double>();
// 无效,String 及其父类不扩展 num。
var manager = NumberManager<String>();

字面量中的泛型

#

MapSetList 字面量可以接受类型参数。当 Dart 无法推断类型或无法正确推断类型时,这很有帮助。

例如, List 类具有泛型定义: class List<E> 。类型参数 E 指的是列表内容的类型。通常,此类型会自动推断,这在一些 List 类的成员类型中使用。(例如,它的第一个 getter 返回类型为 E 的值。)定义 List 字面量时,您可以显式定义泛型类型,如下所示:

dart
// 自动类型推断
var objList = [5, 2.0]; // 类型:List<num>
// 显式类型定义:
var objList = <Object>[5, 2.0]; // 类型:List<Object>
// 集合的工作方式相同:
var objSet = <Object>{5, 2.0};

这对于 Map 也是如此, Map 也使用泛型定义其键和值类型 (class Map<K, V>):

dart
// 自动类型推断
var map = {
  'foo': 'bar'
}; // 类型:Map<String, String>
// 显式类型定义:
var map = <String, Object>{
  'foo': 'bar'
}; // 类型:Map<String, Object>

文档注释

#

Dart 中的常规注释与 JavaScript 中的注释工作方式相同。使用 // 将后面的所有内容注释掉

它将剩余的行注释掉,您可以使用 /* ... */ 来进行跨越多行的块注释。

除了常规注释之外,Dart 还具有 文档注释 ,它们与 dart doc 协同工作:这是一个第一方工具,用于生成 Dart 包的 HTML 文档。将文档注释放在所有公共成员的声明之上的做法被认为是最佳实践。

使用三个正斜杠而不是两个 (///) 来定义文档注释:

dart
/// 未拆分时此块中的字符数。
int get length => ...

下一步

#

本指南介绍了 Dart 和 JavaScript 之间的主要区别。此时,请考虑阅读 Dart 文档。您还可以阅读 Flutter 文档。Flutter 使用 Dart 构建,是一个开源框架,使用 Dart 从单个代码库构建本机编译的多平台应用程序。这些文档提供了有关该语言和入门方法的深入信息。

一些可能的下一步: