内容

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

约定和 Lint

#

JavaScript 和 Dart 都具有 Lint 工具来强制执行标准约定。虽然 JavaScript 提供了许多工具、标准和配置,但 Dart 有一套官方的布局和样式约定以及一个 Lint 工具来简化合规性。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

#

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 还包含一个~/运算符(称为截断除法运算符),它除以一个双精度数并输出一个向下取整的整数。

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

#

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

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

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

js
const httpResponseBody = func();

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

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

类似地,future 可能会像 promise 一样失败。捕获错误的方式也相同。

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

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

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

Async/Await

#

如果您熟悉 JavaScript 中的 promise,那么您可能也熟悉 async/await 语法。此语法在 Dart 中是相同的:函数标记为 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));

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

#

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

"this" 上下文

#

Dart 中的 this 关键字比 JavaScript 中的 this 关键字更简单。在 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

或者,您可以将 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 为真,编译器不知道是否要分配 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 构建的开源框架,用于从单个代码库构建本机编译的多平台应用程序。这些文档提供了有关该语言的深入信息以及入门的一些实用方法。

一些可能的下一步