JavaScript/オブジェクト

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動
イベント処理 JavaScript
オブジェクト
DOM

オブジェクトとは[編集]

JavaScriptは、プロトタイプベースのオブジェクト指向プログラミング言語です。 JavaScriptでのオブジェクトは標準組み込みクラス Object を継承し、Object は他の多くの言語で連想配列ハッシュテーブルと呼ばれるもので重複しないキーとそれに一対一に対応する値の対(プロパティ)を保持します。

プリミティブ[編集]

大概のインスタンスはオブジェクトですが、文字列数値BigInt真偽値undefinedそしてシンボル はプロパティを持たず他のクラスをその要素にすることは有りません。この様なデータをプリミティブまたはプリミティブ値と呼びます。

ここまで読んで少しおかしいと思った人がいるかも知れません。 たとえば"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 です。

プリミティブとラッパーオブジェクトの対応関係。

連想配列[編集]

もっとも単純なオブジェクトは次のような形をしています。

const object = {
    'zero': 0,
    'one' : 1,
    'two' : 2
};

alert( object.zero );    // 0
alert( object['zero'] ); // 0

オブジェクトにドット記法またはブラケット記法でキーの文字列を指定すると対応する値を取り出すことができます。 このようなデータ構造を連想配列(れんそうはいれつ、associative array)といいます。 キーと値の対はプロパティ (property) と呼ばれます。

JavaScriptにおいて、オブジェクトと連想配列はまったく同じものです。たとえばStringオブジェクトのlengthプロパティというのは、まさにStringという連想配列のlengthプロパティのことを指しています。

プロパティのキーは通常識別子で数値を指定すると文字列に変換されます。また、シンボルをプロパティのキーとすることも出来ます。値には数値や文字列だけでなく、関数などあらゆるオブジェクトを指定することができます。たとえばStringオブジェクトのreplaceメソッドというのは、まさにString.prototype.replaceプロパティに指定された関数のことを指しているに過ぎません。

もう一つの例は、Mathオブジェクトでしょう。

const Math = {
    'E'  : Math.exp(1),
    'PI' : 4 * Math.atan2(1, 1),
    'exp': function(x){
        return Math.pow( Math.E, x );
    },
    /* 以下略 */
};

実際のMathオブジェクトのメソッド本体は Math.sin.toString ⇒ ƒ toString() { [native code] } とJavaScripitエンジンのネイティブコードを指しています。

Object[編集]

ObjectオブジェクトはJavaScriptのオブジェクトをラップします。

const object = new Object();

JavaScriptのすべてのオブジェクトはObjectオブジェクトのインスタンスを継承します。 すなわち、Object.prototypeに定義されたプロパティやメソッドは、すべてのオブジェクトから参照することができます。 オブジェクトのプロパティ記述子 (property descriptor) とは、プロパティの値やプロパティが書き換え可能かどうかなどのフラグを保持するオブジェクトのことです。

プロパティ[編集]

prototype
Objectプロトタイプオブジェクト

メソッド[編集]

create
プロトタイプオブジェクトとプロパティを指定してオブジェクトを生成します。
defineProperty
オブジェクトにプロパティを定義します。
defineProperties
オブジェクトに複数のプロパティを定義します。
getOwnPropertyDescriptor
オブジェクトのプロパティ記述子を返します。
getPrototypeOf
オブジェクトのプロトタイプを返します。
keys
オブジェクトの列挙可能なすべてのプロパティを配列で返します。

アクセサプロパティ[編集]

アクセサプロパティは、実際には存在しないプロパティが存在しているかのように見せる仕掛けです[1]。 右辺値式としては 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);
  1. getter はそのままの _name プロパティを返すことにする
  2. setter は、全て大文字にして _name プロパティに保存する(多重を考え値を返す)
  3. 暗黙にsetterが呼び出される
  4. 暗黙にgetterが呼び出され、ABC が表示される
  5. オブジェクトを複製
  6. コピーも同じ値を保持
  7. 多重代入
  8. XYZ XYZ を表示

オブジェクトの拡張[編集]

NumberStringArray、Objectなどの標準グローバルオブジェクトにプロパティを追加したり、既存のメソッドをオーバーライドしたりしてこれらのオブジェクトを拡張することができます。

たとえばJavaScriptの文字列はStringオブジェクトのプロトタイプであるString.prototypeに定義されたプロパティやメソッドを継承するので、これを用いるとJavaScriptの文字列からあなたが定義したプロパティを参照したり、メソッドを呼び出したりすることができます。

