ES6 的最新提案

2020-06-11 15:35 更新

1. do 表達(dá)式

本質(zhì)上,塊級作用域是一個語句,將多個操作封裝在一起,沒有返回值。

{
  let t = f();
  t = t * t + 1;
}

上面代碼中,塊級作用域?qū)蓚€語句封裝在一起。但是,在塊級作用域以外,沒有辦法得到 t 的值,因?yàn)閴K級作用域不返回值,除非 t 是全局變量。

現(xiàn)在有一個提案,使得塊級作用域可以變?yōu)楸磉_(dá)式,也就是說可以返回值,辦法就是在塊級作用域之前加上 do ,使它變?yōu)?do 表達(dá)式,然后就會返回內(nèi)部最后執(zhí)行的表達(dá)式的值。

let x = do {
  let t = f();
  t * t + 1;
};

上面代碼中,變量 x 會得到整個塊級作用域的返回值( t * t + 1 )。

do 表達(dá)式的邏輯非常簡單:封裝的是什么,就會返回什么。

// 等同于 <表達(dá)式>
do { <表達(dá)式>; }


// 等同于 <語句>
do { <語句> }

do 表達(dá)式的好處是可以封裝多個語句,讓程序更加模塊化,就像樂高積木那樣一塊塊拼裝起來。

let x = do {
  if (foo()) { f() }
  else if (bar()) { g() }
  else { h() }
};

上面代碼的本質(zhì),就是根據(jù)函數(shù) foo 的執(zhí)行結(jié)果,調(diào)用不同的函數(shù),將返回結(jié)果賦給變量 x 。使用 do 表達(dá)式,就將這個操作的意圖表達(dá)得非常簡潔清晰。而且, do 塊級作用域提供了單獨(dú)的作用域,內(nèi)部操作可以與全局作用域隔絕。

值得一提的是, do 表達(dá)式在 JSX 語法中非常好用。

return (
  <nav>
    <Home />
    {
      do {
        if (loggedIn) {
          <LogoutButton />
        } else {
          <LoginButton />
        }
      }
    }
  </nav>
)

上面代碼中,如果不用 do 表達(dá)式,就只能用三元判斷運(yùn)算符( ?: )。那樣的話,一旦判斷邏輯復(fù)雜,代碼就會變得很不易讀。

2. throw 表達(dá)式

JavaScript 語法規(guī)定throw是一個命令,用來拋出錯誤,不能用于表達(dá)式之中。

// 報錯
console.log(throw new Error());

上面代碼中, console.log 的參數(shù)必須是一個表達(dá)式,如果是一個 throw 語句就會報錯。

現(xiàn)在有一個提案,允許 throw 用于表達(dá)式。

// 參數(shù)的默認(rèn)值
function save(filename = throw new TypeError("Argument required")) {
}


// 箭頭函數(shù)的返回值
lint(ast, {
  with: () => throw new Error("avoid using 'with' statements.")
});


// 條件表達(dá)式
function getEncoder(encoding) {
  const encoder = encoding === "utf8" ?
    new UTF8Encoder() :
    encoding === "utf16le" ?
      new UTF16Encoder(false) :
      encoding === "utf16be" ?
        new UTF16Encoder(true) :
        throw new Error("Unsupported encoding");
}


// 邏輯表達(dá)式
class Product {
  get id() {
    return this._id;
  }
  set id(value) {
    this._id = value || throw new Error("Invalid value");
  }
}

上面代碼中, throw 都出現(xiàn)在表達(dá)式里面。

語法上, throw 表達(dá)式里面的 throw 不再是一個命令,而是一個運(yùn)算符。為了避免與 throw 命令混淆,規(guī)定 throw 出現(xiàn)在行首,一律解釋為 throw 語句,而不是 throw 表達(dá)式。

3. 函數(shù)的部分執(zhí)行

語法

多參數(shù)的函數(shù)有時需要綁定其中的一個或多個參數(shù),然后返回一個新函數(shù)。

function add(x, y) { return x + y; }
function add7(x) { return x + 7; }

上面代碼中, add7 函數(shù)其實(shí)是 add 函數(shù)的一個特殊版本,通過將一個參數(shù)綁定為 7 ,就可以從 add 得到 add7 。

// bind 方法
const add7 = add.bind(null, 7);


// 箭頭函數(shù)
const add7 = x => add(x, 7);

上面兩種寫法都有些冗余。其中, bind 方法的局限更加明顯,它必須提供 this ,并且只能從前到后一個個綁定參數(shù),無法只綁定非頭部的參數(shù)。

現(xiàn)在有一個提案,使得綁定參數(shù)并返回一個新函數(shù)更加容易。這叫做函數(shù)的部分執(zhí)行(partial application)。

const add = (x, y) => x + y;
const addOne = add(1, ?);


const maxGreaterThanZero = Math.max(0, ...);

根據(jù)新提案, ? 是單個參數(shù)的占位符, ... 是多個參數(shù)的占位符。以下的形式都屬于函數(shù)的部分執(zhí)行。

f(x, ?)
f(x, ...)
f(?, x)
f(..., x)
f(?, x, ?)
f(..., x, ...)

? 和 ... 只能出現(xiàn)在函數(shù)的調(diào)用之中,并且會返回一個新函數(shù)。

const g = f(?, 1, ...);
// 等同于
const g = (x, ...y) => f(x, 1, ...y);

函數(shù)的部分執(zhí)行,也可以用于對象的方法。

let obj = {
  f(x, y) { return x + y; },
};


const g = obj.f(?, 3);
g(1) // 4

注意點(diǎn)

函數(shù)的部分執(zhí)行有一些特別注意的地方。

(1)函數(shù)的部分執(zhí)行是基于原函數(shù)的。如果原函數(shù)發(fā)生變化,部分執(zhí)行生成的新函數(shù)也會立即反映這種變化。

let f = (x, y) => x + y;


const g = f(?, 3);
g(1); // 4


// 替換函數(shù) f
f = (x, y) => x * y;


g(1); // 3

上面代碼中,定義了函數(shù)的部分執(zhí)行以后,更換原函數(shù)會立即影響到新函數(shù)。

(2)如果預(yù)先提供的那個值是一個表達(dá)式,那么這個表達(dá)式并不會在定義時求值,而是在每次調(diào)用時求值。

let a = 3;
const f = (x, y) => x + y;


const g = f(?, a);
g(1); // 4


// 改變 a 的值
a = 10;
g(1); // 11

上面代碼中,預(yù)先提供的參數(shù)是變量 a ,那么每次調(diào)用函數(shù) g 的時候,才會對 a 進(jìn)行求值。

(3)如果新函數(shù)的參數(shù)多于占位符的數(shù)量,那么多余的參數(shù)將被忽略。

const f = (x, ...y) => [x, ...y];
const g = f(?, 1);
g(2, 3, 4); // [2, 1]

上面代碼中,函數(shù) g 只有一個占位符,也就意味著它只能接受一個參數(shù),多余的參數(shù)都會被忽略。

寫成下面這樣,多余的參數(shù)就沒有問題。

const f = (x, ...y) => [x, ...y];
const g = f(?, 1, ...);
g(2, 3, 4); // [2, 1, 3, 4];

(4) ... 只會被采集一次,如果函數(shù)的部分執(zhí)行使用了多個 ... ,那么每個 ... 的值都將相同。

const f = (...x) => x;
const g = f(..., 9, ...);
g(1, 2, 3); // [1, 2, 3, 9, 1, 2, 3]

上面代碼中, g 定義了兩個 ... 占位符,真正執(zhí)行的時候,它們的值是一樣的。

4. 管道運(yùn)算符

Unix 操作系統(tǒng)有一個管道機(jī)制(pipeline),可以把前一個操作的值傳給后一個操作。這個機(jī)制非常有用,使得簡單的操作可以組合成為復(fù)雜的操作。許多語言都有管道的實(shí)現(xiàn),現(xiàn)在有一個提案,讓 JavaScript 也擁有管道機(jī)制。

JavaScript 的管道是一個運(yùn)算符,寫作|> 。它的左邊是一個表達(dá)式,右邊是一個函數(shù)。管道運(yùn)算符把左邊表達(dá)式的值,傳入右邊的函數(shù)進(jìn)行求值。

