「Class-fields-proposal」または「tc39 commitで問題が発生した」

私たち全員がずっと前にJSで通常のカプセル化を望んでいます。これは不必要なジェスチャーなしで使用できます。 また、クラスプロパティを宣言するための便利な構造も必要です。 そして最後に、言語のこれらすべての機能が、既存のアプリケーションを壊さないように表示されるようにします。


ここでそれは幸福であるように思えます: class-fields-proposalは、 tc39委員会の長年の苦痛の後、まだstage 3 達し 、さらにchromeで実装されました


正直に言って、新しい言語機能を使用する理由とその方法についての記事を書きたいと思いますが、残念ながら、その記事についてはまったく触れません。


現在の欠落の説明


ここでは、 元の説明FAQ 、仕様の変更 については繰り返しませんが、要点を簡単に説明します。


クラスフィールド


フィールドを宣言してクラス内で使用する:


 class A { x = 1; method() { console.log(this.x); } } 

クラス外のフィールドへのアクセス:


 const a = new A(); console.log(ax); 

すべてが明らかであるように思われ、長年にわたり、 BabelTypeScriptを使用してこの構文を使用してきました。


ニュアンスのみがあります。 この新しい構文は[[Define]]使用し、この間ずっと一緒にいた[[Set]]セマンティクスを使用しません。


実際には、これは上記のコードがこれと等しくないことを意味します。


 class A { constructor() { this.x = 1; } method() { console.log(this.x); } } 

しかし、実際にはこれと同等です:


 class A { constructor() { Object.defineProperty(this, "x", { configurable: true, enumerable: true, writable: true, value: 1 }); } method() { console.log(this.x); } } 

また、上記の例では、どちらのアプローチも基本的に同じことを行いますが、これは非常に深刻な違いです。その理由は次のとおりです。


次のような親クラスがあるとしましょう:


 class A { x = 1; method() { console.log(this.x); } } 

それに基づいて、別のものを作成しました:


 class B extends A { x = 2; } 

そして彼らはそれを使用しました:


 const b = new B(); b.method(); //   2   

次に、何らかの理由で、クラスAが一見下位互換性のある方法で変更されました。


 class A { _x = 1; //  ,   ,        get x() { return this._x; }; set x(val) { return this._x = val; }; method() { console.log(this._x); } } 

[[Set]]セマンティクスの場合、これは実際には後方互換性のある変更ですが、 [[Define]]はそうではありません。 b.method()を呼び出すと、 2ではなくコンソール1に出力されます。 これは、 Object.definePropertyがプロパティ記述子を再定義し、それに応じてクラスAゲッター/セッターが呼び出されないために発生します。 実際、子クラスでは、レキシカルスコープでこれを行う方法と同様に、親のxプロパティを隠しています。


 const x = 1; { const x = 2; } 

確かに、この場合、 no-shadowed-variable / no-shadowルールを持つリンターは私たちを救いますが、誰かがno-shadowed-class-fieldを作る可能性はゼロになる傾向があります。


ちなみに、私はロシア語で「 shadowedという用語がより成功していることに感謝します。

上記のすべてにもかかわらず、私は新しいセマンティクスの相容れない相手ではありません(別のセマンティクスを好むでしょうが)。 しかし、残念なことに、これらの利点は最も重要なマイナスを上回っていません。デフォルトでbabel6およびTypeScript使用されているため、長年[[Set]]セマンティクスを使用しています。


babel7 デフォルト値が変更されていることに注意してbabel7

このテーマに関するより独創的な議論は、 ここここで読むことができます


プライベートフィールド


そして今、私たちはこの部分の最も議論の余地のある部分に移ります。 物議を醸すこと:


  1. Chrome Canaryに既に実装されており、パブリックフィールドはデフォルトですでに有効になっているにもかかわらず、プライベートフィールドはまだフラグの後ろにあります。
  2. プライベートフィールドの最初の提案が現在の提案とマージされたという事実にもかかわらず、これらの2つの機能(たとえば、 1、2、3、4)を分離する要求がまだ作成されています。
  3. ステージ3にもかかわらず、一部の委員( Allen Wirfs-BrockKevin Smithなど )でも意見を述べ代替案提供しています
  4. これは、 現在のリポジトリの 129 + オリジナルの 96に対して、発行数のレコードを設定できなかったのに対し、 BigIntの 126であり、レコード所有者はほとんど否定的なコメントを持っています
  5. 私はそれに対するすべての主張を何らかの形で要約しようとして、 別のスレッドを作成する必要がありました。
  6. この部分をカバーする別のFAQを書く必要がありました
    しかし、かなり弱い議論のために、そのような議論が現れました( 1、2
  7. 私は個人的に、すべての時間を把握しなぜ彼がそのようになったかの説明を見つけたり、 適切な代替手段を提供したりするために、長い間、すべての自由時間を過ごしました。
  8. 最後に、私はこのレビュー記事を書くことにしました。

プライベートフィールドは次のように宣言されます。


 class A { #priv; } 

そして、それらへのアクセスは次のとおりです。


 class A { #priv = 1; method() { console.log(this.#priv); } } 

私はこの背後にある精神モデルがあまり直感的ではない( this.#priv !== this['#priv'] )、すでに予約protectedいるprivate / protected単語を使用しないというトピックを取り上げませんTypeScript開発者向け)、これを他のアクセス修飾子に拡張する方法は不明であり、構文自体はあまり美しくありません。 これらすべてが、私をより深い研究と議論への参加に導いた元の理由でしたが。


これはすべて、主観的な審美的好みが非常に強い構文に関連しています。 そして、それと一緒に生きて、時間の経過とともにそれに慣れることができます。 1つではない場合:セマンティクスの非常に重要な問題があります...


セマンティクスWeakMap


既存の提案の背後にあるものを見てみましょう。 新しい構文を使用せずにカプセル化を使用して上記の例を書き換えることができますが、現在の構文のセマンティクスを保持します。


 const privatesForA = new WeakMap(); class A { constructor() { privatesForA.set(this, {}); privatesForA.get(this).priv = 1; } method() { console.log(privatesForA.get(this).priv); } } 

ちなみに、このセマンティクスに基づいて、委員会のメンバーの1人は、このような機能が委員会によって過大評価されていることを示すために、プライベートステートを使用できる小さなユーティリティライブラリを構築しました。 書式設定されたコードは27行のみです。

一般に、すべてが非常に良好であり、外部コードから取得/インターセプト/追跡できないhard-privateを取得し、同時に同じクラスの別のインスタンスのprivateフィールドにアクセスできます。たとえば、次のようになります。


 isEquals(obj) { return privatesForA.get(this).id === privatesForA.get(obj).id; } 

まあ、これは非常に便利です。カプセル化自体に加えて、このセマンティクスにはbrand-checkingも含まれているという事実を除いて(それが何であるかをGoogleで検索することはできません-関連情報を見つける可能性は低いです)。
brand-checkingは、オブジェクトのパブリックインターフェイスをチェックするのではなく、オブジェクトが信頼できるコードを使用して構築されたという点で、 duck-typing反対です。
実際、このようなチェックには一定の範囲があります。主に、単一のアドレス空間で信頼できないコードを信頼できるコードで呼び出すセキュリティと、シリアル化せずにオブジェクトを直接交換する機能に関連しています。


一部のエンジニアは、これを適切なカプセル化の必要な部分と考えていますが。

これは、 パターン( 短く長い説明)と密接に関連するかなり興味深い機会であるという事実にもかかわらず、 Realms宣伝とコンピューターサイエンスの分野での科学的研究であり、 Mark Samuel Miller (彼は委員会のメンバーでもあります)が私の経験で取り組んでいます、ほとんどの開発者の実践では、これはほとんど発生しません。


ちなみに、 vm2を自分のニーズ合わせて書き直したとき、私はまだ膜に出くわしました(当時は何だったのかわかりませんでしたが)。

brand-checking問題


前に述べたように、 brand-checkingは、 duck-typing反対です。 実際には、これは次のコードを持つことを意味します。


 const brands = new WeakMap(); class A { constructor() { brands.set(this, {}); } method() { return 1; } brandCheckedMethod() { if (!brands.has(this)) throw 'Brand-check failed'; console.log(this.method()); } } 

brandCheckedMethodはクラスAインスタンスでのみbrandCheckedMethodことができ、ターゲットがこのクラスの不変式を保持するオブジェクトであっても、このメソッドは例外をスローします。


 const duckTypedObj = { method: A.prototype.method.bind(duckTypedObj), brandCheckedMethod: A.prototype.brandCheckedMethod.bind(duckTypedObj), }; duckTypedObj.method(); //        1 duckTypedObj.brandCheckedMethod(); //      

明らかに、この例は非常に合成的であり、 Proxyを覚えているまで、このようなduckTypedObjの使用duckTypedObj疑わしいです。
非常に重要なプロキシ使用シナリオの1つは、メタプログラミングです。 プロキシがすべての必要な有用な作業を行うために、プロキシを使用してラップされるオブジェクトのメソッドは、ターゲットのコンテキストではなく、プロキシのコンテキストで実行する必要があります。


 const a = new A(); const proxy = new Proxy(a, { get(target, p, receiver) { const property = Reflect.get(target, p, receiver); doSomethingUseful('get', retval, target, p, receiver); return (typeof property === 'function') ? property.bind(proxy) : property; } }); 

proxy.method();呼び出しproxy.method(); proxy.brandCheckedMethod();を呼び出しながら、プロキシで宣言された有用な作業を行い、 1を返しproxy.brandCheckedMethod(); a !== proxyであるため、プロキシから有用な作業を2回行う代わりに例外がスローされますa !== proxyこれは、 brand-checkがパスしなかったことを意味します。


はい、プロキシではなく実際のターゲットのコンテキストでメソッド/関数を実行できます。一部のシナリオではこれで十分です(たとえば、 パターンを実装するため)。しかし、これはすべての場合に十分ではありません(たとえば、リアクティブプロパティを実装する場合: MobX 5は既にプロキシを使用しています)このため、 Vue.jsAureliaは将来のリリースのためにこのアプローチを実験しています)。


一般に、 brand-check明示的に行う必要がある限り、これは問題ではありません-開発者は意識的に、どのトレードオフを行うか、それが必要かどうかを決定する必要があります。さらに、明示的なbrand-check場合はbrand-check次のように実装できます。信頼できるプロキシではエラーはスローされません。


残念ながら、現在のものはこの柔軟性を私たちから奪っています。


 class A { #priv; method() { this.#priv; //    brand-check   } } 

このようなmethodは、コンストラクターAを使用して構築されたオブジェクトのコンテキストで呼び出されない場合、常に例外をスローしますA 最悪の部分は、ここではbrand-checkが暗黙的であり、カプセル化という別の機能と混合していることです。


どのコードにもほぼ必要ですが、 brand-check範囲はかなり狭いです。 そして、それらを1つの構文に結合すると、開発者が実装の詳細のみを隠そうとしたときに、多くの意図しないbrand-checkがユーザーコードに表示されるという事実につながります。
そして、これを促進するために使用されるスローガンは、状況を悪化させるだけ# is the new _でした。


また、既存のプロザルがプロキシを破る方法の詳細な説明を読むこともできます。 Aureliaの開発者の1人であり、 著者であるVue.jsがディスカッションで話しました

また、さまざまなプロキシシナリオの違いをより詳細に説明する私の解説は 、誰かにとって興味深いかもしれません。 全体として、 プライベートフィールドと膜の接続に関する議論全体

代替案


これらの議論はすべて、代替手段がなければ意味がありません。 残念ながら、 stage1でさえも1人の代替者がいなかったため、十分に解決する機会すらありませんでした。 ただし、上記の問題を何らかの形で解決する代替手段をここにリストします。


  1. Symbol.private-委員の1人の代替メンバー。
    1. 上記の問題をすべて解決します(独自の問題がある場合がありますが、積極的な作業が行われていないため、それらを見つけるのは困難です)
    2. 統合されたbrand-check欠如、膜パターンの問題( これ + これは適切な解決策を提供しますが)、便利な構文の欠如のため、委員会の最後の会議で再び投げ返されました
    3. ここここで示したように、実際の構文の上に便利な構文を構築できます
  2. クラス 1.1-同じ著者による以前のポゾザル
  3. オブジェクトとしてプライベートを使用する

結論の代わりに


記事の口調から、おそらく私は委員会を非難するように見えるかもしれません-これはそうではありません。 委員会が業界の多くでJSのカプセル化に取り組んでおり、外観がぼやけ、優先順位の誤ったランキングにつながった可能性があるように思えます。 。


さらに、コミュニティとして、 tc39押して、機能をより早くリリースするように強制しますが、prozosの初期段階ではほとんどフィードバックを与えず、変更がほとんどできないときにのみonlyりを抑えます。


この場合、プロセスは単に失敗したと考えられています。


私の頭にそれを突っ込んでいくつかの代表者と話した後、私は同様の状況が再び起こらないように最善を尽くすことに決めました-しかし、私は少しはできます(レビュー記事を書いて、 stage1の実装をstage1見逃しました)。


しかし、最も重要なことはフィードバックです。この小さな調査に参加するようお願いします。 そして、私はそれを委員会に伝えようとします。



Source: https://habr.com/ru/post/J428119/


All Articles