一个 JSer 的 Dart 学习日志(三):类

本文是“一个 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 map){
    >     x = map['x'];
    >   }
    >
    >   encode(): Map{
    >     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 方法都可以是抽象的,定义一个接口方法而不去做具体的实现让实现它的类去实现该方法,抽象方法只能存在于抽象类中

你可能感兴趣的:(一个 JSer 的 Dart 学习日志(三):类)