本文是“一個 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
關鍵字定義靜態成員; 定義
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
,而是採用了#
號語法。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
方法都可以是抽象的,定義一個介面方法而不去做具體的實現讓實現它的類去實現該方法,抽象方法只能存在於抽象類中。