/* 文字列を 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の数値や文字列などのプリミティブも含め、すべてのインスタンスから参照することができます。

Object.prototype.print = function(){
    return alert(this);
};

"Hello, world!".print(); // "Hello, world!" と表示

さらにトップレベルのオブジェクト(ウェブブラウザ上のJavaScriptではwindow)もObjectオブジェクトのインスタンスを継承するので、Object.prototypeに定義されたプロパティやメソッドはグローバル変数やグローバル関数のようにトップレベルで呼び出すことができます。

Object.prototype.x = true;
x; // true
prototype プロパティを変更すべきか?

Object とその子孫の prototype プロパティを変更すると、フリーハンドなオブジェクトシステムへの変更が実現できます。 とても魅力的な機能ですが、これには痛みを伴います。 具体的には JavaScript を実行する JavaScript エンジンには、prototype プロパティが変更されていないないならば、prototype プロパティを参照しないで既定の処理を行う最適化が施されています。 他方、prototype プロパティが変更されると、全ての prototype プロパティのプロパティが変わっている可能性があるので、毎回 prototype プロパティを参照する必要性が発生します。 prototype プロパティを参照するオーバーヘッドは慎重に小さくするよう努力されていますが、メソッドを含めプロパティが参照されるたびに毎回 prototype プロパティを参照するのは軽い処理では有りません。 このことから、小さくない規模あるいは短くない時間実行される JavaScript プログラムでは、標準組み込みオブジェクトの prototype プロパティを変更する代わりに当該オブジェクトの prototype プロパティ を複製したオブジェクトを雛形にインスタンスを生成することも検討に値します。


for-in[編集]

for-in文はオブジェクトのプロパティを順番に取り出して反復処理します。

// 0, 1, 2 と順番にアラート
const object = { "a": 0, "b": 1, "c": 2 };
for ( const key in object ) {
    alert( object[key] );
}

オブジェクトに属するすべてのプロパティを配列で取得するには、Object.keysメソッドを使用することができます。

// 0, 1, 2 と順番にアラート
const object = { "a": 0, "b": 1, "c": 2 };
Object.keys(object).forEach(function(key){
    alert( object[key] );
});

for each-in[編集]

JavaScript 1.6の for each-in 文は非推奨を経て廃止されました。下のプログラム例もモダンブラウザでは SyntaxError となります。for-of 文を使うようにして下さい。
JavaScript 1.6で追加されたfor each-in文はオブジェクトの値を順番に取り出して反復処理します。

// 0, 1, 2 と順番にアラート
var object = { "a": 0, "b": 1, "c": 2 };
for each ( var value in object ) {
    alert(value);
}

for-of[編集]

ES2015で追加されたfor-of文とES2017で追加されたObject.entries()を組み合わせると、はオブジェクトの値を順番に取り出して反復処理をすることが出来ます。

// 0, 1, 2 と順番にアラート
const object = { "a": 0, "b": 1, "c": 2 };
for ( const  [k, v]  of Object.entries(object) ) {
    alert(v);
}

メソッドチェーン[編集]

空白文字で区切られた単語を含んだ文字列

"h he li be b c n o f ne na mg al si p s cl ar k ca sc ti v cr" 

がある。 この文字列から単語を取り出しキャピタライズし番号を振って , で区切って表示してみよう。

非メソッドチェーン版

let 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";
let 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(;;) を使うべきだったかも)。

メソッドチェーン版

let 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";
let result = str
  .split(' ')
  .map((s, i) => `${1+i}:${s.charAt(0).toUpperCase() + s.slice(1)}`)
  .join(", ")
console.log(result)
/*
 :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)になります。 この様に . を挟んで左から右にメソッドが次々と評価されます。 この連鎖構造を鎖に例えてメソッドチェーンと呼ばれます。

メソッドチェーン節の課題[編集]

非メソッドチェーン版とメソッドチェーン版を比較すると出力に小さな違いがある。探してみよう。 また、その原因をしらべ結果を揃えるようコードを改修してみよう(どちらに合わせても構わない)。

このページ「JavaScript/オブジェクト」は、まだ書きかけです。加筆・訂正など、協力いただける皆様の編集を心からお待ちしております。また、ご意見などがありましたら、お気軽にトークページへどうぞ。
  1. ^ ECMA-262::15.4 Method Definitions