跳过主内容

JavaScript 开发者学习 Dart

本指南旨在利用您的 JavaScript 编程知识来学习 Dart。它展示了两种语言之间的主要异同,并介绍了 JavaScript 不支持的 Dart 概念。作为一名 JavaScript 开发者,Dart 应该会感觉相当熟悉,因为两种语言共享许多概念。

与 JavaScript 类似,Dart 运行在事件循环上,因此两种语言执行代码的方式相似。例如,Future(JavaScript 中的 Promise)和 async/await 语法等异步概念非常相似。

与 JavaScript 不同,Dart 是强类型语言。如果您使用过 TypeScript 或 Flow,这应该会简化 Dart 的学习。如果您主要使用纯 JavaScript,这可能需要一些适应。通过强类型,Dart 在编译前捕获许多在 JavaScript 代码中可能存在的错误。

Dart 默认启用空安全。JavaScript 不支持空安全。作为一名 JavaScript 开发者,学习如何编写空安全代码可能需要一段时间,但其好处是能更好地防止空引用异常,这些异常甚至在编译 Dart 代码之前就能被检测到。(从而避免在对 JavaScript 变量进行操作时,该变量结果为 null 时出现的那些可怕的 TypeError。)

约定与 Linting

#

JavaScript 和 Dart 都拥有 Linting 工具来强制执行标准约定。虽然 JavaScript 提供了许多工具、标准和配置,但 Dart 拥有一套官方的布局和样式约定,以及一个 Linter 来简化合规性。Dart 分析器会对代码进行 Linting,并提供更多分析功能。要自定义项目的 Lint 规则,请遵循 自定义静态分析 指南。

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

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

Dart 支持在集合、参数或实参的逗号分隔列表中使用尾随逗号。当您添加尾随逗号时,格式化程序会将每个列表项放在单独的行上。当您认为列表将来可能包含更多项时,请添加尾随逗号。避免仅仅为了格式化效果而添加尾随逗号。

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

内置类型

#

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

JavaScript 提供原始类型 numstringboolean 以及 null 值,还有 数组Map 类型。

Dart 支持以下内置类型:

  • 数字 (num, int, double)
  • 字符串 (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); // Prints true
print(identical(a, b)); // Prints true; only one "2" object exists

原始类型

#

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

数字

#

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

num
相当于 JavaScript 中的通用数字类型。
int
不带小数部分的数值。
double
任何 64 位(双精度)浮点数。

Dart API 将所有这些类型作为类包含。intdouble 类型都以 num 作为它们的父类。

num subclasses Object and int and double each subclass 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 scalar 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
// Declare and initialize a variable at once
let name = "bob";
dart
// Declare a variable with a specific type
// when you don't provide an initial value
String name;
// Declare and initialize a variable
// at the same time and Dart infers
// the type
var name = 'bob';

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

dart
var name = 'bob';
name = 5; // Forbidden, as `name` has type `String`.

如果您不提供初始值或显式类型,Dart 会将变量的类型推断为包罗万象的 dynamic 类型。

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

js
// Declare a variable
let name;
// Initialize the variable
name = "bob";
dart
// Declare a variable without a type or assigned value
// and Dart infers the 'dynamic' type
var name;
// Initialize the variable and the type remains `dynamic`
name = 'bob';
name = 5; // Allowed, as `name` has type `dynamic`.

final 与 const

#

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

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

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

dart
final String name;
// Cannot read name here, not initialized.
if (useNickname) {
  name = "Bob";
} else {
  name = "Robert";
}
print(name); // Properly initialized here.

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

dart
var foo = const [];
  // foo is not constant, but the value it points to is.
  // You can reassign foo to a different list value,
  // but its current list value cannot be altered.

const baz = []; // Equivalent to `const []`

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

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

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

空安全

#

与 JavaScript 不同,Dart 支持空安全。在 Dart 中,所有类型默认都是非可空的。这使得 Dart 开发者受益,因为 Dart 在编写代码时而非运行时捕获空引用异常。

可空类型与非可空类型

#

以下代码示例中的任何变量都不能为 null

dart
// In null-safe Dart, none of these can ever be null.
var i = 42; // Inferred to be an int.
String name = getFileName();
final b = Foo(); // Foo() invokes a constructor

要指示变量可能具有 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; // Not allowed.
int b = a!; // Allowed.

在运行时,如果某个值结果为 null,则会发生运行时错误。

?. 运算符类似,在访问对象的属性或方法时使用 ! 运算符。

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

如果在运行时 myObjectnull,则会发生运行时错误。

函数

#

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

js
// On the top level
function multiply(a, b) {
  return a * b;
}

// As a class field
class Multiplier {
  multiply(a, b) {
    return a * b;
  }
}

// In a local scope
function main() {
  function multiply(a, b) {
    return a * b;
  }

  console.log(multiply(3, 4));
}
dart
// On the top level
int multiply(a, b) {
  return a * b;
}

// As a class field
class Multiplier {
  multiply(a, b) {
    return a * b;
  }
}

// In a local scope
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); // Valid. All parameters are provided.
  multiply(3); // Invalid. All parameters must be provided.
}

这在两种情况下可能会改变:

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

要定义可选的位置参数,请将其放在任何必需位置参数之后的方括号中。您不能在可选参数之后跟必需参数。

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

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

dart
// Valid: `b` has a default value of 5. `c` is marked as nullable.
multiply(int a, [int b = 5, int? c]) {
  ...
}
// Invalid: a required positional parameter follows an optional one.
multiply(int a, [int b = 5], int c) {
  ...
}
// Invalid: Neither optional positional parameter has a default
//          value or has been flagged as nullable.
multiply(int a, [int b, int c]) {
  ...
}

以下示例展示了如何调用带可选参数的函数:

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

main() {
  // All are valid function calls.
  multiply(3);
  multiply(3, 5);
  multiply(3, 5, 7);
}

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

同样,在空安全的情况下,未标记为必需的命名参数要么需要有默认值,要么需要标记为可空。

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

dart
// Valid:
// - `a` has been flagged as required
// - `b` has a default value of 5
// - `c` is marked as nullable
// - Named parameters follow the positional one
multiply(bool x, {required int a, int b = 5, int? c}) {
  ...
}

以下示例调用带命名参数的函数:

dart
// All are valid function calls.
// Beyond providing the required positional parameter:
multiply(false, a: 3); // Only provide required named parameters
multiply(false, a: 3, b: 9); // Override default value of `b`
multiply(false, c: 9, a: 3, b: 2); // Provide all named parameters out of order

一等函数

#

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

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

var list = [1, 2, 3];

// Pass printElement as a parameter.
list.forEach(printElement);

匿名函数

#

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

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

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

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

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

js
// A regular function expression
// assigned to a variable
let funcExpr = function(a, b) {
  return a * b;
}
// The same anonymous function
// expressed as an arrow
// function with curly braces.
let arrowFuncExpr = (a, b) => {
  return a * b;
}
// An arrow function with only
// one return statement as
// its contents does not
// require a block.
let arrowFuncExpr2 = (a, b) => a * b;
dart
// Assign an anonymous function
// to a variable.
var blockFunc =
  optionalCallback ?? (int a, int b) {
    return a * b;
};

// For an expression with only a return statement,
// you can use the arrow syntax:
var singleFunc = (int a, int b) => a * b;

与 JavaScript 一样,您可以将匿名函数传递给其他函数。开发者在使用 map 函数处理数组和列表时,通常会传递匿名函数:

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

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

// returns [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++;
  }
}

// Returns [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++;
  }
}

// Returns an iterable with [0, 1, 2, 3, 4]
print(naturalsTo(5).toList());
js
function* doubleNaturalsTo(n) {
  let k = 0;
  while (k < n) {
    yield* [k, k];
    k++;
  }
}

// Returns [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++;
  }
}

// Returns an iterable with [0, 0, 1, 1, 2, 2]
print(doubleNaturalsTo(3));

您还可以定义异步生成器函数,它们返回 Stream 而不是 Iterable。在即将到来的异步 部分了解更多。

语句

#

本节描述 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 (表达式值为 x)x++x++
x = x - 1 (表达式值为 x - 1)--x--x
x = x - 1 (表达式值为 x)x--x--

例如

dart
assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // Result is a double
assert(5 ~/ 2 == 2); // Result is an int
assert(5 % 2 == 1); // Remainder

a = 0;
b = ++a; // Increment a before b gets its value.
assert(a == b); // 1 == 1

a = 0;
b = a++; // Increment a AFTER b gets its value.
assert(a != b); // 1 != 0

a = 0;
b = --a; // Decrement a before b gets its value.
assert(a == b); // -1 == -1

a = 0;
b = a--; // Decrement a AFTER b gets its value.
assert(a != b); // -1 != 0

您可能已经注意到 Dart 还包含一个 ~/ 运算符(称为截断除法运算符),它将双精度浮点数相除并输出一个向下取整的整数。

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
如果对象具有指定类型则为 truex instanceof Tx is T
如果对象缺乏指定类型则为 true!(x instanceof T)x is! T

如果 obj 实现了 T 指定的接口,则 obj is T 的结果为 true。例如,obj is Object? 总是 true。

使用类型转换运算符 (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 视为“falsy”值。Dart 允许 bool 值用于条件和作为逻辑运算符的操作数。

例如

dart
if (!done && (col == 0 || col == 3)) {
  // ...Do something...
}

位运算和移位运算符

#

您可以使用位运算和移位运算符对整数的单个位进行操作。两种语言的运算符几乎相同,如下表所示:

含义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); // Shift left
assert((value >> 4) == 0x02); // Shift right
assert((-value >> 4) == -0x03); // Shift right
assert((value >>> 4) == 0x02); // Unsigned shift right
assert((-value >>> 4) > 0); // Unsigned shift right

条件运算符

#

Dart 和 JavaScript 都包含用于评估表达式的条件运算符 (?:)。一些开发者将其称为三元运算符,因为它需要三个操作数。由于 Dart 还有另一个接受三个操作数的运算符 ([]=),所以将此运算符 (?:) 称为条件运算符。此运算符对表达式的作用类似于 if-else 对语句的作用。

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

赋值运算符

#

使用 (=) 运算符来赋值。

dart
// Assign value to a
a = value;

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

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

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

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

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

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

dart
var a = 5;
a *= 2; // Multiply `a` by 2 and assign the result back to a.
print(a); // `a` is now 10.

级联 (.. 运算符)

#

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

JavaScript 缺乏这种语法。

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

dart
var animal = Animal() // Sets multiple properties and methods
  ..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
// Initialize list and specify full type
final List<String> list1 = <String>['one', 'two', 'three'];

// Initialize list using shorthand type
final list2 = <String>['one', 'two', 'three'];

// Dart can also infer the type
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'];
// Remove the value 'pear' from the list.
fruits.remove('pear');
// Removes the last element from the list.
fruits.removeLast();
// Removes the element at position 1 from the list.
fruits.removeAt(1);
// Removes the elements with positions greater than
// or equal to start (1) and less than end (3) from the list.
fruits.removeRange(1, 3);
// Removes all elements from the list that match the given predicate.
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);

填充

#

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

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

生成

#

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

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

集合

#

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

以下代码片段展示了如何初始化一个 Set

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

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

dart
final names = <String>{};
// Set<String> names = {}; // This works, too.
// final names = {}; // Creates an empty map, not a set.

以下示例概述了您可以在 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'};
// Remove the value 'pear' from the set.
fruits.remove('pear');
// Remove all elements in the supplied list from the set.
fruits.removeAll(['orange', 'apple']);
// Removes all elements from the list that match the given predicate.
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'; // Gets added
gifts['second'] = 'turtle doves'; // Gets updated

使用 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]
// When the list being inserted could be null:
list1 = null;
var list2 = [0, ...?list1]; // [0]

这也适用于 Set 和 Map:

dart
// Spread operator with maps
var map1 = {'foo': 'bar', 'key': 'value'};
var map2 = {'foo': 'baz', ...map1}; // {foo: bar, key: value}
// Spread operator with sets
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',
];

它对 Map 和 Set 也类似工作。

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

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

这对于 Map 和 Set 也以同样的方式工作。

异步

#

与 JavaScript 类似,Dart 虚拟机 (VM) 运行一个单一的事件循环来处理所有 Dart 代码。这意味着类似的异步规则也适用于此。您的所有代码都是同步运行的,但您可以以不同的顺序处理它,这取决于您如何使用可用的异步工具。以下是其中一些构造及其与 JavaScript 对应物的关系。

Future

#

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

Dart 或 Dart 包中的函数可能返回一个 Future,而不是它们所代表的值,因为该值可能要到稍后才能可用。

以下示例显示了在 Dart 中处理 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 中是相同的:函数被标记为 async,并且 async 函数总是返回一个 Future。如果函数返回 String 并被标记为 async,它会返回 Future<String>。如果它不返回任何内容,但它是 async,它会返回 Future<void>

以下示例展示了如何编写一个 async 函数:

js
// Returns a Promise of a string,
// as the method is async
async fetchString() {
  // Typically some other async
  // operations would be done here.
  return "String Value";
}
dart
// Returns a future of a string,
// as the method is async
Future<String> fetchString() async {
  // Typically some other async
  // operations would be done here.
  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
// We can only await futures within an async context.
Future<void> asyncFunction() async {
  var str = await fetchString();
  print(str); // 'String Value'
}

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

Stream

#

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

对于不熟悉这些概念的人来说:Stream 基本功能类似 Future,但它具有随时间推移分散的多个值,就像一个事件总线。您的代码可以监听一个 Stream,它既可以完成也可以达到失败状态。

监听

#

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

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

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

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

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

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

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

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

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

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

创建 Stream

#

Future 类似,您有多种不同的方式来创建 Stream。Stream 类具有实用构造函数,可用于从 FutureIterable 创建 Stream,或创建在定时间隔发出值的 Stream。要了解更多信息,请参阅 Stream API 页面。

StreamController
#

实用类 StreamController 可以创建和控制 Stream。其 stream 属性暴露了它控制的 Stream。其方法提供了向该 Stream 添加事件的方式。

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

以下示例展示了 Stream 控制器的基本用法:

dart
var listeners = 0;
StreamController<int>? controller;
controller = StreamController<int>(
  onListen: () {
    // Emit a new value every time the stream gets a new listener.
    controller!.add(listeners++);
    // Close the stream after the fifth listener.
    if (listeners > 5) controller.close();
  }
);
// Get the stream for the stream controller
var stream = controller.stream;
// Listen to the stream
stream.listen((int value) {
  print('$value');
});
异步生成器
#

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

在异步生成器函数中,yield 关键字将给定值发出到 Stream。然而,yield* 关键字与 Stream 而不是其他 Iterable 一起工作。这允许来自其他 Stream 的事件被发出到此 Stream。在以下示例中,函数在新发出的 Stream 完成后继续执行。

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

Stream<int> stream = asynchronousNaturalsTo(5);

// Prints each of 0 1 2 3 4 in succession.
stream.forEach(print(value));

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

#

表面上看,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 { }
}

// Create a new instance of the Point class
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;

  // Syntactic sugar for setting x and y
  // before the constructor body runs.
  Point(this.x, this.y);
}

// Create a new instance of the Point class
Point p = Point(3, 5);

与函数类似,构造函数可以选择接受位置参数或命名参数。

dart
class Point {
  ...
  // With an optional positioned parameter
  Point(this.x, [this.y = 5]);
  // With named parameters
  Point({ required this.y, this.x = 5 });
  // With both positional and named parameters
  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);

  // Named constructor
  Point.origin()
      : x = xOrigin,
        y = yOrigin;
}

Const 构造函数

#

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

dart
class ImmutablePoint {
  final double x, y;

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

构造函数重定向

#

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

dart
class Point {
  double x, y;

  // The main constructor for this class.
  Point(this.x, this.y);

  // Delegates to the main constructor.
  Point.alongXAxis(double x) : this(x, 0);
}

工厂构造函数

#

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

dart
class Logger {
  static final Map<String, Logger> _cache =
      <String, Logger>{};
 
  final String name;
 
  // Factory constructor that returns a cached copy,
  // or creates a new one if it is not yet available.
  factory Logger(String name) {
    return _cache.putIfAbsent(
        name, () => _cache[name] ??= Logger._internal(name);
  }

  // Private constructor for internal use only
  Logger._internal(this.name);
}

方法

#

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

js
function doSomething() { // This is a function
  // Implementation..
}

class Example {
  doSomething() { // This is a method
    // Implementation..
  }
}
dart
void doSomething() { // This is a function
 // Implementation..
}

class Example {
 void doSomething() { // This is a method
   // Implementation..
 }
}

扩展类

#

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(); 
    // Invalid. The superclass `Object` has no `consume` method.
  }
}

抽象类与方法

#

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

被标记为 abstract 的类可以拥有抽象方法,这些方法不需要函数体,而是在类被扩展或其接口被实现时才需要实现。

dart
abstract class Consumer {
  consume();
}
// Extending the full class
class Dog extends Consumer {
  consume() {
    print('Eating cookies...');
  }
}
// Just implementing the interface
class Cat implements Consumer {
  consume() {
    print('Eating mice...');
  }
}
Consumer consumer;
consumer = Dog();
consumer.consume(); // Eating cookies...
consumer = Cat();
consumer.consume(); // Eating mice...

