JavaScriptの不純物の新鮮な外観

この記事では、JavaScriptで混合物を詳細に調べ、あまり一般的ではないが、私の意見では、より自然な「混合」戦略を示します。 最終的に、各手法のパフォーマンスへの影響を要約したプロファイリングマトリックスが作成されます。 (この投稿のベースとなっているコードをレビューおよび改善してくれた素晴らしい@kitcambridgeに感謝します!)



関数の再利用

JavaScriptでは、すべてのオブジェクトは、プロパティを継承できるプロトタイプオブジェクトを参照します。 プロトタイプは、コードを再利用するための優れたツールです。1つのプロトタイプインスタンスで、無数の依存エンティティのプロパティを決定できます。 プロトタイプは他のプロトタイプからも継承できるため、JavaやC ++などの「クラス」言語の継承階層を多かれ少なかれ繰り返すプロトタイプのチェーンを形成します。 マルチストーリーの継承階層は、物事の自然な順序を説明するのに役立つ場合がありますが、コードの再利用が主な動機である場合、そのような階層はすぐに意味のないサブクラス、面倒な冗長性、制御不能なロジックのねじれた迷路になる可能性があります(「ボタンは長方形かコントロールか? Rectangle



からButton



を継承し、 Control



からRectangle



を継承することができます。



幸いなことに、関数の再利用に関しては、JavaScriptは実行可能な代替手段を提供します。 より厳密に構造化された言語とは対照的に、JavaScriptのオブジェクトは、血統に関係なくすべてのパブリック関数を呼び出すことができます。 最も簡単なアプローチは委任です。 パブリック関数は、 call



またはapply



介して直接call



ことができapply



。 これは効果的な機能であり、広く使用しています。 場合によっては、委任は非常に便利なので、コードの構造的な規律に反して動作することがあります。 さらに、構文が少し冗長になる場合があります。 不純物は、最小限の構文を使用して機能単位全体を借用してアクセスできるようにする大きな妥協であり、プロトタイプと完全に連携します。 これらは、同じルートに戻る複数階層の継承に関連する脳の問題なしに、階層的継承の記述力を提供します。



基本

プログラミングでは、混合物は、タイプに関連する一連の関数( Person



Circle



Observer



)を定義するクラスです。 不純物クラスは、通常、独自のインスタンスを持たないという意味で抽象と見なされます-代わりに、メソッドは、動作プロバイダーとの正式な関係に入ることなく、動作の「継承」として特定のクラスによってコピー(または「借入」)されます。

わかりましたが、これはJavaScriptであり、クラスはありません。 実際、これは良いことです。代わりに、オブジェクト(インスタンス)を代わりに使用できるため、明確さと柔軟性が得られます。不純物は、通常のオブジェクト、プロトタイプ、または関数になります。いずれにしても、「混合」プロセスは透明になり、明らかです。



使用する

いくつかの不純な手法について説明しますが、すべてのコード例は、丸いボタン、楕円形のボタン、または長方形のボタンを作成するという1つのユースケースになります。 これは概略図です(最新のハイテクガジェットを使用して作成)。 長方形で-不純物、円で-完全なボタン。





1.古典的な不純物

「javascript mixin」のリクエストでのGoogleの発行の最初の2ページを見て、ほとんどの著者が、プロトタイプで定義されたコンストラクターとメソッドを使用して不純オブジェクトをソリッドタイプとして定義していることに気付きました。 これは自然な進歩と考えることができます-以前は不純物はクラスでしたが、これはJSにあるクラスに最も近いものです。 このスタイルで作成されたサークルの混合物を次に示します。

 var Circle = function() {}; Circle.prototype = { area: function() { return Math.PI * this.radius * this.radius; }, grow: function() { this.radius++; }, shrink: function() { this.radius--; } };
      
      





ただし、実際には、このような重い不純物は不要です。 単純なオブジェクトリテラルで十分です。

 var circleFns = { area: function() { return Math.PI * this.radius * this.radius; }, grow: function() { this.radius++; }, shrink: function() { this.radius--; } };
      
      







拡張機能

このような不純なオブジェクトは、どのようにオブジェクトと混合しますか extend



機能を使用する( augment



と呼ばれることもあります)。 通常、混合関数を受信オブジェクトに単純にコピーします(ただし、クローンは作成しません)。 クイックレビューでは、この実装の小さなバリエーションがいくつか示されています。 たとえば、Prototype.jsはhasOwnProperty



チェックをスキップします(プロトタイプチェーンに不純物に列挙可能なプロパティがないことを前提としています)が、他のバージョンでは、不純物プロトタイプからプロパティのみをコピーすることを前提としています。 安全で柔軟なオプションは次のとおりです...

 function extend(destination, source) { for (var k in source) { if (source.hasOwnProperty(k)) { destination[k] = source[k]; } } return destination; }
      
      





...プロトタイプを拡張するために呼び出すことができます...

 var RoundButton = function(radius, label) { this.radius = radius; this.label = label; }; extend(RoundButton.prototype, circleFns); extend(RoundButton.prototype, buttonFns); //etc. ...
      
      







2.機能性不純物

不純物で定義された関数が他のオブジェクトのみによる使用を目的としている場合、なぜ不純物をオブジェクトとして作成するのですか? 言い換えれば、不純物はオブジェクトではなくプロセスでなければなりません。 不純物を、委任によってオブジェクトがそれ自体を実装する関数に変えることは論理的です。 これにより、中間機能の必要性がなくなります-機能をextend



ます。

 var asCircle = function() { this.area = function() { return Math.PI * this.radius * this.radius; }; this.grow = function() { this.radius++; }; this.shrink = function() { this.radius--; }; return this; }; var Circle = function(radius) { this.radius = radius; }; asCircle.call(Circle.prototype); var circle1 = new Circle(5); circle1.area(); //78.54
      
      





このアプローチは正しく見えます。 名詞ではなく動詞としての不純物。 軽量デパート。 あなたが好きかもしれない他のことがあります-コードのスタイルは自然で簡潔です: this



常に関数セットの受信者を示しており、私たちが必要としない、使用しない抽象的なオブジェクトではありません。 さらに、従来のアプローチとは対照的に、継承されたプロパティの意図しないコピーに対する保護は必要ありません。また、(それが何であれ)関数はコピーされずに複製されます。

ボタンの混合関数は次のとおりです。

 var asButton = function() { this.hover = function(bool) { bool ? mylib.appendClass('hover') : mylib.removeClass('hover'); }; this.press = function(bool) { bool ? mylib.appendClass('pressed') : mylib.removeClass('pressed'); }; this.fire = function() { return this.action(); }; return this; };
      
      





2つの不純物を一緒に取り、丸いボタンを取得します。

 var RoundButton = function(radius, label, action) { this.radius = radius; this.label = label; this.action = action; }; asButton.call(RoundButton.prototype); asCircle.call(RoundButton.prototype); var button1 = new RoundButton(4, 'yes!', function() {return 'you said yes!'}); button1.fire(); //'you said yes!'
      
      







3.オプションを追加する

関数を使用した戦略では、オプションの引数を渡すことにより、借用した動作をパラメータ化することもできます。 grow



およびshrink



カスタムオプションを使用してasOval



混合物を作成することにより、これがどのように機能するかを見てみましょう。

 var asOval = function(options) { this.area = function() { return Math.PI * this.longRadius * this.shortRadius; }; this.ratio = function() { return this.longRadius/this.shortRadius; }; this.grow = function() { this.shortRadius += (options.growBy/this.ratio()); this.longRadius += options.growBy; }; this.shrink = function() { this.shortRadius -= (options.shrinkBy/this.ratio()); this.longRadius -= options.shrinkBy; }; return this; } var OvalButton = function(longRadius, shortRadius, label, action) { this.longRadius = longRadius; this.shortRadius = shortRadius; this.label = label; this.action = action; }; asButton.call(OvalButton.prototype); asOval.call(OvalButton.prototype, {growBy: 2, shrinkBy: 2}); var button2 = new OvalButton(3, 2, 'send', function() {return 'message sent'}); button2.area(); //18.84955592153876 button2.grow(); button2.area(); //52.35987755982988 button2.fire(); //'message sent'
      
      







4.キャッシングを追加する

呼び出しごとに同じ関数を何度も再定義するため、このアプローチではパフォーマンスが低下することが心配される場合があります。 jsperf.comのような素晴らしいものの助けを借りて、私はすべての不純な戦略のメトリックを取りました(結果は記事の最後にあります)。 驚くべきことに、Chrome 12では、機能的なアプローチを使用するとパフォーマンスが向上しますが、他のブラウザーでは、こうした不純物は従来の2倍遅く実行されます。 このような不純物は、タイプごとに1回呼び出される可能性が最も高いと仮定すると(各インスタンスを作成するときではない)、IE8でも毎秒26,000の不純物について話していることを考えると、この違いは重要な役割を果たしません!

念のために、そのような数値で上司が夜眠れない場合は、次の解決策があります。 回路に不純物を配置することにより、測定結果をキャッシュできます。これはパフォーマンスに大きく影響します。 機能的な不純物は、すべてのブラウザーで従来のパフォーマンスを簡単に上回ることができます(私のテストでは、Chromeで約20回、FF4で約13回)。 繰り返しになりますが、これはそれほど重要ではありませんが、心地よい感じが残ります:)

キャッシュを追加したasRectangle



バージョンはasRectangle



です...

 var asRectangle = (function() { function area() { return this.length * this.width; } function grow() { this.length++, this.width++; } function shrink() { this.length--, this.width--; } return function() { this.area = area; this.grow = grow; this.shrink = shrink; return this; }; })(); var RectangularButton = function(length, width, label, action) { this.length = length; this.width = width; this.label = label; this.action = action; } asButton.call(RectangularButton.prototype); asRectangle.call(RectangularButton.prototype); var button3 = new RectangularButton(4, 2, 'delete', function() {return 'deleted'}); button3.area(); //8 button3.grow(); button3.area(); //15 button3.fire(); //'deleted'
      
      







5.カレーを追加する

人生のすべては妥協の結果であり、前述のキャッシングによる改善も例外ではありません。 各不純物の実際のクローンを作成する機能を失い、さらに、パラメーターを渡すことによって借用した関数をカスタマイズすることもできなくなりました。 最後の問題は、キャッシュされた各関数をカリー化することで解決できます。したがって、後続の呼び出しまで、事前にパラメーターを割り当てます。 次に、 asRectangle



と適切にasRectangle



関数の混合物をasRectangle



ます。これにより、 grow



およびshrink



パラメーター化が可能になります。

 Function.prototype.curry = function() { var fn = this; var args = [].slice.call(arguments, 0); return function() { return fn.apply(this, args.concat([].slice.call(arguments, 0))); }; } var asRectangle = (function() { function area() { return this.length * this.width; } function grow(growBy) { this.length += growBy, this.width +=growBy; } function shrink(shrinkBy) { this.length -= shrinkBy, this.width -= shrinkBy; } return function(options) { this.area = area; this.grow = grow.curry(options['growBy']); this.shrink = shrink.curry(options['shrinkBy']); return this; }; })(); asButton.call(RectangularButton.prototype); asRectangle.call(RectangularButton.prototype, {growBy: 2, shrinkBy: 2}); var button4 = new RectangularButton(2, 1, 'add', function() {return 'added'}); button4.area(); //2 button4.grow(); button4.area(); //12 button4.fire(); //'added'
      
      





パフォーマンス指標



約束通り、ここにjsperfテストの結果があり、技術とブラウザーごとに表にまとめられています。

結果は1秒間に数千の操作で表されるため、数値が大きいほど優れています。



(翻訳者の注意:jsperf.comで最新の結果を直接確認することをお勧めします。元の投稿の表は、数字の順序を示すためだけのものです)



おわりに

JavaScriptは機能と状態の合金です。 通常、状態はインスタンス固有ですが、関数は共有される可能性が高くなります。 これらの2つの基本的な責任領域を分離することは私たちの利益になる可能性があり、おそらく不純物がこれに役立ちます。

特に、機能的な不純物パターンは明確な区別を提供します。 オブジェクトは状態であり、機能は収穫の熟した木の上の果物のクラスターのように編成されます。 実際、この戦略は不純物をはるかに超えて拡張できます。機能セットは、あらゆるオブジェクトのリポジトリとして機能します。

 var myCircle = asCircle.call({radius:25}); myCircle.area(); //1963.50
      
      





不純物の研究にご幸運をお祈りします。訂正やその他のフィードバックをお送りください。



All Articles