本質(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ì)變得很不易讀。
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á)式。
多參數(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
函數(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í)候,它們的值是一樣的。
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));
歐美語言中,較長(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)。
下面的寫法都會(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('123_456') // NaN
parseInt('123_456') // 123
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ù)可以綁定 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));
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 。
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ì)把 #! 理解成注釋,忽略掉這一行。
開發(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"
更多建議: