前端轉Flutter - 對照Javascript學Dart

最近在學flutter,發現flutter的編程語言Dart和Javascript有諸多相似,對於前端開發者而已好處在於有JavaScript經驗學習Dart會快一些,缺點在於容易搞混這兩種語言。因此在學習的過程中記錄了一下Javascript和Dart的對比,方便記憶。

1. 程序入口(Entry Point)

Javascript:

JS不像很多語言有一個main()函數作爲程序入口,JS並沒有標準的程序入口,JS會從代碼的第一行開始執行(在執行之前會有一些預處理的工作,比如變量提升和函數提升)。在實際項目中我們通常會有一個index.js這樣的入口文件。

Dart:

Dart有一個標準的程序入口:

main(){
}

2. 數據類型(Data Types)

Javascript:

JS 有 8 種內置數據類型,分別爲:

  • 基本類型:

    • Boolean:布爾類型,有兩個值truefalse
    • Null:空類型,只有一個值null
    • Undefined:變量未初始化則爲Undefined類型
    • Number:數字類型,取值範圍爲-(2^53-1) ~ 2^53 - 1,可以爲整數和小數
    • Bigint:表示任意精度的整數,如const x = 2983479827349701793697123n
    • String:字符串類型,可用 "", '', ``表示。其中``用於字符串模板,比如:`1 + 2 = ${1+2}`
    • Symbol:符號類型,用於定義匿名且唯一的值,一般用作 Object 屬性的 key
  • Object

其中 7 個基本類型的值是不可變的(immutable value)。Object 用來定義複雜數據類型,JS內置了一些複雜類型比如:FunctionDateArrayMapSet等。

Dart:

Dart 也有 8 種內置數據類型:

  • Boolean:布爾類型,有兩個值truefalse
  • Number:數字類型,又分爲int和double類型

    • int:整型,取值範圍爲-2^63 ~ 2^63 - 1
    • double:64位雙精度浮點型
  • String:字符串類型,可用"", ''表示。與 JS 類似,可使用模板語法'${expression}',當expression是一個標識符時可省略{}
  • List:相當於 Javascript 中的 Array,例如:var arr = [1, 2, 3]
  • Set:與 JavaScript 的 Set 類似,表示無序且無重複元素的集合,例如:var countries = {'china', 'usa', 'russia', 'german'}
  • Map:與 JavaScript 的 Map 類似,表示一組鍵值對的集合,其中鍵必須唯一,鍵和值都可以爲任意類型,例如:

    var gifts = {
      'first': 'partridge',
      'second': 'turtledoves',
      'fifth': 'golden rings'
    };
  • Runes:表示字符串的 UTF-32 編碼值(Unicode 爲世界上所有書寫系統中使用的每個字母,數字和符號定義了唯一的數值。由於 Dart 字符串是 UTF-16 編碼的序列,因此在字符串中表示 32 位 Unicode 值需要特殊的語法),例如:

    Runes input = new Runes('\u2665  \u{1f605}  \u{1f60e}  \u{1f47b}  \u{1f596}  \u{1f44d}');
    print(new String.fromCharCodes(input)); // ♥  😅  😎  👻  🖖  👍
  • Symbol:與 JS 的 Symbol 不同,Dart 引入 Symbol 的意義在於在壓縮代碼後(壓縮代碼一般會修改標識符的名稱,如用a, b, c代替原有 class、function、variable 的名稱),依然能通過標識符的 Symbol 去訪問相關的成員。

與 JS 不同的是 Dart 種所有類型都是 class,所有的值都是 class 的實例,而所有的 class 都繼承於Object類。

3. 變量(Variables)

(1). 變量定義和賦值(Creating and assigning variables)

JavaScript:

JS中的變量爲動態類型,定義變量不需要也無法指定類型。

var name = 'Javascript';

變量可任意賦值其他類型的值。

name = 123; // 正確

Dart:

Dart中的變量爲靜態類型,定義變量需要指定類型,或者由編譯器進行類型推斷。

String name = 'Dart'; // 指定類型
var name2 = 'flutter'; // 推斷類型爲String

變量類型一旦確定則不能賦值其他類型的值。

name = 123; // 錯誤

如果不想指定類型或者類型未知,可以使用dynamic定義動態類型的變量:

dynamic name = 'something';

(2). 默認值(Default Values)

Javascript:

若變量未初始化,默認值爲undefined

Dart:

不管何種類型,默認值都爲null

(3). 真假值(Truthy and Falsy Values)

Javascript:

