表現力豊かなJavaScript:オブジェクトの秘密の生活

内容




オブジェクト指向言語の問題は、暗黙の環境をすべて持っていることです。 あなたはバナナが必要でした-そして、あなたはバナナとさらにジャングル全体でゴリラを手に入れました。

ジョーアームストロング、Coders at Workとのインタビュー

プログラミングにおける「オブジェクト」という用語には、意味が非常に多く含まれています。 私の職業では、オブジェクトはライフスタイルであり、聖戦のテーマであり、魔法の力を失わないお気に入りの呪文です。

部外者には、これはすべて明確ではありません。 プログラミングの概念としてのオブジェクトの簡単な歴史から始めましょう。

物語


このストーリーは、ほとんどのプログラミングストーリーと同様に、複雑さの問題から始まります。 あるアイデアは、複雑さを互いに分離された小さな部分に分割することで管理しやすくすることができると言います。 これらの部分はオブジェクトと呼ばれ始めました。

オブジェクトは、内部のスティッキーな複雑さを隠すハードシェルであり、代わりに、オブジェクトを使用する必要があるインターフェイスを表すチューニングノブとコンタクト(メソッドなど)を提供します。 この考え方は、インターフェイスが比較的シンプルであり、それを操作するときに、オブジェクト内で発生するすべての複雑なプロセスを無視できるようにすることです。

シンプルなインターフェースは、多くの複雑さを隠すことができます。

たとえば、画面の一部へのインターフェイスを提供するオブジェクトを想像してください。 このセクションでは、このセクションに図形を描画したり、テキストを表示したりできますが、同時に、テキストまたは図形のピクセルへの変換に関するすべての詳細が非表示になります。 drawCircleなどの一連のメソッドがあり、そのようなオブジェクトを使用するために知っておく必要があるのはそれだけです。

このようなアイデアは70〜80年代に開発され、90年代には広告の波、つまりオブジェクト指向プログラミングの革命によって表面化されました。 突然、大勢の人々が、オブジェクトがプログラミングの正しい方法であると発表しました。 そして、オブジェクトを持たないものはすべて時代遅れのナンセンスです。

そのような狂信は常に無意味な無意味なものの束につながり、それ以来一種の反革命がありました。 一部のサークルでは、一般にオブジェクトの評判が非常に低くなっています。

私はイデオロギーの観点よりも実用的な観点からそれらを検討することを好みます。 オブジェクト指向の文化によって普及している、特にカプセル化(内部の複雑さと外部の単純さの違い)に役立つアイデアがいくつかあります。 彼らは探索する価値があります。

この章では、JavaScriptのオブジェクトに対するやや風変わりなアプローチと、それらが従来のオブジェクト指向技術とどのように関係するかについて説明します。

方法


メソッドは、関数を含むプロパティです。 簡単な方法:

var rabbit = {}; rabbit.speak = function(line) { console.log("  '" + line + "'"); }; rabbit.speak(" ."); // →   ' .' 


通常、メソッドは、呼び出し元のオブジェクトに対して何かを行う必要があります。 関数がメソッドの形式で呼び出された場合-object.method()などのオブジェクトのプロパティとして-本体の特別な変数は、それを呼び出したオブジェクトを示します。

 function speak(line) { console.log(" " + this.type + "   '" + line + "'"); } var whiteRabbit = {type: "", speak: speak}; var fatRabbit = {type: "", speak: speak}; whiteRabbit.speak("   , " + "   !"); // →     '    ,    !' fatRabbit.speak("   ."); // →     '    .' 


コードはthisキーワードを使用して、話しているウサギのタイプを出力します。

applyおよびbindメソッドは、メソッド呼び出しをエミュレートするために使用できる最初の引数を取ることを思い出してください。 この最初の引数は、this変数の値を与えるだけです。

呼ばれた呼び出しを適用するのに似た方法があります。 また、そのメソッドである関数を呼び出し、通常の引数のみを使用し、配列の形式では使用しません。 適用およびバインドと同様に、この値を呼び出しに渡すことができます。

 speak.apply(fatRabbit, ["!"]); // →     ' !' speak.call({type: ""}, " , ."); // →     ', .' 


プロトタイプ


手を見てください。

 var empty = {}; console.log(empty.toString); // → function toString(){…} console.log(empty.toString()); // → [object Object] 


空のオブジェクトのプロパティを取得しました。 魔法!

もちろん、魔法ではありません。 JavaScriptでオブジェクトがどのように機能するかについてすべてを説明したわけではありません。 機能セットに加えて、ほぼ全員がプロトタイプも持っています。 プロトタイプは、プロパティのバックアップソースとして使用される別のオブジェクトです。 オブジェクトが所有していないプロパティの要求を受け取ると、このプロパティはプロトタイプによって、次にプロトタイププロトタイプなどによって検索されます。

さて、空のオブジェクトのプロトタイプは誰ですか? これは、すべてのオブジェクトの偉大な祖先であるObject.prototypeです。

 console.log(Object.getPrototypeOf({}) == Object.prototype); // → true console.log(Object.getPrototypeOf(Object.prototype)); // → null 


ご想像のとおり、Object.getPrototypeOf関数はオブジェクトのプロトタイプを返します。

JavaScriptのプロトタイプ関係はツリーのように見え、Object.prototypeはそのルートにあります。 オブジェクトを文字列ビューに変換するtoStringなど、すべてのオブジェクトに表示されるいくつかのメソッドを提供します。

多くのオブジェクトのプロトタイプは、直接Object.prototypeではなく、デフォルトプロパティを提供する他のオブジェクトです。 関数はFunction.prototypeに由来し、配列はArray.prototypeに由来します。

 console.log(Object.getPrototypeOf(isNaN) == Function.prototype); // → true console.log(Object.getPrototypeOf([]) == Array.prototype); // → true 


このようなプロトタイプには独自のプロトタイプがあります-多くの場合Object.prototypeなので、直接ではありませんが、とにかくtoStringのようなメソッドを提供します。

Object.getPrototypeOf関数は、オブジェクトのプロトタイプを返します。 Object.createを使用して、特定のプロトタイプを持つオブジェクトを作成できます。

 var protoRabbit = { speak: function(line) { console.log(" " + this.type + "   '" + line + "'"); } }; var killerRabbit = Object.create(protoRabbit); killerRabbit.type = ""; killerRabbit.speak("!"); // →     ' !' 


原ウサギは、すべてのウサギが持っているプロパティのコンテナとして機能します。 キラーオブジェクトなどの特定のウサギオブジェクトには、そのタイプなど、そのオブジェクトにのみ適用されるプロパティが含まれ、プロトタイプから他のオブジェクトと共有されるプロパティを継承します。

コンストラクター


特定のプロトタイプから継承されたオブジェクトを作成するより便利な方法は、コンストラクターです。 JavaScriptでは、前述の新しいキーワードを使用して関数を呼び出すと、関数がコンストラクターとして機能します。 コンストラクタは、新しく作成されたオブジェクトにバインドされたthis変数を自由に使用でき、オブジェクトを含む別の値を直接返さない場合、代わりにこの新しいオブジェクトが返されます。

彼らは、newで作成されたオブジェクトはコンストラクターのインスタンスであると言います。

これは単純なウサギのコンストラクタです。 コンストラクター名は通常、他の関数と区別するために大文字になります。

 function Rabbit(type) { this.type = type; } var killerRabbit = new Rabbit(""); var blackRabbit = new Rabbit(""); console.log(blackRabbit.type); // →  


コンストラクター(および一般にすべての関数)は、prototypeというプロパティを自動的に取得します。このプロパティには、デフォルトでObject.prototypeから派生したシンプルで空のオブジェクトが含まれています。 このコンストラクターによって作成された各インスタンスには、プロトタイプとしてこのオブジェクトがあります。 したがって、Rabbitコンストラクターによって作成されたウサギにspeakメソッドを追加するには、次のようにします。

 Rabbit.prototype.speak = function(line) { console.log(" " + this.type + "   '" + line + "'"); }; blackRabbit.speak(" ..."); // →     '  ...' 


