一個 JSer 的 Dart 學習日誌(一):函式

知名噴子發表於2021-09-25
近來想擴充一下技能,感覺 Flutter 挺有奔頭,所以就扎進來了,但要使用 Flutter 必先學會 Dart。粗略看了一下,Dart 在計上與 JS 很像:事件驅動、單執行緒環境、熟悉的 var/const 關鍵字……
既然我已經學會 JS 了,從挖掘兩種語言的共同點並帶入相關的不同點入手可能是一種不錯的學習方案,也可以順道複習一下 JS 。
當然,這系列文章更多的是自學者的一些見解,疏漏和謬誤難免,如果諸君慧眼識蟲,望不吝指正。
如無特殊說明,本文中的 JS 包含 ES5 至 ES2021 的全部特性。

1. 普通函式

相同之處

  • 函式結構相似:首先是函式宣告關鍵字(這裡略有不同,後面會說),後面是函式名,然後分別是以圓括號()包裹、以逗號 , 區分的引數列表,最後是以花括號{}包裹的函式體:

    > // JavaScript                    |  // Dart
    > function foo(){                  |  int foo() {
    >     // do some thing here        |      // do some thing here 
    > }                                |  }

  • 使用 = 為函式引數指定預設值;
  • JS 可以使用物件作為引數,從而規定該物件的某些值,Dart 也支援形似的語法(不過呼叫函式的時候傳值方案不太一樣,後面詳說)。

    > // JavaScript                    |  // Dart
    > function foo(a = 1, b = 2){      |  int foo(a = 1, b = 2){
    >    return a + b;                 |      return a + b;
    > }                                |  }

不同之處

宣告關鍵字不同

  • JS 中的普通函式須以 function 關鍵字宣告;
  • Dart 中的普通函式以型別宣告開始,該宣告也可以預設。

    > // JS: starts with `function`    | // Dart 可預設返回型別
    > function foo(a = 1, b = 2){      | /*int*/ foo(a = 1, b = 2){
    >     return a + b;                |    return a + b;
    > }                                | }

JS 不支援型別宣告

  • JS 不支援型別宣告,因此返回值和引數的型別只有在執行時才會確定;
  • Dart 支援型別宣告,可以指定返回值與引數的型別,但均可以預設。

    > // JS                            | // Dart
    > function foo(a = 1, b = 2){      | int foo(int a = 1, int b = 2){
    >     return a + b;                |    return a + b;
    > }                                | }

可選引數

  • JS 的所有引數(除了作為命名引數載體的物件,及其前面的位置引數之外)都是可選引數;
  • Dart 函式可使用一個 [] 包裹其引數列表尾部的可選引數,所有未標為required 的命名引數都是可選引數。

    // Dart 位置引數
    foo(int a = 1, [int b = 2]){
      return a + b;
    }
    // Dart 命名引數
    foo({required a, b = 2}){
      return a + b;
    }

命名引數的傳入方式

  • 如前所述,JS 的命名引數實際上是使用物件佔位,所以呼叫函式的時候,直接在對應位置傳入一個物件即可:

    function testFunc(a, {b = 1}){
      return a + b;
    }
    testFunc(a, {b: 2}); // 3
    testFunc(a); // 會報錯,因為第二個位置引數必須是一個物件

  • Dart 的命名引數是特定的語法,呼叫函式的時候用 name: value 的方式指定:

    int testFunc(a, {b = 1}){
      return a + b;
    }
    testFunc(a, b : 2); // 3

相比之下 Dart 的語法可以簡潔也可以精確,但是初見的時候多少有些不習慣。

