本文是“一个 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 构造函数特有的性质
1. 命名式构造函数
在 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 > } > } > }
当然,鉴于 JS 是一门十分灵活的语言,我们也可以自行把
JS
的静态方法包装成类似Dart
的命名式构造函数的用法。
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(single == null) {
> A.single = this; | single = A();
> } | }
> return A.single; | return single;
> } | }
> } | }
> var instance = new A(); | var instance = 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
关键字定义静态成员; 定义
getter
和setter
:> /* 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 最终没有钦定_
作为私有成员的声明方案,也没有采用Java
和TS
中使用的private
,而是采用了#
号语法。
Dart 中,#
语法用于Symbol
的字面量了。
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)
;const d = Vector.add(a, b, c)
。
4. 抽象方法
- Dart 中抽象类的实例方法、
Getter
方法以及Setter
方法都可以是抽象的,定义一个接口方法而不去做具体的实现,让实现它的类去实现该方法,抽象方法只能存在于抽象类中。
5. call
方法与可调用类实例
- JS 中只有函数可以调用,我们期望实现“有状态”的函数(比如递增的 ID 生成器)的时候,通常采用函数体外变量或者函数返回内层函数(利用闭包的性质);
在 Dart 中,部署了
call
方法的类,其实例可以作为函数来调用,调用的是该实例的call
方法:> /* JS */ | // Dart > const IdGen = () => { | class IdGen { > let seed = 0; | int _seed = 0; > return () => { | call(){ > return ++seed; | return ++_seed; > } | } > }; | } > const idGen = IdGen(); | final idGen = IdGen(); > console.log(idGen()); | print(idGen()); > // 1 | // 1
。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。