x |> f
// 等同于
f(x)

管道運(yùn)算符最大的好處,就是可以把嵌套的函數(shù),寫成從左到右的鏈?zhǔn)奖磉_(dá)式。

function doubleSay (str) {
  return str + ", " + str;
}


function capitalize (str) {
  return str[0].toUpperCase() + str.substring(1);
}


function exclaim (str) {
  return str + '!';
}

上面是三個簡單的函數(shù)。如果要嵌套執(zhí)行,傳統(tǒng)的寫法和管道的寫法分別如下。

// 傳統(tǒng)的寫法
exclaim(capitalize(doubleSay('hello')))
// "Hello, hello!"


// 管道的寫法
'hello'
  |> doubleSay
  |> capitalize
  |> exclaim
// "Hello, hello!"

管道運(yùn)算符只能傳遞一個值,這意味著它右邊的函數(shù)必須是一個單參數(shù)函數(shù)。如果是多參數(shù)函數(shù),就必須進(jìn)行柯里化,改成單參數(shù)的版本。

function double (x) { return x + x; }
function add (x, y) { return x + y; }


let person = { score: 25 };
person.score
  |> double
  |> (_ => add(7, _))
// 57

上面代碼中, add 函數(shù)需要兩個參數(shù)。但是,管道運(yùn)算符只能傳入一個值,因此需要事先提供另一個參數(shù),并將其改成單參數(shù)的箭頭函數(shù) => add(7, ) 。這個函數(shù)里面的下劃線并沒有特別的含義,可以用其他符號代替,使用下劃線只是因?yàn)?,它能夠形象地表示這里是占位符。

管道運(yùn)算符對于 await 函數(shù)也適用。

x |> await f
// 等同于
await f(x)


const userAge = userId |> await fetchUserById |> getAgeFromUser;
// 等同于
const userAge = getAgeFromUser(await fetchUserById(userId));

5. 數(shù)值分隔符

歐美語言中,較長的數(shù)值允許每三位添加一個分隔符(通常是一個逗號),增加數(shù)值的可讀性。比如, 1000 可以寫作1,000。

現(xiàn)在有一個提案,允許 JavaScript 的數(shù)值使用下劃線( _)作為分隔符。

let budget = 1_000_000_000_000;
budget === 10 ** 12 // true

JavaScript 的數(shù)值分隔符沒有指定間隔的位數(shù),也就是說,可以每三位添加一個分隔符,也可以每一位、每兩位、每四位添加一個。

123_00 === 12_300 // true


12345_00 === 123_4500 // true
12345_00 === 1_234_500 // true

小數(shù)和科學(xué)計數(shù)法也可以使用數(shù)值分隔符。

// 小數(shù)
0.000_001
// 科學(xué)計數(shù)法
1e10_000

數(shù)值分隔符有幾個使用注意點(diǎn)。

  • 不能在數(shù)值的最前面(leading)或最后面(trailing)。
  • 不能兩個或兩個以上的分隔符連在一起。
  • 小數(shù)點(diǎn)的前后不能有分隔符。
  • 科學(xué)計數(shù)法里面,表示指數(shù)的 e 或 E 前后不能有分隔符。

下面的寫法都會報錯。

// 全部報錯
3_.141
3._141
1_e12
1e_12
123__456
_1464301
1464301_

除了十進(jìn)制,其他進(jìn)制的數(shù)值也可以使用分隔符。

// 二進(jìn)制
0b1010_0001_1000_0101
// 十六進(jìn)制
0xA0_B0_C0

注意,分隔符不能緊跟著進(jìn)制的前綴 0b 、 0B 、 0o 、 0O 、 0x 、 0X 。

// 報錯
0_b111111000
0b_111111000

下面三個將字符串轉(zhuǎn)成數(shù)值的函數(shù),不支持?jǐn)?shù)值分隔符。主要原因是提案的設(shè)計者認(rèn)為,數(shù)值分隔符主要是為了編碼時書寫數(shù)值的方便,而不是為了處理外部輸入的數(shù)據(jù)。

  • Number()
  • parseInt()
  • parseFloat()

Number('123_456') // NaN
parseInt('123_456') // 123

6. Math.signbit()

