跳到主要内容

作为 JavaScript 开发者学习 Dart

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

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

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

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

约定和 lint

#

JavaScript 和 Dart 都具有 lint 工具来强制执行标准约定。虽然 JavaScript 提供了许多工具、标准和配置,但 Dart 拥有一套官方的布局和样式约定以及一个 linter,以简化合规性。Dart 分析器除了提供更多分析功能外,还会检查代码。要自定义项目的 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.

在运行时,如果 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));

您还可以定义异步生成器函数,它们返回 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(表达式值为 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 并输出一个向下取整的整数

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
逻辑 OR||||
逻辑 AND&&&&

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

例如

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

按位和移位运算符

#

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

含义JavaScript 运算符Dart 运算符
按位 AND&&
按位 OR||
按位 XOR^^
一元按位补码(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 都包含运算符,用于计算和分配新值给表达式中的变量。这些赋值运算符使用右侧的值和变量初始值作为操作数。

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

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

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

Filled

#

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

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

Generate

#

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 中使用以下方法之一从 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 提供了多种方法使集合(如数组、set 或字典)不可变。

  • 如果集合是编译时常量且不应修改,请使用 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> 而不是 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 中一样,等待 future 只能在 async 上下文(例如另一个 async 函数)中进行。

以下示例展示了如何等待 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 语法类似,您可以将 stream 与 async 上下文中的 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 而不是其他可迭代对象一起使用。这允许将来自其他 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 用于在类之间共享功能。您可以在类中使用 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

或者,您可以将 class 关键字替换为 mixin,以防止 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 字段不能在稍后分配 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 类的成员类型中使用。(例如,它的 first 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 从单个代码库构建本地编译的多平台应用程序。这些文档提供了有关该语言的深入信息以及入门的实用方法。

一些可能的后续步骤