内容

Dart 值和 JS 值属于不同的语言域。编译为 Wasm 时,它们也在不同的运行时中执行。因此,您应该将 JS 值视为外来类型。为了为 JS 值提供 Dart 类型,dart:js_interop 暴露了一组以 JS 为前缀的类型,称为“JS 类型”。这些类型用于在编译时区分 Dart 值和 JS 值。

重要的是,这些类型会根据编译到 Wasm 还是 JS 而以不同的方式具体化。这意味着它们的运行时类型将有所不同,因此你无法使用 is 检查和 as 转换。为了与这些 JS 值进行交互并检查它们,你应该使用external互操作成员或转换

类型层次结构

#

JS 类型形成一个自然的类型层次结构

  • 顶级类型:JSAny,它是非空 JS 值
    • 基本类型:JSNumberJSBooleanJSString
    • JSSymbol
    • JSBigInt
    • JSObject,它是一个 JS 对象
      • JSFunction
        • JSExportedDartFunction,它表示已转换为 JS 函数的 Dart 回调
      • JSArray
      • JSPromise
      • JSDataView
      • JSTypedArray
        • JS 类型数组,如 JSUint8Array
      • JSBoxedDartObject,它允许用户在同一个 Dart 运行时中以不透明的方式打包和传递 Dart 值
        • 从 Dart 3.4 开始,dart:js_interop 中的 ExternalDartReference 类型也允许用户以不透明的方式传递 Dart 值,但不是 JS 类型。详细了解每个选项之间的权衡此处

你可以在dart:js_interop API 文档中找到每种类型的定义。

转换

#

要将一个域中的值用于另一个域,你可能希望将该值转换为另一个域的相应类型。例如,你可能希望将 Dart List<JSString> 转换为 JS 字符串数组,它由 JS 类型 JSArray<JSString> 表示,以便可以将数组传递给 JS 互操作 API。

Dart 提供了各种 Dart 类型和 JS 类型上的多个转换成员,以便为你转换域之间的值。

用于将值从 Dart 转换为 JS 的成员通常以 toJS 开头

dart
String str = 'hello world';
JSString jsStr = str.toJS;

用于将值从 JS 转换为 Dart 的成员通常以 toDart 开头

dart
JSNumber jsNum = ...;
int integer = jsNum.toDartInt;

并非所有 JS 类型都有转换,也并非所有 Dart 类型都有转换。通常,转换表如下所示

dart:js_interop 类型Dart 类型
JSNumberJSBooleanJSStringnumintdoubleboolString
JSExportedDartFunctionFunction
JSArray<T extends JSAny?>List<T extends JSAny?>
JSPromise<T extends JSAny?>Future<T extends JSAny?>
类型化数组,如 JSUint8Arraydart:typed_data 中的类型化列表
JSBoxedDartObject不透明的 Dart 值
ExternalDartReference不透明的 Dart 值

external 声明和 Function.toJS 的要求

#

为了确保类型安全和一致性,编译器对可以流入和流出 JS 的类型提出了要求。不允许将任意 Dart 值传递到 JS 中。相反,编译器要求用户使用兼容的互操作类型、ExternalDartReference 或基元,然后由编译器隐式转换。例如,以下内容将被允许

gooddart
@JS()
external void primitives(String a, int b, double c, num d, bool e);
gooddart
@JS()
external JSArray jsTypes(JSObject _, JSString __);
gooddart
extension type InteropType(JSObject _) implements JSObject {}

@JS()
external InteropType get interopType;
gooddart
@JS()
external void externalDartReference(ExternalDartReference _);

而以下内容将返回错误

baddart
@JS()
external Function get function;
baddart
@JS()
external set list(List _);

当您使用 Function.toJS 在 JS 中调用 Dart 函数时,也会存在这些相同的要求。流入和流出此回调的值必须是兼容的互操作类型或基元。

如果您使用 String 等 Dart 基元,则编译器中会发生隐式转换,将该值从 JS 值转换为 Dart 值。如果性能至关重要,并且您不需要检查字符串的内容,那么使用 JSString 来避免转换成本可能是有意义的,就像在第二个示例中一样。

兼容性、类型检查和强制转换

#

JS 类型在运行时的类型可能因编译器而异。这会影响运行时类型检查和强制转换。因此,几乎始终避免对值是互操作类型或目标类型是互操作类型的情况进行 is 检查

baddart
void f(JSAny a) {
  if (a is String) { … }
}
baddart
void f(JSAny a) {
  if (a is JSObject) { … }
}

此外,避免在 Dart 类型和互操作类型之间进行强制转换

baddart
void f(JSString s) {
  s as String;
}

若要对 JS 值进行类型检查,请使用互操作成员,例如 typeofEqualsinstanceOfString,它们会检查 JS 值本身

gooddart
void f(JSAny a) {
  // Here `a` is verified to be a JS function, so the cast is okay.
  if (a.typeofEquals('function')) {
    a as JSFunction;
  }
}

从 Dart 3.4 开始,可以使用 isA 帮助函数来检查值是否为任何互操作类型

gooddart
void f(JSAny a) {
  if (a.isA<JSString>()) {} // `typeofEquals('string')`
  if (a.isA<JSArray>()) {} // `instanceOfString('Array')`
  if (a.isA<CustomInteropType>()) {} // `instanceOfString('CustomInteropType')`
}

根据类型参数,它会将调用转换为该类型的适当类型检查。

Dart 可能会添加提示,以便更容易避免使用 JS 互操作类型的运行时检查。有关更多详细信息,请参阅问题 #4841

nullundefined

#

JS 同时具有 nullundefined 值。这与仅具有 null 的 Dart 不同。为了使 JS 值更符合人体工程学,如果互操作成员返回 JS nullundefined,则编译器会将这些值映射到 Dart null。因此,在以下示例中,value 等成员可以解释为返回 JS 对象、JS nullundefined

dart
@JS()
external JSObject? get value;

如果未将返回类型声明为可空,则如果返回的值为 JS nullundefined,程序将抛出错误以确保健全性。

JSBoxedDartObjectExternalDartReference

#

从 Dart 3.4 开始,JSBoxedDartObjectExternalDartReference 都可用于通过 JavaScript 传递对 Dart Object 的不透明引用。但是,JSBoxedDartObject 将不透明引用包装在 JavaScript 对象中,而 ExternalDartReference 是引用本身,因此不是 JS 类型。

如果您需要 JS 类型,或者需要额外的检查以确保 Dart 值不会传递到另一个 Dart 运行时,请使用 JSBoxedDartObject。例如,如果 Dart 对象需要放置在 JSArray 中或传递给接受 JSAny 的 API,请使用 JSBoxedDartObject。否则,请使用 ExternalDartReference,因为它会更快。

请参阅 toExternalReferencetoDartObject 以转换为 ExternalDartReference 并从中转换。