一個 JSer 的 Dart 學習日誌(三):類

知名噴子發表於2021-10-02

本文是“一個 JSer 的 Dart 學習日誌”系列的第三篇,本系列文章主要以挖掘 JS 與 Dart 異同點的方式,在複習和鞏固 JS 的同時平穩地過渡到 Dart 語言。
鑑於作者尚屬 Dart 初學者,所以認識可能會比較膚淺和片面,如您慧眼識蟲,希望不吝指正。
如無特殊說明,本文中 JS 包含了自 ES5 至 ES2021 的全部特性, Dart 版本則為 2.0 以上版本。

在 ES6 問世之前,廣泛流行的 JS 物件導向程式設計是使用原型鏈而非使用類,開發者需要對相關特性有足夠的瞭解,並遵循一些預設的規則,才能勉強模擬出一個大致可用的“類”。即便是 ES6 引入了 class 關鍵字來彌補,作為新一代 JS 基礎設施的類還是有待完善。
相比之下,Dart 對類的支援就要完善和強大得多。

一. 相似的整體結構

  • 兩種語言中,用於定義類的語法結構高度相似,主要包括class關鍵字、類名、包裹在花括號{}內部的成員。

    > /* Both JS and Dart */
    > class ClassName {
    >   attrA;
    >   attrB = 1;
    >
    >   methodA(a, b){
    >     // do something
    >     this.attrA = a;
    >     this.attrB = b;
    >   }
    > }

二. 建構函式

相同之處

  • 建構函式在例項化類的時候呼叫,用於處理例項化引數、初始化例項屬性等;
  • 使用 super 訪問超類的建構函式;
  • 沒有超類,或超類的建構函式沒有引數的時候,建構函式可以省略,省略建構函式的子類例項化的時候會隱式地呼叫超類的建構函式。

不同之處

1. constructor vs SameName

  • JS 中的建構函式為 constructor
  • Dart 中的建構函式為與類名一致的函式
> /* JS */                         | /* Dart */
> class Point{                     | class Point{
>   constructor(){                 |   Point(){
>   }                              |   }
> }                                | }

Dart 建構函式特有的性質

命名式建構函式

  • 在 Dart 中可以為一個類宣告多個命名式建構函式,來表達更明確的意圖,比如將一個 Map 物件對映為一個例項:

    > class PointCanBeEncode{
    >   int x = 0;
    > 
    >    // 名為 `eval` 的命名式建構函式
    >    Point.eval(Map<String, dynamic> map){
    >     x = map['x'];
    >   }
    >
    >   encode(): Map<String, dynamic>{
    >     return {
    >       'x': this.x
    >     }
    >   } 
    > }

2. 屬性賦值語法糖

  • 大多數情況下,建構函式的作用包括將給定的值作為例項的屬性, Dart 為此情形提供了一個十分方便的語法糖:

    > /* Dart */                              | /* Also Dart */
    > class Point {                           | class Point {
    >   Point(this.x, this.y);                |   Point(x, y){
    > }                                       |     this.x = x;
    >                                         |     this.y = y;
    >                                         |   }
    >                                         | }

    ↑ 可以看到左邊的程式碼明顯要簡潔得多。

3. 初始化列表

  • Dart 可以在建構函式執行之前初始化例項變數:

    class Point {
      final double x, y, distanceFromOrigin;
    
      Point(double x, double y)
          : x = x,
          y = y,
          distanceFromOrigin = sqrt(x * x + y * y)
      {
          print('still good');
      }
    }
    初始化列表的執行實際甚至早於父類建構函式的執行時機。

4. 重定向建構函式

  • Dart 可以有多個建構函式,可將某個建構函式重定向到另一個建構函式:

    class Point {
      double x, y;
    
      Point(this.x, this.y);
      Point.alongXAxis(double x) : this(x, 0);
    }
    除了預設引數外沒看到啥使用場景,試了一下 alongXAxis似乎不能有函式體。。。

5. 常量建構函式

  • 如果類生成的物件都是不變的,可以在生成這些物件時就將其變為編譯時常量。你可以在類的建構函式前加上 const 關鍵字確保所有例項變數均為 final 來實現該功能。

    class ImmutablePoint {
      // 所有變數均為 final
      final double x, y;
      // 建構函式為 const
      const ImmutablePoint(this.x, this.y);
    }

6. 工廠建構函式

  • JS 是一門相當靈活的語言,建構函式沒有返回值的時候可視為返回新的例項,但同時建構函式也可以返回任何值作為新的例項;
  • Dart 中則可以使用 factory 關鍵字,來宣告有返回值的建構函式。
> /*************** 分別用兩種語言實現單例模式 *****************/
> /* JS */                       | /* Dart */
> class A {                      | class A {
>   static single;               |   static var single;
>                                |   A(){}
>   constructor() {              |   factory A.single(){
>     if(!A.single) {            |     if(A.single == null) {
>       A.single = this;         |       A.single = A();
>     }                          |     }
>     return A.single;           |     return A.single;
>   }                            |   }
> }                              | }
工廠建構函式內不能訪問 this

7. 抽象類

  • 使用 abstruct 關鍵字宣告抽象類,抽象類常用於宣告介面方法、有時也會有具體的方法實現。
下面會提到抽象方法,抽象方法只能在抽象類中

三. 使用

相同之處

  • 均可使用 new 關鍵字例項化類;
  • 使用 . 訪問成員;
  • 使用 extends 關鍵字擴充套件類,並繼承其屬性。
> /* Both JS and Dart */
> var instance = new ClassName('propA', 42);
> instance.attrA; // 'propA'

不同之處

1. Dart 可省略 new 關鍵字

  • Dart 例項化類的 new 關鍵字可以省略,像使用一個函式那樣地初始化類:

    > var instance = ClassName('propA', 42);

  • ES5 的 也是函式,省略 new 關鍵字的話等於執行這個函式,而 ES6 的類不再是函式,省略 new 關鍵字會出錯。

2. Dart 命名式建構函式

  • 有了“命名式建構函式”,就能以更為靈活的方式建立一個例項,比如快速地將一個 Map 的屬性對映成一個例項:

    > var instance = PointCanBeEncode.eval({'x': 42});
如果有儲存、傳輸例項的需求,可以通過例項 -> Map/List -> JSON字串的方案序列化一個例項,然後通過 JSON字串 -> Map/List -> 新例項的方法將其“恢復”。

3. Dart 的編譯時常量例項

  • 常量建構函式可以例項化編譯時常量,減輕執行時負擔:

    var a = const ImmutablePoint(1, 1);

  • 而 JS 根本沒有編譯時常量。
側面說明了原生型別的建構函式都是常量建構函式。

4. 重寫超類的成員

  • 在 JS 中,子類的靜態方法可以通過 super.xxx 訪問超類的靜態方法,子類成員會覆蓋同名的超類成員,但子類可在成員函式中用 super.xxx 呼叫超類的成員(須為非靜態成員、且要先在 constructor 中呼叫 super);
  • Dart 中,通過 @override 註解來標名重寫超類的方法(實測發現不寫註解也可以編譯,Lint 會有提示,應該和環境配置有關)。

    > /* JS */                              | /* Dart */
    > class Super{                          | class Super{
    >   test(){}                            |   test(){}
    > }                                     | }
    > class Sub{                            | class Sub{
    >   /* just override it */              |   @override
    >   test(){                             |   test(){
    >     super.test();                     |     super.test()';
    >   }                                   |   }
    > }                                     | }      

Dart 的 mixin

  • 在 Dart 中宣告類的時候,用 with 關鍵字來混入一個沒有constructor的類,該類可由mixin 關鍵字宣告:

    mixin Position{
      top: 0;
      left: 0;
    }
    
    class Side with Position{
    }

四. 成員屬性和成員方法

相同之處

  • 成員函式內部使用 this 訪問當前例項,使用點號(.)訪問成員;
  • 使用 static 關鍵字定義靜態成員;
  • 定義 gettersetter

    > /* JS */                         | /* Dart */
    > class Test {                     | class Test {
    >     #a = 0;                      |   private a = 0;
    >     get b(){                     |   get b(){
    >         return this.#a;          |       return this.a;
    >     }                            |   }
    >     set b(val){                  |   set b(val){
    >         this.#a = a;             |       this.a = val;
    >     }                            |    }
    > }                                | }

不同之處

1. 類的閉合作用域

  • JS 的類沒有作用域,因此訪問成員必須用 this
  • Dart 的類有閉合作用域,在成員函式中可以直接訪問其他成員,而不必指明this
> /* JS */                         | /* Dart */
> const a = 3;                     | const a = 3;
> class Test{                      | class Test{
>   a = 1;                         |   a = 1;
>   test(){                        |   test(){
>     console.log(a === this.a);   |     print('${a == this.a}');
>     /* 'false' */                |     // 'true'
>   }                              |   }
> }                                | }

2. 私有變數/屬性

  • JS 中類例項的私有成員使用#字首宣告,訪問時也要帶上此字首;
  • Dart 中例項的私有成員使用_字首宣告,在“類作用域”中直接可以訪問。
> /* JS */                        | /* Dart */
> class Test{                     | class Test{
>   #secret = 1234;               |   _secret = 1234;
>   test(){                       |   test(){
>     console.log(this.#secret);  |     print('${_secret}');
>   }                             |   }
> }                               | }

JS 的私有成員是一個很“年輕”的屬性,在此之前,使用下劃線命名私有成員是一個被 JS 社群廣泛接受的約定。
ES 最終沒有欽定_作為私有成員的宣告方案,也沒有采用 JavaTS 中使用的 private ,而是採用了#號語法。

TypeScript:TC39老哥,你這樣讓我很難辦誒!

3. 操作符過載

  • Dart 支援過載操作符,開發者可以為例項間的運算定製邏輯,比如向量運算:

    class Vector{
      final double x, y;
    
      const Vector(this.x, this.y);
    
      Vector operator +(Vector obj) => Vector(obj.x + x, obj.y + y);
    }

    向量相加就可以寫作const c = a + b + c

憑藉操作符過載可以定義一些非常直觀的語法,例如使用 &&|| 求集合與圖形的交、並集。

JS 不支援操作符過載,所以在類似上面的向量運算場景中,我們需要自定義一些方法來進行例項之間的運算,比如多個向量相加可能會寫成:
const d = a.add(b).add(c)

4. 抽象方法

  • Dart 中抽象類的例項方法、Getter 方法以及 Setter 方法都可以是抽象的,定義一個介面方法而不去做具體的實現讓實現它的類去實現該方法,抽象方法只能存在於抽象類中

相關文章