JavaScript/オブジェクト
オブジェクトとは[編集]
JavaScriptは、プロトタイプベースのオブジェクト指向プログラミング言語です。 全てのオブジェクトのプロトタイプチェインをたどると、標準組み込みオブジェクト Object に行き着きます。
Object は他の多くの言語で連想配列やハッシュテーブルと呼ばれるもので重複しないキー(典型的には文字列)とそれに一対一に対応する値の対(プロパティ)を保持します。
プリミティブ[編集]
大概のインスタンス(変数に代入できるものと考えて下さい)はオブジェクトですが、Undefined
, Null
, 真偽値
, 数値
, 長整数
, シンボル
および 文字列
の7種類はプロパティを持たず他のオブジェクトをその要素にすることは有りません。
この様なデータをプリミティブまたはプリミティブ値と呼びます[1]。
ここまで読んで少しおかしいと思った人がいるかも知れません。
たとえば"Hello"
について考えてみましょう。この文字列リテラルの length(長さ)というプロパティを表示してみます(プリミティブはプロパティを持たないはず)。
console.log("Hello".length); // コンソールに 5 を表示
何事もなく長さが表示されました。
これは、プリミティブのプロパティを参照するとプリミティブに対応したラッパーオブジェクト(この場合はStringオブジェクト)がプリミティブの値をコンストラクタのパラメータとして生成され、生成されたラッパーオブジェクトのプロパティが参照されるからです。
console.log((new String("Hello")).length);
に相当することが自動的に行われます。
常々のプログラミングでプリミティブとラッパーオブジェクトの違いを意識する事は稀ですが、両者は明確に異なります(例えば、eval()関数に文字列を渡すと評価されStringオブジェクトを渡すと文字列がかえります)。
console.log(typeof "Hello"); // string console.log(typeof (new String("Hello"))); // object
このようにラッパーオブジェクトの型は Object です。
プリミティブとラッパーオブジェクトの対応関係(UndefinedとNullには、ラッパーオブジェクトは有りません)
構文[編集]
もっとも単純なオブジェクトは次のような形をしています。
オブジェクトリテラル[編集]
const obj = { name: "愛", age : 14 };
プロパティアクセサ[編集]
オブジェクトにドット記法またはブラケット記法でキーの文字列を指定すると対応する値を取り出すことができます。
- ドット記法
オブジェクト . プロパティキー
- ブラケット記法
オブジェクト [ プロパティキー文字列 ]
- プロパティアクセサの使用例
console.log( obj.name ); // 愛 console.log( obj['age'] ); // 14
このようなデータ構造を連想配列(れんそうはいれつ、associative array)といいます。 キーと値のペアはプロパティ (property) と呼ばれます(プロパティキーをプロパティと言いがちです)。
JavaScriptにおいてオブジェクトは連想配列です[2]。たとえばStringオブジェクトのlengthプロパティというのは、まさにStringという連想配列のprototypeプロパティのlengthプロパティのことを指しています。
プロパティのキーは通常識別子で数値を指定すると文字列に変換されます。
const obj = {}; obj[10] = "xyz"; console.log(Object.keys(obj)); // ["10"]
また、プロパティのキーにシンボルをとることも出来ます。プロパティの値には数値や文字列だけでなく関数などあらゆるオブジェクトとプリミティブをとることができます。
Objectオブジェクト[編集]
ObjectオブジェクトはJavaScriptのオブジェクトをラップします。
const object = new Object();
これは
const object = {};
と同じです。
JavaScriptのすべてのオブジェクトはObjectオブジェクトのインスタンスを継承します。 すなわち、Object.prototypeに定義されたプロパティやメソッドは、すべてのオブジェクトから参照することができます。 オブジェクトのプロパティ記述子 (property descriptor) とは、プロパティの値やプロパティが書き換え可能かどうかなどのフラグを保持する仕組みのことです。
プロパティ[編集]
- Object.prototype
- Objectプロトタイプオブジェクト
静的メソッド[編集]
この節は書きかけです。この節を編集してくれる方を心からお待ちしています。 Objectは、prototypeに含まれるメソッドの他、静的メソッドも標準で用意されています[3]。
- Object.assign()
- 1つまたは複数のソース・オブジェクトからターゲット・オブジェクトに、列挙可能なすべての自己プロパティの値をコピーします。
- Object.create()
- Object.defineProperties()
- オブジェクトに複数のプロパティを定義します。
- Object.defineProperty()
- オブジェクトにプロパティを定義します。
- Object.entries()
- 列挙可能な文字列プロパティのすべての [key, value] ペアを配列で返します。
- Object.freeze()
- プロパティを全て凍結します
- Object.fromEntries()
- Object.getOwnPropertyDescriptors()
- オブジェクトの複数のプロパティ記述子を返します。
- Object.getOwnPropertyDescriptor()
- オブジェクトのプロパティ記述子を返します。
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.getPrototypeOf()
- オブジェクトのプロトタイプを返します。
- Object.isExtensible()
- Object.isFrozen()
- Object.isSealed()
- Object.is()
- Object.keys()
- オブジェクトの列挙可能なすべてのプロパティを配列で返します。
- Object.preventExtensions()
- Object.seal()
- Object.setPrototypeOf()
- Object.values()
この一覧の雛形は下記のように Object.getOwnPropertyDescriptors(Object) を使って生成しました。
const a = []; for (p in Object.getOwnPropertyDescriptors(Object)) { if (typeof Object[p] === "function") { a.push(`; [[{{PAGENAME}}/${p}|Object.${p}()]]`); } } console.log(a.sort().join("\n"));
アクセサプロパティ[編集]
アクセサプロパティは、実際には存在しないプロパティが存在しているかのように見せる仕掛けです[4]。
前置詞 get と set を使う方法[編集]
右辺値式としては get を、左辺値式としては set を前置したメソッドを定義すると、メソッド名が仮想のプロパティ名になります。
const obj = { _name: "", get name(){ return this._name; }, set name(v){ return this._name = v.toUpperCase(); }, }; obj.name = "abc"; console.log(obj.name); obj2 = Object.create(obj); console.log(obj2.name); obj2.name = obj.name = "xyz"; console.log(obj.name, obj2.name);
- getter はそのままの _name プロパティを返すことにする
- setter は、全て大文字にして _name プロパティに保存します(多重を考え値を返す)
- 暗黙にsetterが呼び出される
- 暗黙にgetterが呼び出され、ABC が表示される
- オブジェクトを複製
- コピーも同じ値を保持
- 多重代入
- XYZ XYZ を表示
Object.defineProperty() で[[Get]]と[[Set]]を操作する方法[編集]
アクセサプロパティの内部属性に[[Get]]と[[Set]]があります(データプロパティにはなく)[5]。 それぞれ、getter関数とsetter関数を格納します。 Object.defineProperty()でこの属性を操作することで、getterとsetterを追加したり変更したりすることが出来ます[6]。 get/set前置詞を使う方法ではオブジェクト生成時にしか getterやsetterを定義できませんでしたが、[[Get]]と[[Set]]を操作する方法であれば生成後も追加・変更が可能です。
obj = { _name: "", }; Object.defineProperty(obj, 'name', { get: function(){ return this._name; }, set: function(v){ return this._name = v.toUpperCase(); }, configurable: true, }); obj.name = "abc"; console.log(obj.name); obj2 = Object.create(obj); console.log(obj2.name); obj2.name = obj.name = "xyz"; console.log(obj.name, obj2.name);
- getter はそのままの _name プロパティを返すことにする
- setter は、全て大文字にして _name プロパティに保存します(多重を考え値を返す)
- configurable属性をtrueにすると、プロパティを後から操作することは出来るようになります(忘れがち)
- 暗黙にsetterが呼び出される
- 暗黙にgetterが呼び出され、ABC が表示される
- オブジェクトを複製
- コピーも同じ値を保持
- 多重代入
- XYZ XYZ を表示
アクセサプロパテの使用例[編集]
class Length { constructor() { this.l = void 0; } get m() {return this.l; } set m(n) { return this.l = n; } get cm() { return this.l * 100; } set cm(n) { return this.l = n / 100; } get mm() { return this.l * 1000; } set mm(n) { return this.l = n / 1000; } } const l = new Length() l.m = 1.77; console.log(`${l.m}メートルは、${l.cm}センチメートルで${l.mm}ミリメートルです。`)
「1.77メートルは、177センチメートルで1770ミリメートルです。」と表示されます。
オブジェクトの拡張[編集]
NumberやString、Array、Objectなどの標準グローバルオブジェクトにプロパティを追加したり、既存のメソッドをオーバーライドしたりしてこれらのオブジェクトを拡張することができます。
たとえばJavaScriptの文字列はStringオブジェクトのプロトタイプであるString.prototypeに定義されたプロパティやメソッドを継承するので、これを用いるとJavaScriptの文字列からあなたが定義したプロパティを参照したり、メソッドを呼び出したりすることができます。
- 標準組込みオブジェクトの prototype を書換えることは、著しい性能劣化を来たします。
- 文字列を n 回反復するメソッド(悪い例)
String.prototype.repeat = function(n) { return Array(n).fill(this.valueOf()).join(""); } "Xy".repeat(10); // "XyXyXyXyXyXyXyXyXyXy"
NumberやStringなどのすべてのオブジェクトはObjectオブジェクトのプロトタイプであるObject.prototypeを継承するので、Object.prototypeに定義されたプロパティやメソッドはJavaScriptの数値や文字列などのプリミティブも含め、すべてのインスタンスから参照することができます。
- 標準組込みオブジェクトの prototype を書換えることは、著しい性能劣化を来たします。
- Objectオブジェクトのプロトタイプへのメソッドの追加(悪い例)
Object.prototype.print = function(){ return console.log(this); }; "Hello, world!".print(); // "Hello, world!" と表示
さらにトップレベルのオブジェクト(ウェブブラウザ上のJavaScriptではwindow)もObjectオブジェクトのインスタンスを継承するので、Object.prototypeに定義されたプロパティやメソッドはグローバル変数やグローバル関数のようにトップレベルで呼び出すことができます。
- 標準組込みオブジェクトの prototype を書換えることは、著しい性能劣化を来たします。
- Objectオブジェクトのプロトタイプへのプロパティの追加(悪い例)
Object.prototype.x = true; x; // true
prototype プロパティを変更すべきか? |
Object とその子孫の prototype プロパティを変更すると、フリーハンドなオブジェクトシステムへの変更が実現できます。 とても魅力的な機能ですが、これには痛みを伴います。 具体的には JavaScript を実行する JavaScript エンジンには、prototype プロパティが変更されていないないならば、prototype プロパティを参照しないで既定の処理を行う最適化が施されています。 他方、prototype プロパティが変更されると、全ての prototype プロパティのプロパティが変わっている可能性があるので、毎回 prototype プロパティを参照する必要性が発生します。 prototype プロパティを参照するオーバーヘッドは慎重に小さくするよう努力されていますが、メソッドを含めプロパティが参照されるたびに毎回 prototype プロパティを参照するのは軽い処理では有りません。 このことから、小さくない規模あるいは短くない時間実行される JavaScript プログラムでは、標準組み込みオブジェクトの prototype プロパティを変更する代わりに当該オブジェクトの prototype プロパティ を複製したオブジェクトを雛形にインスタンスを生成することも検討に値します。
|
順列を求めるメソッドを配列に追加する[編集]
- 順列を求めるメソッドを配列に追加します(良い例)
if (!Array.prototype.permutation) { Object.defineProperty(Array.prototype, 'permutation', { value: function(n = this.length) { if (this === null) { throw new TypeError('Array.prototype.permutation: ' + 'called on null or undefined'); } if (typeof n !== 'number') { throw new TypeError(`${n} is not a number`); } if (n === 1) { return this.map(x => [x]); } const result = []; this.forEach((q, i) => this.filter((x, ix) => ix != i) .permutation(n - 1).forEach(e => result.push([q,...e]))) return result; } }); } const perm = "ABC".split("").permutation(3); console.log(JSON.stringify(perm)); // [["A","B","C"],["A","C","B"],["B","A","C"],["B","C","A"],["C","A","B"],["C","B","A"]]
組合わせを求めるメソッドを配列に追加する[編集]
- 組合わせを求めるメソッドを配列に追加します(良い例)
if (!Array.prototype.combination) { Object.defineProperty(Array.prototype, 'combination', { value: function(n = this.length) { if (this === null) { throw new TypeError('Array.prototype.combination: ' + 'called on null or undefined'); } if (typeof n !== 'number') { throw new TypeError(`${n} is not a number`); } if (n === 1) { return this.map(x => [x]); } const result = []; this.forEach((q, i) => this.slice(i + 1) .combination(n - 1).forEach(e => result.push([q,...e]))) return result; } }); } const comb = "ABC".split("").combination(2); console.log(JSON.stringify(comb)); // [["A","B"],["A","C"],["B","C"]]
メソッドチェーン[編集]
空白文字で区切られた単語を含んだ文字列
"h he li be b c n o f ne na mg al si p s cl ar k ca sc ti v cr"
があります。 この文字列から単語を取り出しキャピタライズし番号を振って , で区切って表示してみましょう。
- 非メソッドチェーン版
const str = "h he li be b c n o f ne na mg al si p s cl ar k ca sc ti v cr"; const ary = str.split(' '); let ary2 = [] for (let [i, s] of ary.entries()) ary2[i] = `${1+i}:${s.charAt(0).toUpperCase() + s.slice(1)}`; let result = "" for (let word of ary2) result += (word+", ") console.log(result) /* 1:H, 2:He, 3:Li, 4:Be, 5:B, 6:C, 7:N, 8:O, 9:F, 10:Ne, 11:Na, 12:Mg, 13:Al, 14:Si, 15:P, 16:S, 17:Cl, 18:Ar, 19:K, 20:Ca, 21:Sc, 22:Ti, 23:V, 24:Cr, */
オーソドックスなメソッドとfor-ofで出来ています(もっとオーソドックスにするなら、C言語風の f(;;) を使うべきだったかも)。
- メソッドチェーン版
console.log("h he li be b c n o f ne na mg al si p s cl ar k ca sc ti v cr".split(' ') .map((s, i) => `${1+i}:${s.charAt(0).toUpperCase() + s.slice(1)}`) .join(", ")) /* 1:H, 2:He, 3:Li, 4:Be, 5:B, 6:C, 7:N, 8:O, 9:F, 10:Ne, 11:Na, 12:Mg, 13:Al, 14:Si, 15:P, 16:S, 17:Cl, 18:Ar, 19:K, 20:Ca, 21:Sc, 22:Ti, 23:V, 24:Cr */
この例では Arrayオブジェクトのメソッドが使われているが、他のオブジェクトでもメソッドチェーンを作ることは出来ます。 非メソッドチェーン版とメソッドチェーン版を比較すると変数の数がメソッドチェーン版では減っている事にきがつくとおもいます。メソッドは左結合(左から順に評価、読みやすさのため適宜改行しているので上から下に評価)なので左のメソッドから順に評価され、メソッドの評価した結果は次の(右の)メソッド呼び出しオブジェクト項(メソッドから見ると this)になります。 この様に . を挟んで左から右にメソッドが次々と評価されます。 この連鎖構造を鎖に例えてメソッドチェーンと呼ばれます。
メソッドチェーン節の演習問題[編集]
非メソッドチェーン版とメソッドチェーン版を比較すると出力に小さな違いがあります。探してみましょう。 また、その原因をしらべ結果を揃えるようコードを改修してみましょう(どちらに合わせても構わないです)。
脚註[編集]
- ^ member of one of the types Undefined, Null, Boolean, Number, BigInt, Symbol, or String ECMA-262::4.4.5 primitive value
- ^ 連想配列専用オブジェクトとしてMapとWeakMapがあります。これらが存在するのは Objectはプロパティとくに prototype があり単なる連想配列として扱うと混乱が生じるためです。またMapとWeakMapのキーは全てのオブジェクトとプリミティブが使えますが、Objectオブジェクトのプロパティキーには文字列かSymbolしか使えません。
- ^ ECMA-262::20.1.2 Properties of the Object Constructor
- ^ ECMA-262::15.4 Method Definitions
- ^ ECMA-262::6.1.7.1 Property Attributes
- ^ ECMA-262::7.3.8 DefinePropertyOrThrow ( O, P, desc )