在 Javascript 中有七種值會被判定爲假值,除此之外都是真值,其中假值分別爲:

  1. false:關鍵詞false
  2. 0:數字 0
  3. 0n:BigInt 0
  4. '', "", ``:空字符串
  5. null:空值
  6. undefined:初始值
  7. NaN:not a number

Dart:

只有布爾值true是真值,其餘都是假值。

4. 常量(Constants)

Javascript:

ES6 中引入了const來定義常量。

const name = 'JavaScript';

Dart:

Dart 中有兩種方式定義常量:finalconst

區別在於:

  • final:final定義的常量只在使用時纔會初始化和分配內存
  • const:const用於定義編譯時常量(compile-time constant),即在編譯時就初始化,且值爲不變值(constant value,比如基本數據類型和String)。
  • final可以用於類實例的屬性(instance variable)而const不可以
final pi = 3.1415926;
const g = 9.8;

5. 函數(Functions)

在JS和Dart中,函數都是 “first-class object”,意味着函數可以像普通對象一樣賦值給變量、作爲參數傳遞。

(1). 普通函數

Javascipt:

function fn(a, b){
  return a + b;
}

Dart:

int sum(int a, int b){
  return a + b;
}

或者省略掉返回值(會降低可讀性,不推薦):

sum(a, b){
  return a + b;
}

(2). 匿名函數(anonymous function)

javascript:

JS中匿名函數有多種用途:

  1. 賦值給變量:
var sum = function(a, b) { return a + b };
  1. 作爲函數的參數:
setTimeout(function () {
  console.log('hello world');
}, 1000);
  1. 用於IIFE(Immediately Invoked Function Expression):
(function () {
  console.log('hello world');
})();

Dart:

Dart同樣支持匿名函數,且用法與JS類似。

  1. 賦值給變量:
Function sum = (int a, int b) {
  return a + b;
};
  1. 作爲函數的參數:
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
  print('${list.indexOf(item)}: $item');
});

(3). 箭頭函數(arrow functions)

Javascript:

ES6 中引入了箭頭函數:

(a, b) => {
  return a + b;
}

或者簡寫爲:

(a ,b) => a + b;

Dart:

Dart 中也有類似的語法:

int sum(int a, int b) => a + b;

或者,省略返回值和參數類型:

sum(a, b) => a + b;

Dart 和 JS 中箭頭函數的區別在於:

  1. JS 中箭頭函數都是匿名的,但 Dart 中可以指定名稱,當然也可以匿名,比如作爲參數傳入的時候。
  2. Dart 的箭頭函數只能用在函數體爲單個表達式的情況,比如下面寫法是不允許的:
int sum(int a, int b) => {
  int c = a + b;
  return c;
}

(4). 命名參數(named parameters)

JavaScript:

ES6 中引入了參數解構的特性(parameter destructuring)。通過傳入一個對象,並對其進行解構賦值來實現命名參數的特性。示例如下:

function sum({ a, b }) {
  return a + b;
}

sum({ a: 3, b: 4 }); // 7

Dart:

Dart原生支持命名參數:

int sum({int a, int b}) {
  return a + b;
}

sum(a: 3, b: 4); // 7

(5). 可選參數(optional parameters)

JavaScript:

JS中所有的參數都是可選參數。這可以理解爲JS的特性也可以說是設計缺陷,畢竟有時候漏傳參數又不報錯容易導致各種問題。

Dart:

在Dart中,常規的參數都是必傳的,而命名參數和位置參數(positional parameter)都可以是可選參數。當然方法體中需要增加容錯邏輯,已防止可選參數不傳導致的報錯。

  1. 可選命名參數:
int sum({int a, int b}) {
  if (b == null) b = 0;
  return a + b;
}

sum(a: 3); // 3
sum(a: 3, b: 4); // 7
  1. 可選位置參數:
int sum(int a, [int b, int c]) {
  if (b == null) b = 0;
  if (c == null) c = 0;
  return a + b + c;
}

sum(3); // 3
sum(3, 4); // 7
sum(3, 4, 5); // 12

(6). 參數默認值(default parameters)

JavaScript:

JS中實現參數默認有新舊兩種方法:

  1. 判斷參數是否爲undefined,如果是,則賦值爲默認值:
function sum(a, b){
  if(typeof a === 'undefined') a = 0;
  if(typeof b === 'undefined') b = 0;
  return a + b;
}

sum(); // 0
sum(3); // 3
sum(3,4); // 7
  1. 使用ES6的默認參數特性(default parameters):
function sum(a = 0, b = 0){
  return a + b;
}

sum(); // 0
sum(3); // 3
sum(3,4); // 7

Dart:

