2019-03 の TC39 meeting
まとめ
決まったこと
Web compatibility issues / Needs Consensus PRs
歴史的な理由から typeof 演算子の項目に Implementation-defined が存在している。
table: typeof Operator Results
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Symbol "symbol"
Object (ordinary and does not implement Call) "object"
Object (standard exotic and does not implement Call) "object"
Object (implements Call) "function"
Object (non-standard exotic and does not implement Call) Implementation-defined. Must not be "undefined",
"boolean", "function", "number", "symbol", or "string".
table: new typeof Operator Results
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Symbol "symbol"
Object (does not implement Call) "object"
Object (implements Call) "function"
コンセンサスが得られた。
函数のボディに --> が来るとやばいことになる。
code: (console)
eshost -sx 'Function("-->")'
# ch, xs
SyntaxError
# d8, jsc, jsshell, node
(no error, --> read as HTML comment)
現在の仕様では SyntaxError になるはずだが、V8, SpiderMonkey そして JavaScriptCore では動くようになっている。Web 互換性の観点から CreateDynamicFunction の仕様を変えて valid にする PR。コンセンサスが得られず、もし進めたい場合は Stage として進める必要があるということになった。
Async Generator Functions 内で Sync Iterator を yield* すると内部で AsyncFromSyncIterator が作られる。これを使うと %AsyncIteratorPrototype% にアクセスすることが出来てしまうので %AsyncIteratorPrototype%[@@asyncIterator] を上書きすることが出来てしまう。このバグを修正する PR。コンセンサスが得られた。
前提知識として Generator Functions によって作られた IterableIterator を for-of でイテレートして途中で中断しても、そのイテレーターはすでに終了している仕様がある。
code: (js)
function* numberGenerator() {
for (let i = 0; i < 10; ++i) { yield i; }
}
const numberIterableIterator = numberGenerator();
for (const val of numberIterableIterator) {
console.log(val); // 0, 1, 2
if (val === 2) { break; }
}
numberIterableIterator.next(); // { value: undefined, done: true }
これは for-of が中断された際に IteratorClose が実行され、そこで Itarator に "return" メソッドが(存在したときに)呼ばれることによって実現される。
code: (js)
function* numberGenerator() {
for (let i = 0; i < 10; ++i) { yield i; }
}
const numberIterableIterator = numberGenerator();
numberIterableIterator.next(); // { value: 0, done: false }
numberIterableIterator.next(); // { value: 1, done: false }
numberIterableIterator.next(); // { value: 2, done: false }
numberIterableIterator.return(); // { value: undefined, done: true }
numberIterableIterator.next(); // { value: undefined, done: true }
この IteratorClose で Iterator の return プロパティに非 Callable なものが入っていると、正しく例外を投げてくれないためそれを修正する PR。コンセンサスが得られた。
以前から提案されていた export * as ns from "mod" を修正して、改めて仕様に入れる PR。コンセンサスが得られた。
Date#{toString, toUTCString} は Date#toISOString と違い Date の year に負数が入っているときに環境依存になってしまっている。
code: (js)
let year1BC = new Date("0001-10-13T05:16:33Z");
let year11BC = new Date("0001-10-13T05:16:33Z");
year1BC.setFullYear(-1, 10, 13);
year11BC.setFullYear(-11, 10, 13);
print(year1BC.toString());
print(year11BC.toString());
print(year1BC.toUTCString());
print(year11BC.toUTCString());
code: result
# d8, jsc
Sat Nov 13 -001 22:16:33 GMT-0800 (Pacific Standard Time)
Mon Nov 13 -011 22:16:33 GMT-0800 (Pacific Standard Time)
Sun, 14 Nov -001 06:16:33 GMT
Tue, 14 Nov -011 06:16:33 GMT
# sm
Sat Nov 13 -0001 22:16:33 GMT-0800 (Pacific Standard Time)
Mon Nov 13 -0011 22:16:33 GMT-0800 (Pacific Standard Time)
Sun, 14 Nov -0001 06:16:33 GMT
Tue, 14 Nov -0011 06:16:33 GMT
# ch
Sat Nov 13 -1 22:16:33 GMT-0800 (Pacific Standard Time)
Mon Nov 13 -11 22:16:33 GMT-0800 (Pacific Standard Time)
Sun, 14 Nov 2 B.C. 06:16:33 GMT
Tue, 14 Nov 12 B.C. 06:16:33 GMT
この問題を修正するとともに、Date#toUTCString が RFC 7231 をもとにしていることを記載する。コンセンサスが得られた。
Intl.NumberFormat#format において BigInt をどういう扱いにするかの議論。前回オーバーロードするか別メソッドにするかが話されたが、今回前者でコンセンサスが得られた。仕様としては Number もしくは BigInt に変換される ToNumeric が導入され、また BigInt#toLocalString も Number#toLocalString と同じ形で定義された。
Stage 4 (ES2020)
String#match は正規表現のキャプチャが取れないため、取れるようにするために新しいメソッドを作る提案。無事 Stage 4 になった 🎉
Stage 3
Top-level await や更新された ECMAScript の仕様との衝突があったりして難しい状況らしい。6月に Stage 4 になるのを目標にしている。
Intl.DateTimeFormat のオプションを拡張する提案。
code: (js)
const o1 = new Intl.DateTimeFormat("en" , {
timeStyle: "short"
});
o1.format(Date.now()); // "13:31"
const o2 = new Intl.DateTimeFormat("en" , {
dateStyle: "short"
});
o2.format(Date.now()); // "21.03.2012"
const o3 = new Intl.DateTimeFormat("en" , {
timeStyle: "medium",
dateStyle: "short"
});
o3.format(Date.now()); // "21.03.2012, 13:31"
順調に Stage 3 になった。
Intl.DateTimeFormat#formatRangeToParts を使うと、生成する文字列を type ごとに分けて得ることも出来る。
code: (js)
const date1 = new Date(Date.UTC(2007, 0, 10)); // "Jan 10, 2007"
const date2 = new Date(Date.UTC(2007, 0, 20)); // "Jan 20, 2007"
const fmt = new Intl.DateTimeFormat("en", {
hour: 'numeric',
minute: 'numeric'
});
fmt.formatRange(date1, date2);
// > '10:00 – 11:00 AM'
fmt.formatRangeToParts(date1, date2);
// [
// { type: 'hour', value: '10', source: "startRange" },
// { type: 'literal', value: ':', source: "startRange" },
// { type: 'minute', value: '00', source: "startRange" },
// { type: 'literal', value: ' – ', source: "shared" },
// { type: 'hour', value: '11', source: "endRange" },
// { type: 'literal', value: ':', source: "endRange" },
// { type: 'minute', value: '00', source: "endRange" },
// { type: 'literal', value: ' ', source: "shared" },
// { type: 'dayPeriod', value: 'AM', source: "shared" }
// ]
前回からの変更点としては渡された Date の順序が逆のときや undefined が入れられたときに RangeError を投げるようになった。こちらも順調に Stage 3 になった。
fulfilled, rejected にかかわらずに全ての Promise が settled されたときに fulfilled される Promise.allSettled の提案。Promise では現在のステータスは隠蔽されているため、それらの情報も含めたオブジェクトの形で返す。
code: (ts)
interface Fulfilled {
status: "fulfilled";
value: any;
}
interface Rejected {
status: "rejected";
reason: any;
}
interface Promise {
allSettled: (Iterable<Promise<any>>) => Promise<Array<Fulfilled | Rejected>>;
}
議論の中でもっとローレベルな Bluebird の Promise.reflect を採用しないか話された。これは Promise の現在の状態を Promise<PromiseInspection> として受け取ることが出来るようになっている。
code: (ts)
interface PromiseInspection {
any reason()
any value()
boolean isPending()
boolean isRejected()
boolean isFulfilled()
boolean isCancelled()
}
これがあると Promise.allSettled を作ることが出来るが、こちらは今回は見送られた。
Stage 3 になった。
Number のリテラルに _ のセパレーターを入れる提案、前回将来的に他の仕様と衝突する恐れがないかを考慮するために一旦保留となったが、今回無事 Stage 3 になった。
Stage 2
Function#toString や Error#stack において、函数の中身を出力しないようにする提案。censorship から hiding に名前が変わった。Strict Mode と似ていて、函数の一番始めに "hide implementation" を入れると中身が見れない状態になる。
トップレベルで await 出来るようにする提案。
code: (js)
// awaiting.mjs
import { process } from "./some-module.mjs";
const dynamic = await import(computedModuleSpecifier);
const data = await fetch(url);
export const output = process(dynamic.default, data);
HTML Modules や WebAssembly Modules など、必ず非同期になってしまうモジュールに有用とのこと。今のところ実装やテストはなく V8 チームが仕様を進め、レビューしている状態。
Qiita に Decorators の変遷についてざっくり書いた。
現在のディスクリプタベースの Decorators の提案は柔軟性が高いものの実行時に遅くなってしまう問題がある。予めビルトインで Decorators を用意してそれを組み合わせて扱うようにすると複雑性は高くなるものの最適化が効きやすいため、それらを用意する方がいいのではという話がされた。@register, @wrap, @initialize そして @expose が基本的なビルトイン Decorators となる。
code: @register (js)
class C {
@register(f) method() { }
}
// ↓
class C {
method() { }
}
f(C.prototype, "method");
@register(f)
class C { }
// ↓
class C {
method() { }
}
f(C);
code: (js)
decorator @defineElement(name, options) {
@register(klass => customElements.define(name, klass, options))
}
@defineElement('my-class')
class MyClass extends HTMLElement {
/* ... */
}
code: @wrap (js)
class C {
@wrap(f) method() { }
}
// ↓
class C {
method() { }
}
C.prototype.method = f(C.prototype.method);
@wrap(f)
class C { }
// ↓
class C { }
C = f(C);
code: @initialize (js)
class C {
@initialize(f) a = b;
}
// ↓
class C {
constructor() {
f(this, "a", b);
}
}
@initialize(f)
class C { }
// ↓
class C {
constructor() {
f(this);
}
}
code: (js)
decorator @bound {
@initialize((instance, name) => instancename = instancename.bind(instance)) }
class Foo {
x = 1;
@bound method() {
console.log(this.x);
}
queueMethod() {
setTimeout(this.method, 1000);
}
}
new Foo().queueMethod();
// logs 1, not undefined
code: @expose (js)
class C {
}
// ↓
class C {
@register(proto =>
f(proto,"#x", instance => instance.#x, (instance, value) => instance.#x = value ))
}
こちらの提案でフィードバックを得るらしい。面白そうではあるが技巧的なコーディング能力が求められそうではある。
GC された後で呼ばれるコールバックを FinalizationGroup として分ける形になった。
code: (ts)
interface WeakRef<T extends object> {
constructor(target: T);
deref(): T | undefined;
}
interface FinalizationGroup<T, U = any> {
constructor(cleanupCallback: (items: IterableIterator<T>) => void);
register(target: object, holdings: T, unregisterToken?: U): void;
unregister(unregisterToken: U): boolean;
cleanupSome(cleanupCallback: (items: IterableIterator<T>) => void): void;
}
Date を置き換えるやつ。前回からの変更点として入力値として ToNumeric を採用して BigInt 対応をしたのと、タイムゾーンがある ZonedInstance を ZonedDateTime にリネームした。
今後は toLocalString の詳細を決めることになった。どうやら Standard Library の提案にブロックされており、先に進めない状態になっているらしい。
Error#stack の標準化。Error から得られた文字列を解析せずに構造化された情報を得られることを目的としている。
今後の流れとしては
1. 各ブラウザのスタックの状況を調査する
2. 各ブラウザの実装の類似点、相違点をまとめる
3. 1つのアルゴリズムとしてまとめることが出来るか試みる
4. 全く新しいスタックの構造を作成する必要性があるか確かめる
5. 各実装に必要な変更点を調べる
String#replace で第一引数に文字列を入れたときにマッチした部分全てを変更することが出来ない。その目的なら正規表現を使うか、String#split, String#join を組み合わせて使う必要がある。これを解決するために文字列を入れること前提で String#replaceAll を導入する提案。
code: (js)
const queryString = "q=query+string+parameters"; // 本来は WHATWG URL を使うべきではある……
// 単に文字列を入れてしまうと、最初にマッチした部分しか変換されない
queryString.replace("+", " "); // "q=query string+parameters"
// 正規表現を使う
queryString.replace(/\+/g, " "); // "q=query string parameters"
// String#split, String#join を使う
queryString.split('+').join(" "); // "q=query string parameters"
// この提案
queryString.replaceAll("+", " "); // "q=query string parameters"
String#replaceAll では第一引数に RegExp オブジェクトを入れると RangeError を投げるっぽい。うーん、VM での最適化に有利らしいが、そこまでしてほしいかなという気持ちになるな……。Stage 2 になった。
Stage 1
Numeric リテラルを Decorators で拡張する提案。今回は特に進展がなさそうに見える。
Date.parse の標準化。ISO 8601 や RFC 3339 との関係などが確認された。現状の Date.parse にはアルゴリズムが定義されておらず、完全に実装依存状態になっている。そもそもどういう方針で進めていくかにたいしてコンセンサスが得られず、Stage 2 にはならなかったっぽい。
Promise.any の提案。Promise.race と似ているが、全ての Promise が rejected されて初めて rejected される。もし rejected された場合は AggregateError を返し、AggregateError#errors が全ての rejected された reasons を配列として持つようになっている。
code: (js)
Promise.any([
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c')
]).then((first) => {
// Any of the promises was fulfilled.
console.log(first);
// → e.g. 'b'
}).catch((error) => {
// All of the promises were rejected.
console.log(error);
console.assert(error instanceof AggregateError);
});
Stage 1 になった。
Private State のプロパティを外部でシェア出来るようにするために Private Declarations を導入する提案。
code: (js)
class Example {
}
const ex = new Example();
console.log(ex.#hello);
// => 'world!'
うーん、やっていることは Private Symbol な気もするが、ECMAScript としては Private Symbol を辞めて Private State を選択したというのもあるのでこっちが正当な進化みたいな扱いなのかな。色々と仕様が複雑になりそうな気がする。Stage 1 になった。
Stage 0
Promise のライブラリ互換のために "then" メソッドを持つオブジェクトはすべて thenable 扱いになっている。これは Bluebird のようなライブラリを使用した場合は助かる仕様ではあるがアクシデントも引き起こしてしまう(とくに await した場合)。
code: (js)
function foo() {
return { value: 'foo', then(resolve) { resolve('bar') } }
}
await foo(); // bar
code: (js)
async function foo() {
return Promise.result({ value: 'foo', then(resolve) { resolve('bar') } })
}
await foo(); // { value: 'foo', then(resolve) { resolve('bar') } }
今回もコンセンサスが得られなかった。また熟考するらしい。
その他
IoT のために I/O やローレベルなセンサーの API (非 W3C ベース)を定義していくらしい。
Let's ship it: replace es-discuss with moderateable forum
今まで Mozilla の ES Discuss というメーリングリストでユーザーからの提案を受け付けていたが、Discourse に移行する話が上がった。 総括
多くの Normative が解決され、String#matchAll が Stage 4 になりその他多くの提案が Stage 3 に進んだ。今回も順調だった用に思える。
Decorators が仕様が膨れ上がってどうなるかと思っていたら、全く新しい仕様となってフィードバックを得る方向性になり驚いた。個人的には FinalizationGroup が待ち遠しいので、早くブラウザで試せるステータスになって欲しい。