混入

#

混入(Mixin)用于在类之间共享功能。您可以在类中使用混入的字段和方法,就像它们是类的一部分一样。一个类可以使用多个混入。当多个类共享相同功能,而无需相互继承或共享共同祖先时,这很有帮助。

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

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

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

js
class Animal {}

// Defining the mixins
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 {}

// Composing the class instances with
// their correct functionality.
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()
    );

// Correct calls
bat.fly();
goose.fly();
goose.walk();
dog.walk();
// Incorrect calls
bat.walk(); // `bat` lacks the `walk` method
dog.fly(); // `dog` lacks the `fly` method
dart
abstract class Animal {}

// Defining the mixins
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 {}

// Correct calls
Bat().fly();
Goose().fly();
Goose().walk();
Dog().walk();
// Incorrect calls
Bat().walk(); // Not using the Walker mixin
Dog().fly(); // Not using the Flyer mixin

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

dart
mixin Walker {
  walk() => print('Walks legs');
}
// Not possible, as Walker is no longer a class.
class Bat extends Walker {}

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

举个例子:

dart
class Bird extends Animal with Consumer, Flyer {

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

扩展

#

扩展类、实现接口或使用混入在受影响的类可编辑时都有效。然而,有时扩展一个已存在的类或属于另一个库或 Dart SDK 的类会很有用。

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

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

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

要使扩展可用,它必须存在于同一文件中,或其文件必须被导入。

使用方法如下:

dart
import 'string_apis.dart'; // Import the file the extension is in
var age = '42'.parseInt(); // Use the extension method.

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; // Public field
  #paws; // Private field

  #printEyes() { // Private method
    print(this.eyes);
  }

  printPaws() { // Public method
    print(this.#paws);
  }
}

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

dart
class Animal {
  int eyes; // Public field
  int _paws; // Private field

  void _printEyes() { // Private method
    print(this.eyes);
  }

  void printPaws() { // Public method
    print(this._paws);
  }
}

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

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

Late 变量

#

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

  • (非空) late 字段不能在稍后被赋值为 null。

  • (非空) late 字段在初始化之前被访问时会抛出运行时错误。应避免这种情况。

dart
class PetOwner {
  final String name;
  late final Pet _pet;
  PetOwner(this.name, String petName) {
    // Cyclic object graph, cannot set _pet before owner exists.
    _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 为 true,编译器不知道是否要赋值 captures。使用 late 会将正常的“已赋值”检查延迟到运行时。

泛型

#

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

泛型方法

#

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

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

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

dart
// Defining a method with multiple generics.
T transform<T, Q>(T param1, Q param2) {
   ...
}
// Calling the method with explicitly defined types.
transform<int, String>(5, 'string value');
// Types are optional when the analyzer can infer them.
transform(5, 'string value');

泛型类

#

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

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

dart
class Cache<T> {
  T getByKey(String key) {}
  void setByKey(String key, T value) {}
}
// Creating a cache for strings
var stringCache = Cache<String>(); // stringCache has type Cache<String>
stringCache.setByKey('Foo', 'Bar'); // Valid, setting a string value.
stringCache.setByKey('Baz', 5); // Invalid, int type does not match generic.

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

限制泛型

#

您可以使用泛型通过 extends 将代码限制为一系列类型。这确保了您的类使用扩展特定类型的泛型类型实例化。

dart
class NumberManager<T extends num> {
   ...
}
// Valid.
var manager = NumberManager<int>();
var manager = NumberManager<double>();
// Invalid, String nor its parent classes extend num.
var manager = NumberManager<String>();

字面量中的泛型

#

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

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

dart
// Automatic type inference
var objList = [5, 2.0]; // Type: List<num>
// Explicit type definition:
var objList = <Object>[5, 2.0]; // Type: List<Object>
// Sets work identically:
var objSet = <Object>{5, 2.0};

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

dart
// Automatic type inference
var map = {
  'foo': 'bar'
}; // Type: Map<String, String>
// Explicit type definition:
var map = <String, Object>{
  'foo': 'bar'
}; // Type: Map<String, Object>

文档注释

#

常规注释在 Dart 中的工作方式与 JavaScript 中相同。使用 // 会注释掉该行其余部分的所有内容,您可以使用 /* ... */ 来进行跨多行的块注释。

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

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

dart
/// The number of characters in this chunk when unsplit.
int get length => ...

下一步

#

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

一些可能的下一步: