ES6 (二十九)ES6最新提案、do、throw、函式部分執行、管道運算子|>、柯里化、::、Realm API
文章目錄
- ES6 (二十九)ES6最新提案、do、throw、函式部分執行、管道運算子|>、柯里化、::、Realm API
- 1. do 運算式
- 2. throw 運算式
- 3. 函式的部分執行
- 語法
- 注意點
- 4. 管道運算子
- 5. Math.signbit()
- 6. 雙冒號運算子
- 7. Realm API
- 8. `#!`命令
- 9. import.meta
- 10. JSON 模塊
1. do 運算式
本質上,塊級作用域是一個陳述句,將多個操作封裝在一起,沒有回傳值,
{
let t = f();
t = t * t + 1;
}
上面代碼中,塊級作用域將兩個陳述句封裝在一起,但是,在塊級作用域以外,沒有辦法得到t的值,因為塊級作用域不回傳值,除非t是全域變數,
現在有一個提案,使得塊級作用域可以變為運算式,也就是說可以回傳值,辦法就是在塊級作用域之前加上do,使它變為do運算式,然后就會回傳內部最后執行的運算式的值,
let x = do {
let t = f();
t * t + 1;
};
上面代碼中,變數x會得到整個塊級作用域的回傳值(t * t + 1),
do運算式的邏輯非常簡單:封裝的是什么,就會回傳什么,
// 等同于 <運算式>
do { <運算式>; }
// 等同于 <陳述句>
do { <陳述句> }
do運算式的好處是可以封裝多個陳述句,讓程式更加模塊化,就像樂高積木那樣一塊塊拼裝起來,
let x = do {
if (foo()) { f() }
else if (bar()) { g() }
else { h() }
};
上面代碼的本質,就是根據函式foo的執行結果,呼叫不同的函式,將回傳結果賦給變數x,使用do運算式,就將這個操作的意圖表達得非常簡潔清晰,而且,do塊級作用域提供了單獨的作用域,內部操作可以與全域作用域隔絕,
值得一提的是,do運算式在 JSX 語法中非常好用,
return (
<nav>
<Home />
{
do {
if (loggedIn) {
<LogoutButton />
} else {
<LoginButton />
}
}
}
</nav>
)
上面代碼中,如果不用do運算式,就只能用三元判斷運算子(?:),那樣的話,一旦判斷邏輯復雜,代碼就會變得很不易讀,
2. throw 運算式
JavaScript 語法規定throw是一個命令,用來拋出錯誤,不能用于運算式之中,
// 報錯
console.log(throw new Error());
上面代碼中,console.log的引數必須是一個運算式,如果是一個throw陳述句就會報錯,
現在有一個提案,允許throw用于運算式,
// 引數的默認值
function save(filename = throw new TypeError("Argument required")) {
}
// 箭頭函式的回傳值
lint(ast, {
with: () => throw new Error("avoid using 'with' statements.")
});
// 條件運算式
function getEncoder(encoding) {
const encoder = encoding === "utf8" ?
new UTF8Encoder() :
encoding === "utf16le" ?
new UTF16Encoder(false) :
encoding === "utf16be" ?
new UTF16Encoder(true) :
throw new Error("Unsupported encoding");
}
// 邏輯運算式
class Product {
get id() {
return this._id;
}
set id(value) {
this._id = value || throw new Error("Invalid value");
}
}
上面代碼中,throw都出現在運算式里面,
語法上,throw運算式里面的throw不再是一個命令,而是一個運算子,為了避免與throw命令混淆,規定throw出現在行首,一律解釋為throw陳述句,而不是throw運算式,
3. 函式的部分執行
語法
多引數的函式有時需要系結其中的一個或多個引數,然后回傳一個新函式,
function add(x, y) { return x + y; }
function add7(x) { return x + 7; }
上面代碼中,add7函式其實是add函式的一個特殊版本,通過將一個引數系結為7,就可以從add得到add7,
// bind 方法
const add7 = add.bind(null, 7);
// 箭頭函式
const add7 = x => add(x, 7);
上面兩種寫法都有些冗余,其中,bind方法的局限更加明顯,它必須提供this,并且只能從前到后一個個系結引數,無法只系結非頭部的引數,
現在有一個提案,使得系結引數并回傳一個新函式更加容易,這叫做函式的部分執行(partial application),
const add = (x, y) => x + y;
const addOne = add(1, ?);//`?`和`...`只能出現在函式的呼叫之中,并且會回傳一個新函式,
const maxGreaterThanZero = Math.max(0, ...);
根據新提案,?是單個引數的占位符,...是多個引數的占位符,以下的形式都屬于函式的部分執行,
f(x, ?)
f(x, ...)
f(?, x)
f(..., x)
f(?, x, ?)
f(..., x, ...)
?和...只能出現在函式的呼叫之中,并且會回傳一個新函式,
const g = f(?, 1, ...);
// 等同于
const g = (x, ...y) => f(x, 1, ...y);
函式的部分執行,也可以用于物件的方法,
let obj = {
f(x, y) { return x + y; },
};
const g = obj.f(?, 3);
g(1) // 4
注意點
函式的部分執行有一些特別注意的地方,
(1)函式的部分執行是基于原函式的,如果原函式發生變化,部分執行生成的新函式也會立即反映這種變化,
let f = (x, y) => x + y;
const g = f(?, 3);
g(1); // 4
// 替換函式 f
f = (x, y) => x * y;
g(1); // 3
上面代碼中,定義了函式的部分執行以后,更換原函式會立即影響到新函式,
(2)如果預先提供的那個值是一個運算式,那么這個運算式并不會在定義時求值,而是在每次呼叫時求值,
let a = 3;
const f = (x, y) => x + y;
const g = f(?, a);
g(1); // 4
// 改變 a 的值
a = 10;
g(1); // 11 每次呼叫函式`g`的時候,才會對`a`進行求值,
上面代碼中,預先提供的引數是變數a,那么每次呼叫函式g的時候,才會對a進行求值,
(3)如果新函式的引數多于占位符的數量,那么多余的引數將被忽略,
const f = (x, ...y) => [x, ...y];
const g = f(?, 1);
g(2, 3, 4); // [2, 1]
上面代碼中,函式g只有一個占位符,也就意味著它只能接受一個引數,多余的引數都會被忽略,
寫成下面這樣,多余的引數就沒有問題,
const f = (x, ...y) => [x, ...y];
const g = f(?, 1, ...);
g(2, 3, 4); // [2, 1, 3, 4];
(4)...只會被采集一次,如果函式的部分執行使用了多個...,那么每個...的值都將相同,
const f = (...x) => x;
const g = f(..., 9, ...);
g(1, 2, 3); // [1, 2, 3, 9, 1, 2, 3]
上面代碼中,g定義了兩個...占位符,真正執行的時候,它們的值是一樣的,
4. 管道運算子
Unix 作業系統有一個管道機制(pipeline),可以把前一個操作的值傳給后一個操作,這個機制非常有用,使得簡單的操作可以組合成為復雜的操作,許多語言都有管道的實作,現在有一個提案,讓 JavaScript 也擁有管道機制,
JavaScript 的管道是一個運算子,寫作|>,它的左邊是一個運算式,右邊是一個函式,管道運算子把左邊運算式的值,傳入右邊的函式進行求值,
x |> f
// 等同于
f(x)
管道運算子最大的好處,就是可以把嵌套的函式,寫成從左到右的鏈式運算式,
function doubleSay (str) {
return str + ", " + str;
}
function capitalize (str) {
return str[0].toUpperCase() + str.substring(1);
}
function exclaim (str) {
return str + '!';
}
上面是三個簡單的函式,如果要嵌套執行,傳統的寫法和管道的寫法分別如下,
// 傳統的寫法
exclaim(capitalize(doubleSay('hello')))
// "Hello, hello!"
// 管道的寫法
'hello'
|> doubleSay
|> capitalize
|> exclaim
// "Hello, hello!"
管道運算子只能傳遞一個值,這意味著它右邊的函式必須是一個單引數函式,如果是多引數函式,就必須進行柯里化,改成單引數的版本,
柯里化(回傳新函式):
- 柯里化,英語:Currying(果然是滿滿的英譯中的既視感),是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,并且回傳接受余下的引數而且回傳結果的新函式的技術,
- 看這個解釋有一點抽象,我們就拿被做了無數次示例的add函式,來做一個簡單的實作,
// 普通的add函式 function add(x, y) { return x + y } // Currying后 function curryingAdd(x) { return function (y) { return x + y } } add(1, 2) // 3 curryingAdd(1)(2) // 3
- 實際上就是把add函式的x,y兩個引數變成了先用一個函式接收x然后回傳一個函式去處理y引數,現在思路應該就比較清晰了,就是只傳遞給函式一部分引數來呼叫它,讓它回傳一個函式去處理剩下的引數,
- Currying有哪些好處呢:
- 引數復用
- 提前確認
- 延遲運行:像我們js中經常使用的bind,實作的機制就是Currying.
function double (x) { return x + x; }
function add (x, y) { return x + y; }
let person = { score: 25 };
person.score
|> double
|> (_ => add(7, _))
// 57
上面代碼中,add函式需要兩個引數,但是,管道運算子只能傳入一個值,因此需要事先提供另一個引數,并將其改成單引數的箭頭函式_ => add(7, _),這個函式里面的下劃線并沒有特別的含義,可以用其他符號代替,使用下劃線只是因為,它能夠形象地表示這里是占位符,
管道運算子對于await函式也適用,
x |> await f
// 等同于
await f(x)
const userAge = userId |> await fetchUserById |> getAgeFromUser;
// 等同于
const userAge = getAgeFromUser(await fetchUserById(userId));
5. Math.signbit()
Math.sign()用來判斷一個值的正負,但是如果引數是-0,它會回傳-0,
Math.sign(-0) // -0
這導致對于判斷符號位的正負,Math.sign()不是很有用,JavaScript 內部使用 64 位浮點數(國際標準 IEEE 754)表示數值,IEEE 754 規定第一位是符號位,0表示正數,1表示負數,所以會有兩種零,+0是符號位為0時的零值,-0是符號位為1時的零值,實際編程中,判斷一個值是+0還是-0非常麻煩,因為它們是相等的,
+0 === -0 // true
目前,有一個提案,引入了Math.signbit()方法判斷一個數的符號位是否設定了,
Math.signbit(2) //false
Math.signbit(-2) //true
Math.signbit(0) //false
Math.signbit(-0) //true
可以看到,該方法正確回傳了-0的符號位是設定了的,
該方法的演算法如下,
- 如果引數是
NaN,回傳false - 如果引數是
-0,回傳true - 如果引數是負值,回傳
true - 其他情況回傳
false
6. 雙冒號運算子
箭頭函式可以系結this物件,大大減少了顯式系結this物件的寫法(call、apply、bind),但是,箭頭函式并不適用于所有場合,所以現在有一個提案,提出了“函式系結”(function bind)運算子,用來取代call、apply、bind呼叫,
函式系結運算子是并排的兩個冒號(::),雙冒號左邊是一個物件,右邊是一個函式,該運算子會自動將左邊的物件,作為背景關系環境(即this物件),系結到右邊的函式上面,
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);
如果雙冒號運算子的運算結果,還是一個物件,就可以采用鏈式寫法,
import { map, takeWhile, forEach } from "iterlib";
getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));
7. Realm API
Realm API 提供沙箱功能(sandbox),允許隔離代碼,防止那些被隔離的代碼拿到全域物件,
以前,經常使用<iframe>作為沙箱,
const globalOne = window;
let iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const globalTwo = iframe.contentWindow;
上面代碼中,<iframe>的全域物件是獨立的(iframe.contentWindow),Realm API 可以取代這個功能,
const globalOne = window;
const globalTwo = new Realm().global;
上面代碼中,Realm API單獨提供了一個全域物件new Realm().global,
Realm API 提供一個Realm()建構式,用來生成一個 Realm 物件,該物件的global屬性指向一個新的頂層物件,這個頂層物件跟原始的頂層物件類似,
const globalOne = window;
const globalTwo = new Realm().global;
globalOne.evaluate('1 + 2') // 3
globalTwo.evaluate('1 + 2') // 3
上面代碼中,Realm 生成的頂層物件的evaluate()方法,可以運行代碼,
下面的代碼可以證明,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 沙箱里面的陣列的原型物件,跟原始環境里面的陣列是不一樣的,
Realm 沙箱里面只能運行 ECMAScript 語法提供的 API,不能運行宿主環境提供的 API,
globalTwo.evaluate('console.log(1)')
// throw an error: console is undefined
上面代碼中,Realm 沙箱里面沒有console物件,導致報錯,因為console不是語法標準,是宿主環境提供的,
如果要解決這個問題,可以使用下面的代碼,
globalTwo.console = globalOne.console;
Realm()建構式可以接受一個引數物件,該引數物件的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()建構式接受{ intrinsics: 'inherit' }作為引數以后,就會繼承原始頂層物件的方法,
用戶可以自己定義Realm的子類,用來定制自己的沙箱,
class FakeWindow extends Realm {
init() {
super.init();
let global = this.global;
global.document = new FakeDocument(...);
global.alert = new Proxy(fakeAlert, { ... });
// ...
}
}
上面代碼中,FakeWindow模擬了一個假的頂層物件window,
8. #!命令
Unix 的命令列腳本都支持#!命令,又稱為 Shebang 或 Hashbang,這個命令放在腳本的第一行,用來指定腳本的執行器,
比如 Bash 腳本的第一行,
#!/bin/sh
Python 腳本的第一行,
#!/usr/bin/env python
現在有一個提案,為 JavaScript 腳本引入了#!命令,寫在腳本檔案或者模塊檔案的第一行,
// 寫在腳本檔案第一行
#!/usr/bin/env node
'use strict';
console.log(1);
// 寫在模塊檔案第一行
#!/usr/bin/env node
export {};
console.log(1);
有了這一行以后,Unix 命令列就可以直接執行腳本,
# 以前執行腳本的方式
$ node hello.js
# hashbang 的方式
$ ./hello.js
對于 JavaScript 引擎來說,會把#!理解成注釋,忽略掉這一行,
9. import.meta
開發者使用一個模塊時,有時需要知道模板本身的一些資訊(比如模塊的路徑),現在有一個提案,為 import 命令添加了一個元屬性import.meta,回傳當前模塊的元資訊,
import.meta只能在模塊內部使用,如果在模塊外部使用會報錯,
這個屬性回傳一個物件,該物件的各種屬性就是當前運行的腳本的元資訊,具體包含哪些屬性,標準沒有規定,由各個運行環境自行決定,一般來說,import.meta至少會有下面兩個屬性,
(1)import.meta.url
import.meta.url回傳當前模塊的 URL 路徑,舉例來說,當前模塊主檔案的路徑是https://foo.com/main.js,import.meta.url就回傳這個路徑,如果模塊里面還有一個資料檔案data.txt,那么就可以用下面的代碼,獲取這個資料檔案的路徑,
new URL('data.txt', import.meta.url)
注意,Node.js 環境中,import.meta.url回傳的總是本地路徑,即是file:URL協議的字串,比如file:///home/user/foo.js,
(2)import.meta.scriptElement
import.meta.scriptElement是瀏覽器特有的元屬性,回傳加載模塊的那個<script>元素,相當于document.currentScript屬性,
// HTML 代碼為
// <script type="module" src="my-module.js" data-foo="abc"></script>
// my-module.js 內部執行下面的代碼
import.meta.scriptElement.dataset.foo
// "abc"
10. JSON 模塊
import 命令目前只能用于加載 ES 模塊,現在有一個提案,允許加載 JSON 模塊,
假定有一個 JSON 模塊檔案config.json,
{
"appName": "My App"
}
目前,只能使用fetch()加載 JSON 模塊,
const response = await fetch('./config.json');
const json = await response.json();
import 命令能夠直接加載 JSON 模塊以后,就可以像下面這樣寫,
import configData from './config.json' assert { type: "json" };
console.log(configData.appName);
上面示例中,整個 JSON 物件被匯入為configData物件,然后就可以從該物件獲取 JSON 資料,
==import命令匯入 JSON 模塊時,命令結尾的assert {type: "json"}不可缺少,這叫做匯入斷言,用來告訴 JavaScript 引擎,現在加載的是 JSON 模塊,==你可能會問,為什么不通過.json后綴名判斷呢?因為瀏覽器的傳統是不通過后綴名判斷檔案型別,標準委員會希望遵循這種做法,這樣也可以避免一些安全問題,
匯入斷言是 JavaScript 匯入其他格式模塊的標準寫法,JSON 模塊將是第一個使用這種語法匯入的模塊,以后,還會支持匯入 CSS 模塊、HTML 模塊等等,
動態加載模塊的import()函式也支持加載 JSON 模塊,
import('./config.json', { assert: { type: 'json' } })
腳本加載 JSON 模塊以后,還可以再用 export 命令輸出,這時,可以將 export 和 import 結合成一個陳述句,
export { config } from './config.json' assert { type: 'json' };
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/293749.html
標籤:其他
上一篇:javascript高級(二)