プロトタイプとコンストラクターとの関連付け(プロトタイププロパティ経由)とオブジェクトのプロトタイプ(Object.getPrototypeOfから取得可能)の違いに注意することが重要です。 実際、コンストラクターは関数であるため、コンストラクターのプロトタイプはFunction.prototypeです。 プロトタイププロパティは、それによって作成されたインスタンスのプロトタイプですが、プロトタイプではありません。

継承されたプロパティの再読み込み


オブジェクトにプロパティを追加すると、プロトタイプにあるかどうかに関係なく、オブジェクト自体に直接追加されます。 今、これは彼の財産です。 プロトタイプに同じ名前のプロパティがある場合、オブジェクトには影響しません。 プロトタイプ自体は変更されません。

 Rabbit.prototype.teeth = ""; console.log(killerRabbit.teeth); // →  killerRabbit.teeth = ",    "; console.log(killerRabbit.teeth); // → ,    console.log(blackRabbit.teeth); // →  console.log(Rabbit.prototype.teeth); // →  


この図は、コードを実行した後の状況を示しています。 RabbitとObjectのプロトタイプは、背景のようにkillerRabbitの背後にあり、オブジェクト自体にはないプロパティを要求できます。

多くの場合、プロトタイプに存在するプロパティを再ロードすると有益です。 ウサギの歯の例のように、より一般的な特性の例外的な特性を表現するために使用できますが、通常のオブジェクトはプロトタイプから取得した標準値を使用します。

また、異なるtoStringメソッドの関数と配列を割り当てるためにも使用されます。

 console.log(Array.prototype.toString == Object.prototype.toString); // → false console.log([1, 2].toString()); // → 1,2 


配列のtoStringを呼び出すと、.join( "、")と同様の結果が表示されます-コンマ区切りのリストが取得されます。 Object.prototype.toStringを配列に直接呼び出すと、異なる結果が生成されます。 この関数は配列について何も知りません:

 console.log(Object.prototype.toString.call([1, 2])); // → [object Array] 


望ましくないプロトタイプの相互作用


プロトタイプは、いつでも、それに基づいているすべてのオブジェクトに新しいプロパティとメソッドを追加するのに役立ちます。 たとえば、ウサギにはダンスが必要な場合があります。

 Rabbit.prototype.dance = function() { console.log(" " + this.type + "   ."); }; killerRabbit.dance(); // →     . 


これは便利です。 しかし、場合によってはこれが問題につながります。 前の章では、値を名前に関連付ける方法としてオブジェクトを使用しました。これらの名前のプロパティを作成し、対応する値を与えました。 4章の例を次に示します。

 var map = {}; function storePhi(event, phi) { map[event] = phi; } storePhi("", 0.069); storePhi(" ", -0.081); 


for / inループを使用してオブジェクト内のすべてのphi値を反復処理し、in演算子を使用して名前の存在を確認できます。 残念ながら、オブジェクトのプロトタイプは私たちを悩ませています。

 Object.prototype.nonsense = ""; for (var name in map) console.log(name); // →  // →   // → nonsense console.log("nonsense" in map); // → true console.log("toString" in map); // → true //    delete Object.prototype.nonsense; 


これは間違っています。 「ナンセンス」と呼ばれるイベントはありません。 さらに、「toString」というイベントはありません。

in演算子はアカウントでtrueを返しますが、toStringがfor / inループで抜けなかったことは興味深いです。 これは、JavaScriptがカウント可能なプロパティとカウントできないプロパティを区別するためです。

値を割り当てて作成するすべてのプロパティはカウント可能です。 Object.prototypeのすべての標準プロパティは数えられないため、for / inループでクロールアウトされません。

カウントできないプロパティは、Object.defineProperty関数を使用して宣言できます。この関数を使用すると、作成するプロパティのタイプを指定できます。

 Object.defineProperty(Object.prototype, "hiddenNonsense", {enumerable: false, value: ""}); for (var name in map) console.log(name); // →  // →   console.log(map.hiddenNonsense); // →  


現在はプロパティがありますが、ループ内では表示されません。 いいね しかし、Object.prototypeのプロパティがオブジェクト内に存在すると主張しているin演算子の問題にまだ悩まされています。 そのためにはhasOwnPropertyメソッドが必要です。

 console.log(map.hasOwnProperty("toString")); // → false 


彼は、プロトタイプに関係なく、プロパティがオブジェクトのプロパティであるかどうかを言います。 多くの場合、これはinステートメントが生成するよりも有用な情報です。

コードをプログラムにダウンロードした他の誰かがオブジェクトのメインプロトタイプを台無しにしてしまうのではないかと心配している場合は、次のようなfor / inループを作成することをお勧めします。

 for (var name in map) { if (map.hasOwnProperty(name)) { // ...     } } 


プロトタイプのないオブジェクト


しかし、ウサギの穴はそこで終わりません。 そして、誰かがマップオブジェクトにhasOwnProperty名を登録し、それに値42を割り当てた場合はどうなりますか? これで、map.hasOwnPropertyの呼び出しは、関数ではなく番号を含むローカルプロパティにアクセスします。

この場合、プロトタイプは邪魔になるだけで、プロトタイプのないオブジェクトが必要です。 特定のプロトタイプを使用してオブジェクトを作成できるObject.create関数を見ました。 プロトタイプにnullを渡して、プロトタイプのない新鮮な小さなオブジェクトを作成できます。 これは、任意のプロパティが可能なタイプマップのオブジェクトに必要なものです。

 var map = Object.create(null); map[""] = 0.069; console.log("toString" in map); // → false console.log("" in map); // → true 


それは良いです! オブジェクトのすべてのプロパティは私たちが個人的に設定するため、hasOwnPropertyの迷いはもう必要ありません。 人々がObject.prototypeで何をしたかを見ることなく、静かにfor / inループを使用します

多型


値をオブジェクトの文字列に変換するString関数を呼び出すと、toStringメソッドを呼び出して意味のある文字列を作成します。 一部の標準プロトタイプは、toStringのバージョンを宣言して、単なる「[object Object]」よりも有用な文字列を作成することを述べました。

これは強力なアイデアの簡単な例です。 特定のインターフェイス(この場合はtoStringメソッド)を介してオブジェクトを操作するコードが記述されている場合、このインターフェイスをサポートするオブジェクトはコードに接続でき、すべてが機能します。

このテクニックはポリモーフィズムと呼ばれます-誰も形を変えません。 ポリモーフィックコードは、同じインターフェイスをサポートしている限り、さまざまな形式の値で機能します。

テーブルをフォーマットする


ポリモーフィズムがどのように見えるか、実際にオブジェクト指向プログラミングを理解するために例を見てみましょう。 プロジェクトは次のとおりです。テーブルセルから配列の配列を受け取り、きれいにフォーマットされたテーブルを含む行を作成するプログラムを作成します。 つまり、列と行が整列されます。 このように:

 name height country ------------ ------ ------------- Kilimanjaro 5895 Tanzania Everest 8848 Nepal Mount Fuji 3776 Japan Mont Blanc 4808 Italy/France Vaalserberg 323 Netherlands Denali 6168 United States Popocatepetl 5465 Mexico 


これは次のように機能します。メイン関数は各セルの幅と高さを尋ね、この情報を使用して列の幅と行の高さを決定します。 次に、セルに自分自身を描画するように依頼し、結果を1行で収集します。

プログラムは、明確に定義されたインターフェイスを介してセルオブジェクトと通信します。 セルタイプは強く設定されません。 新しいセルスタイルを追加できます。たとえば、見出しに下線付きのセルを追加できます。 そして、彼らが私たちのインターフェースをサポートしていれば、彼らはプログラムを変更することなく単純に動作します。
インターフェース:

minHeight()は、セルに必要な最小の高さを示す数値を返します(行で表されます)

minWidth()は、セルに必要な最小幅を示す数値を返します(文字で表されます)

draw(width、height)は、文字列のセットを含む長さheightの配列を返します。各文字列は文字幅です。 これはセルの内容です。

ここでは関連性が高いため、高階関数を使用します。

プログラムの最初の部分では、セルマトリックスの最小列幅と行高さの配列を計算します。 rows変数には配列の配列が含まれます。各内部配列はセルの行です。

 function rowHeights(rows) { return rows.map(function(row) { return row.reduce(function(max, cell) { return Math.max(max, cell.minHeight()); }, 0); }); } function colWidths(rows) { return rows[0].map(function(_, i) { return rows.reduce(function(max, row) { return Math.max(max, row[i].minWidth()); }, 0); }); } 


名前がアンダースコア(_)で始まる(または全体で構成される)変数を使用して、この引数が使用されないことをコードを読み取る人に示します。

rowHeights関数は簡単なはずです。 reduceを使用してセル配列の最大の高さを計算し、それをmapにラップして、rows配列のすべての行を走査します。

外部配列は列ではなく行の配列であるため、colWidthsの状況はより複雑です。 マップ(forEach、filter、および同様の配列メソッドなど)が2番目の引数を指定された関数(現在の要素のインデックス)に渡すことを忘れていました。 mapで最初の行の要素を渡し、関数の2番目の引数のみを使用して、colWidthsは列インデックスごとに1つの要素を持つ配列を作成します。 reduce呼び出しは、各インデックスの外側の行の配列を調べ、そのインデックス内で最も幅の広いセルの幅を選択します。

テーブルを表示するコード:

 function drawTable(rows) { var heights = rowHeights(rows); var widths = colWidths(rows); function drawLine(blocks, lineNo) { return blocks.map(function(block) { return block[lineNo]; }).join(" "); } function drawRow(row, rowNum) { var blocks = row.map(function(cell, colNum) { return cell.draw(widths[colNum], heights[rowNum]); }); return blocks[0].map(function(_, lineNo) { return drawLine(blocks, lineNo); }).join("\n"); } return rows.map(drawRow).join("\n"); } 


drawTable関数は、内部のdrawRow関数を使用してすべての線を描画し、それらの場所を改行文字で接続します。

drawRow関数は、行セルオブジェクトをブロックに変換します。ブロックは、行で区切られたセルの内容を表す文字列の配列です。 番号3776を含む単一のセルは1つの要素の配列["3776"]で表すことができ、下線付きのセルは2行を占有して配列["name"、 "----"]のように見えます。

同じ高さの行のブロックは、隣同士に表示する必要があります。 drawRowでのmapの2番目の呼び出しは、左端のブロックの行から始めて、出力のこの行を行ごとに構築し、次にそれぞれの行をテーブルの全幅に追加します。 次に、これらの行は改行で結合され、drawRowが返す一連の全体が作成されます。

drawLine関数は、ブロックの配列から隣り合う行をキャプチャし、それらをスペースで連結して、テーブルの列間に1文字のギャップを作成します。

セルのインターフェースを提供するテキストを含むセルのコンストラクターを作成しましょう。 splitメソッドを使用して文字列を文字列の配列に分割します。splitメソッドは、引数が出現するたびに文字列を切り取り、これらの断片の配列を返します。 minWidthメソッドは、配列内の最大線幅を見つけます。

 function repeat(string, times) { var result = ""; for (var i = 0; i < times; i++) result += string; return result; } function TextCell(text) { this.text = text.split("\n"); } TextCell.prototype.minWidth = function() { return this.text.reduce(function(width, line) { return Math.max(width, line.length); }, 0); }; TextCell.prototype.minHeight = function() { return this.text.length; }; TextCell.prototype.draw = function(width, height) { var result = []; for (var i = 0; i < height; i++) { var line = this.text[i] || ""; result.push(line + repeat(" ", width - line.length)); } return result; }; 


補助関数repeatが使用され、指定された回数だけ指定された値で行が作成されます。 drawメソッドはこれを使用して、必要な長さになるように行をインデントします。

経験のために5x5のチェス盤を描いてみましょう。

 var rows = []; for (var i = 0; i < 5; i++) { var row = []; for (var j = 0; j < 5; j++) { if ((j + i) % 2 == 0) row.push(new TextCell("##")); else row.push(new TextCell(" ")); } rows.push(row); } console.log(drawTable(rows)); // → ## ## ## // ## ## // ## ## ## // ## ## // ## ## ## 


動作します! ただし、すべてのセルのサイズは同じであるため、テーブルの書式設定コードは何も面白くありません。

作成中の山のテーブルの初期データは、MOUNTAINS変数に含まれています。この変数は、 ここからダウンロードできます

アンダースコアを使用して、列名を含む一番上の行を強調表示する必要があります。 問題ありません-これを行うセルのタイプを設定するだけです。

 function UnderlinedCell(inner) { this.inner = inner; }; UnderlinedCell.prototype.minWidth = function() { return this.inner.minWidth(); }; UnderlinedCell.prototype.minHeight = function() { return this.inner.minHeight() + 1; }; UnderlinedCell.prototype.draw = function(width, height) { return this.inner.draw(width, height - 1) .concat([repeat("-", width)]); }; 


下線のセルには別のセルが含まれています。 (minWidthメソッドとminHeightメソッドを呼び出して)内部セルと同じサイズを返しますが、ダッシュが占めるスペースのために高さに1を追加します。

描画は簡単です-内側のセルの内容を取得し、ダッシュで満たされた1行を追加します。

これで、メインエンジンを使用して、データセットからセルのグリッドを作成する関数を作成できます。

 function dataTable(data) { var keys = Object.keys(data[0]); var headers = keys.map(function(name) { return new UnderlinedCell(new TextCell(name)); }); var body = data.map(function(row) { return keys.map(function(name) { return new TextCell(String(row[name])); }); }); return [headers].concat(body); } console.log(drawTable(dataTable(MOUNTAINS))); // → name height country // ------------ ------ ------------- // Kilimanjaro 5895 Tanzania // …    


標準のObject.keys関数は、オブジェクトプロパティ名の配列を返します。 テーブルの一番上の行には、列名の付いた下線付きセルが含まれている必要があります。 データセットのすべてのオブジェクトの値は、見出しの下にある通常のセルのように見えます。キー配列にマップ関数を渡すことで抽出し、各行でセルの順序を1つにするようにします。

結果のテーブルは、例のテーブルに似ており、数字だけが右揃えされていません。 これについては少し後で説明します。

ゲッターとセッター


インターフェイスを作成するときに、メソッドではないプロパティを入力できます。 数値を格納するための変数としてminHeightとminWidthを簡単に定義できます。 しかし、コンストラクターで値を計算するためのコードを記述する必要があります。オブジェクトの構築はそれらに直接関係しないため、これは悪いことです。 たとえば、内側のセルまたは下線の付いたセルが変更されたときに問題が発生する可能性があり、そのサイズも変更する必要があります。

これらの考慮事項により、多くの非メソッドプロパティがインターフェイスから省略されました。 プロパティ値に直接アクセスする代わりに、getSomethingやsetSomethingなどのメソッドを使用してプロパティ値を読み書きします。 しかし、マイナスがあります-あなたは多くの追加のメソッドを書く(そして読む)必要があります。

幸いなことに、JavaScriptは両方のアプローチの長所を使用した手法を提供します。 外部では普通に見えるプロパティを設定できますが、それらに関連付けられたメソッドを密かに持っています。

 var pile = { elements: ["", "", ""], get height() { return this.elements.length; }, set height(value) { console.log("    ", value); } }; console.log(pile.height); // → 3 pile.height = 100; // →     100 


オブジェクトの宣言では、レコードの取得または設定により、プロパティの読み取りまたは書き込み時に呼び出される関数を指定できます。Object.defineProperty関数を使用して、プロトタイプなどの既存のオブジェクトにこのようなプロパティを追加することもできます(以前に使用して、数え切れないプロパティを作成しました)。

 Object.defineProperty(TextCell.prototype, "heightProp", { get: function() { return this.text.length; } }); var cell = new TextCell("\n"); console.log(cell.heightProp); // → 2 cell.heightProp = 100; console.log(cell.heightProp); // → 2 


definePropertyに渡されるオブジェクトにsetプロパティを設定して、setterメソッドを指定することもできます。ゲッターはあるがセッターがない場合、プロパティへの書き込みは無視されます。

継承


ただし、表の書式設定の演習はまだ完了していません。数値列が右揃えの場合、読みやすくなります。 TextCellのような別のタイプのセルを作成する必要がありますが、テキストを右側にスペースで埋める代わりに、左側に埋めて右側に揃えます。

プロトタイプの3つのメソッドすべてを使用して、新しいコンストラクターを作成できます。しかし、プロトタイプ自体にプロトタイプを含めることができるため、よりスマートに行うことができます。

 function RTextCell(text) { TextCell.call(this, text); } RTextCell.prototype = Object.create(TextCell.prototype); RTextCell.prototype.draw = function(width, height) { var result = []; for (var i = 0; i < height; i++) { var line = this.text[i] || ""; result.push(repeat(" ", width - line.length) + line); } return result; }; 


通常のTextCellのコンストラクターとminHeightおよびminWidthメソッドを再利用しました。また、RTextCellは、drawメソッドに別の関数があることを除いて、一般にTextCellと同等です。

このようなスキームは、継承と呼ばれます。多くの労力を費やすことなく、既存のものに基づいて優れたデータ型を構築できます。通常、新しいコンストラクターは古いオブジェクトを呼び出します(callメソッドを介して、新しいオブジェクトとその値を渡します)。その後、古いオブジェクトにあるはずのすべてのフィールドが追加されたと想定できます。この型のインスタンスが古いプロトタイプのプロパティにアクセスできるように、プロトタイプコンストラクターを古いものから継承します。最後に、いくつかのプロパティを新しいプロトタイプに追加することでオーバーライドできます。

数値セルにRTextCellsを使用するようにdataTable関数を少し編集すると、必要なテーブルが取得されます。

 function dataTable(data) { var keys = Object.keys(data[0]); var headers = keys.map(function(name) { return new UnderlinedCell(new TextCell(name)); }); var body = data.map(function(row) { return keys.map(function(name) { var value = row[name]; //  : if (typeof value == "number") return new RTextCell(String(value)); else return new TextCell(String(value)); }); }); return [headers].concat(body); } console.log(drawTable(dataTable(MOUNTAINS))); // → …    


継承は、カプセル化と多態性とともに、オブジェクト指向の伝統の主要な部分です。しかし、後者の2つは素晴らしいアイデアとして認識されていますが、最初の2つは議論の余地があります。

主に、通常、ポリモーフィズムと混同され、実際よりも強力なツールとして提示され、他の目的に使用されるためです。カプセル化とポリモーフィズムを使用してコードの一部を分離し、プログラムの一貫性を減らしますが、継承は型を結合し、より多くの結合を作成します。

継承なしでポリモーフィズムを使用できます。継承を完全に避けることはお勧めしません-私はプログラムでそれを定期的に使用します。ただし、コードを整理する基本原則としてではなく、最小限のコードで新しい型を定義できるようにする、より巧妙なトリックとして扱ってください。UnderlinedCellは別のセルオブジェクトを使用して構築されるため、構成を使用して型を拡張することをお勧めします。彼は単にそれをプロパティに保存し、呼び出しを自分のメソッドにリダイレクトします。

Instanceof演算子


オブジェクトが特定のコンストラクターから来たかどうかを知るのが便利な場合があります。このために、JavaScriptはinstanceofバイナリ演算子を提供します。

 console.log(new RTextCell("A") instanceof RTextCell); // → true console.log(new RTextCell("A") instanceof TextCell); // → true console.log(new TextCell("A") instanceof RTextCell); // → false console.log([1] instanceof Array); // → true 


演算子は継承された型も通過します。 RTextCell.prototypeはTextCell.prototypeから派生しているため、RTextCellはTextCellのインスタンスです。演算子は、配列などの標準コンストラクターにも適用できます。ほとんどすべてのオブジェクトはObjectのインスタンスです。

まとめ


オブジェクトは、最初に送信したものよりも少し複雑であることがわかりました。プロトタイプがあります-これらは他のオブジェクトであり、プロトタイプにこのプロパティがある場合、実際には存在しないプロパティがあるかのように動作します。単純なオブジェクトのプロトタイプはObject.prototype /

Constructorsです。名前が通常大文字で始まる関数は、new演算子でオブジェクトを作成するために使用できます。新しいオブジェクトのプロトタイプは、コンストラクターのprototypeプロパティに含まれるオブジェクトになります。これは、特定の型のすべての値をプロトタイプ内で共有するプロパティを配置することで使用できます。 instanceof演算子は、オブジェクトとコンストラクターを指定すると、オブジェクトがこのコンストラクターのインスタンスであるかどうかを判断できます。

オブジェクトの場合、インターフェイスを作成し、このインターフェイスを介してのみオブジェクトと通信するように全員に指示できます。オブジェクトの残りの実装の詳細はカプセル化され、インターフェイスの背後に隠されています。

その後、同じインターフェースを使用して異なるオブジェクトを使用することを禁止する人はいません。異なるオブジェクトが同じインターフェースを持っている場合、それらで動作するコードは異なるオブジェクトで同じように動作できます。これはポリモーフィズムと呼ばれ、非常に便利です。

細部のみが異なるいくつかのタイプを定義する場合、新しいタイプのプロトタイプを古いタイプのプロトタイプから単純に継承すると、新しいコンストラクターが古いタイプを呼び出すのに便利です。これにより、古いものと同様のオブジェクトタイプが得られますが、プロパティを追加したり、古いものを再定義したりできます。

演習


ベクタータイプ

2次元空間のベクトルを表すVectorコンストラクターを記述します。パラメータxとy(数値)を取り、これらは同じ名前のプロパティに保存されます。

Vectorプロトタイプにプラスとマイナスの2つのメソッドを与えます。これらは別のベクターをパラメーターとして受け取り、xとyの2つの和または差を格納する新しいベクターを返します(1つ、2番目の引数)

ベクターの長さをカウントするプロトタイプにゲッター長を追加します-(0、0)から(x、y)までの距離。

 //   console.log(new Vector(1, 2).plus(new Vector(2, 3))); // → Vector{x: 3, y: 5} console.log(new Vector(1, 2).minus(new Vector(2, 3))); // → Vector{x: -1, y: -1} console.log(new Vector(3, 4).length); // → 5 


もう一つのセル

この章のテーブルセルインターフェイスに一致するセルタイプStretchCell(内部、幅、高さ)を作成します。彼は(UnderlinedCellのように)別のセルをラップし、結果のセルが少なくとも指定された幅と高さを持っていることを確認する必要があります(内側のセルが小さくても)。

 //  . var sc = new StretchCell(new TextCell("abc"), 1, 2); console.log(sc.minWidth()); // → 3 console.log(sc.minHeight()); // → 2 console.log(sc.draw(3, 2)); // → ["abc", " "] 


シーケンスへのインターフェース

値のセットを抽象化するインターフェースを設計します。そのようなインターフェイスを持つオブジェクトはシーケンスであり、インターフェイスはコードがシーケンスを通過し、それを構成する値を処理し、何らかの方法でシーケンスの終わりに到達したことを通知できるようにする必要があります。

インターフェースを定義したら、logFive関数を作成してみてください。logFive関数は、シーケンスオブジェクトを取得し、最初の5つの要素に対してconsole.logを呼び出します。

次に、配列をラップし、開発したインターフェイスを使用して配列を走査できるArraySeqオブジェクトタイプを作成します。別のタイプのオブジェクトRangeSeqを作成します。これは、数値の範囲を通過します(コンストラクターは、from引数とto引数を受け入れる必要があります)。

 //  . logFive(new ArraySeq([1, 2])); // → 1 // → 2 logFive(new RangeSeq(100, 1000)); // → 100 // → 101 // → 102 // → 103 // → 104 

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


All Articles