JavaScriptには、オブジェクトを作成するさまざまな方法があります。 特に、
class
キーワードといわゆるファクトリー関数を使用する構成について話します。 本書の翻訳者である著者は、これらの2つの概念を探求し、それぞれの長所と短所に関する質問への回答を探して比較します。
復習
class
キーワードはECMAScript 2015(ES6)に登場しました。その結果、オブジェクトを作成するための2つの競合するパターンができました。 それらを比較するために、クラス構文を使用してファクトリー関数を適用し、同じオブジェクト(
TodoModel
)を説明します。
class
キーワードを使用すると、
TodoModel
説明は次のようになります。
class TodoModel { constructor(){ this.todos = []; this.lastChange = null; } addToPrivateList(){ console.log("addToPrivateList"); } add() { console.log("add"); } reload(){} }
ファクトリー関数によって作成された同じオブジェクトの
説明は次のとおりです。
function TodoModel(){ var todos = []; var lastChange = null; function addToPrivateList(){ console.log("addToPrivateList"); } function add() { console.log("add"); } function reload(){} return Object.freeze({ add, reload }); }
クラスを作成するこれらの2つのアプローチの機能を検討してください。
カプセル化
クラスとファクトリー関数を比較するときに見られる最初の機能は、
class
キーワードを使用して作成されたオブジェクトのすべてのメンバー、フィールド、およびメソッドが公開されていることです。
var todoModel = new TodoModel(); console.log(todoModel.todos); //[] console.log(todoModel.lastChange) //null todoModel.addToPrivateList(); //addToPrivateList
ファクトリー関数を使用する場合、意識的に開いているもののみが公開され、他のすべては受信したオブジェクト内に隠されます。
var todoModel = TodoModel(); console.log(todoModel.todos); //undefined console.log(todoModel.lastChange) //undefined todoModel.addToPrivateList(); //taskModel.addToPrivateList is not a function
API耐性
オブジェクトが作成された後、そのAPIが変更されないこと、つまり、それに対する耐性が期待されます。 ただし、
class
キーワードを使用して作成されたオブジェクトのパブリックメソッドの実装を簡単に変更できます。
todoModel.reload = function() { console.log("a new reload"); } todoModel.reload();
この問題は、クラスの宣言後に
Object.freeze(TodoModel.prototype)
呼び出すか、サポートされている場合にデコレーターを使用してクラスを「フリーズ」することで
解決できます。
一方、ファクトリ関数を使用して作成されたオブジェクトのAPIは不変です。
Object.freeze()
コマンドを使用して、新しいオブジェクトのパブリックメソッドのみを含む戻りオブジェクトを処理することに注意してください。 このオブジェクトのプライベートデータは変更できますが、これはこれらのパブリックメソッドを使用してのみ実行できます。
todoModel.reload = function() { console.log("a new reload"); } todoModel.reload();
このキーワード
class
キーワードで作成されたオブジェクトは、
this
コンテキストを失うという長年の問題を起こしやすい傾向があります。 たとえば、ネストされた関数ではコンテキストが失われます。 これはプログラミングプロセスを複雑にするだけでなく、このような動作はエラーの恒常的な原因でもあります。
class TodoModel { constructor(){ this.todos = []; } reload(){ setTimeout(function log() { console.log(this.todos);
そして、DOMイベントで適切なメソッドを使用すると、コンテキストが失われます。
$("#btn").click(todoModel.reload);
ファクトリー関数を使用して作成されたオブジェクトは、thisキーワードがここでは使用されないため、同様の問題の影響を受けません。
function TodoModel(){ var todos = []; function reload(){ setTimeout(function log() { console.log(todos); //[] }, 0); } } todoModel.reload(); //[] $("#btn").click(todoModel.reload); //[]
このキーワードと矢印関数
矢印関数は、クラスを使用するときに
this
コンテキストが失われることに関連する問題を部分的に解決しますが、同時に新しい問題を作成します。 つまり、クラスで矢印関数を使用する場合、
this
はネストされた関数のコンテキストを失わなくなりました。 ただし、
this
はDOMイベントを処理するときにコンテキストを失います。
矢印関数を使用して
TodoModel
クラスを
再設計しました。 リファクタリングの過程で、通常の関数を矢印関数に置き換えると、コードの可読性にとって重要なもの、つまり関数名が失われることに注意してください。 次の例を見てください。
// setTimeout(function renderTodosForReview() { }, 0); // setTimeout(() => { }, 0);
矢印関数を使用する場合、関数の機能を正確に理解するには、関数のテキストを読む必要があります。 すべてのコードを読むのではなく、関数の名前を読んでその本質を理解したいと思います。 もちろん、矢印関数を使用すると、コードを読みやすくすることができます。 たとえば、次のような矢印関数を使用する習慣を作ることができます。
var renderTodosForReview = () => { }; setTimeout(renderTodosForReview, 0);
新しいオペレーター
クラスに基づいてオブジェクトを作成する場合、
new
演算子を使用する必要があります。 また、ファクトリ関数を使用してオブジェクトを作成する場合、
new
必要
new
ません。 ただし、
new
使用するとコードの可読性が向上する場合、この演算子はファクトリー関数でも使用できますが、これによる害はありません。
var todoModel= new TodoModel();
ファクトリ関数で
new
を
使用する場合、関数は作成したオブジェクトを単に返します。
安全性
アプリケーションが
User
オブジェクトを使用して認証メカニズムを操作するとします。 ここで説明した両方のアプローチを使用して、このようなオブジェクトをいくつか作成しました。
クラスを使用した
User
オブジェクトの
説明は次のとおりです。
class User { constructor(){ this.authorized = false; } isAuthorized(){ return this.authorized; } } const user = new User();
ファクトリー関数を使用して
記述された同じオブジェクト
は次のようになります。
function User() { var authorized = false; function isAuthorized(){ return authorized; } return Object.freeze({ isAuthorized }); } const user = User();
class
キーワードを使用して作成されたオブジェクトは、攻撃者がオブジェクトへのリンクを持っている場合、攻撃に対して脆弱です。 すべてのオブジェクトのすべてのプロパティは公開されているため、攻撃者は他のオブジェクトを使用して、関心のあるオブジェクトにアクセスできます。
たとえば、
user
変数がグローバルの場合、開発者コンソールから適切な権限を直接取得できます。 これを確認するには、
サンプルコードを開き、コンソールから
user
変数を変更し
user
。
この例は
Plunkerリソースを使用して準備されました。 グローバル変数にアクセスするには、コンソールタブのコンテキストを
top
から
plunkerPreviewTarget(run.plnkr.co/)
ます。
user.authorized = true; // user.isAuthorized = function() { return true; } // API console.log(user.isAuthorized()); //true
開発者コンソールを使用してオブジェクトを変更するファクトリー関数を使用して作成されたオブジェクトは、外部から変更できません。
構成と継承
クラスは、オブジェクトの継承と構成の両方をサポートします。
SpecialService
クラスが
Service
クラスの継承で
ある継承の
例を作成しまし
た 。
class Service { log(){} } class SpecialService extends Service { logSomething(){ console.log("logSomething"); } } var specialService = new SpecialService(); specialService.log(); specialService.logSomething();
ファクトリ関数を使用する場合、継承はサポートされていません;ここでは、構成のみを使用できます。 または、
Object.assign()
コマンドを使用して、既存のオブジェクトからすべてのプロパティをコピーできます。
たとえば 、
SpecialService
オブジェクトで
Service
オブジェクトのすべてのメンバーを再利用する必要があるとします。
function Service() { function log(){} return Object.freeze({ log }); } function SpecialService(args){ var standardService = args.standardService; function logSomething(){ console.log("logSomething"); } return Object.freeze(Object.assign({}, standardService, { logSomething })); } var specialService = SpecialService({ standardService : Service() }); specialService.log(); specialService.logSomething();
ファクトリー関数は、継承の代わりに構成の使用を容易にします。これにより、開発者はアプリケーション設計に関してより高いレベルの柔軟性を得ることができます。
クラスを使用する場合、継承よりも合成を優先することもできます。実際、これらは既存の動作の再利用に関するアーキテクチャ上の決定にすぎません。
記憶
クラスを使用すると、プロトタイプシステムに基づいて実装されるため、メモリを節約できます。 すべてのメソッドはプロトタイプで一度だけ作成され、クラスのすべてのインスタンスで使用されます。
ファクトリ関数を使用して作成されたオブジェクトによって消費されるメモリの追加コストは、数千の同様のオブジェクトが作成された場合にのみ顕著です。
ファクトリー機能を使用するための典型的なメモリコストを見つけるために使用される
ページは次のとおりです。 以下は、10個と20個のメソッドを使用して、異なる数のオブジェクトについてChromeで得られた結果です。
メモリオーバーヘッド(Chromeで)OOPオブジェクトとデータ構造
メモリコストの分析を続ける前に、2種類のオブジェクトを区別する必要があります。
- OOPオブジェクト
- データオブジェクト(データ構造)。
オブジェクトは動作を提供し、データを隠します。
データ構造はデータを提供しますが、重要な動作はありません。
ロバートマーティン、 クリーンコードオブジェクトとデータ構造の違いを明確にするために、すでに
TodoModel
いる
TodoModel
オブジェクトの例を見てください。
function TodoModel(){ var todos = []; function add() { } function reload(){ } return Object.freeze({ add, reload }); }
TodoModel
オブジェクト
TodoModel
、
todo
オブジェクトのリストを保存および管理します。
TodoModel
はOOPオブジェクトであり、動作を提供し、データを隠します。 アプリケーションにはインスタンスが1つしかありません。そのため、ファクトリ関数を使用して作成する場合、追加のメモリコストは必要ありません。
todos
配列に格納されているオブジェクトはデータ構造です。 プログラムにはそのようなオブジェクトが多数存在する場合がありますが、これらは通常のJavaScriptオブジェクトです。 それらのメソッドをプライベートにすることに興味はありません。 むしろ、すべてのプロパティとメソッドが公開されるように努めています。 その結果、これらすべてのオブジェクトはプロトタイプシステムを使用して構築されるため、メモリを節約できます。 通常のオブジェクトリテラル
Object.create()
か
Object.create()
コマンド
Object.create()
作成できます。
ユーザーインターフェイスコンポーネント
アプリケーションは、数百または数千のユーザーインターフェイスコンポーネントのインスタンスを持つことができます。 これは、カプセル化とメモリの節約の間で妥協点を見つけなければならない状況です。
コンポーネントは、使用されているフレームワークで採用されている方法に従って作成されます。 たとえば、Vueはオブジェクトリテラルを使用し、Reactはクラスを使用します。 コンポーネントオブジェクトの各メンバーは公開されますが、プロトタイプシステムの使用のおかげで、そのようなオブジェクトを使用するとメモリを節約できます。
反対のOOPパラダイム
広い意味で、クラスとファクトリー関数は、オブジェクト指向プログラミングの2つの相反するパラダイムの戦いを示しています。
JavaScriptに適用されるクラスベースのOOPは、次のことを意味します。
- アプリケーション内のすべてのオブジェクトは、クラスによって指定された型を使用して、クラス構文を使用して記述されます。
- プログラムを書くために、彼らは静的型付けの言語を探しており、そのコードはその後JavaScriptに変換されます。
- 開発中、インターフェイスを使用します。
- 構成と継承を適用します。
- 関数型プログラミングはほとんど使用されないか、ほとんど関心がありません。
クラスを使用しないOOPは次のようになります。
- 開発者が定義したタイプは使用されません。 このパラダイムには
instanceof
ようなもののための場所はありません。 すべてのオブジェクトはオブジェクトリテラルを使用して作成されます。一部はパブリックメソッド(OOPオブジェクト)、一部はパブリックプロパティ(データ構造)を使用します。 - 開発時には、動的型付けが使用されます。
- インターフェイスは使用されません。 開発者は、オブジェクトに必要なプロパティがあるかどうかのみに関心があります。 このようなオブジェクトは、ファクトリー関数を使用して作成できます。
- 構成は適用されますが、継承は適用されません。 必要に応じて、
Object.assign()
を使用して、あるオブジェクトのすべてのメンバーを別のオブジェクトにコピーします。 - 関数型プログラミングが使用されます。
まとめ
クラスの長所は、開発がクラスに基づいている言語からJSに来るプログラマーに馴染みがあることです。 JSのクラスは、プロトタイプシステムの構文糖衣です。 ただし、セキュリティの問題と、コンテキストの喪失による永続的なエラーにつながる
this
の使用により、ファクトリー関数と比較してクラスが
2番目になります。 例外として、クラスは、Reactなどで使用されるフレームワークで使用される場合に再分類されます。
ファクトリ関数は、安全でカプセル化された柔軟なOOPオブジェクトを作成するためのツールだけではありません。 クラスを作成するこのアプローチは、JavaScriptに固有の新しいプログラミングパラダイムへの扉も開きます。
この記事の終わりに、
Douglas Crockfordを引用できます。「クラスのないOOPはJavaScriptからの人類への贈り物だと思います。」
親愛なる読者! クラスとファクトリー関数のどちらに近いのですか?