ES6 的最新提案

2020-06-11 15:35 更新

1. do 表達(dá)式

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

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

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

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

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

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

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

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


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

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

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á)式,就將這個(gè)操作的意圖表達(dá)得非常簡(jiǎn)潔清晰。而且, do 塊級(jí)作用域提供了單獨(dú)的作用域,內(nèi)部操作可以與全局作用域隔絕。

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

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

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

2. throw 表達(dá)式

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

// 報(bào)錯(cuò)
console.log(throw new Error());

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

現(xiàn)在有一個(gè)提案,允許 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 不再是一個(gè)命令,而是一個(gè)運(yùn)算符。為了避免與 throw 命令混淆,規(guī)定 throw 出現(xiàn)在行首,一律解釋為 throw 語句,而不是 throw 表達(dá)式。

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

語法

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

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

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

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


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

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

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

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


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

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

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

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

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

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

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ù)也會(huì)立即反映這種變化。

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ù)會(huì)立即影響到新函數(shù)。

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

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 的時(shí)候,才會(huì)對(duì) 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 只有一個(gè)占位符,也就意味著它只能接受一個(gè)參數(shù),多余的參數(shù)都會(huì)被忽略。

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

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

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

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

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

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

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

JavaScript 的管道是一個(gè)運(yùn)算符,寫作|> 。它的左邊是一個(gè)表達(dá)式,右邊是一個(gè)函數(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 + '!';
}

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

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


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

管道運(yùn)算符只能傳遞一個(gè)值,這意味著它右邊的函數(shù)必須是一個(gè)單參數(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ù)需要兩個(gè)參數(shù)。但是,管道運(yùn)算符只能傳入一個(gè)值,因此需要事先提供另一個(gè)參數(shù),并將其改成單參數(shù)的箭頭函數(shù) => add(7, ) 。這個(gè)函數(shù)里面的下劃線并沒有特別的含義,可以用其他符號(hào)代替,使用下劃線只是因?yàn)?,它能夠形象地表示這里是占位符。

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

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


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

5. 數(shù)值分隔符

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

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

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

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

123_00 === 12_300 // true


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

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

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

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

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

下面的寫法都會(huì)報(bào)錯(cuò)。

// 全部報(bào)錯(cuò)
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 。

// 報(bào)錯(cuò)
0_b111111000
0b_111111000

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

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

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

6. Math.signbit()

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

Math.sign(-0) // -0

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

+0 === -0 // true

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

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

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

該方法的算法如下。

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

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

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

函數(shù)綁定運(yùn)算符是并排的兩個(gè)冒號(hào)( ::),雙冒號(hào)左邊是一個(gè)對(duì)象,右邊是一個(gè)函數(shù)。該運(yùn)算符會(huì)自動(dòng)將左邊的對(duì)象,作為上下文環(huán)境(即 this 對(duì)象),綁定到右邊的函數(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);
}

如果雙冒號(hào)左邊為空,右邊是一個(gè)對(duì)象的方法,則等于將該方法綁定在該對(duì)象上面。

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


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

如果雙冒號(hào)運(yùn)算符的運(yùn)算結(jié)果,還是一個(gè)對(duì)象,就可以采用鏈?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),允許隔離代碼,防止那些被隔離的代碼拿到全局對(duì)象。

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

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

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

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

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

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

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


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

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

下面的代碼可以證明,Realm 頂層對(duì)象與原始頂層對(duì)象是兩個(gè)對(duì)象。

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ù)組的原型對(duì)象,跟原始環(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 對(duì)象,導(dǎo)致報(bào)錯(cuò)。因?yàn)?console 不是語法標(biāo)準(zhǔn),是宿主環(huán)境提供的。

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

globalTwo.console = globalOne.console;

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

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 對(duì)象。但是, Realm() 構(gòu)造函數(shù)接受 { intrinsics: 'inherit' } 作為參數(shù)以后,就會(huì)繼承原始頂層對(duì)象的方法。

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

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


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

上面代碼中, FakeWindow 模擬了一個(gè)假的頂層對(duì)象 window 。

9. #! 命令

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

比如 Bash 腳本的第一行。

#!/bin/sh

Python 腳本的第一行。

#!/usr/bin/env python

現(xiàn)在有一個(gè)提案,為 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

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

10. import.meta

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

import.meta只能在模塊內(nèi)部使用,如果在模塊外部使用會(huì)報(bào)錯(cuò)。

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

(1)import.meta.url

import.meta.url 返回當(dāng)前模塊的 URL 路徑。舉例來說,當(dāng)前模塊主文件的路徑是 https://foo.com/main.js , import.meta.url 就返回這個(gè)路徑。如果模塊里面還有一個(gè)數(shù)據(jù)文件 data.txt ,那么就可以用下面的代碼,獲取這個(gè)數(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 是瀏覽器特有的元屬性,返回加載模塊的那個(gè) 元素,相當(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)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)