6

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

 2 years ago
source link: https://segmentfault.com/a/1190000040750334
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文是“一个 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 方法都可以是抽象的,定义一个接口方法而不去做具体的实现让实现它的类去实现该方法,抽象方法只能存在于抽象类中

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK