翻譯:瘋狂的技術宅
原文: http://2ality.com/2019/01/fut...
本文首發微信公衆號:jingchengyideng
歡迎關注,天天都給你推送新鮮的前端技術文章javascript
近年來,JavaScript 的功能獲得了大幅度的增長,本文探討了其仍然缺失的東西。html
說明:前端
有關前兩個問題的更多想法,請參閱本文第8節:語言設計部分。java
目前,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
若是對象經過身份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 的管理。 例如:
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);
JavaScript 的數字是基於 IEEE 754 標準的64位浮點數(雙精度數)。鑑於它們的表示形式是基於二進制的,在處理小數部分時可能會出現舍入偏差:
> 0.1 + 0.2 0.30000000000000004
這在科學計算和金融技術(金融科技)中尤爲成問題。基於十進制運算的提案目前處於階段0。它們可能最終被這樣使用(注意十進制數的後綴 m
):
> 0.1m + 0.2m 0.3m
目前,在 JavaScript 中對值進行分類很是麻煩:
typeof
或 instanceof
。typeof
有一個衆所周知的的怪癖,就是把 null
歸類爲「對象」。我還認爲函數被歸類爲 'function'
一樣是奇怪的。> typeof null 'object' > typeof function () {} 'function' > typeof [] 'object'
instanceof
不適用於來自其餘realm(框架等)的對象。也許可能經過庫來解決這個問題(若是我有時間,就會實現一個概念性的驗證)。
不幸的是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; }; };
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階段。
管道操做目前有兩個競爭提案 。在本文,咱們研究其中的 智能管道(另外一個提議被稱爲 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 ;
一直以來 JavaScript 對併發性的支持頗有限。併發進程的事實標準是 Worker API,能夠在 web browsers 和 Node.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(全局變量等)。我想在將來看到一個更加輕量級的構造。
JavaScript 仍然明顯落後於其餘語言的一個領域是它的標準庫。固然保持最小化是有意義的,由於外部庫更容易進化和適應。可是有一些核心功能也是有必要的。
JavaScript 標準庫是在其語言具備模塊以前建立的。所以函數被放在命名空間對象中,例如Object
,Reflect
,Math
和JSON
:
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 只提供了不多的工具來處理 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
筆記:
很高興能看到對數據的非破壞性轉換有更多的支持。兩個相關的庫是:
JavaScript 對日期和時間的內置支持有許多奇怪的地方。這就是爲何目前建議用庫來完成除了最基本任務以外的其它全部工做。
值得慶幸的是 temporal
是一個更好的時間 API:
const dateTime = new CivilDateTime(2000, 12, 31, 23, 59); const instantInChicago = dateTime.withZone('America/Chicago');
一個相對流行的提議功能是 optional chaining。如下兩個表達式是等效的。
obj?.prop (obj === undefined || obj === null) ? undefined : obj.prop
此功能對於屬性鏈特別方便:
obj?.foo?.bar?.baz
可是,仍然存在缺點:
optional chaining 的替代方法是在單個位置提取一次信息:
不管採用哪一種方法,均可以執行檢查並在出現問題時儘早拋出異常。
進一步閱讀:
目前正在爲 運算符重載 進行早期工做,可是 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 函數的好處是:
下面是嵌套表達式的例子:
a @plus b @minus c @times d times(minus(plus(a, b), c), d)
有趣的是,管道操做符還有助於提升可讀性:
plus(a, b) |> minus(#, c) |> times(#, d)
如下是我偶爾會遺漏的一些東西,但我認爲不如前面提到的那些重要:
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);
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'])
不會很快!當前開發時的靜態類型(經過 TypeScript 或 Flow)和運行時的純 JavaScript 之間的分離效果很好。因此沒有什麼合理的理由改變它。
Web 的一個關鍵要求是:永遠不要破壞向後兼容性:
經過引入當前功能的更好版本,仍然能夠修復一些錯誤。
有關此主題的更多信息,請參閱「針對不耐煩的程序員的 JavaScript 」。
做爲一名語言設計師,不管你作什麼,都會使一些人開心,而另外一些人會傷心。所以,設計將來 JavaScript 功能的主要挑戰不是讓每一個人都滿意,而是讓語言儘量保持一致。
可是對於「一致」的含義,也存在分歧。所以,咱們能夠作到的最好的事情就是創建一致的「風格」,由一小羣人(最多三人)構思和執行。不過這並不排除他們接受許多其餘人的建議和幫助,但他們應該設定一個基調。
引用 Fred Brooks):
稍微回顧一下,儘管許多優秀實用的軟件系統都是由委員會設計的,而且是做爲一些項目的一部分而構建的,可是從本質上說,那些擁有大量激情粉絲的軟件就是 一個或幾個設計思想的產品,——致偉大的設計師。
這些核心設計師的一個重要職責是對功能說「不」,以防止 JavaScript 變得太大。
他們還須要一個強大的支持系統,由於語言設計者每每會遭到嚴重的濫用(由於人們關心而且不喜歡聽到「不」)。 最近的一個例子是 Guido van Rossum 辭去了首席 Python 語言設計師的工做,由於他受到了虐待。
這些想法可能也有助於設計和見證 JavaScript:
鳴謝:感謝Daniel Ehrenberg對本博文的反饋!
歡迎繼續閱讀本專欄其它高贊文章: