内容

作为 JavaScript 开发人员学习 Dart

本指南旨在利用您在学习 Dart 时的 JavaScript 编程知识。它展示了两种语言的关键相似之处和差异,并介绍了 JavaScript 中不支持的 Dart 概念。作为一名 JavaScript 开发人员,Dart 应该感觉很熟悉,因为这两种语言共享许多概念。

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

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

Dart 默认启用空安全。JavaScript 不支持空安全。作为一名 JavaScript 开发人员,可能需要一段时间才能学习如何编写空安全代码,但权衡是更好地防止空引用异常,即使在编译 Dart 代码之前也能检测到这些异常。(从而避免在对 JavaScript 变量执行操作时发生的那些可怕的 TypeError,这些变量结果为 null。)

约定和 linting

#

JavaScript 和 Dart 都有 linting 工具来强制执行标准约定。虽然 JavaScript 提供了许多工具、标准和配置,但 Dart 有一套官方的布局和样式约定以及一个 linter 来简化合规性。Dart 分析器对代码进行 lint,并提供更多分析功能。要自定义项目的 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 支持以下内置类型

  • 数字 (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); // 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.

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

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

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

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

函数

#

虽然 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));

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

语句

#

本节介绍 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 语句中使用 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); // 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 还包含一个~/运算符(称为截断除法运算符),它将一个 double 除以另一个 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)) {
  // ...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 类包含一种方法来创建一个列表,其中每个项目都具有相同的值。此 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 定义集合的方式与列表相同,但使用花括号而不是方括号。集合是无序的集合,只包含唯一项。Dart 使用哈希码强制执行这些项的唯一性,这意味着对象需要哈希值才能存储在 Set 中。

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

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

创建空集合的语法乍一看可能令人困惑,因为指定空花括号 ({}) 会导致创建空 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]

这也适用于集合和映射

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',
];

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

集合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 对应部分的关系。

期货

#

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

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

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

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 语法的更多信息,请参阅 异步编程 代码实验室。

#

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: () {
    // 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');
});
异步生成器
#

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

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

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));

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

#

从表面上看,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 构造函数的类只能具有 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 用于在类之间共享功能。您可以使用 mixin 的字段和方法在类中,使用它们的功能就好像它们是类的一部分一样。一个类可以使用多个 mixin。当多个类共享相同的功能时,这很有帮助,而无需彼此继承或共享共同的祖先。

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

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

或者,您可以用 mixin 替换 class 关键字,以防止 mixin 被用作普通类

dart
mixin Walker {
  walk() => print('Walks legs');
}
// Not possible, as Walker is no longer a class.
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'; // 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 保留给代码生成器。

延迟变量

#

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

  • (不可为空)late 字段不能在以后分配空值。

  • (不可为空)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 为真,是否要分配 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 也是如此,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 doc 协同工作:一个用于生成 Dart 包的 HTML 文档的原生工具。将文档注释放在所有公共成员声明之上被认为是最佳实践。

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

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

下一步

#

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

一些可能的下一步