JavaScript 的將來:它還少些什麼?

翻譯:瘋狂的技術宅
原文: http://2ality.com/2019/01/fut...

本文首發微信公衆號:jingchengyideng
歡迎關注,天天都給你推送新鮮的前端技術文章javascript


近年來,JavaScript 的功能獲得了大幅度的增長,本文探討了其仍然缺失的東西。html

說明:前端

  1. 我只列出了我所發現的最重要的功能缺失。固然還有不少其它有用的功能,但同時也會增長太多的風險。
  2. 個人選擇是主觀的。
  3. 本文中說起的幾乎全部內容都在 TC39 的技術雷達上。 也就是說,它還能夠做爲將來可能的 JavaScript 的預覽。

有關前兩個問題的更多想法,請參閱本文第8節:語言設計部分。java

1. 值

1.1 按值比較對象

目前,JavaScript 只能對原始值(value)進行比較,例如字符串的值(經過查看其內容):node

> 'abc' === 'abc'
true

相反,對象則經過身份ID(identity)進行比較(對象僅嚴格等於自身):python

> {x: 1, y: 4} === {x: 1, y: 4}
false

若是有一種可以建立按值進行比較對象的方法,那將是很不錯的:git

> #{x: 1, y: 4} === #{x: 1, y: 4}
true

另外一種可能性是引入一種新的類(確切的細節還有待肯定):程序員

@[ValueType]
class Point {
  // ···
}

旁註:這種相似裝飾器的將類標記爲值類型的的語法基於草案提案github

1.2 將對象放入數據結構中

若是對象經過身份ID進行比較,將它們放入 ECMAScript 數據結構(如Maps)中並無太大意義:web

const m = new Map();
m.set({x: 1, y: 4}, 1);
m.set({x: 1, y: 4}, 2);
assert.equal(m.size, 2);

能夠經過自定義值類型修復此問題。 或者經過自定義 Set 元素和 Map keys 的管理。 例如:

  • 經過哈希表映射:須要一個操做來檢查值是否相等,另外一個操做用於建立哈希碼。 若是使用哈希碼,則對象應該是不可變的。 不然破壞數據結構就太容易了。
  • 經過排序樹映射:須要一個比較值的操做用來管理它存儲的值。

1.3 大整數

JavaScript 的數字老是64位的(雙精度),它能爲整數提供53位二進制寬度。這意味着若是超過53位,就很差使了:

> 2 ** 53
9007199254740992
> (2 ** 53) + 1  // can’t be represented
9007199254740992
> (2 ** 53) + 2
9007199254740994