Dart中也支持默認參數,但是隻有命名參數和位置參數可以設置默認值:

  1. 命名參數:
int sum({int a, int b = 0}) {
  return a + b;
}

sum(a: 3); // 3
sum(a: 3, b: 4); // 7
  1. 位置參數:
int sum(int a, [int b = 0, int c = 0]) {
  return a + b + c;
}

sum(3); // 3
sum(3, 4); // 7
sum(3, 4, 5); // 12

6. 閉包(Closure)

閉包本質上也是函數,放在和函數並列的位置講是爲了凸顯閉包的重要性,也方便大家閱讀。

JS和Dart都有閉包,本質上是因爲它們都使用詞法作用域(Lexical Scope)且可以在函數內部再定義函數。所謂的詞法作用域又叫靜態作用域(Static Scope),也是大部分編程語言採用的機制,即作用域僅由代碼的本文結構確定,比如內層大括號中可以訪問外層大括號中定義的變量,而外層大括號不能訪問內層大括號中定義的變量。與詞法作用域相對的是動態作用域(Dynamic Scope),動態作用域不取決於代碼的文本結構而是程序的執行狀態、執行上下文。

當在函數內部再定義函數,而內部函數使用了外部函數的變量、參數,當外部函數返回後內部函數仍然保存了這些變量、參數。此時內部函數就成爲了一個閉包。

JS和Dart中的閉包用法也幾乎一樣,示例:

JavaScript:

function makeAdder(addBy) {
  return (x) => addBy + x;
}

var add2 = makeAdder(2);
var add4 = makeAdder(4);

add2(3); // 5
add4(3); // 7

Dart:

Function makeAdder(num addBy) {
  return (num x) => addBy + x;
}

var add2 = makeAdder(2);
var add4 = makeAdder(4);

add2(3); // 5
add4(3); // 7

7. 類(Class)

Class是面向對象編程語言的核心特性,JavaScript雖然是函數式語言,但在ES6中還是引入了class。JavaScript的class本質上是基於原型繼承的封裝,是語法糖而已,並不是真正的面向對象繼承模型的實現。而Dart天生就是面向對象的語言,所以Dart的class相比JS更加的強大和完善。

本文只比較JS和Dart的class都有的特性,而Dart的其他特性大家看 官方文檔 更合適。

(1). 定義class

JavaScript:

JS中定義一個class,有兩種方式:類聲明(class declaration)和類表達式(class expression)。我們一般都會使用類聲明。

類聲明:

class Rectangle {}

類表達式:

var Rectangle = class {}

Dart:

而Dart中只支持類聲明的方式,語法和JS一致:

class Rectangle {}

(2). 構造函數

JavaScript:

JS中class的構造函數爲統一的constructor函數,每個class只能定義一個構造函數。也可以不定義,這時會使用一個默認的構造函數。

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

Dart:

Dart中,構造函數名稱和類名相同,而且初始化成員變量之前需要先定義。

class Rectangle {
  num width, height;

  Rectangle(num height, num width) {
    this.height = height;
    this.width = width;
  }
}

爲了減少先定義再初始化成員變量的繁瑣,Dart提供了語法糖來簡化這個過程:

class Rectangle {
  num width, height;
  Rectangle(this.width, this.height);
}

與JS不同的是Dart的class能定義多個構造函數,被稱爲命名構造函數(named constructor),例如下面的Rectangle.square()

class Rectangle {
  num width, height;

  Rectangle(this.width, this.height);

  Rectangle.square() {
    width = 100;
    height = 100;
  }
}

(3). 構造函數的繼承

JavaScript:

JS中class的構造函數可以繼承,當子類未定義構造函數時默認會使用父類的構造函數:

constructor(...args) {
  super(...args);
}

而子類也可以通過調用父類的構造函數創建:

class Square extends Rectangle {}

const square = new Square(100, 100);

Dart:

Dart中,構造函數是不能繼承的! 這是Dart區別於其他很多高級語言的地方。但是當子類未定義任何構造函數時會默認使用父類的無參構造函數(no-argument constructor)。

如果要在子類中使用和父類一樣的構造函數,必須在子類中再次定義,例如這樣是不行的:

class Rectangle {
  num width, height;

  Rectangle();

  Rectangle.size(width, height) {
    this.width = width;
    this.height = height;
  }
}

class Square extends Rectangle {}

var square = Square.size(100, 100);

需要在子類中再定義一個.size()構造函數:

class Square extends Rectangle {
  Square.size(size) {
    width = size;
    height = size;
  }
}

var square = Square.size(100);

或者也可以重定向到父類的構造函數:

class Square extends Rectangle {
  Square.size(size) : super.size(size, size);
}