其他

  • 在某個例子裡,我似乎瞥見了 foo.call ,看來某些 JS 的“精粹”也被 Dart 借用了,不知道二者有多大的相似之處;
  • JS 中沒有可變引數的概念,但可以用 arguments 或者 [...args] 訪問函式的全部引數,這是在開發中使用高階函式(HOC)的基礎,但是我暫未看到類似的 Dart 語法或方法,不知道使用 Dart 如何獲取未知長度的引數列表;
  • 兩種語言中的匿名函式的異同點基本與命名函式一致,按下不表。
  • 提到 JS ,就不得不說那“迷人”的 this 指向,幸運的是 Dart 的普通函式沒有這個東西!
  • JS 的命名函式可以在宣告的同時賦給一個變數,但是在 Dart 中只能對匿名函式這麼做:

    > JS                               | // Dart 把註釋去掉,將無法通過編譯
    > var foo = function fooName (){   | var foo = /*fooName*/ (){
    >   // do some thing               |   // do some thing
    > }                                | };
    >                                  | //↑ 注意!賦值語句後的分號不可或缺

2. 箭頭函式(Dart中叫“箭頭表示式”)

相似之處

  • 兩者的語法都是 (引數列表) => 表示式

    > // JS                           | // Dart
    > let foo = (a) => a++;           | var foo = (a) => a++;

不同之處

圓括號不可省

  • JS 的箭頭函式,如果只有一個引數的話,引數列表外可以不套圓括號;
  • Dart 的箭頭函式必須用圓括號包括引數列表。

    > // JS                           | // Dart
    > var foo = a => a++;             | var foo = (a) => a++;

Dart 箭頭表示式只能寫一個表示式

嚴格來說二者都只能寫一個表示式,但是兩種語言中{}的語意不同,表現出來就是 JS 的箭頭函式表達力更強一點。
  • 在 JS 中,胖箭頭後面的花括號和普通函式的花括號一樣,表示函式體,其中包含的是一個程式碼塊,如果有返回值的話需要在末尾處 return
  • 但在 Dart 中,胖箭頭後的花括號是 SetMap 的字面量語法,加了花括號意味著函式的返回值變成了一個 SetMap 例項。

    > // JS                            | // Dart:return 你就錯了
    > var foo = (a) => {return a++};   | var foo = (a) => {/*return*/ a++};
    > console.log(`res: ${foo(2)}`);   | print(`res: ${foo(2)}`));
    > // res: 3                        | // res: {3}  // 返回的是一個 Set

    ,如果你對 Dart 的處理方式不太適應,想想 JS 箭頭函式後面是[]的情形,另外還有人想這樣返回一個物件:

    var foo = a => { a: a };
    // 當然是不可能的,這會被識別成 label 語法,而非物件

    ,在 JS 中無法做到,現在 Dart 幫你實現了:這樣的語法會返回一個 Map例項。

  • 不過如果你想在箭頭函式中多放些語句,可以用一個 IIFE 包裹起來:

    var testFunc = (start, {end = 7}) => 
    (() {
      var finalEnd = start + end;
      for(num i = start; i < finalEnd; i++){
        display((i + 1).toString());
      }
    })();

    ,嗯……讓人不由得想起 ES5 的某經典面試題。

如果我強迫自己加個圓括號——

var foo = (a) => ({'a': a});

的形式,那麼 JS 和 Dart 的表現就很相似,前者返回物件,後者返回 Map

或者,下面的程式碼

var fun = (a) => [a];

在兩種語言中的表現高度一致:JS 返回 Array;Dart 返回 List。

3. Dart 的 main 函式

  • 眾所周知, JS 沒有入口函式的說法,在當前執行上下文中“裸泳”的表示式會直接被執行,而 JS 的頂層作用域也是第一個執行上下文。
  • 不過 Dart 並不阻止你在全域性作用域寫表示式——前提是把它們寫在變數宣告語句裡:

    void main(){
      // invoke functions
    }
    var a = 3 + 2 - 5 * 0;

    ;Dart 也不阻止你在全域性呼叫函式——但是這個語句可能在編譯時優化掉了:

    void main(){
      // invoke functions
    }
    
    testFunc(){
      print('called me');
    }
    
    var testVar = testFunc();
    // 控制檯空空如也

    (當然這可能是 web 版 DartPad 的特色,其他語法檢查器/編譯器可能會報錯,規範之外的sao操作建議還是少做一點比較好)。

相關文章