對於某些場景,這是一個至關大的限制。如今有[BigInts提案](http://2ality.com/2017/03/es-...),這是真正的整數,其精度能夠隨着須要而增加:

> 2n ** 53n
9007199254740992n
> (2n ** 53n) + 1n
9007199254740993n

BigInts還支持 casting,它爲你提供固定位數的值:

const int64a = BigInt.asUintN(64, 12345n);
const int64b = BigInt.asUintN(64, 67890n);
const result = BigInt.asUintN(64, int64a * int64b);

1.4 小數計算

JavaScript 的數字是基於 IEEE 754 標準的64位浮點數(雙精度數)。鑑於它們的表示形式是基於二進制的,在處理小數部分時可能會出現舍入偏差:

> 0.1 + 0.2
0.30000000000000004

這在科學計算和金融技術(金融科技)中尤爲成問題。基於十進制運算的提案目前處於階段0。它們可能最終被這樣使用(注意十進制數的後綴 m):

> 0.1m + 0.2m
0.3m

1.5 對值進行分類

目前,在 JavaScript 中對值進行分類很是麻煩:

  • 首先,你必須決定是否使用 typeofinstanceof
  • 其次,typeof 有一個衆所周知的的怪癖,就是把 null 歸類爲「對象」。我還認爲函數被歸類爲 'function' 一樣是奇怪的。
> typeof null
'object'
> typeof function () {}
'function'
> typeof []
'object'
  • 第三,instanceof 不適用於來自其餘realm(框架等)的對象。

也許可能經過庫來解決這個問題(若是我有時間,就會實現一個概念性的驗證)。

2. 函數式編程

2.1 更多表達式

不幸的是C風格的語言在表達式和語句之間作出了區分:

// 條件表達式
let str1 = someBool ? 'yes' : 'no';

// 條件聲明
let str2;
if (someBool) {
  str2 = 'yes';
} else {
  str2 = 'no';
}

特別是在函數式語言中,一切都是表達式。 Do-expressions 容許你在全部表達式上下文中使用語句:

let str3 = do {
  if (someBool) {
    'yes'
  } else {
    'no'
  }
};

下面的代碼是一個更加現實的例子。若是沒有 do-expression,你須要一個當即調用的箭頭函數來隱藏範圍內的變量 result

const func = (() => {
  let result; // cache
  return () => {
    if (result === undefined) {
      result = someComputation();
    }
    return result;
  }
})();

使用 do-expression,你能夠更優雅地編寫這段代碼:

const func = do {
  let result;
  () => {
    if (result === undefined) {
      result = someComputation();
    }
    return result;
  };
};

2.2 匹配:解構 switch

JavaScript 使直接使用對象變得容易。可是根據對象的結構,沒有內置的切換 case 分支的方法。看起來是這樣的(來自提案的例子):

const resource = await fetch(jsonService);
case (resource) {
  when {status: 200, headers: {'Content-Length': s}} -> {
    console.log(`size is ${s}`);
  }
  when {status: 404} -> {
    console.log('JSON not found');
  }
  when {status} if (status >= 400) -> {
    throw new RequestError(res);
  }
}

正如你所看到的那樣,新的 case 語句在某些方面相似於 switch,不過它使用解構來挑選分支。當人們使用嵌套數據結構(例如在編譯器中)時,這種功能很是有用。 模式匹配提案目前處於第1階段。

2.3 管道操做

管道操做目前有兩個競爭提案 。在本文,咱們研究其中的 智能管道(另外一個提議被稱爲 F# Pipelines)。

管道操做的基本思想以下。請考慮代碼中的嵌套函數調用。

const y = h(g(f(x)));

可是,這種表示方法一般不能體現咱們對計算步驟的見解。在直覺上,咱們將它們描述爲:

  • 從值 x 開始。
  • 而後把 f() 做用在 x 上。
  • 而後將 g() 做用於結果。
  • 而後將 h() 應用於結果。
  • 最後將結果賦值給 y

管道運算符能讓咱們更好地表達這種直覺:

const y = x |> f |> g |> h;

換句話說,如下兩個表達式是等價的。

f(123)
123 |> f

另外,管道運算符支持部分應用程序(相似函數的 .bind() 方法):如下兩個表達式是等價的。

123 |> f(#)
123 |> (x => f(x))

使用管道運算符一個最大的好處是,你能夠像使用方法同樣使用函數——而無需更改任何原型:

import {map} from 'array-tools';
const result = arr |> map(#, x => x * 2);

最後,讓咱們看一個長一點的例子(取自提案並稍做編輯):

promise
|> await #
|> # || throw new TypeError(
  `Invalid value from ${promise}`)
|> capitalize // function call
|> # + '!'
|> new User.Message(#)
|> await stream.write(#)
|> console.log // method call
;

3 併發

一直以來 JavaScript 對併發性的支持頗有限。併發進程的事實標準是 Worker API,能夠在 web browsersNode.js (在 v11.7 及更高版本中沒有標記)中找到。

在Node.js中的使用方法它以下所示:

const {
  Worker, isMainThread, parentPort, workerData
} = require('worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename, {
    workerData: 'the-data.json'
  });
  worker.on('message', result => console.log(result));
  worker.on('error', err => console.error(err));
  worker.on('exit', code => {
    if (code !== 0) {
      console.error('ERROR: ' + code);
    }
  });
} else {
  const {readFileSync} = require('fs');
  const fileName = workerData;
  const text = readFileSync(fileName, {encoding: 'utf8'});
  const json = JSON.parse(text);
  parentPort.postMessage(json);
}

唉,相對來講 Workers 是重量級的 —— 每一個都有本身的 realm(全局變量等)。我想在將來看到一個更加輕量級的構造。

4. 標準庫

JavaScript 仍然明顯落後於其餘語言的一個領域是它的標準庫。固然保持最小化是有意義的,由於外部庫更容易進化和適應。可是有一些核心功能也是有必要的。

4.1 用模塊替代命名空間對象

JavaScript 標準庫是在其語言具備模塊以前建立的。所以函數被放在命名空間對象中,例如ObjectReflectMathJSON

  • Object.keys()
  • Reflect.ownKeys()
  • Math.sign()
  • JSON.parse()

若是將這個功能放在模塊中會更好。它必須經過特殊的URL訪問,例如使用僞協議 std

// Old:
assert.deepEqual(
  Object.keys({a: 1, b: 2}),
  ['a', 'b']);

// New:
import {keys} from 'std:object';
assert.deepEqual(
  keys({a: 1, b: 2}),
  ['a', 'b']);

好處是:

  • JavaScript 將變得更加模塊化(這能夠加快啓動時間並減小內存消耗)。
  • 調用導入的函數比調用存儲在對象中的函數更快。

4.2 可迭代工具 (sync 與 async)

迭代 的好處包括按需計算和支持許多數據源。可是目前 JavaScript 只提供了不多的工具來處理 iterables。例如,若是要 過濾、映射或消除重複,則必須將其轉換爲數組:

const iterable = new Set([-1, 0, -2, 3]);
const filteredArray = [...iterable].filter(x => x >= 0);
assert.deepEqual(filteredArray, [0, 3]);

若是 JavaScript 具備可迭代的工具函數,你能夠直接過濾迭代:

const filteredIterable = filter(iterable, x => x >= 0);
assert.deepEqual(
  // We only convert the iterable to an Array, so we can
  // check what’s in it:
  [...filteredIterable], [0, 3]);

如下是迭代工具函數的一些示例:

// Count elements in an iterable
assert.equal(count(iterable), 4);

// Create an iterable over a part of an existing iterable
assert.deepEqual(
  [...slice(iterable, 2)],
  [-1, 0]);

// Number the elements of an iterable
// (producing another – possibly infinite – iterable)
for (const [i,x] of zip(range(0), iterable)) {
  console.log(i, x);
}
// Output:
// 0, -1
// 1, 0
// 2, -2
// 3, 3

筆記:

  • 有關迭代器的工具函數示例,請參閱 Python 的 itertoolshttps://docs.python.org/3/lib...)。
  • 對於 JavaScript,迭代的每一個工具函數應該有兩個版本:一個用於同步迭代,一個用於異步迭代。

4.3 不可變數據

很高興能看到對數據的非破壞性轉換有更多的支持。兩個相關的庫是:

  • Immer 相對輕量,適用於普通對象和數組。
  • Immutable.js 更強大,更重量級,並附帶本身的數據結構。

4.4 更好地支持日期和時間

JavaScript 對日期和時間的內置支持有許多奇怪的地方。這就是爲何目前建議用庫來完成除了最基本任務以外的其它全部工做。

值得慶幸的是 temporal 是一個更好的時間 API:

const dateTime = new CivilDateTime(2000, 12, 31, 23, 59);
const instantInChicago = dateTime.withZone('America/Chicago');

5. 可能不須要的功能

5.1 optional chaining 的優缺點

一個相對流行的提議功能是 optional chaining。如下兩個表達式是等效的。

obj?.prop
(obj === undefined || obj === null) ? undefined : obj.prop

此功能對於屬性鏈特別方便:

obj?.foo?.bar?.baz

可是,仍然存在缺點:

  • 深層嵌套的結構更難管理。
  • 因爲在訪問數據時過於寬容,隱藏了之後可能會出現的問題,更難以調試。

optional chaining 的替代方法是在單個位置提取一次信息:

  • 你能夠編寫一個提取數據的輔助函數。
  • 或者你能夠編寫一個函數,使其輸入是深層嵌套數據,可是輸出更簡單、標準化的數據。

不管採用哪一種方法,均可以執行檢查並在出現問題時儘早拋出異常。

進一步閱讀:

5.2 咱們須要運算符重載嗎?

目前正在爲 運算符重載 進行早期工做,可是 infix 函數可能就足夠了(目前尚未提案):

import {BigDecimal, plus} from 'big-decimal';
const bd1 = new BigDecimal('0.1');
const bd2 = new BigDecimal('0.2');
const bd3 = bd1 @plus bd2; // plus(bd1, bd2)

infix 函數的好處是:

  • 你能夠建立 JavaScript 之外的運算符。
  • 與普通函數相比,嵌套表達式的可讀性仍然很好。

下面是嵌套表達式的例子:

a @plus b @minus c @times d
times(minus(plus(a, b), c), d)

有趣的是,管道操做符還有助於提升可讀性:

plus(a, b)
  |> minus(#, c)
  |> times(#, d)

6. 各類小東西

如下是我偶爾會遺漏的一些東西,但我認爲不如前面提到的那些重要:

  • 鏈式異常:使你可以捕獲錯誤,可以包含其餘信息並再次拋出。
new ChainedError(msg, origError)
re`/^${RE_YEAR}-${RE_MONTH}-${RE_DAY}$/u`
  • 正則表達式的轉義文本(對於.replace()很重要):
> const re = new RegExp(RegExp.escape(':-)'), 'ug');
> ':-) :-) :-)'.replace(re, '🙂')
'🙂 🙂 🙂'
  • 支持負索引的 Array.prototype.get()
> ['a', 'b'].get(-1)
  'b'
function f(...[x, y] as args) {
    if (args.length !== 2) {
      throw new Error();
    }
    // ···
  }
  • 檢查對象的深度相等性(有可能:可選地使用謂詞進行參數化,以支持自定義數據結構):
assert.equal(
    {foo: ['a', 'b']} === {foo: ['a', 'b']},
    false);
  assert.equal(
    deepEqual({foo: ['a', 'b']}, {foo: ['a', 'b']}),
    true);
  • 枚舉:向 JavaScript 添加枚舉的一個好處是能夠縮小與 TypeScript 的差距。目前有兩份提案草案(還沒有處於正式階段)。 一個是Rick Waldron另外一個是Ron Buckton。在兩個提案中,最簡單的語法以下所示:
enum WeekendDay {
    Saturday, Sunday
  }
  const day = WeekendDay.Sunday;
const myMap = Map!{1: 2, three: 4, [[5]]: 6}
    // new Map([1,2], ['three',4], [[5],6])
  
  const mySet = Set!['a', 'b', 'c'];
    // new Set(['a', 'b', 'c'])

7. FAQ:將來的JavaScript

7.1 JavaScript 會不會支持靜態類型?

不會很快!當前開發時的靜態類型(經過 TypeScript 或 Flow)和運行時的純 JavaScript 之間的分離效果很好。因此沒有什麼合理的理由改變它。

7.2 爲何咱們不能經過刪除怪異和過期的功能來清理 JavaScript?

Web 的一個關鍵要求是:永遠不要破壞向後兼容性:

  • 缺點是語言會有許多遺留功能。
  • 但好處大於缺點:大型代碼庫仍然是同質的,遷移到新版本很簡單,引擎仍然較小(不須要支持多個版本)等等

經過引入當前功能的更好版本,仍然能夠修復一些錯誤。

有關此主題的更多信息,請參閱「針對不耐煩的程序員的 JavaScript 」。

8. 關於語言設計的思考

做爲一名語言設計師,不管你作什麼,都會使一些人開心,而另外一些人會傷心。所以,設計將來 JavaScript 功能的主要挑戰不是讓每一個人都滿意,而是讓語言儘量保持一致。

可是對於「一致」的含義,也存在分歧。所以,咱們能夠作到的最好的事情就是創建一致的「風格」,由一小羣人(最多三人)構思和執行。不過這並不排除他們接受許多其餘人的建議和幫助,但他們應該設定一個基調。

引用 Fred Brooks):

稍微回顧一下,儘管許多優秀實用的軟件系統都是由委員會設計的,而且是做爲一些項目的一部分而構建的,可是從本質上說,那些擁有大量激情粉絲的軟件就是 一個或幾個設計思想的產品,——致偉大的設計師。

這些核心設計師的一個重要職責是對功能說「不」,以防止 JavaScript 變得太大。

他們還須要一個強大的支持系統,由於語言設計者每每會遭到嚴重的濫用(由於人們關心而且不喜歡聽到「不」)。 最近的一個例子是 Guido van Rossum 辭去了首席 Python 語言設計師的工做,由於他受到了虐待。

8.1 其餘想法

這些想法可能也有助於設計和見證 JavaScript:

  • 建立描述 JavaScript 將來前景的路線圖。這樣的路線圖能夠用講故事的方式並將許多單獨的部分鏈接成一個連貫的總體。我所知的最後一個這樣的路線圖是 Brendan Eich 的「和諧的夢想」。
  • 記錄設計理念。如今,ECMAScript 規範只記錄了 怎樣 作,而沒有 爲何 。舉個例子:可枚舉性的目的是什麼?
  • 規範的解釋者。半正式的規範部分幾乎已經可執行。若是可以像編程語言同樣對待和運行它們會很棒。 (你可能須要一個約定來區分規範代碼和非規範輔助函數。)

鳴謝:感謝Daniel Ehrenberg對本博文的反饋!


本文首發微信公衆號:jingchengyideng

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章


歡迎繼續閱讀本專欄其它高贊文章:

相關文章
相關標籤/搜索