(4). 成員變量

JavaScript:

JS中成員變量無需定義就能使用,但是爲了類結構更清晰還是推薦先定義。成員變量可以在定義時初始化,也可以只定義而不初始化,例如:

class Rectangle {
  height = 0;
  width;
  constructor(height, width) {    
    this.height = height;
    this.width = width;
  }
}

Private變量:

成員變量默認是公共的(public),也可以定義爲私有(private),只需在變量名前面加#

class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {    
    this.#height = height;
    this.#width = width;
  }
}

Static變量:

JS的class還支持靜態(static)變量,靜態變量只會創建一份,由所有的實例公用,適合存放固定不變的值。例如:

class ClassWithStaticField {
  static staticField = 'static field';
}

Dart:

Dart中的成員變量定義方式和JS類似,可以只聲明也可以聲明+初始化:

class Rectangle {
  num width;
  num height = 100;
}

Private變量:

同樣Dart中也可以定義private變量,與JS在變量名之前加#不同,Dart在前面加_。但Dart中的private是相對於library,而不是class。

Static變量:

Dart也支持定義靜態變量:

class Queue {
  static const initialCapacity = 16;
}

print(Queue.initialCapacity); // 16 

(5). 成員函數

和成員變量類似,成員函數也分爲public、private、static。

JavaScript:

JS中成員函數默認爲public,比如:

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }

  getArea() {
    return this.height * this.width;
  }
}

Private方法:

定義private的成員函數也在函數名前面加#就行了:

class Rectangle {
  #getArea() {
    return this.height * this.width;
  }
}

Static方法:

定義Static的成員函數只需在函數名前面加static關鍵詞:

class DemoClass {
    static #privateStaticMethod() {
        return 42;
    }

    static publicStaticMethod() {
        return DemoClass.#privateStaticMethod();
    }
}

Dart:

Dart中class的成員函數定義和JS類似:

class Rectangle {
  num width;
  num height = 200;

  Rectangle(this.width, this.height);

  num getArea() {
    return width * height;
  }
}

var rect = Rectangle(30, 40);
rect.getArea(); // 1200

Private方法:

與變量一樣,方法名前面加_也會被認定爲私有方法。

Static方法:

靜態方法的定義也是在前面加上static關鍵詞。

import 'dart:math';

class Point {
  num x, y;
  Point(this.x, this.y);

  static num distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

8. 異步編程(Asynchronous Programming)

使用Dart進行異步編程總會有似曾相識感,和JS一樣都可以使用回調函數、和Promise如出一轍的Future還有async/await語法。

(1). 回調函數(Callback Function)

Javascript:

function fn(callback) {
  setTimeout(callback, 1000);
}

fn(() => { console.log('hello world') });

Dart:

import 'dart:async';

fn(Function callback){
  Timer(Duration(seconds: 1), callback);
}

fn((){
  print('hello world');
});

(2). Promise 和 Future

和 Javascript 中的Promise類似,Dart 提供了Future用於表示異步操作最終完成的結果。

Javascript:

function getIP() {
  return fetch('https://httpbin.org/ip')
    .then(res => res.json())
    .then(resJson => {
      const ip = resJson.origin;
      return ip;
    });
}

getIP()
  .then(ip => console.log(`my ip is ${ip}`))
  .catch(err => console.error(err));

Dart:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<String> getIP() {
  return http.get('https://httpbin.org/ip').then((res) {
    String ip = jsonDecode(res.body)['origin'];
    return ip;
  });
}

getIP().then((ip) => print('my ip is $ip')).catchError((error) => print(error));

(3). Async 和 Await

ES2017中引入的async/await語法進一步提升了異步編程的體驗,用同步語法進行異步編程,比如:

JavaScript:

async function getIP() {
  const res = await fetch('https://httpbin.org/ip');
  const resJson = await res.json();
  const ip = resJson.origin;
  return ip;
}

async function main() {
  try {
    const ip = await getIP();
    console.log(`my ip is ${ip}`);
  } catch (err) {
    console.error(err);
  }
}

main();

Dart:

Dart的async/await語法幾乎和JS相同,與JS的async方法返回Promise對象類似,Dart的async方法返回一個Future對象。

void main() async {
  Future<String> getIP() async {
    final res = await http.get('https://httpbin.org/ip');
    String ip = jsonDecode(res.body)['origin'];
    return ip;
  }

  try {
    final ip = await getIP();
    print('my ip is $ip');
  } catch (err) {
    print(err);
  }
}

有未涉及到的JS和Dart的語法對比,歡迎大家補充,我會及時更新。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章