Math.sign()用來判斷一個值的正負(fù),但是如果參數(shù)是 -0 ,它會返回 -0 。

Math.sign(-0) // -0

這導(dǎo)致對于判斷符號位的正負(fù),` Math.sign() ``不是很有用。JavaScript 內(nèi)部使用 64 位浮點(diǎn)數(shù)(國際標(biāo)準(zhǔn) IEEE 754)表示數(shù)值,IEEE 754 規(guī)定第一位是符號位, 0 表示正數(shù), 1 表示負(fù)數(shù)。所以會有兩種零, +0 是符號位為 0 時的零值, -0 是符號位為 1 時的零值。實(shí)際編程中,判斷一個值是 +0 還是 -0 非常麻煩,因?yàn)樗鼈兪窍嗟鹊摹?/p>

+0 === -0 // true

目前,有一個提案,引入了 Math.signbit() 方法判斷一個數(shù)的符號位是否設(shè)置了。

Math.signbit(2) //false
Math.signbit(-2) //true
Math.signbit(0) //false
Math.signbit(-0) //true

可以看到,該方法正確返回了 -0 的符號位是設(shè)置了的。

該方法的算法如下。

  • 如果參數(shù)是 NaN ,返回 false
  • 如果參數(shù)是 -0 ,返回 true
  • 如果參數(shù)是負(fù)值,返回 true
  • 其他情況返回 false

7. 雙冒號運(yùn)算符

箭頭函數(shù)可以綁定 this 對象,大大減少了顯式綁定 this 對象的寫法(call 、apply 、bind )。但是,箭頭函數(shù)并不適用于所有場合,所以現(xiàn)在有一個提案,提出了“函數(shù)綁定”(function bind)運(yùn)算符,用來取代 call、applybind 調(diào)用。

函數(shù)綁定運(yùn)算符是并排的兩個冒號( ::),雙冒號左邊是一個對象,右邊是一個函數(shù)。該運(yùn)算符會自動將左邊的對象,作為上下文環(huán)境(即 this 對象),綁定到右邊的函數(shù)上面。

foo::bar;
// 等同于
bar.bind(foo);


foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);


const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
  return obj::hasOwnProperty(key);
}

如果雙冒號左邊為空,右邊是一個對象的方法,則等于將該方法綁定在該對象上面。

var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;


let log = ::console.log;
// 等同于
var log = console.log.bind(console);

如果雙冒號運(yùn)算符的運(yùn)算結(jié)果,還是一個對象,就可以采用鏈?zhǔn)綄懛ā?/p>

import { map, takeWhile, forEach } from "iterlib";


getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));

8. Realm API

Realm API 提供沙箱功能(sandbox),允許隔離代碼,防止那些被隔離的代碼拿到全局對象。

以前,經(jīng)常使用<iframe>作為沙箱。

const globalOne = window;
let iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const globalTwo = iframe.contentWindow;

上面代碼中,<iframe> 的全局對象是獨(dú)立的( iframe.contentWindow )。Realm API 可以取代這個功能。

const globalOne = window;
const globalTwo = new Realm().global;

上面代碼中, Realm API 單獨(dú)提供了一個全局對象 new Realm().global 。

Realm API 提供一個 Realm() 構(gòu)造函數(shù),用來生成一個 Realm 對象。該對象的 global 屬性指向一個新的頂層對象,這個頂層對象跟原始的頂層對象類似。

const globalOne = window;
const globalTwo = new Realm().global;


globalOne.evaluate('1 + 2') // 3
globalTwo.evaluate('1 + 2') // 3

上面代碼中,Realm 生成的頂層對象的 evaluate() 方法,可以運(yùn)行代碼。

下面的代碼可以證明,Realm 頂層對象與原始頂層對象是兩個對象。

let a1 = globalOne.evaluate('[1,2,3]');
let a2 = globalTwo.evaluate('[1,2,3]');
a1.prototype === a2.prototype; // false
a1 instanceof globalTwo.Array; // false
a2 instanceof globalOne.Array; // false

上面代碼中,Realm 沙箱里面的數(shù)組的原型對象,跟原始環(huán)境里面的數(shù)組是不一樣的。

Realm 沙箱里面只能運(yùn)行 ECMAScript 語法提供的 API,不能運(yùn)行宿主環(huán)境提供的 API。

globalTwo.evaluate('console.log(1)')
// throw an error: console is undefined

上面代碼中,Realm 沙箱里面沒有 console 對象,導(dǎo)致報錯。因?yàn)?console 不是語法標(biāo)準(zhǔn),是宿主環(huán)境提供的。

如果要解決這個問題,可以使用下面的代碼。

globalTwo.console = globalOne.console;

Realm() 構(gòu)造函數(shù)可以接受一個參數(shù)對象,該參數(shù)對象的 intrinsics 屬性可以指定 Realm 沙箱繼承原始頂層對象的方法。

const r1 = new Realm();
r1.global === this;
r1.global.JSON === JSON; // false


const r2 = new Realm({ intrinsics: 'inherit' });
r2.global === this; // false
r2.global.JSON === JSON; // true

上面代碼中,正常情況下,沙箱的 JSON 方法不同于原始的 JSON 對象。但是, Realm() 構(gòu)造函數(shù)接受 { intrinsics: 'inherit' } 作為參數(shù)以后,就會繼承原始頂層對象的方法。

用戶可以自己定義 Realm 的子類,用來定制自己的沙箱。

class FakeWindow extends Realm {
  init() {
    super.init();
    let global = this.global;


    global.document = new FakeDocument(...);
    global.alert = new Proxy(fakeAlert, { ... });
    // ...
  }
}

上面代碼中, FakeWindow 模擬了一個假的頂層對象 window 。

9. #! 命令

Unix 的命令行腳本都支持 #!命令,又稱為 Shebang 或 Hashbang。這個命令放在腳本的第一行,用來指定腳本的執(zhí)行器。

比如 Bash 腳本的第一行。

#!/bin/sh

Python 腳本的第一行。

#!/usr/bin/env python

現(xiàn)在有一個提案,為 JavaScript 腳本引入了 #! 命令,寫在腳本文件或者模塊文件的第一行。

// 寫在腳本文件第一行
#!/usr/bin/env node
'use strict';
console.log(1);


// 寫在模塊文件第一行
#!/usr/bin/env node
export {};
console.log(1);

有了這一行以后,Unix 命令行就可以直接執(zhí)行腳本。

## 以前執(zhí)行腳本的方式
$ node hello.js


## hashbang 的方式
$ ./hello.js

對于 JavaScript 引擎來說,會把 #! 理解成注釋,忽略掉這一行。

10. import.meta

開發(fā)者使用一個模塊時,有時需要知道模板本身的一些信息(比如模塊的路徑)?,F(xiàn)在有一個提案,為 import 命令添加了一個元屬性 import.meta ,返回當(dāng)前模塊的元信息。

import.meta只能在模塊內(nèi)部使用,如果在模塊外部使用會報錯。

這個屬性返回一個對象,該對象的各種屬性就是當(dāng)前運(yùn)行的腳本的元信息。具體包含哪些屬性,標(biāo)準(zhǔn)沒有規(guī)定,由各個運(yùn)行環(huán)境自行決定。一般來說, import.meta至少會有下面兩個屬性。

(1)import.meta.url

import.meta.url 返回當(dāng)前模塊的 URL 路徑。舉例來說,當(dāng)前模塊主文件的路徑是 https://foo.com/main.js , import.meta.url 就返回這個路徑。如果模塊里面還有一個數(shù)據(jù)文件 data.txt ,那么就可以用下面的代碼,獲取這個數(shù)據(jù)文件的路徑。

new URL('data.txt', import.meta.url)

注意,Node.js 環(huán)境中, import.meta.url 返回的總是本地路徑,即是 file:URL 協(xié)議的字符串,比如 file:///home/user/foo.js 。

(2)import.meta.scriptElement

import.meta.scriptElement 是瀏覽器特有的元屬性,返回加載模塊的那個 元素,相當(dāng)于 document.currentScript 屬性。

// HTML 代碼為
// <script type="module" src="my-module.js" data-foo="abc"></script>


// my-module.js 內(nèi)部執(zhí)行下面的代碼
import.meta.scriptElement.dataset.foo
// "abc"
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號