內容

Dart 值和 JS 值屬於不同的語言網域。編譯成 Wasm 時,它們也會在不同的執行環境中執行。因此,您應該將 JS 值視為外來類型。為了提供 JS 值的 Dart 類型,dart:js_interop 公開了一組以 JS 為字首的類型,稱為「JS 類型」。這些類型用於在編譯時區分 Dart 值和 JS 值。

重要的是,這些類型會根據您編譯為 Wasm 或 JS 而以不同的方式具象化。這表示它們的執行時間類型會有所不同,因此您無法使用is 檢查和as 轉換。為了與這些 JS 值互動並檢查這些值,您應該使用external 互操作成員或轉換

類型階層

#

JS 類型形成一個自然的類型層級

  • 頂層類型:JSAny,這是任何非 null 的 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?>
JSUint8Array 等的類型化陣列來自 dart: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 或從 ExternalDartReference 轉換。