JavaScriptにおけるオブジェクト指向プログラミング(OOP)には、いくつかの階層があります。
新しい階層は前の階層にのみ依存しているため、JavaScript OOPを段階的に学習できます。階層1と2はシンプルなコアを形成しており、階層3と4が複雑になって混乱したときは、いつでもこのコアを参照できます。
大まかに言えば、JavaScriptのすべてのオブジェクトは、文字列から値へのマップ(辞書)です。オブジェクト内の(キー、値)のエントリーは、プロパティと呼ばれます。プロパティのキーは常にテキスト文字列です。プロパティの値は、関数を含む任意のJavaScriptの値にすることができます。メソッドは、値が関数であるプロパティです。
プロパティには3つの種類があります。
[[Prototype]]
はオブジェクトのプロトタイプを保持し、Object.getPrototypeOf()
を介して読み取ることができます。JavaScriptのオブジェクトリテラルを使用すると、プレーンオブジェクト(Object
の直接インスタンス)を直接作成できます。次のコードでは、オブジェクトリテラルを使用して、オブジェクトを変数jane
に割り当てています。オブジェクトには、name
とdescribe
の2つのプロパティがあります。describe
はメソッドです。
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
// (1)
},
// (2)
};
this
を使用して、現在のオブジェクトを参照します。(メソッド呼び出しのレシーバーとも呼ばれます)。オブジェクトは、文字列から値への単なるマップであるという印象を受けるかもしれません。しかし、オブジェクトはそれ以上のものです。オブジェクトは真の汎用オブジェクトです。たとえば、オブジェクト間で継承を使用したり(「第2層:オブジェクト間のプロトタイプ関係」を参照)、オブジェクトが変更されないように保護することができます。オブジェクトを直接作成する機能は、JavaScriptの傑出した機能の1つです。具体的なオブジェクト(クラスは不要!)から始めて、後で抽象概念を導入することができます。たとえば、オブジェクトのファクトリーであるコンストラクター(「第3層:コンストラクター—インスタンスのファクトリー」で説明)は、他の言語のクラスとほぼ同様です。
ドット演算子は、プロパティにアクセスするためのコンパクトな構文を提供します。プロパティのキーは識別子である必要があります(「有効な識別子」を参照)。任意の名前を持つプロパティを読み書きする場合は、角括弧演算子を使用する必要があります(「角括弧演算子([]):計算されたキーによるプロパティへのアクセス」を参照)。
このセクションの例では、次のオブジェクトを使用します。
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
ドット演算子を使用すると、プロパティを「取得」できます(その値を読み取ります)。次に例をいくつか示します。
> jane.name // get property `name` 'Jane' > jane.describe // get property `describe` [Function]
存在しないプロパティを取得すると、undefined
が返されます。
> jane.unknownProperty undefined
ドット演算子は、メソッドを呼び出すためにも使用されます。
> jane.describe() // call method `describe` 'Person named Jane'
代入演算子(=
)を使用して、ドット表記で参照されるプロパティの値を設定できます。例:
> jane.name = 'John'; // set property `name` > jane.describe() 'Person named John'
プロパティがまだ存在しない場合は、設定すると自動的に作成されます。プロパティが既に存在する場合は、設定すると値が変更されます。
delete
演算子を使用すると、オブジェクトからプロパティ(キーと値のペア全体)を完全に削除できます。例:
> var obj = { hello: 'world' }; > delete obj.hello true > obj.hello undefined
プロパティをundefined
に設定するだけでは、プロパティはまだ存在し、オブジェクトにはまだキーが含まれています。
> var obj = { foo: 'a', bar: 'b' }; > obj.foo = undefined; > Object.keys(obj) [ 'foo', 'bar' ]
プロパティを削除すると、そのキーもなくなります。
> delete obj.foo true > Object.keys(obj) [ 'bar' ]
delete
は、オブジェクトの直接の(「own」、継承されていない)プロパティのみに影響します。そのプロトタイプは変更されません(「継承されたプロパティを削除する」を参照)。
delete
演算子は控えめに使用してください。ほとんどの最新のJavaScriptエンジンは、コンストラクターによって作成されたインスタンスの「形状」が変更されない場合(大まかに言えば、プロパティが削除または追加されない場合)に、そのパフォーマンスを最適化します。プロパティを削除すると、その最適化が妨げられます。
プロパティが自身のプロパティであるが削除できない場合、delete
はfalse
を返します。それ以外の場合はすべてtrue
を返します。次にいくつかの例を示します。
準備として、削除できるプロパティと削除できない別のプロパティを作成します(「記述子によるプロパティの取得と定義」では、Object.defineProperty()
について説明しています)。
var
obj
=
{};
Object
.
defineProperty
(
obj
,
'canBeDeleted'
,
{
value
:
123
,
configurable
:
true
});
Object
.
defineProperty
(
obj
,
'cannotBeDeleted'
,
{
value
:
456
,
configurable
:
false
});
delete
は、削除できない自身のプロパティに対してfalse
を返します。
> delete obj.cannotBeDeleted false
delete
は、それ以外のすべての場合にtrue
を返します。
> delete obj.doesNotExist true > delete obj.canBeDeleted true
delete
は、何も変更しない場合でもtrue
を返します(継承されたプロパティは削除されません)。
> delete obj.toString true > obj.toString // still there [Function: toString]
予約語(var
やfunction
など)を変数名として使用することはできませんが、プロパティキーとして使用することはできます。
> var obj = { var: 'a', function: 'b' }; > obj.var 'a' > obj.function 'b'
数値はオブジェクトリテラルでプロパティキーとして使用できますが、文字列として解釈されます。ドット演算子は、キーが識別子であるプロパティにのみアクセスできます。したがって、キーが数値であるプロパティにアクセスするには、角括弧演算子(次の例に示します)が必要です。
> var obj = { 0.7: 'abc' }; > Object.keys(obj) [ '0.7' ] > obj['0.7'] 'abc'
オブジェクトリテラルでは、任意の文字列(識別子でも数値でもない)をプロパティキーとして使用することもできますが、引用符で囲む必要があります。この場合も、プロパティ値にアクセスするには角括弧演算子が必要です。
> var obj = { 'not an identifier': 123 }; > Object.keys(obj) [ 'not an identifier' ] > obj['not an identifier'] 123
ドット演算子は固定プロパティキーを使用しますが、角括弧演算子を使用すると、式を介してプロパティを参照できます。
角括弧演算子を使用すると、式を介してプロパティのキーを計算できます。
> var obj = { someProperty: 'abc' }; > obj['some' + 'Property'] 'abc' > var propKey = 'someProperty'; > obj[propKey] 'abc'
これにより、キーが識別子でないプロパティにもアクセスできます。
> var obj = { 'not an identifier': 123 }; > obj['not an identifier'] 123
角括弧演算子は内部を文字列に強制変換することに注意してください。例:
> var obj = { '6': 'bar' }; > obj[3+3] // key: the string '6' 'bar'
メソッドの呼び出しは、期待どおりに機能します。
> var obj = { myMethod: function () { return true } }; > obj['myMethod']() true
プロパティの設定は、ドット演算子と同様に機能します。
> var obj = {}; > obj['anotherProperty'] = 'def'; > obj.anotherProperty 'def'
プロパティの削除も、ドット演算子と同様に機能します。
> var obj = { 'not an identifier': 1, prop: 2 }; > Object.keys(obj) [ 'not an identifier', 'prop' ] > delete obj['not an identifier'] true > Object.keys(obj) [ 'prop' ]
頻繁に使用するケースではありませんが、場合によっては、任意の値をオブジェクトに変換する必要があります。Object()
は、関数として(コンストラクターとしてではなく)使用すると、その機能を提供します。これにより、次の結果が生成されます。
値 | 結果 |
(パラメーターなしで呼び出された場合) |
|
|
|
|
|
ブール値 |
|
数値 |
|
文字列 |
|
オブジェクト |
|
次に例をいくつか示します。
> Object(null) instanceof Object true > Object(false) instanceof Boolean true > var obj = {}; > Object(obj) === obj true
次の関数は、value
がオブジェクトであるかどうかを確認します。
function
isObject
(
value
)
{
return
value
===
Object
(
value
);
}
前の関数は、value
がオブジェクトでない場合、オブジェクトを作成することに注意してください。typeof
を使用して、それを行わずに同じ関数を実装できます(「落とし穴:typeof null」を参照)。
Object
をコンストラクターとして呼び出すこともできます。これは、関数として呼び出す場合と同じ結果を生成します。
> var obj = {}; > new Object(obj) === obj true > new Object(123) instanceof Number true
コンストラクターは避けてください。空のオブジェクトリテラルの方がほとんどの場合、より適切な選択肢です。
var
obj
=
new
Object
();
// avoid
var
obj
=
{};
// prefer
関数を呼び出すと、this
は常に(暗黙的な)パラメーターになります。
通常の関数はthis
を使用する必要がなくても、その値が常にグローバルオブジェクトである(ブラウザではwindow
、「グローバルオブジェクト」を参照)特別な変数として存在します。
> function returnThisSloppy() { return this } > returnThisSloppy() === window true
this
は常に undefined
> function returnThisStrict() { 'use strict'; return this } > returnThisStrict() === undefined true
this
はメソッドが呼び出されたオブジェクトを参照します
> var obj = { method: returnThisStrict }; > obj.method() === obj true
メソッドの場合、this
の値は、メソッド呼び出しのレシーバーと呼ばれます。
関数もオブジェクトであることを忘れないでください。したがって、各関数は独自のメソッドを持っています。このセクションでは、関数呼び出しを支援する3つのメソッドを紹介します。これらの3つのメソッドは、関数呼び出しのいくつかの落とし穴を回避するために、以下のセクションで使用されます。以下の例はすべて、次のオブジェクト jane
を参照しています。
var
jane
=
{
name
:
'Jane'
,
sayHelloTo
:
function
(
otherName
)
{
'use strict'
;
console
.
log
(
this
.
name
+
' says hello to '
+
otherName
);
}
};
最初のパラメータは、呼び出される関数内で this
が持つ値です。残りのパラメータは、呼び出される関数への引数として渡されます。次の3つの呼び出しは同等です。
jane
.
sayHelloTo
(
'Tarzan'
);
jane
.
sayHelloTo
.
call
(
jane
,
'Tarzan'
);
var
func
=
jane
.
sayHelloTo
;
func
.
call
(
jane
,
'Tarzan'
);
2回目の呼び出しでは、call()
は呼び出された関数をどのように取得したかを知らないため、jane
を繰り返す必要があります。
最初のパラメータは、呼び出される関数内で this
が持つ値です。2番目のパラメータは、呼び出しの引数を提供する配列です。次の3つの呼び出しは同等です。
jane
.
sayHelloTo
(
'Tarzan'
);
jane
.
sayHelloTo
.
apply
(
jane
,
[
'Tarzan'
]);
var
func
=
jane
.
sayHelloTo
;
func
.
apply
(
jane
,
[
'Tarzan'
]);
2回目の呼び出しでは、apply()
は呼び出された関数をどのように取得したかを知らないため、jane
を繰り返す必要があります。
コンストラクターのためのapply()では、コンストラクターで apply()
を使用する方法を説明します。
このメソッドは、部分関数適用を実行します。つまり、次の方法で bind()
のレシーバーを呼び出す新しい関数を作成します。this
の値は thisValue
であり、引数は arg1
から argN
まで続き、その後に新しい関数の引数が続きます。言い換えれば、新しい関数は、元の関数を呼び出すときに、その引数を arg1, ..., argN
に追加します。例を見てみましょう。
function
func
()
{
console
.
log
(
'this: '
+
this
);
console
.
log
(
'arguments: '
+
Array
.
prototype
.
slice
.
call
(
arguments
));
}
var
bound
=
func
.
bind
(
'abc'
,
1
,
2
);
配列メソッド slice
は、arguments
を配列に変換するために使用されます。これは、ログ記録に必要です(この操作は配列のようなオブジェクトとジェネリックメソッドで説明されています)。 bound
は新しい関数です。次にインタラクションを示します。
> bound(3) this: abc arguments: 1,2,3
次の sayHelloTo
の3つの呼び出しはすべて同等です。
jane
.
sayHelloTo
(
'Tarzan'
);
var
func1
=
jane
.
sayHelloTo
.
bind
(
jane
);
func1
(
'Tarzan'
);
var
func2
=
jane
.
sayHelloTo
.
bind
(
jane
,
'Tarzan'
);
func2
();
JavaScript に配列を実際のパラメーターに変換する三点リーダー演算子 (...
) があると仮定しましょう。そのような演算子を使用すると、配列で Math.max()
(その他の関数を参照)を使用できるようになります。 その場合、次の2つの式は同等になります。
Math
.
max
(...[
13
,
7
,
30
])
Math
.
max
(
13
,
7
,
30
)
関数については、apply()
を介して三点リーダー演算子の効果を実現できます。
> Math.max.apply(null, [13, 7, 30]) 30
三点リーダー演算子は、コンストラクターにも理にかなっています。
new
Date
(...[
2011
,
11
,
24
])
// Christmas Eve 2011
残念ながら、ここでは apply()
は機能しません。関数またはメソッドの呼び出しには役立ちますが、コンストラクターの呼び出しには役立たないためです。
2つのステップで apply()
をシミュレートできます。
(まだ配列ではない)メソッド呼び出しを介して引数を Date
に渡します。
new
(
Date
.
bind
(
null
,
2011
,
11
,
24
))
上記のコードでは、bind()
を使用して、パラメーターなしでコンストラクターを作成し、new
経由で呼び出します。
apply()
を使用して、配列を bind()
に渡します。bind()
はメソッド呼び出しであるため、apply()
を使用できます。
new
(
Function
.
prototype
.
bind
.
apply
(
Date
,
[
null
,
2011
,
11
,
24
]))
上記の配列には、arr
の要素の後に null
が含まれています。 concat()
を使用して、null
を arr
の先頭に追加して作成できます。
var
arr
=
[
2011
,
11
,
24
];
new
(
Function
.
prototype
.
bind
.
apply
(
Date
,
[
null
].
concat
(
arr
)))
上記の回避策は、Mozillaが公開したライブラリメソッドに触発されています。次に、少し編集したバージョンを示します。
if
(
!
Function
.
prototype
.
construct
)
{
Function
.
prototype
.
construct
=
function
(
argArray
)
{
if
(
!
Array
.
isArray
(
argArray
))
{
throw
new
TypeError
(
"Argument must be an array"
);
}
var
constr
=
this
;
var
nullaryFunc
=
Function
.
prototype
.
bind
.
apply
(
constr
,
[
null
].
concat
(
argArray
));
return
new
nullaryFunc
();
};
}
次に、使用中のメソッドを示します。
> Date.construct([2011, 11, 24]) Sat Dec 24 2011 00:00:00 GMT+0100 (CET)
以前のアプローチの代替は、Object.create()
を介して初期化されていないインスタンスを作成し、次にコンストラクターを(関数として)apply()
を介して呼び出すことです。これは、事実上、new
演算子を再実装することを意味します(一部のチェックは省略されています)。
Function
.
prototype
.
construct
=
function
(
argArray
)
{
var
constr
=
this
;
var
inst
=
Object
.
create
(
constr
.
prototype
);
var
result
=
constr
.
apply
(
inst
,
argArray
);
// (1)
// Check: did the constructor return an object
// and prevent `this` from being the result?
return
result
?
result
:
inst
;
};
上記のコードは、関数として呼び出されると常に新しいインスタンスを生成するほとんどの組み込みコンストラクターでは機能しません。言い換えれば、(1)行のステップでは、意図したとおりに inst
を設定しません。
オブジェクトからメソッドを抽出すると、再び真の関数になります。オブジェクトとの接続が切断され、通常は正常に機能しなくなります。たとえば、次のオブジェクト counter
を見てみましょう。
var
counter
=
{
count
:
0
,
inc
:
function
()
{
this
.
count
++
;
}
}
inc
を抽出して、(関数として!)呼び出すと失敗します。
> var func = counter.inc; > func() > counter.count // didn’t work 0
説明は次のとおりです。counter.inc
の値を関数として呼び出しました。したがって、this
はグローバルオブジェクトであり、window.count++
を実行しました。window.count
は存在せず、undefined
です。 ++
演算子を適用すると、NaN
に設定されます。
> count // global variable NaN
メソッド inc()
が厳格モードの場合、警告が表示されます。
> counter.inc = function () { 'use strict'; this.count++ }; > var func2 = counter.inc; > func2() TypeError: Cannot read property 'count' of undefined
理由は、厳格モード関数 func2
を呼び出すと、this
が undefined
になり、エラーが発生するためです。
bind()
のおかげで、inc
が counter
との接続を失わないようにすることができます。
> var func3 = counter.inc.bind(counter); > func3() > counter.count // it worked! 1
JavaScriptには、コールバックを受け入れる多くの関数とメソッドがあります。ブラウザーの例としては、setTimeout()
やイベント処理などがあります。counter.inc
をコールバックとして渡すと、それも関数として呼び出され、先ほど説明したのと同じ問題が発生します。この現象を示すために、シンプルなコールバック呼び出し関数を使用してみましょう。
function
callIt
(
callback
)
{
callback
();
}
callIt
を介して counter.count
を実行すると、(厳格モードのために)警告がトリガーされます。
> callIt(counter.inc) TypeError: Cannot read property 'count' of undefined
以前と同様に、bind()
を使用して問題を修正します。
> callIt(counter.inc.bind(counter)) > counter.count // one more than before 2
bind()
を呼び出すたびに、新しい関数が作成されます。これは、コールバックを登録および登録解除する場合(たとえば、イベント処理の場合)に影響します。登録した値をどこかに保存し、登録解除にも使用する必要があります。
関数はパラメーター(例:コールバック)になり、関数式を介してインプレースで作成できるため、JavaScriptで関数定義をネストすることがよくあります。メソッドに通常の関数が含まれており、後者の中で前者の this
にアクセスしたい場合に問題が発生します。メソッドの this
は、通常の関数の this
によってシャドウされるためです(通常の関数の this
には独自の this
を使用する理由はありません)。次の例では、(1)の関数が (2) でメソッドの this
にアクセスしようとしています。
var
obj
=
{
name
:
'Jane'
,
friends
:
[
'Tarzan'
,
'Cheeta'
],
loop
:
function
()
{
'use strict'
;
this
.
friends
.
forEach
(
function
(
friend
)
{
// (1)
console
.
log
(
this
.
name
+
' knows '
+
friend
);
// (2)
}
);
}
};
これは失敗します。(1) の関数には独自の this
があり、ここでは undefined
であるためです。
> obj.loop(); TypeError: Cannot read property 'name' of undefined
この問題を回避する方法は3つあります。
ネストされた関数内でシャドウされない変数に this
を割り当てます。
loop
:
function
()
{
'use strict'
;
var
that
=
this
;
this
.
friends
.
forEach
(
function
(
friend
)
{
console
.
log
(
that
.
name
+
' knows '
+
friend
);
});
}
次にインタラクションを示します。
> obj.loop(); Jane knows Tarzan Jane knows Cheeta
bind()
を使用して、コールバックに this
の固定値、つまり、メソッドの this
(行 (1)) を指定できます。
loop
:
function
()
{
'use strict'
;
this
.
friends
.
forEach
(
function
(
friend
)
{
console
.
log
(
this
.
name
+
' knows '
+
friend
);
}.
bind
(
this
));
// (1)
}
forEach()
(検査メソッドを参照)に固有の回避策は、コールバックの後にコールバックの this
になる2番目のパラメーターを指定することです。
loop
:
function
()
{
'use strict'
;
this
.
friends
.
forEach
(
function
(
friend
)
{
console
.
log
(
this
.
name
+
' knows '
+
friend
);
},
this
);
}
2つのオブジェクト間のプロトタイプ関係は継承に関するものです。すべてのオブジェクトは、別のオブジェクトをプロトタイプとして持つことができます。次に、前者のオブジェクトは、プロトタイプのすべてのプロパティを継承します。オブジェクトは、内部プロパティ [[Prototype]]
を介してプロトタイプを指定します。すべてのオブジェクトにはこのプロパティがありますが、null
になる可能性があります。[[Prototype]]
プロパティによって接続されたオブジェクトのチェーンは、プロトタイプチェーン(図17-1)と呼ばれます。
プロトタイプベース(またはプロトタイプ型)の継承がどのように機能するかを確認するために、例を見てみましょう([[Prototype]]
プロパティを指定するための発明された構文を使用)。
var
proto
=
{
describe
:
function
()
{
return
'name: '
+
this
.
name
;
}
};
var
obj
=
{
[[
Prototype
]]
:
proto
,
name
:
'obj'
};
オブジェクト obj
は、proto
からプロパティ describe
を継承します。また、いわゆる独自の(非継承の、直接の)プロパティである name
を持っています。
obj
はプロパティ describe
を継承します。オブジェクト自体がそのプロパティを持っているかのようにアクセスできます。
> obj.describe [Function]
obj
を介してプロパティにアクセスするたびに、JavaScriptはそのオブジェクトで検索を開始し、そのプロトタイプ、プロトタイプのプロトタイプなどで続行します。これが、obj.describe
を介して proto.describe
にアクセスできる理由です。プロトタイプチェーンは、単一のオブジェクトであるかのように動作します。メソッドを呼び出すときに、そのイリュージョンは維持されます。this
の値は常に、メソッドの検索が開始されたオブジェクトであり、メソッドが見つかった場所ではありません。これにより、メソッドはプロトタイプチェーンのすべてのプロパティにアクセスできます。たとえば、
> obj.describe() 'name: obj'
describe()
の内部では、this
は obj
になり、メソッドが obj.name
にアクセスできるようになります。
プロトタイプチェーンでは、オブジェクト内のプロパティは、後続のオブジェクトにある同じキーを持つプロパティを オーバーライド します。前者のプロパティが最初に見つかります。後者のプロパティは隠され、アクセスできなくなります。例として、obj
内でメソッド proto.describe()
をオーバーライドしてみましょう。
> obj.describe = function () { return 'overridden' }; > obj.describe() 'overridden'
これは、クラスベースの言語でのメソッドのオーバーライドの動作と似ています。
プロトタイプは、オブジェクト間でデータを共有するのに最適です。複数のオブジェクトが同じプロトタイプを取得し、そのプロトタイプにすべての共有プロパティが保持されます。例を見てみましょう。オブジェクト jane
と tarzan
は両方とも同じメソッド describe()
を含んでいます。これは、共有を使用することで避けたいものです。
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
tarzan
=
{
name
:
'Tarzan'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
どちらのオブジェクトも人物です。name
プロパティは異なりますが、メソッド describe
を共有させることができます。これを行うには、PersonProto
という共通のプロトタイプを作成し、そこに describe
を配置します(図17-2)。
次のコードは、プロトタイプ PersonProto
を共有するオブジェクト jane
と tarzan
を作成します。
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Jane'
};
var
tarzan
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Tarzan'
};
そして、これがそのやり取りです。
> jane.describe() Person named Jane > tarzan.describe() Person named Tarzan
これは一般的なパターンです。データはプロトタイプチェーンの最初のオブジェクトに存在し、メソッドは後続のオブジェクトに存在します。JavaScript のプロトタイプ継承の仕組みは、このパターンをサポートするように設計されています。プロパティの設定はプロトタイプチェーンの最初のオブジェクトのみに影響しますが、プロパティの取得は完全なチェーンを考慮します(設定と削除は自身のプロパティにのみ影響するを参照)。
これまで、JavaScript から内部プロパティ [[Prototype]]
にアクセスできると想定してきました。しかし、言語ではそれが許可されていません。代わりに、プロトタイプを読み取るための関数と、指定されたプロトタイプを持つ新しいオブジェクトを作成するための関数があります。
次の呼び出し:
Object
.
create
(
proto
,
propDescObj
?
)
プロトタイプが proto
であるオブジェクトを作成します。オプションで、記述子を介してプロパティを追加できます(記述子についてはプロパティ記述子で説明します)。次の例では、オブジェクト jane
はプロトタイプ PersonProto
と、値が 'Jane'
である可変プロパティ name
を(プロパティ記述子を介して指定されたとおりに)取得します。
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
Object
.
create
(
PersonProto
,
{
name
:
{
value
:
'Jane'
,
writable
:
true
}
});
これがそのやり取りです。
> jane.describe() 'Person named Jane'
ただし、記述子は冗長であるため、空のオブジェクトを作成してからプロパティを手動で追加することがよくあります。
var
jane
=
Object
.
create
(
PersonProto
);
jane
.
name
=
'Jane'
;
このメソッド呼び出し:
Object
.
getPrototypeOf
(
obj
)
obj
のプロトタイプを返します。前の例を続けます。
> Object.getPrototypeOf(jane) === PersonProto true
次の構文:
Object
.
prototype
.
isPrototypeOf
(
obj
)
メソッドのレシーバーが obj
の(直接または間接の)プロトタイプであるかどうかを確認します。言い換えれば、レシーバーと obj
は同じプロトタイプチェーン内にあり、obj
はレシーバーより前にありますか?例えば
> var A = {}; > var B = Object.create(A); > var C = Object.create(B); > A.isPrototypeOf(C) true > C.isPrototypeOf(A) false
次の関数は、オブジェクト obj
のプロパティチェーンを反復処理します。キーが propKey
の自身のプロパティを持つ最初のオブジェクト、またはそのようなオブジェクトがない場合は null
を返します。
function
getDefiningObject
(
obj
,
propKey
)
{
obj
=
Object
(
obj
);
// make sure it’s an object
while
(
obj
&&
!
{}.
hasOwnProperty
.
call
(
obj
,
propKey
))
{
obj
=
Object
.
getPrototypeOf
(
obj
);
// obj is null if we have reached the end
}
return
obj
;
}
上記のコードでは、メソッド Object.prototype.hasOwnProperty
を汎用的に呼び出しました(汎用メソッド:プロトタイプからメソッドを借りるを参照)。
一部の JavaScript エンジンには、オブジェクトのプロトタイプを取得および設定するための特別なプロパティ __proto__
があります。これにより、[[Prototype]]
への直接アクセスが言語に提供されます。
> var obj = {}; > obj.__proto__ === Object.prototype true > obj.__proto__ = Array.prototype > Object.getPrototypeOf(obj) === Array.prototype true
__proto__
について知っておくべきことがいくつかあります。
__proto__
は、「ダブルアンダースコアプロト」の略である「ダンダープロト」と発音されます。この発音は、Python プログラミング言語から(2006 年に Ned Batchelder によって提案されたように)借用されています。ダブルアンダースコアを持つ特殊な変数は、Python では非常に頻繁に使用されます。__proto__
は ECMAScript 5 標準の一部ではありません。したがって、コードをその標準に準拠させ、現在の JavaScript エンジンで確実に実行する場合は、それを使用しないでください。__proto__
のサポートを追加しており、ECMAScript 6 の一部になる予定です。次の式は、エンジンが特殊なプロパティとして __proto__
をサポートしているかどうかを確認します。
Object
.
getPrototypeOf
({
__proto__
:
null
})
===
null
プロパティの取得のみが、オブジェクトの完全なプロトタイプチェーンを考慮します。設定と削除は継承を無視し、自身のプロパティにのみ影響します。
プロパティを設定すると、そのキーを持つ継承されたプロパティがある場合でも、自身のプロパティが作成されます。たとえば、次のソースコードが与えられたとします。
var
proto
=
{
foo
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
obj
は proto
から foo
を継承します。
> obj.foo 'a' > obj.hasOwnProperty('foo') false
foo
を設定すると、目的の結果が得られます。
> obj.foo = 'b'; > obj.foo 'b'
ただし、自身のプロパティを作成し、proto.foo
は変更していません。
> obj.hasOwnProperty('foo') true > proto.foo 'a'
その理由は、プロトタイププロパティが複数のオブジェクトで共有されることを意図しているためです。このアプローチにより、破壊的でない方法でそれらを「変更」できます。影響を受けるのは現在のオブジェクトのみです。
自身のプロパティのみを削除できます。もう一度、プロトタイプ proto
を持つオブジェクト obj
を設定しましょう。
var
proto
=
{
foo
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
継承されたプロパティ foo
を削除しても効果はありません。
> delete obj.foo true > obj.foo 'a'
delete
演算子の詳細については、プロパティの削除を参照してください。
継承されたプロパティを変更する場合は、まずそれを所有するオブジェクトを見つけ(プロパティが定義されているオブジェクトの検索を参照)、そのオブジェクトに対して変更を実行する必要があります。たとえば、前の例からプロパティ foo
を削除してみましょう。
> delete getDefiningObject(obj, 'foo').foo; true > obj.foo undefined
プロパティの反復処理および検出のための操作は、次のものによって影響を受けます。
true
または false
にできるフラグです。列挙可能性が問題になることはめったになく、通常は無視できます(列挙可能性:ベストプラクティスを参照)。自身のプロパティキーを一覧表示したり、すべての列挙可能なプロパティキーを一覧表示したり、プロパティが存在するかどうかを確認したりできます。次のサブセクションでは、その方法を示します。
すべての自身のプロパティキー、または列挙可能なキーのみを一覧表示できます。
Object.getOwnPropertyNames(obj)
は、obj
のすべての自身のプロパティのキーを返します。Object.keys(obj)
は、obj
のすべての列挙可能な自身のプロパティのキーを返します。プロパティは通常、列挙可能であることに注意してください(列挙可能性:ベストプラクティスを参照)。したがって、特に作成したオブジェクトには、Object.keys()
を使用できます。
オブジェクトのすべてのプロパティ(自身のプロパティと継承されたプロパティの両方)を一覧表示する場合は、2つのオプションがあります。
オプション 1 はループを使用することです。
for
(
«
variable
»
in
«
object
»
)
«
statement
»
object
のすべての列挙可能なプロパティのキーを反復処理します。for-in を参照して、より詳細な説明を確認してください。
オプション 2 は、すべてのプロパティ(列挙可能なプロパティだけでなく)を反復処理する関数を自分で実装することです。例えば
function
getAllPropertyNames
(
obj
)
{
var
result
=
[];
while
(
obj
)
{
// Add the own property names of `obj` to `result`
result
=
result
.
concat
(
Object
.
getOwnPropertyNames
(
obj
));
obj
=
Object
.
getPrototypeOf
(
obj
);
}
return
result
;
}
オブジェクトにプロパティがあるかどうか、またはプロパティがオブジェクト内に直接存在するかどうかを確認できます。
propKey in obj
obj
にキーが propKey
のプロパティがある場合は true
を返します。継承されたプロパティはこのテストに含まれます。Object.prototype.hasOwnProperty(propKey)
this
)にキーが propKey
である自身の(非継承)プロパティがある場合は true
を返します。オブジェクトで hasOwnProperty()
を直接呼び出すことは避けてください。オーバーライドされる可能性があるためです(例:キーが hasOwnProperty
である自身のプロパティによって)。
> var obj = { hasOwnProperty: 1, foo: 2 }; > obj.hasOwnProperty('foo') // unsafe TypeError: Property 'hasOwnProperty' is not a function
代わりに、汎用的に呼び出すことをお勧めします(汎用メソッド:プロトタイプからメソッドを借りるを参照)。
> Object.prototype.hasOwnProperty.call(obj, 'foo') // safe true > {}.hasOwnProperty.call(obj, 'foo') // shorter true
var
proto
=
Object
.
defineProperties
({},
{
protoEnumTrue
:
{
value
:
1
,
enumerable
:
true
},
protoEnumFalse
:
{
value
:
2
,
enumerable
:
false
}
});
var
obj
=
Object
.
create
(
proto
,
{
objEnumTrue
:
{
value
:
1
,
enumerable
:
true
},
objEnumFalse
:
{
value
:
2
,
enumerable
:
false
}
});
Object.defineProperties()
は、記述子を介したプロパティの取得と定義で説明されていますが、どのように機能するかはかなり明白なはずです。proto
には、自身のプロパティ protoEnumTrue
と protoEnumFalse
があり、obj
には、自身のプロパティ objEnumTrue
と objEnumFalse
があります(また、proto
のすべてのプロパティを継承します)。
オブジェクト(前の例の proto
など)は通常、少なくともプロトタイプ Object.prototype
を持っていることに注意してください(ここでは、toString()
や hasOwnProperty()
などの標準メソッドが定義されています)。
> Object.getPrototypeOf({}) === Object.prototype true
プロパティ関連の操作の中で、列挙可能性が影響を与えるのは for-in
ループと Object.keys()
だけです(JSON.stringify()
にも影響を与えます。JSON.stringify(value, replacer?, space?)を参照)。
for-in
ループは、継承されたものも含め、すべての列挙可能なプロパティのキーを反復処理します(Object.prototype
の列挙不可能なプロパティは表示されないことに注意してください)。
> for (var x in obj) console.log(x); objEnumTrue protoEnumTrue
Object.keys()
は、すべての自身(継承されていない)の列挙可能なプロパティのキーを返します。
> Object.keys(obj) [ 'objEnumTrue' ]
すべての自身のプロパティのキーが必要な場合は、Object.getOwnPropertyNames()
を使用する必要があります。
> Object.getOwnPropertyNames(obj) [ 'objEnumTrue', 'objEnumFalse' ]
継承を考慮するのは、for-in
ループ(前の例を参照)と in
演算子だけです。
> 'toString' in obj true > obj.hasOwnProperty('toString') false > obj.hasOwnProperty('objEnumFalse') true
for-in で説明されているように、for-in
と hasOwnProperty()
を組み合わせます。これは、古い JavaScript エンジンでも機能します。例:
for
(
var
key
in
obj
)
{
if
(
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
key
))
{
console
.
log
(
key
);
}
}
Object.keys()
または Object.getOwnPropertyNames()
を forEach()
配列反復処理と組み合わせます。
var
obj
=
{
first
:
'John'
,
last
:
'Doe'
};
// Visit non-inherited enumerable keys
Object
.
keys
(
obj
).
forEach
(
function
(
key
)
{
console
.
log
(
key
);
});
プロパティ値または(キー、値)ペアを反復処理するには
ECMAScript 5 では、プロパティを取得または設定しているように見えるメソッドを記述できます。つまり、プロパティは仮想であり、ストレージスペースではありません。たとえば、プロパティの設定を禁止し、読み取り時に返される値を常に計算することができます。
次の例では、オブジェクトリテラルを使用してプロパティ foo
のセッターとゲッターを定義します。
var
obj
=
{
get
foo
()
{
return
'getter'
;
},
set
foo
(
value
)
{
console
.
log
(
'setter: '
+
value
);
}
};
次にインタラクションを示します。
> obj.foo = 'bla'; setter: bla > obj.foo 'getter'
ゲッターとセッターを指定するもう 1 つの方法は、プロパティ記述子を使用することです(プロパティ記述子を参照)。次のコードは、前のリテラルと同じオブジェクトを定義します。
var
obj
=
Object
.
create
(
Object
.
prototype
,
{
// object with property descriptors
foo
:
{
// property descriptor
get
:
function
()
{
return
'getter'
;
},
set
:
function
(
value
)
{
console
.
log
(
'setter: '
+
value
);
}
}
}
);
ゲッターとセッターはプロトタイプから継承されます。
> var proto = { get foo() { return 'hello' } }; > var obj = Object.create(proto); > obj.foo 'hello'
プロパティ属性とプロパティ記述子は、高度なトピックです。通常、それらがどのように機能するかを知る必要はありません。
このセクションでは、プロパティの内部構造を見ていきます。
プロパティのデータとそのメタデータの両方を含む、プロパティのすべての状態は、属性に格納されます。それらは、オブジェクトがプロパティを持っているように、プロパティが持っているフィールドです。属性キーは、二重角かっこで囲んで記述されることがよくあります。属性は、通常のプロパティとアクセサ(ゲッターとセッター)にとって重要です。
次の属性は、通常のプロパティに固有です。
[[Value]]
は、プロパティの値、つまりデータを保持します。[[Writable]]
は、プロパティの値を変更できるかどうかを示すブール値を保持します。次の属性は、アクセサに固有です。
[[Get]]
は、プロパティが読み取られたときに呼び出される関数であるゲッターを保持します。この関数は、読み取りアクセスの結果を計算します。[[Set]]
は、プロパティが値に設定されたときに呼び出される関数であるセッターを保持します。この関数は、その値をパラメーターとして受け取ります。すべてのプロパティには、次の属性があります。
[[Enumerable]]
は、ブール値を保持します。プロパティを列挙不可能にすると、一部の操作から隠されます(プロパティの反復と検出を参照)。[[Configurable]]
は、ブール値を保持します。これが false
の場合、プロパティを削除したり、その属性([[Value]]
を除く)を変更したり、データプロパティからアクセサプロパティに、またはその逆に変換したりすることはできません。言い換えれば、[[Configurable]]
は、プロパティのメタデータの書き込み可能性を制御します。このルールには例外が 1 つあります。JavaScript では、設定不可能なプロパティを書き込み可能から読み取り専用に変更できます。これは歴史的な理由によるものです。配列のプロパティ length
は常に書き込み可能で設定不可能です。この例外がないと、配列をフリーズ(フリーズを参照)することができなくなります。属性を指定しない場合、次のデフォルトが使用されます。
属性キー | デフォルト値 |
|
|
|
|
|
|
|
|
|
|
|
|
これらのデフォルトは、プロパティ記述子を使用してプロパティを作成している場合に重要です(次のセクションを参照)。
{
value
:
123
,
writable
:
false
,
enumerable
:
true
,
configurable
:
false
}
アクセサを使用して同じ目標、つまり不変性を達成できます。この場合、記述子は次のようになります。
{
get
:
function
()
{
return
123
},
enumerable
:
true
,
configurable
:
false
}
プロパティを定義することは、プロパティが既に存在するかどうかによって意味が異なります。
プロパティが存在しない場合は、記述子で指定された属性を持つ新しいプロパティを作成します。属性に記述子に対応するプロパティがない場合は、デフォルト値を使用します。デフォルトは、属性名の意味によって決まります。これらは、代入によってプロパティを作成するときに使用される値の反対です(この場合、プロパティは書き込み可能、列挙可能、設定可能です)。 例:
> var obj = {}; > Object.defineProperty(obj, 'foo', { configurable: true }); > Object.getOwnPropertyDescriptor(obj, 'foo') { value: undefined, writable: false, enumerable: false, configurable: true }
私は通常、デフォルトに頼らず、すべてを明確にするために、すべての属性を明示的に記述します。
プロパティが既に存在する場合は、記述子で指定されたプロパティの属性を更新します。属性に記述子に対応するプロパティがない場合は、変更しないでください。次に例を示します(前の例から続きます)。
> Object.defineProperty(obj, 'foo', { writable: true }); > Object.getOwnPropertyDescriptor(obj, 'foo') { value: undefined, writable: true, enumerable: false, configurable: true }
次の操作では、プロパティ記述子を介してプロパティの属性を取得および設定できます。
Object.getOwnPropertyDescriptor(obj, propKey)
キーが propKey
である obj
の自身(継承されていない)のプロパティの記述子を返します。そのようなプロパティがない場合は、undefined
が返されます。
> Object.getOwnPropertyDescriptor(Object.prototype, 'toString') { value: [Function: toString], writable: true, enumerable: false, configurable: true } > Object.getOwnPropertyDescriptor({}, 'toString') undefined
Object.defineProperty(obj, propKey, propDesc)
キーが propKey
であり、属性が propDesc
を介して指定されている obj
のプロパティを作成または変更します。変更されたオブジェクトを返します。例:
var
obj
=
Object
.
defineProperty
({},
'foo'
,
{
value
:
123
,
enumerable
:
true
// writable: false (default value)
// configurable: false (default value)
});
Object.defineProperties(obj, propDescObj)
Object.defineProperty()
のバッチバージョンです。propDescObj
の各プロパティは、プロパティ記述子を保持します。プロパティのキーと値は、Object.defineProperties
に、obj
で作成または変更するプロパティを伝えます。例:
var
obj
=
Object
.
defineProperties
({},
{
foo
:
{
value
:
123
,
enumerable
:
true
},
bar
:
{
value
:
'abc'
,
enumerable
:
true
}
});
Object.create(proto, propDescObj?)
まず、プロトタイプが proto
であるオブジェクトを作成します。次に、オプションのパラメーター propDescObj
が指定されている場合は、Object.defineProperties
と同じ方法で、プロパティをそれに追加します。最後に、結果を返します。たとえば、次のコードスニペットは、前のスニペットと同じ結果を生成します。
var
obj
=
Object
.
create
(
Object
.
prototype
,
{
foo
:
{
value
:
123
,
enumerable
:
true
},
bar
:
{
value
:
'abc'
,
enumerable
:
true
}
});
オブジェクトの同一コピーを作成するには、次の 2 つのことを正しく行う必要があります。
次の関数は、そのようなコピーを実行します。
function
copyObject
(
orig
)
{
// 1. copy has same prototype as orig
var
copy
=
Object
.
create
(
Object
.
getPrototypeOf
(
orig
));
// 2. copy has all of orig’s properties
copyOwnPropertiesFrom
(
copy
,
orig
);
return
copy
;
}
プロパティは、この関数を介して orig
から copy
にコピーされます。
function
copyOwnPropertiesFrom
(
target
,
source
)
{
Object
.
getOwnPropertyNames
(
source
)
// (1)
.
forEach
(
function
(
propKey
)
{
// (2)
var
desc
=
Object
.
getOwnPropertyDescriptor
(
source
,
propKey
);
// (3)
Object
.
defineProperty
(
target
,
propKey
,
desc
);
// (4)
});
return
target
;
};
これらは、関連する手順です。
source
のすべての自身のプロパティのキーを持つ配列を取得します。target
に自身のプロパティを作成します。この関数は、Underscore.js ライブラリの関数 _.extend()
に非常に似ていることに注意してください。
次の 2 つの操作は非常に似ています。
defineProperty()
と defineProperties()
によるプロパティの定義(記述子を介したプロパティの取得と定義を参照)。=
によるプロパティへの代入。ただし、いくつかの微妙な違いがあります。
プロパティへの代入prop
とは、既存のプロパティを変更することを意味します。プロセスは次のとおりです。
prop
がセッター(自身または継承)である場合は、そのセッターを呼び出します。prop
が読み取り専用(自身または継承)である場合は、例外をスローするか(厳格モード)、何も実行しません(非厳格モード)。次のセクションでは、この(やや予想外の)現象について詳しく説明します。prop
が自身のものであり(かつ書き込み可能)、そのプロパティの値を変更します。prop
が存在しないか、継承されていて書き込み可能です。どちらの場合も、書き込み可能、構成可能、列挙可能な自身のプロパティ prop
を定義します。後者の場合、継承されたプロパティを上書き(非破壊的に変更)したことになります。前者の場合、存在しなかったプロパティが自動的に定義されました。この種の自動定義は、代入の際のタイプミスを検出するのが難しい可能性があるため、問題があります。オブジェクト obj
が、プロトタイプからプロパティ foo
を継承しており、foo
が書き込み可能でない場合、obj.foo
に代入することはできません。
var
proto
=
Object
.
defineProperty
({},
'foo'
,
{
value
:
'a'
,
writable
:
false
});
var
obj
=
Object
.
create
(
proto
);
obj
は、proto
から読み取り専用プロパティ foo
を継承します。非厳格モードでは、プロパティを設定しても効果はありません。
> obj.foo = 'b'; > obj.foo 'a'
厳格モードでは、例外が発生します。
> (function () { 'use strict'; obj.foo = 'b' }()); TypeError: Cannot assign to read-only property 'foo'
これは、代入が継承されたプロパティを非破壊的に変更するという考え方に一致します。継承されたプロパティが読み取り専用の場合、非破壊的な変更を含め、すべての変更を禁止する必要があります。
自身のプロパティを定義することによって、この保護を回避できることに注意してください(定義と代入の違いについては、前のサブセクションを参照)。
> Object.defineProperty(obj, 'foo', { value: 'b' }); > obj.foo 'b'
一般的なルールは、システムによって作成されたプロパティは列挙不可であり、ユーザーによって作成されたプロパティは列挙可能であるということです。
> Object.keys([]) [] > Object.getOwnPropertyNames([]) [ 'length' ] > Object.keys(['a']) [ '0' ]
これは、組み込みインスタンスプロトタイプのメソッドに特に当てはまります。
> Object.keys(Object.prototype) [] > Object.getOwnPropertyNames(Object.prototype) [ hasOwnProperty', 'valueOf', 'constructor', 'toLocaleString', 'isPrototypeOf', 'propertyIsEnumerable', 'toString' ]
列挙可能性の主な目的は、for-in
ループにどのプロパティを無視すべきかを伝えることです。先ほど組み込みコンストラクターのインスタンスを見たときに、ユーザーが作成したものではないものはすべて for-in
から隠されていることがわかりました。
列挙可能性の影響を受ける操作は次のとおりです。
for-in
ループObject.keys()
(自身のプロパティキーのリスト)JSON.stringify()
(JSON.stringify(value, replacer?, space?))以下に、覚えておくべきベストプラクティスをいくつか示します。
for-in
ループは避ける必要があります(ベストプラクティス:配列の反復処理)。オブジェクトを保護するには、次の3つのレベルがあり、弱いものから強いものの順にリストされています。
Object
.
preventExtensions
(
obj
)
obj
にプロパティを追加することを不可能にします。例えば
var
obj
=
{
foo
:
'a'
};
Object
.
preventExtensions
(
obj
);
これで、プロパティの追加は非厳格モードでは何も起こらずに失敗します。
> obj.bar = 'b'; > obj.bar undefined
厳格モードではエラーをスローします。
> (function () { 'use strict'; obj.bar = 'b' }()); TypeError: Can't add property bar, object is not extensible
ただし、プロパティを削除することはできます。
> delete obj.foo true > obj.foo undefined
オブジェクトが拡張可能かどうかは、次の方法で確認できます。
Object
.
isExtensible
(
obj
)
次の方法によるシーリング
Object
.
seal
(
obj
)
拡張を防止し、すべてのプロパティを「構成不可」にします。後者は、プロパティの属性(プロパティ属性とプロパティ記述子を参照)をもう変更できないことを意味します。たとえば、読み取り専用プロパティは、永久に読み取り専用のままです。
次の例は、シーリングによってすべてのプロパティが構成不可になることを示しています。
> var obj = { foo: 'a' }; > Object.getOwnPropertyDescriptor(obj, 'foo') // before sealing { value: 'a', writable: true, enumerable: true, configurable: true } > Object.seal(obj) > Object.getOwnPropertyDescriptor(obj, 'foo') // after sealing { value: 'a', writable: true, enumerable: true, configurable: false }
プロパティ foo
を変更することはできます。
> obj.foo = 'b'; 'b' > obj.foo 'b'
ただし、その属性を変更することはできません。
> Object.defineProperty(obj, 'foo', { enumerable: false }); TypeError: Cannot redefine property: foo
オブジェクトがシールされているかどうかは、次の方法で確認できます。
Object
.
isSealed
(
obj
)
Object
.
freeze
(
obj
)
var
point
=
{
x
:
17
,
y
:
-
5
};
Object
.
freeze
(
point
);
ここでも、非厳格モードでは何も起こらずに失敗します。
> point.x = 2; // no effect, point.x is read-only > point.x 17 > point.z = 123; // no effect, point is not extensible > point { x: 17, y: -5 }
そして、厳格モードではエラーが発生します。
> (function () { 'use strict'; point.x = 2 }()); TypeError: Cannot assign to read-only property 'x' > (function () { 'use strict'; point.z = 123 }()); TypeError: Can't add property z, object is not extensible
オブジェクトが凍結されているかどうかは、次の方法で確認できます。
Object
.
isFrozen
(
obj
)
オブジェクトの保護は浅いです。つまり、自身のプロパティに影響しますが、それらのプロパティの値には影響しません。たとえば、次のオブジェクトについて考えてみましょう。
var
obj
=
{
foo
:
1
,
bar
:
[
'a'
,
'b'
]
};
Object
.
freeze
(
obj
);
obj
を凍結しても、完全に不変というわけではありません。プロパティ bar
の(可変の)値を変更できます。
> obj.foo = 2; // no effect > obj.bar.push('c'); // changes obj.bar > obj { foo: 1, bar: [ 'a', 'b', 'c' ] }
さらに、obj
はプロトタイプ Object.prototype
を持っており、これも可変です。
コンストラクター関数(略して コンストラクター)は、ある意味で類似したオブジェクトを生成するのに役立ちます。これは通常の関数ですが、名前が付けられ、セットアップされ、異なる方法で呼び出されます。このセクションでは、コンストラクターの仕組みについて説明します。これらは他の言語のクラスに対応します。
すでに、(プロトタイプを介したオブジェクト間でのデータの共有で)類似した2つのオブジェクトの例を見てきました。
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Jane'
};
var
tarzan
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Tarzan'
};
オブジェクト jane
と tarzan
はどちらも「person」と見なされ、プロトタイプオブジェクト PersonProto
を共有します。このプロトタイプを、jane
や tarzan
のようなオブジェクトを作成するコンストラクター Person
に変換してみましょう。コンストラクターが作成するオブジェクトは、そのインスタンスと呼ばれます。このようなインスタンスは、jane
や tarzan
と同じ構造を持ち、次の2つの部分で構成されます。
jane
と tarzan
)。PersonProto
)を持ちます。コンストラクターは、new
演算子を介して呼び出される関数です。慣例により、コンストラクターの名前は大文字で始まり、通常の関数とメソッドの名前は小文字で始まります。関数自体がパート1をセットアップします。
function
Person
(
name
)
{
this
.
name
=
name
;
}
Person.prototype
内のオブジェクトは、Person
のすべてのインスタンスのプロトタイプになります。これにより、パート2が提供されます。
Person
.
prototype
.
describe
=
function
()
{
return
'Person named '
+
this
.
name
;
};
Person
のインスタンスを作成して使用してみましょう。
> var jane = new Person('Jane'); > jane.describe() 'Person named Jane'
Person
は通常の関数であることがわかります。new
を介して呼び出された場合にのみ、コンストラクターになります。new
演算子は、次の手順を実行します。
Person.
prototype
である新しいオブジェクトが作成されます。Person
はそのオブジェクトを暗黙のパラメーター this
として受け取り、インスタンスプロパティを追加します。図 17-3 は、インスタンス jane
がどのように見えるかを示しています。Person.prototype
のプロパティ constructor
はコンストラクターを指し、インスタンスの constructor プロパティで説明されています。
instanceof
演算子を使用すると、オブジェクトが特定のコンストラクターのインスタンスであるかどうかを確認できます。
> jane instanceof Person true > jane instanceof Date false
手動で new
演算子を実装すると、おおよそ次のようになります。
function
newOperator
(
Constr
,
args
)
{
var
thisValue
=
Object
.
create
(
Constr
.
prototype
);
// (1)
var
result
=
Constr
.
apply
(
thisValue
,
args
);
if
(
typeof
result
===
'object'
&&
result
!==
null
)
{
return
result
;
// (2)
}
return
thisValue
;
}
(1)行では、コンストラクター Constr
によって作成されたインスタンスのプロトタイプが Constr.prototype
であることがわかります。
(2)行は、new
演算子のもう1つの機能を示しています。コンストラクターから任意のオブジェクトを返し、それが new
演算子の結果にすることができます。これは、コンストラクターにサブコンストラクターのインスタンスを返させたい場合に便利です(コンストラクターから任意のオブジェクトを返すで例を示します)。
残念ながら、プロトタイプという用語は、JavaScript であいまいな方法で使用されています。
オブジェクトは、別のオブジェクトのプロトタイプになることができます。
> var proto = {}; > var obj = Object.create(proto); > Object.getPrototypeOf(obj) === proto true
前の例では、proto
が obj
のプロトタイプです。
prototype
の値各コンストラクター C
には、オブジェクトを参照する prototype
プロパティがあります。そのオブジェクトは、C
のすべてのインスタンスのプロトタイプになります。
> function C() {} > Object.getPrototypeOf(new C()) === C.prototype true
通常、文脈からどちらのプロトタイプが意図されているかが明らかになります。あいまいさを解消する必要がある場合は、オブジェクト間の関係を記述するために、プロトタイプを使用する必要があります。これは、その名前が getPrototypeOf
と isPrototypeOf
を介して標準ライブラリに入っているためです。したがって、prototype
プロパティによって参照されるオブジェクトには別の名前を見つける必要があります。1つの可能性は コンストラクタープロトタイプ ですが、コンストラクターにもプロトタイプがあるため、問題があります。
> function Foo() {} > Object.getPrototypeOf(Foo) === Function.prototype true
したがって、インスタンスプロトタイプが最適なオプションです。
デフォルトでは、各関数 C
には、プロパティ constructor
が C
を指すインスタンスプロトタイプオブジェクト C.prototype
が含まれています。
> function C() {} > C.prototype.constructor === C true
constructor
プロパティはプロトタイプから各インスタンスに継承されるため、それを使用してインスタンスのコンストラクターを取得できます。
> var o = new C(); > o.constructor [Function: C]
次の catch
句では、キャッチされた例外のコンストラクターに応じて異なるアクションを実行します。
try
{
...
}
catch
(
e
)
{
switch
(
e
.
constructor
)
{
case
SyntaxError
:
...
break
;
case
CustomError
:
...
break
;
...
}
}
このアプローチでは、特定のコンストラクターの直接インスタンスのみが検出されます。対照的に、instanceof
は、直接インスタンスとすべてのサブコンストラクターのインスタンスの両方を検出します。
例えば
> function Foo() {} > var f = new Foo(); > f.constructor.name 'Foo'
すべてのJavaScriptエンジンが関数のプロパティ name
をサポートしているわけではありません。
これは、既存のオブジェクト x
と同じコンストラクターを持つ新しいオブジェクト y
を作成する方法です。
function
Constr
()
{}
var
x
=
new
Constr
();
var
y
=
new
x
.
constructor
();
console
.
log
(
y
instanceof
Constr
);
// true
このトリックは、サブコンストラクターのインスタンスに対して機能する必要があり、this
に似た新しいインスタンスを作成したいメソッドに便利です。その場合、固定のコンストラクターを使用することはできません。
SuperConstr
.
prototype
.
createCopy
=
function
()
{
return
new
this
.
constructor
(...);
};
一部の継承ライブラリは、サブコンストラクタのプロパティにスーパプロトタイプを割り当てます。例えば、YUIフレームワークは、Y.extend
を介してサブクラス化を提供します。
function
Super
()
{
}
function
Sub
()
{
Sub
.
superclass
.
constructor
.
call
(
this
);
// (1)
}
Y
.
extend
(
Sub
,
Super
);
(1)行の呼び出しが機能するのは、extend
がSub.superclass
をSuper.prototype
に設定するためです。constructor
プロパティのおかげで、スーパーコンストラクタをメソッドとして呼び出すことができます。
instanceof
演算子(「instanceof演算子」を参照)は、プロパティconstructor
には依存しません。
すべてのコンストラクタC
に対して、次のアサーションが保持されることを確認してください:
C
.
prototype
.
constructor
===
C
デフォルトでは、すべての関数f
は、正しく設定されたプロパティprototype
を既に持っています
> function f() {} > f.prototype.constructor === f true
したがって、このオブジェクトを置き換えることは避け、プロパティを追加するだけにする必要があります
// Avoid:
C
.
prototype
=
{
method1
:
function
(...)
{
...
},
...
};
// Prefer:
C
.
prototype
.
method1
=
function
(...)
{
...
};
...
置き換える場合は、constructor
に正しい値を手動で割り当てる必要があります
C
.
prototype
=
{
constructor
:
C
,
method1
:
function
(...)
{
...
},
...
};
JavaScriptの重要な部分はconstructor
プロパティに依存していないことに注意してください。しかし、このセクションで述べた手法を可能にするため、設定するのは良いスタイルです。
instanceof
演算子は
value
instanceof
Constr
value
がコンストラクタConstr
またはサブコンストラクタによって作成されたかどうかを判断します。これは、Constr.prototype
がvalue
のプロトタイプチェーンにあるかどうかをチェックすることで行います。したがって、次の2つの式は同等です。
value
instanceof
Constr
Constr
.
prototype
.
isPrototypeOf
(
value
)
次に例をいくつか示します。
> {} instanceof Object true > [] instanceof Array // constructor of [] true > [] instanceof Object // super-constructor of [] true > new Date() instanceof Date true > new Date() instanceof Object true
予想どおり、instanceof
はプリミティブ値に対して常にfalse
になります
> 'abc' instanceof Object false > 123 instanceof Number false
最後に、右辺が関数でない場合、instanceof
は例外をスローします
> [] instanceof 123 TypeError: Expecting a function in instanceof check
ほとんどすべてのオブジェクトはObject
のインスタンスです。これは、Object.prototype
がそれらのプロトタイプチェーンにあるためです。しかし、そうでないオブジェクトも存在します。以下に2つの例を示します。
> Object.create(null) instanceof Object false > Object.prototype instanceof Object false
前者のオブジェクトについては、「dictパターン:プロトタイプを持たないオブジェクトはより良いマップ」で詳しく説明します。後者のオブジェクトは、ほとんどのプロトタイプチェーンが終わる場所です(そしてどこかで終わる必要があります)。どちらのオブジェクトもプロトタイプを持っていません
> Object.getPrototypeOf(Object.create(null)) null > Object.getPrototypeOf(Object.prototype) null
ただし、typeof
はそれらをオブジェクトとして正しく分類します
> typeof Object.create(null) 'object' > typeof Object.prototype 'object'
この落とし穴は、instanceof
のほとんどのユースケースにとって致命的なものではありませんが、注意する必要があります。
Webブラウザでは、各フレームとウィンドウには、レルムが個別に存在し、グローバル変数が別々になっています。そのため、レルムをまたぐオブジェクトではinstanceof
が機能しません。その理由を理解するために、次のコードを見てください。
if
(
myvar
instanceof
Array
)
...
// Doesn’t always work
myvar
が別のレルムからの配列である場合、そのプロトタイプは、そのレルムのArray.prototype
になります。したがって、instanceof
はmyvar
のプロトタイプチェーンに現在のレルムのArray.prototype
を見つけることができず、false
を返します。ECMAScript 5には、常に機能する関数Array.isArray()
があります。:
<head>
<script>
function
test
(
arr
)
{
var
iframe
=
frames
[
0
];
console
.
log
(
arr
instanceof
Array
);
// false
console
.
log
(
arr
instanceof
iframe
.
Array
);
// true
console
.
log
(
Array
.
isArray
(
arr
));
// true
}
</script>
</head>
<body>
<iframe
srcdoc=
"<script>window.parent.test([])</script>"
>
</iframe>
</body>
明らかに、これは組み込み以外のコンストラクタでも問題になります。
Array.isArray()
を使用すること以外に、この問題を回避するためにできることがいくつかあります
postMessage()
メソッドがあります。インスタンスのコンストラクタの名前を確認します(関数のname
プロパティをサポートするエンジンでのみ機能します)
someValue
.
constructor
.
name
===
'NameOfExpectedConstructor'
プロトタイププロパティを使用して、インスタンスがタイプT
に属していることをマークします。これを行うにはいくつかの方法があります。value
がT
のインスタンスかどうかをチェックするには、次のようになります
value.isT()
:T
インスタンスのプロトタイプは、このメソッドからtrue
を返す必要があります。共通のスーパーコンストラクタは、デフォルト値のfalse
を返す必要があります。'T' in value
:T
インスタンスのプロトタイプに、キーが'T'
(またはより固有のもの)であるプロパティでタグ付けする必要があります。value.TYPE_NAME === 'T'
:関連するすべてのプロトタイプには、適切な値を持つTYPE_NAME
プロパティが必要です。このセクションでは、コンストラクタを実装するためのいくつかのヒントを示します。
コンストラクタを使用するときにnew
を忘れると、コンストラクタとしてではなく、関数として呼び出していることになります。非厳格モードでは、インスタンスを取得せず、グローバル変数が作成されます。残念ながら、これはすべて警告なしに行われます。
function
SloppyColor
(
name
)
{
this
.
name
=
name
;
}
var
c
=
SloppyColor
(
'green'
);
// no warning!
// No instance is created:
console
.
log
(
c
);
// undefined
// A global variable is created:
console
.
log
(
name
);
// green
厳格モードでは、例外が発生します。
function
StrictColor
(
name
)
{
'use strict'
;
this
.
name
=
name
;
}
var
c
=
StrictColor
(
'green'
);
// TypeError: Cannot set property 'name' of undefined
class
Expression
{
// Static factory method:
public
static
Expression
parse
(
String
str
)
{
if
(...)
{
return
new
Addition
(...);
}
else
if
(...)
{
return
new
Multiplication
(...);
}
else
{
throw
new
ExpressionException
(...);
}
}
}
...
Expression
expr
=
Expression
.
parse
(
someStr
);
JavaScriptでは、コンストラクタから必要なオブジェクトを返すだけです。したがって、上記のコードのJavaScriptバージョンは次のようになります
function
Expression
(
str
)
{
if
(...)
{
return
new
Addition
(..);
}
else
if
(...)
{
return
new
Multiplication
(...);
}
else
{
throw
new
ExpressionException
(...);
}
}
...
var
expr
=
new
Expression
(
someStr
);
これは良い知らせです:JavaScriptコンストラクタはあなたを束縛しないため、コンストラクタが直接インスタンスを返すか、それ以外のものを返すかをいつでも変更できます。
このセクションでは、ほとんどの場合、プロトタイププロパティにデータを配置すべきではないことを説明します。ただし、そのルールにはいくつかの例外があります。
コンストラクタは通常、インスタンスプロパティを初期値に設定します。そのような値の1つがデフォルトである場合、インスタンスプロパティを作成する必要はありません。同じキーを持つプロトタイププロパティで、値がデフォルトである必要があります。例えば
/**
* Anti-pattern: don’t do this
*
* @param data an array with names
*/
function
Names
(
data
)
{
if
(
data
)
{
// There is a parameter
// => create instance property
this
.
data
=
data
;
}
}
Names
.
prototype
.
data
=
[];
パラメータdata
はオプションです。それが欠落している場合、インスタンスはプロパティdata
を取得しませんが、代わりにNames.prototype.data
を継承します。
このアプローチはほとんどの場合機能します。インスタンスn
のNames
を作成できます。n.data
を取得すると、Names.prototype.data
が読み取られます。n.data
を設定すると、n
に新しい独自のプロパティが作成され、プロトタイプ内の共有デフォルト値が保持されます。デフォルト値を(新しい値で置き換えるのではなく)変更した場合にのみ問題が発生します。
> var n1 = new Names(); > var n2 = new Names(); > n1.data.push('jane'); // changes default value > n1.data [ 'jane' ] > n2.data [ 'jane' ]
前の例では、push()
がNames.prototype.data
の配列を変更しました。その配列は、独自のプロパティdata
を持たないすべてのインスタンスで共有されるため、n2.data
の初期値も変更されました。
これまで説明したことを踏まえると、デフォルト値を共有せず、常に新しい値を作成する方が良いでしょう
function
Names
(
data
)
{
this
.
data
=
data
||
[];
}
明らかに、その値が不変である場合(すべてのプリミティブがそうであるように、「プリミティブ値」を参照)、共有のデフォルト値を変更するという問題は発生しません。しかし、一貫性のために、プロパティを設定する単一の方法に固執するのが最善です。また、懸念事項の通常の分離を維持することも好みます(「レイヤー3:コンストラクタ—インスタンスのファクトリ」を参照):コンストラクタはインスタンスプロパティを設定し、プロトタイプにはメソッドが含まれます。
ECMAScript 6は、コンストラクタパラメータにデフォルト値を設定でき、クラスを介してプロトタイプメソッドを定義できますが、データを持つプロトタイププロパティを定義できないため、これをさらにベストプラクティスにするでしょう。
場合によっては、プロパティ値の作成が(計算上またはストレージの点で)コストのかかる操作になることがあります。その場合は、オンデマンドでインスタンスプロパティを作成できます:
function
Names
(
data
)
{
if
(
data
)
this
.
data
=
data
;
}
Names
.
prototype
=
{
constructor
:
Names
,
// (1)
get
data
()
{
// Define, don’t assign
// => avoid calling the (nonexistent) setter
Object
.
defineProperty
(
this
,
'data'
,
{
value
:
[],
enumerable
:
true
,
configurable
:
false
,
writable
:
false
});
return
this
.
data
;
}
};
JavaScriptは(getterのみを見つけた場合に)欠落しているセッターについて不平を言うため、代入によってインスタンスにプロパティdata
を追加することはできません。したがって、Object.defineProperty()
を介して追加します。定義と代入の違いを確認するには、「プロパティ:定義と代入」を参照してください。(1)行では、プロパティconstructor
が適切に設定されていることを確認しています(「インスタンスのconstructorプロパティ」を参照)。
明らかに、これはかなりの作業量になるため、それだけの価値があることを確認する必要があります。
もし同じプロパティ(同じキー、同じセマンティクス、通常は異なる値)が複数のプロトタイプに存在する場合、それはポリモーフィックと呼ばれます。 この場合、インスタンス経由でプロパティを読み取る結果は、そのインスタンスのプロトタイプによって動的に決定されます。ポリモーフィックに使用されないプロトタイププロパティは、(その非ポリモーフィックな性質をより良く反映する)変数に置き換えることができます。
例えば、プロトタイププロパティに定数を格納し、this
経由でアクセスできます。
function
Foo
()
{}
Foo
.
prototype
.
FACTOR
=
42
;
Foo
.
prototype
.
compute
=
function
(
x
)
{
return
x
*
this
.
FACTOR
;
};
この定数はポリモーフィックではありません。したがって、変数経由でアクセスしても同じです。
// This code should be inside an IIFE or a module
function
Foo
()
{}
var
FACTOR
=
42
;
Foo
.
prototype
.
compute
=
function
(
x
)
{
return
x
*
FACTOR
;
};
以下は、ポリモーフィックなプロトタイププロパティの例です。不変データを使用しています。プロトタイププロパティを介してコンストラクタのインスタンスにタグ付けすることで、異なるコンストラクタのインスタンスと区別できます。
function
ConstrA
()
{
}
ConstrA
.
prototype
.
TYPE_NAME
=
'ConstrA'
;
function
ConstrB
()
{
}
ConstrB
.
prototype
.
TYPE_NAME
=
'ConstrB'
;
ポリモーフィックな「タグ」であるTYPE_NAME
のおかげで、ConstrA
とConstrB
のインスタンスを、それらがレルムをまたいでいる場合でも区別できます(この場合、instanceof
は機能しません。 「落とし穴:レルム(フレームまたはウィンドウ)をまたぐ」を参照してください)。
JavaScriptには、オブジェクトのプライベートデータを管理するための専用の手段はありません。このセクションでは、その制限を回避するための3つの手法について説明します。
さらに、IIFEを介してグローバルデータをプライベートに保つ方法について説明します。
コンストラクタが呼び出されると、2つのものが作成されます。コンストラクタのインスタンスと環境です(「環境:変数の管理」を参照してください)。インスタンスはコンストラクタによって初期化されます。環境は、コンストラクタのパラメータとローカル変数を保持します。コンストラクタ内で作成されたすべての関数(メソッドを含む)は、その環境(作成された環境)への参照を保持します。その参照のおかげで、コンストラクタが終了した後でも、常に環境にアクセスできます。この関数と環境の組み合わせはクロージャと呼ばれます(「クロージャ:関数は誕生時のスコープとの接続を維持する」)。したがって、コンストラクタの環境は、インスタンスとは独立したデータストレージであり、両者が同時に作成されるという理由だけで関連付けられています。それらを適切に接続するには、両方の世界に存在する関数が必要です。ダグラス・クロックフォードの用語を使用すると、インスタンスには3種類の関連付けられた値があります(「図17-4」を参照してください)。
次のセクションでは、それぞれの種類の値をより詳細に説明します。
コンストラクタConstr
が与えられた場合、誰でもアクセスできるパブリックなプロパティが2種類あることを覚えておいてください。まず、プロトタイププロパティはConstr.prototype
に格納され、すべてのインスタンスで共有されます。プロトタイププロパティは通常メソッドです。
Constr
.
prototype
.
publicMethod
=
...;
次に、インスタンスプロパティは、各インスタンスに固有です。それらはコンストラクタで追加され、通常は(メソッドではなく)データを保持します。
function
Constr
(...)
{
this
.
publicData
=
...;
...
}
コンストラクタの環境は、パラメータとローカル変数で構成されます。それらはコンストラクタ内部からのみアクセスでき、したがってインスタンスに対してプライベートです。
function
Constr
(...)
{
...
var
that
=
this
;
// make accessible to private functions
var
privateData
=
...;
function
privateFunction
(...)
{
// Access everything
privateData
=
...;
that
.
publicData
=
...;
that
.
publicMethod
(...);
}
...
}
プライベートデータは、外部からのアクセスから非常に安全であり、プロトタイプメソッドもアクセスできません。それでは、コンストラクタを離れた後にどのように使用すればよいのでしょうか?答えは特権メソッドです。コンストラクタで作成された関数は、インスタンスメソッドとして追加されます。これは、一方ではプライベートデータにアクセスできることを意味し、他方では、パブリックであるため、プロトタイプメソッドによって認識されることを意味します。言い換えれば、それらはプライベートデータとパブリック(プロトタイプメソッドを含む)の間の仲介者として機能します。
function
Constr
(...)
{
...
this
.
privilegedMethod
=
function
(...)
{
// Access everything
privateData
=
...;
privateFunction
(...);
this
.
publicData
=
...;
this
.
publicMethod
(...);
};
}
以下は、Crockfordプライバシーパターンを使用して、StringBuilder
を実装したものです。
function
StringBuilder
()
{
var
buffer
=
[];
this
.
add
=
function
(
str
)
{
buffer
.
push
(
str
);
};
this
.
toString
=
function
()
{
return
buffer
.
join
(
''
);
};
}
// Can’t put methods in the prototype!
これがそのやり取りです。
> var sb = new StringBuilder(); > sb.add('Hello'); > sb.add(' world!'); > sb.toString() ’Hello world!’
Crockfordプライバシーパターンを使用する場合に検討すべきいくつかの点を示します。
ほとんどのセキュリティクリティカルでないアプリケーションの場合、プライバシーはAPIのクライアントへのヒントのようなものです。「これを見る必要はありません。」 それがカプセル化の重要な利点であり、複雑さを隠すことです。内部ではさらに多くのことが行われていますが、APIのパブリック部分のみを理解する必要があります。命名規則の考え方は、プロパティのキーをマークすることで、クライアントにプライバシーについて知らせることです。この目的のために、プレフィックス付きのアンダースコアがよく使用されます。
前のStringBuilder
の例を書き換えて、バッファがプロパティ_buffer
に保持されるようにします。これはプライベートですが、あくまで慣習上のものです。
function
StringBuilder
()
{
this
.
_buffer
=
[];
}
StringBuilder
.
prototype
=
{
constructor
:
StringBuilder
,
add
:
function
(
str
)
{
this
.
_buffer
.
push
(
str
);
},
toString
:
function
()
{
return
this
.
_buffer
.
join
(
''
);
}
};
以下に、マークされたプロパティキーによるプライバシーの長所と短所をいくつか示します。
プライベートプロパティの命名規則の1つの問題は、キーが衝突する可能性があることです(たとえば、コンストラクタのキーとサブコンストラクタのキー、またはmixinのキーとコンストラクタのキー)。コンストラクタの名前を含む、より長いキーを使用することで、このような衝突を減らすことができます。たとえば、前のケースでは、プライベートプロパティ_buffer
は_StringBuilder_buffer
と呼ばれます。このようなキーが長すぎる場合は、具象化して、変数に格納するという選択肢があります。
var
KEY_BUFFER
=
'_StringBuilder_buffer'
;
プライベートデータにはthis[KEY_BUFFER]
を介してアクセスします。
var
StringBuilder
=
function
()
{
var
KEY_BUFFER
=
'_StringBuilder_buffer'
;
function
StringBuilder
()
{
this
[
KEY_BUFFER
]
=
[];
}
StringBuilder
.
prototype
=
{
constructor
:
StringBuilder
,
add
:
function
(
str
)
{
this
[
KEY_BUFFER
].
push
(
str
);
},
toString
:
function
()
{
return
this
[
KEY_BUFFER
].
join
(
''
);
}
};
return
StringBuilder
;
}();
定数KEY_BUFFER
がローカルに留まり、グローバル名前空間を汚染しないように、StringBuilder
をIIFEでラップしました。
具象化されたプロパティキーを使用すると、キーにUUID(Universally Unique Identifier)を使用できます。たとえば、Robert Kiefferのnode-uuidを介して。
var
KEY_BUFFER
=
'_StringBuilder_buffer_'
+
uuid
.
v4
();
KEY_BUFFER
には、コードが実行されるたびに異なる値が設定されます。たとえば、次のようになる可能性があります。
_StringBuilder_buffer_110ec58a-a0f2-4ac4-8393-c866d813b8d1
UUIDを使用した長いキーを使用すると、キーの衝突をほぼ不可能にできます。
このサブセクションでは、IIFEを介して、シングルトンオブジェクト、コンストラクタ、およびメソッドに対してグローバルデータをプライベートに保つ方法について説明します(「IIFEを介して新しいスコープを導入する」を参照してください)。これらのIIFEは新しい環境を作成し(「環境:変数の管理」を参照してください)、そこにプライベートデータを配置します。
環境内のプライベートデータをオブジェクトに関連付けるために、コンストラクタは必要ありません。次の例は、同じ目的でIIFEをシングルトンオブジェクトの周りにラップする方法を示しています。
var
obj
=
function
()
{
// open IIFE
// public
var
self
=
{
publicMethod
:
function
(...)
{
privateData
=
...;
privateFunction
(...);
},
publicData
:
...
};
// private
var
privateData
=
...;
function
privateFunction
(...)
{
privateData
=
...;
self
.
publicData
=
...;
self
.
publicMethod
(...);
}
return
self
;
}();
// close IIFE
グローバルデータの中には、コンストラクタとプロトタイプメソッドにのみ関連するものがあります。IIFEで両方をラップすることで、外部から隠蔽することができます。具象化されたキーを持つプロパティにおけるプライベートデータでは、例として、コンストラクタStringBuilder
とそのプロトタイプメソッドが、プロパティキーを含む定数KEY_BUFFER
を使用していることを示しました。この定数は、IIFEの環境に格納されます。
var
StringBuilder
=
function
()
{
// open IIFE
var
KEY_BUFFER
=
'_StringBuilder_buffer_'
+
uuid
.
v4
();
function
StringBuilder
()
{
this
[
KEY_BUFFER
]
=
[];
}
StringBuilder
.
prototype
=
{
// Omitted: methods accessing this[KEY_BUFFER]
};
return
StringBuilder
;
}();
// close IIFE
モジュールシステムを使用している場合(第31章を参照)、コンストラクタとメソッドをモジュールに入れることで、よりクリーンなコードで同じ効果を得ることができます。
単一のメソッドに対してのみグローバルデータが必要な場合があります。 メソッドをラップするIIFEの環境に入れることで、プライベートに保つことができます。例:
var
obj
=
{
method
:
function
()
{
// open IIFE
// method-private data
var
invocCount
=
0
;
return
function
()
{
invocCount
++
;
console
.
log
(
'Invocation #'
+
invocCount
);
return
'result'
;
};
}()
// close IIFE
};
これがそのやり取りです。
> obj.method() Invocation #1 'result' > obj.method() Invocation #2 'result'
このセクションでは、コンストラクタがどのように継承されるかを調べます。コンストラクタSuper
がある場合、Super
のすべての機能に加えて独自の機能も持つ新しいコンストラクタSub
をどのように記述できるでしょうか? 残念ながら、JavaScriptにはこのタスクを実行するための組み込みメカニズムがありません。したがって、手動で作業を行う必要があります。
図17-5は、その考えを示しています。サブコンストラクタSub
は、独自のプロパティに加えて、Super
のすべてのプロパティ(プロトタイププロパティとインスタンスプロパティの両方)を持つ必要があります。したがって、Sub
がどのようなものになるかの大まかなアイデアはありますが、そこに到達する方法がわかりません。次に説明するいくつかのことを理解する必要があります。
instanceof
が機能することを確認する:sub
がSub
のインスタンスである場合、sub instanceof Super
もtrueになるようにします。Super
のメソッドの1つをSub
で適応させる。Super
のメソッドの1つをオーバーライドした場合、Sub
から元のメソッドを呼び出す必要がある場合があります。インスタンスプロパティは、コンストラクタ自体で設定されるため、スーパークラスのインスタンスプロパティを継承するには、そのコンストラクタを呼び出す必要があります。
function
Sub
(
prop1
,
prop2
,
prop3
,
prop4
)
{
Super
.
call
(
this
,
prop1
,
prop2
);
// (1)
this
.
prop3
=
prop3
;
// (2)
this
.
prop4
=
prop4
;
// (3)
}
Sub
がnew
を介して呼び出されると、その暗黙的なパラメータthis
は新しいインスタンスを参照します。最初に、そのインスタンスをSuper
(1)に渡し、インスタンスプロパティを追加します。その後、Sub
は独自のインスタンスプロパティを設定します(2,3)。コツは、Super
をnew
を介して呼び出さないことです。これは、新しいスーパーインスタンスを作成するためです。代わりに、Super
を関数として呼び出し、現在の(サブ)インスタンスをthis
の値として渡します。
メソッドなどの共有プロパティは、インスタンスプロトタイプに保持されます。 したがって、Sub.prototype
がSuper.prototype
のすべてのプロパティを継承する方法を見つける必要があります。解決策は、Sub.prototype
にプロトタイプSuper.prototype
を与えることです。
はい、ここでのJavaScriptの用語は混乱を招きます。迷った場合は、用語:2つのプロトタイプを参照してください。それらの違いについて説明しています。
これがそれを実現するコードです
Sub
.
prototype
=
Object
.
create
(
Super
.
prototype
);
Sub
.
prototype
.
constructor
=
Sub
;
Sub
.
prototype
.
methodB
=
...;
Sub
.
prototype
.
methodC
=
...;
Object.create()
は、プロトタイプがSuper.prototype
である新しいオブジェクトを作成します。その後、Sub
のメソッドを追加します。インスタンスのコンストラクタプロパティで説明したように、プロパティconstructor
も設定する必要があります。これは、正しい値を持っていた元のインスタンスプロトタイプを置き換えたためです。
図17-6は、Sub
とSuper
がどのように関連付けられているかを示しています。Sub
の構造は、図17-5でスケッチしたものに似ています。図にはインスタンスプロパティは表示されていません。これらは図で説明されている関数呼び出しによって設定されます。
instanceof
が機能することを確認する「instanceof
が機能することを確認する」とは、Sub
のすべてのインスタンスがSuper
のインスタンスでもある必要があることを意味します。図17-7は、Sub
のインスタンスであるsubInstance
のプロトタイプチェーンがどのように見えるかを示しています。その最初のプロトタイプはSub.prototype
であり、その2番目のプロトタイプはSuper.prototype
です。
まず、より簡単な質問から始めましょう。subInstance
はSub
のインスタンスですか?はい、そうです。これは、次の2つのアサーションが同等であるためです(後者は前者の定義と見なすことができます)。
subInstance
instanceof
Sub
Sub
.
prototype
.
isPrototypeOf
(
subInstance
)
前述のように、Sub.prototype
はsubInstance
のプロトタイプの1つであるため、どちらのアサーションもtrueです。同様に、subInstance
はSuper
のインスタンスでもあります。これは、次の2つのアサーションが成り立つためです。
subInstance
instanceof
Super
Super
.
prototype
.
isPrototypeOf
(
subInstance
)
私たちは、Super.prototype
のメソッドを、同じ名前のメソッドをSub.prototype
に追加することでオーバーライドします。methodB
はその例であり、図17-7で、その理由を確認できます。methodB
の検索はsubInstance
で始まり、Super.prototype.methodB
の前にSub.prototype.methodB
を見つけます。
スーパーコールを理解するには、ホームオブジェクトという用語を知る必要があります。 メソッドのホームオブジェクトとは、その値がメソッドであるプロパティを所有するオブジェクトです。たとえば、Sub.prototype.methodB
のホームオブジェクトはSub.prototype
です。メソッドfoo
のスーパーコールには、次の3つのステップが含まれます。
foo
のメソッドを探します。this
でそのメソッドを呼び出します。根拠は、スーパーメソッドは現在のメソッドと同じインスタンスで動作する必要があり、同じインスタンスプロパティにアクセスできる必要があるということです。したがって、サブメソッドのコードは次のようになります。それ自体をスーパーコールし、オーバーライドしたメソッドを呼び出します
Sub
.
prototype
.
methodB
=
function
(
x
,
y
)
{
var
superResult
=
Super
.
prototype
.
methodB
.
call
(
this
,
x
,
y
);
// (1)
return
this
.
prop3
+
' '
+
superResult
;
}
(1)でのスーパーコールの読み方の1つは、スーパーメソッドを直接参照し、現在のthis
で呼び出すことです。ただし、3つの部分に分割すると、前述の手順が見つかります
Super.prototype
:Sub.prototype
のプロトタイプ(現在のメソッドSub.prototype.methodB
のホームオブジェクト)であるSuper.prototype
で検索を開始します。methodB
:名前がmethodB
のメソッドを探します。call(this, ...)
:前のステップで見つけたメソッドを呼び出し、現在のthis
を維持します。これまで、スーパークラスの名前を記述することで、常にスーパーメソッドとスーパーコンストラクタを参照してきました。 このようなハードコーディングは、コードの柔軟性を低下させます。スーパークラスプロトタイプをSub
のプロパティに割り当てることで、これを回避できます。
Sub
.
_super
=
Super
.
prototype
;
その後、スーパークラスコンストラクタとスーパーメソッドの呼び出しは次のようになります
function
Sub
(
prop1
,
prop2
,
prop3
,
prop4
)
{
Sub
.
_super
.
constructor
.
call
(
this
,
prop1
,
prop2
);
this
.
prop3
=
prop3
;
this
.
prop4
=
prop4
;
}
Sub
.
prototype
.
methodB
=
function
(
x
,
y
)
{
var
superResult
=
Sub
.
_super
.
methodB
.
call
(
this
,
x
,
y
);
return
this
.
prop3
+
' '
+
superResult
;
}
Sub._super
の設定は通常、サブプロトタイプをスーパープロトタイプにも接続するユーティリティ関数によって処理されます。例:
function
subclasses
(
SubC
,
SuperC
)
{
var
subProto
=
Object
.
create
(
SuperC
.
prototype
);
// Save `constructor` and, possibly, other methods
copyOwnPropertiesFrom
(
subProto
,
SubC
.
prototype
);
SubC
.
prototype
=
subProto
;
SubC
.
_super
=
SuperC
.
prototype
;
};
このコードでは、オブジェクトのコピーで示され、説明されているヘルパー関数copyOwnPropertiesFrom()
を使用しています。
「サブクラス」を動詞として読んでください:SubC
はサブクラス
SuperC
です。このようなユーティリティ関数は、サブコンストラクタの作成の苦痛を軽減できます。手動で行うことが少なくなり、スーパークラスの名前が冗長に記述されることがなくなります。次の例は、コードをどのように簡略化するかを示しています。
具体的な例として、コンストラクタPerson
が既に存在すると仮定しましょう。
function
Person
(
name
)
{
this
.
name
=
name
;
}
Person
.
prototype
.
describe
=
function
()
{
return
'Person called '
+
this
.
name
;
};
次に、Person
のサブコンストラクタとしてコンストラクタEmployee
を作成します。これを手動で行うと、次のようになります
function
Employee
(
name
,
title
)
{
Person
.
call
(
this
,
name
);
this
.
title
=
title
;
}
Employee
.
prototype
=
Object
.
create
(
Person
.
prototype
);
Employee
.
prototype
.
constructor
=
Employee
;
Employee
.
prototype
.
describe
=
function
()
{
return
Person
.
prototype
.
describe
.
call
(
this
)
+
' ('
+
this
.
title
+
')'
;
};
これがそのやり取りです。
> var jane = new Employee('Jane', 'CTO'); > jane.describe() Person called Jane (CTO) > jane instanceof Employee true > jane instanceof Person true
前のセクションのユーティリティ関数subclasses()
を使用すると、Employee
のコードが少し簡単になり、スーパークラスPerson
のハードコーディングが回避されます
function
Employee
(
name
,
title
)
{
Employee
.
_super
.
constructor
.
call
(
this
,
name
);
this
.
title
=
title
;
}
Employee
.
prototype
.
describe
=
function
()
{
return
Employee
.
_super
.
describe
.
call
(
this
)
+
' ('
+
this
.
title
+
')'
;
};
subclasses
(
Employee
,
Person
);
組み込みコンストラクタは、このセクションで説明したのと同じサブクラス化アプローチを使用します。 たとえば、Array
はObject
のサブコンストラクタです。したがって、Array
のインスタンスのプロトタイプチェーンは次のようになります。
> var p = Object.getPrototypeOf > p([]) === Array.prototype true > p(p([])) === Object.prototype true > p(p(p([]))) === null true
ECMAScript 5とObject.create()
が登場する前は、サブプロトタイプを作成する際に、スーパコンストラクタを呼び出すという方法がよく使われていました。
Sub
.
prototype
=
new
Super
();
// Don’t do this
これはECMAScript 5では推奨されていません。プロトタイプは、Super
のインスタンスプロパティをすべて持つことになりますが、これはプロトタイプにとって不要です。したがって、前述のパターン(Object.create()
を使用する)を使用する方が良いです。
ほとんどすべてのオブジェクトは、Object.prototype
をプロトタイプチェーンに持っています。
> Object.prototype.isPrototypeOf({}) true > Object.prototype.isPrototypeOf([]) true > Object.prototype.isPrototypeOf(/xyz/) true
以下のサブセクションでは、Object.prototype
がそのプロトタイプに対して提供するメソッドについて説明します。
次の2つのメソッドは、オブジェクトをプリミティブ値に変換するために使用されます。
Object.prototype.toString()
> ({ first: 'John', last: 'Doe' }.toString()) '[object Object]' > [ 'a', 'b', 'c' ].toString() 'a,b,c'
Object.prototype.valueOf()
これは、オブジェクトを数値に変換する際の推奨される方法です。デフォルトの実装ではthis
を返します。
> var obj = {}; > obj.valueOf() === obj true
valueOf
は、ラッパーコンストラクタによってオーバーライドされ、ラップされたプリミティブを返します。
> new Number(7).valueOf() 7
数値および文字列への変換(暗黙的または明示的)は、プリミティブへの変換に基づいています(詳細は、アルゴリズム: ToPrimitive() — 値をプリミティブに変換するを参照)。そのため、前述の2つのメソッドを使用して、これらの変換を設定できます。valueOf()
は、数値への変換で優先されます。
> 3 * { valueOf: function () { return 5 } } 15
toString()
は、文字列への変換で優先されます。
> String({ toString: function () { return 'ME' } }) 'Result: ME'
ブール値への変換は設定できません。オブジェクトは常にtrue
とみなされます(ブール値への変換を参照)。
このメソッドは、オブジェクトのロケール固有の文字列表現を返します。デフォルトの実装では、toString()
を呼び出します。ほとんどのエンジンでは、このメソッドのサポートはこれ以上進んでいません。ただし、ECMAScript国際化API(ECMAScript国際化APIを参照)は、多くの最新エンジンでサポートされており、いくつかの組み込みコンストラクタでオーバーライドされています。
次のメソッドは、プロトタイプ継承とプロパティに役立ちます。
Object.prototype.isPrototypeOf(obj)
レシーバーがobj
のプロトタイプチェーンの一部である場合は、true
を返します。
> var proto = { }; > var obj = Object.create(proto); > proto.isPrototypeOf(obj) true > obj.isPrototypeOf(obj) false
Object.prototype.hasOwnProperty(key)
this
がキーがkey
であるプロパティを所有している場合、true
を返します。「所有」とは、プロパティがオブジェクト自体に存在し、そのプロトタイプのいずれにも存在しないことを意味します。
通常、特にプロパティを静的に知らないオブジェクトに対しては、このメソッドをジェネリックに(直接ではなく)呼び出す必要があります。その理由と方法については、プロパティの反復処理と検出で説明します。
> var proto = { foo: 'abc' }; > var obj = Object.create(proto); > obj.bar = 'def'; > Object.prototype.hasOwnProperty.call(obj, 'foo') false > Object.prototype.hasOwnProperty.call(obj, 'bar') true
Object.prototype.propertyIsEnumerable(propKey)
レシーバーが、propKey
というキーを持つ、列挙可能なプロパティを持っている場合はtrue
を返し、それ以外の場合はfalse
を返します。
> var obj = { foo: 'abc' }; > obj.propertyIsEnumerable('foo') true > obj.propertyIsEnumerable('toString') false > obj.propertyIsEnumerable('unknown') false
インスタンスのプロトタイプには、そこから継承するオブジェクトよりも多くのオブジェクトに対して便利なメソッドがある場合があります。このセクションでは、プロトタイプから継承せずに、そのプロトタイプのメソッドを使用する方法について説明します。たとえば、インスタンスプロトタイプWine.prototype
には、メソッドincAge()
があります。
function
Wine
(
age
)
{
this
.
age
=
age
;
}
Wine
.
prototype
.
incAge
=
function
(
years
)
{
this
.
age
+=
years
;
}
インタラクションは次のとおりです。
> var chablis = new Wine(3); > chablis.incAge(1); > chablis.age 4
メソッドincAge()
は、プロパティage
を持つ任意のオブジェクトで機能します。 Wine
のインスタンスではないオブジェクトでこれを呼び出すにはどうすればよいでしょうか?前のメソッド呼び出しを見てみましょう。
chablis
.
incAge
(
1
)
実際には2つの引数があります。
chablis
はメソッド呼び出しのレシーバーであり、this
経由でincAge
に渡されます。1
は引数であり、years
経由でincAge
に渡されます。前者を任意のオブジェクトに置き換えることはできません。レシーバーはWine
のインスタンスである必要があります。そうでない場合、メソッドincAge
は見つかりません。ただし、前のメソッド呼び出しは、以下と同等です(「thisを設定しながら関数を呼び出す: call()、apply()、およびbind()」を参照)。
Wine
.
prototype
.
incAge
.
call
(
chablis
,
1
)
前のパターンを使用すると、レシーバー(call
の最初の引数)をWine
のインスタンスではないオブジェクトにすることができます。これは、レシーバーがメソッドWine.prototype.incAge
を見つけるために使用されないためです。次の例では、メソッドincAge()
をオブジェクトjohn
に適用します。
> var john = { age: 51 }; > Wine.prototype.incAge.call(john, 3) > john.age 54
このように使用できる関数は、ジェネリックメソッドと呼ばれます。これは、this
が「その」コンストラクタのインスタンスではない場合に備えて準備する必要があります。したがって、すべてのメソッドがジェネリックであるわけではありません。ECMAScript言語仕様では、どれがジェネリックであるかが明示的に述べられています(すべてのジェネリックメソッドのリストを参照)。
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
'propKey'
)
空のオブジェクトリテラル{}
によって作成されたObject
のインスタンスを介してhasOwnProperty
にアクセスすることで、これを短縮できます。
{}.
hasOwnProperty
.
call
(
obj
,
'propKey'
)
同様に、次の2つの式は同等です。
Array
.
prototype
.
join
.
call
(
str
,
'-'
)
[].
join
.
call
(
str
,
'-'
)
このパターンの利点は、冗長性が少ないことです。ただし、自己説明的ではありません。エンジンはリテラルがオブジェクトを作成すべきではないことを静的に判断できるため、パフォーマンスは問題にならないはずです(少なくとも長期的には)。
以下は、使用中のジェネリックメソッドの例です。
配列を(個々の要素ではなく)push()
するために、apply()
を使用します(Function.prototype.apply(thisValue, argArray)および要素の追加と削除(破壊的)を参照)。
> var arr1 = [ 'a', 'b' ]; > var arr2 = [ 'c', 'd' ]; > [].push.apply(arr1, arr2) 4 > arr1 [ 'a', 'b', 'c', 'd' ]
この例は、配列を引数に変換することに関するものであり、別のコンストラクタからメソッドを借用することに関するものではありません。
配列メソッドjoin()
を文字列(配列ではない)に適用します。
> Array.prototype.join.call('abc', '-') 'a-b-c'
配列メソッドmap()
を文字列に適用します。[17]
> [].map.call('abc', function (x) { return x.toUpperCase() }) [ 'A', 'B', 'C' ]
map()
をジェネリックに使用する方が、中間配列を作成するsplit('')
を使用するよりも効率的です。
> 'abc'.split('').map(function (x) { return x.toUpperCase() }) [ 'A', 'B', 'C' ]
文字列メソッドを非文字列に適用します。toUpperCase()
はレシーバーを文字列に変換し、その結果を大文字にします。
> String.prototype.toUpperCase.call(true) 'TRUE' > String.prototype.toUpperCase.call(['a','b','c']) 'A,B,C'
プレーンオブジェクトでジェネリック配列メソッドを使用すると、その動作を理解できます。
偽の配列で配列メソッドを呼び出します。
> var fakeArray = { 0: 'a', 1: 'b', length: 2 }; > Array.prototype.join.call(fakeArray, '-') 'a-b'
配列メソッドが配列のように扱うオブジェクトをどのように変換するかを見てください。
> var obj = {}; > Array.prototype.push.call(obj, 'hello'); 1 > obj { '0': 'hello', length: 1 }
JavaScriptには、配列のように感じられるが、実際には配列ではないオブジェクトがいくつかあります。つまり、インデックス付きアクセスとlength
プロパティは持っているものの、配列メソッド(forEach()
、push
、concat()
など)は持っていません。これは残念なことですが、後で説明するように、ジェネリック配列メソッドを使用すると回避策が可能になります。配列のようなオブジェクトの例には、次のようなものがあります。
特別な変数arguments
(「インデックスによるすべてのパラメータ: 特殊な変数arguments」を参照)は、JavaScriptの基本的な部分であるため、重要な配列のようなオブジェクトです。arguments
は配列のように見えます。
> function args() { return arguments } > var arrayLike = args('a', 'b'); > arrayLike[0] 'a' > arrayLike.length 2
ただし、配列メソッドはどれも使用できません。
> arrayLike.join('-') TypeError: object has no method 'join'
これは、arrayLike
がArray
のインスタンスではないためです(Array.prototype
はプロトタイプチェーンにありません)。
> arrayLike instanceof Array false
ブラウザのDOMノードリスト。これは、document.getElementsBy*()
(たとえば、getElementsByTagName()
)、document.forms
などによって返されます。
> var elts = document.getElementsByTagName('h3'); > elts.length 3 > elts instanceof Array false
文字列も配列のようなものです。
> 'abc'[1] 'b' > 'abc'.length 3
「配列のような」という用語は、ジェネリック配列メソッドとオブジェクトの間の契約とみなすこともできます。オブジェクトは特定の要件を満たす必要があります。そうしないと、メソッドはオブジェクトで機能しません。要件は次のとおりです。
配列のようなオブジェクトの要素は、角かっこ([])と0から始まる整数インデックスを使用してアクセスできる必要があります。すべてのメソッドには読み取りアクセスが必要であり、一部のメソッドには追加で書き込みアクセスが必要です。すべてのオブジェクトがこの種のインデックス作成をサポートしていることに注意してください。角かっこ内のインデックスは文字列に変換され、プロパティ値を検索するためのキーとして使用されます。
> var obj = { '0': 'abc' }; > obj[0] 'abc'
length
プロパティを持っている必要があり、その値は要素の数になります。一部のメソッドでは、length
が可変である必要があります(たとえば、reverse()
)。長さが不変の値(たとえば、文字列)は、これらのメソッドでは使用できません。次のパターンは、配列のようなオブジェクトを扱うのに役立ちます。
配列のようなオブジェクトを配列に変換します。
var
arr
=
Array
.
prototype
.
slice
.
call
(
arguments
);
引数なしのメソッドslice()
(連結、スライス、結合(非破壊的)を参照)は、配列のようなレシーバーのコピーを作成します。
var
copy
=
[
'a'
,
'b'
].
slice
();
配列のようなオブジェクトのすべての要素を反復処理するには、単純なfor
ループを使用できます。
function
logArgs
()
{
for
(
var
i
=
0
;
i
<
arguments
.
length
;
i
++
)
{
console
.
log
(
i
+
'. '
+
arguments
[
i
]);
}
}
ただし、Array.prototype.forEach()
を借用することもできます。
function
logArgs
()
{
Array
.
prototype
.
forEach
.
call
(
arguments
,
function
(
elem
,
i
)
{
console
.
log
(
i
+
'. '
+
elem
);
});
}
どちらの場合も、インタラクションは次のようになります。
> logArgs('hello', 'world'); 0. hello 1. world
以下のリストには、ECMAScript言語仕様で言及されているすべてのジェネリックメソッドが含まれています。
Array.prototype
(配列プロトタイプメソッドを参照)
concat
every
filter
forEach
indexOf
join
lastIndexOf
map
pop
push
reduce
reduceRight
reverse
shift
slice
some
sort
splice
toLocaleString
toString
unshift
Date.prototype
(Dateプロトタイプメソッドを参照)
toJSON
Object.prototype
(すべてのオブジェクトのメソッドを参照)
Object
メソッドは自動的にジェネリックです。これらはすべてのオブジェクトに対して機能する必要があります。)
String.prototype
(Stringプロトタイプメソッドを参照)
charAt
charCodeAt
concat
indexOf
lastIndexOf
localeCompare
match
replace
search
slice
split
substring
toLocaleLowerCase
toLocaleUpperCase
toLowerCase
toUpperCase
trim
JavaScript にはマップのための組み込みデータ構造がないため、オブジェクトが文字列から値へのマップとしてよく使用されます。残念ながら、それは見た目よりもエラーが発生しやすいものです。このセクションでは、このタスクに関わる3つの落とし穴について説明します。
プロパティを読み取る操作は、2種類に分けられます。
オブジェクトをマップとして解釈してエントリを読み取る際には、これらの操作の種類を慎重に選択する必要があります。理由を見るために、次の例を考えてみましょう。
var
proto
=
{
protoProp
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
obj
.
ownProp
=
'b'
;
obj
は1つの自身のプロパティを持つオブジェクトで、そのプロトタイプは proto
です。 proto
も1つの自身のプロパティを持っています。proto
は、オブジェクトリテラルで作成されたすべてのオブジェクトと同様に、プロトタイプ Object.prototype
を持ちます。したがって、obj
は proto
と Object.prototype
の両方からプロパティを継承します。
私たちは obj
を、単一のエントリを持つマップとして解釈したいと考えています。
ownProp: 'b'
つまり、継承されたプロパティを無視し、自身のプロパティのみを考慮したいのです。どの読み取り操作がこのように obj
を解釈し、どれがそうでないかを見てみましょう。オブジェクトをマップとして使用する場合、通常、変数に格納された任意のプロパティキーを使用したいことに注意してください。これはドット記法を排除します。
in
演算子は、オブジェクトが指定されたキーを持つプロパティを持っているかどうかを確認しますが、継承されたプロパティも考慮します。
> 'ownProp' in obj // ok true > 'unknown' in obj // ok false > 'toString' in obj // wrong, inherited from Object.prototype true > 'protoProp' in obj // wrong, inherited from proto true
継承されたプロパティを無視するチェックが必要です。hasOwnProperty()
は私たちが望むことを実行します。
> obj.hasOwnProperty('ownProp') // ok true > obj.hasOwnProperty('unknown') // ok false > obj.hasOwnProperty('toString') // ok false > obj.hasOwnProperty('protoProp') // ok false
マップとしての obj
の解釈に従いながら、obj
のすべてのキーを見つけるために使用できる操作は何でしょうか? for-in
が機能するかもしれません。しかし、残念ながらそうではありません。
> for (propKey in obj) console.log(propKey) ownProp protoProp
継承された列挙可能なプロパティを考慮します。ここに Object.prototype
のプロパティが表示されない理由は、それらがすべて非列挙可能であるためです。
対照的に、Object.keys()
は自身のプロパティのみをリストします。
> Object.keys(obj) [ 'ownProp' ]
このメソッドは、列挙可能な自身のプロパティのみを返します。ownProp
は代入によって追加されたため、デフォルトで列挙可能です。すべての自身のプロパティをリストしたい場合は、Object.getOwnPropertyNames()
を使用する必要があります。
プロパティの値を読み取るために、ドット演算子とブラケット演算子のどちらかしか選択できません。前者は、変数に格納された任意のキーがあるため使用できません。そのため、継承されたプロパティを考慮するブラケット演算子を使用することになります。
> obj['toString'] [Function: toString]
これは私たちが望むものではありません。自身のプロパティのみを読み取るための組み込み操作はありませんが、自分で簡単に実装できます。
function
getOwnProperty
(
obj
,
propKey
)
{
// Using hasOwnProperty() in this manner is problematic
// (explained and fixed later)
return
(
obj
.
hasOwnProperty
(
propKey
)
?
obj
[
propKey
]
:
undefined
);
}
この関数を使用すると、継承されたプロパティ toString
は無視されます。
> getOwnProperty(obj, 'toString') undefined
関数 getOwnProperty()
は、obj
でメソッド hasOwnProperty()
を呼び出しました。通常、それは問題ありません。
> getOwnProperty({ foo: 123 }, 'foo') 123
ただし、キーが hasOwnProperty
であるプロパティを obj
に追加すると、そのプロパティはメソッド Object.prototype.hasOwnProperty()
をオーバーライドし、getOwnProperty()
は動作しなくなります。
> getOwnProperty({ hasOwnProperty: 123 }, 'foo') TypeError: Property 'hasOwnProperty' is not a function
この問題を解決するには、hasOwnProperty()
を直接参照します。これにより、obj
を介してそれを検索することを回避できます。
function
getOwnProperty
(
obj
,
propKey
)
{
return
(
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
propKey
)
?
obj
[
propKey
]
:
undefined
);
}
hasOwnProperty()
をジェネリックに呼び出しました (「ジェネリックメソッド: プロトタイプからメソッドを借りる」を参照)。
多くの JavaScript エンジンでは、プロパティ __proto__
(「特殊なプロパティ __proto__」を参照) は特別です。取得するとオブジェクトのプロトタイプを取得し、設定するとオブジェクトのプロトタイプを変更します。 これが、オブジェクトがキーが '__proto__'
であるプロパティにマップデータを格納できない理由です。マップキー '__proto__'
を許可する場合は、プロパティキーとして使用する前にエスケープする必要があります。
function
get
(
obj
,
key
)
{
return
obj
[
escapeKey
(
key
)];
}
function
set
(
obj
,
key
,
value
)
{
obj
[
escapeKey
(
key
)]
=
value
;
}
// Similar: checking if key exists, deleting an entry
function
escapeKey
(
key
)
{
if
(
key
.
indexOf
(
'__proto__'
)
===
0
)
{
// (1)
return
key
+
'%'
;
}
else
{
return
key
;
}
}
競合を避けるために、'__proto__'
のエスケープされたバージョン (など) もエスケープする必要があります。つまり、キー '__proto__'
を '__proto__%'
としてエスケープする場合、'__proto__'
エントリを置き換えないように、キー '__proto__%'
もエスケープする必要があります。それが (1) 行で起こることです。
Mark S. Miller は、メールで、この落とし穴が現実世界に及ぼす影響について述べています。
この演習は学術的であり、現実のシステムでは発生しないと思いますか?サポートスレッドで観察されたように、最近まで、すべての非IEブラウザで、新しい Google ドキュメントの先頭に「__proto__」と入力すると、Google ドキュメントがハングしていました。これは、オブジェクトを文字列マップとして使用するこのようなバグのある使用法に起因していました。
次のようにして、プロトタイプのないオブジェクトを作成します。
var
dict
=
Object
.
create
(
null
);
このようなオブジェクトは、通常のオブジェクトよりも優れたマップ (辞書) であり、そのため、このパターンはdict パターン (辞書のためのdict) と呼ばれることもあります。まず、通常のオブジェクトを調べてから、プロトタイプのないオブジェクトがより優れたマップである理由を見つけてみましょう。
通常、JavaScript で作成する各オブジェクトは、少なくともプロトタイプチェーンに Object.prototype
を持っています。Object.prototype
のプロトタイプは null
であるため、それがほとんどのプロトタイプチェーンが終わる場所です。
> Object.getPrototypeOf({}) === Object.prototype true > Object.getPrototypeOf(Object.prototype) null
プロトタイプのないオブジェクトには、マップとして2つの利点があります。
in
演算子を自由に使い、プロパティを読み取るためにブラケットを使用できるようになりました。__proto__
は無効になります。ECMAScript 6 では、オブジェクトのプロトタイプチェーンに Object.prototype
がない場合、特殊なプロパティ __proto__
は無効になります。JavaScript エンジンは徐々にこの動作に移行すると予想されますが、まだ一般的ではありません。唯一の欠点は、Object.prototype
によって提供されるサービスが失われることです。たとえば、dict オブジェクトは自動的に文字列に変換できなくなります。
> console.log('Result: '+obj) TypeError: Cannot convert object to primitive value
しかし、それは実際には欠点ではありません。dict オブジェクトでメソッドを直接呼び出すのはいずれにしても安全ではないからです。
クイックハックやライブラリの基礎として、dict パターンを使用します。(非ライブラリ) 本番コードでは、すべての落とし穴を回避できるため、ライブラリが推奨されます。次のセクションでは、そのようなライブラリをいくつか紹介します。
オブジェクトをマップとして使用するアプリケーションは多数あります。すべてのプロパティキーが静的に (開発時に) わかっている場合は、継承を無視し、自身のプロパティのみを調べるようにする必要があります。任意のキーを使用できる場合は、このセクションで説明した落とし穴を避けるためにライブラリを使用する必要があります。次に2つの例を示します。
このセクションは、より詳しい説明へのポインターを含む、クイックリファレンスです。
オブジェクトリテラル (「オブジェクトリテラル」を参照)
var
jane
=
{
name
:
'Jane'
,
'not an identifier'
:
123
,
describe
:
function
()
{
// method
return
'Person named '
+
this
.
name
;
},
};
// Call a method:
console
.
log
(
jane
.
describe
());
// Person named Jane
ドット演算子 (.) (「ドット演算子 (.): 固定キーによるプロパティへのアクセス」を参照)
obj
.
propKey
obj
.
propKey
=
value
delete
obj
.
propKey
ブラケット演算子 ([]) (「ブラケット演算子 ([]): 計算されたキーによるプロパティへのアクセス」を参照)
obj
[
'propKey'
]
obj
[
'propKey'
]
=
value
delete
obj
[
'propKey'
]
プロトタイプの取得と設定 (「プロトタイプの取得と設定」を参照)
Object
.
create
(
proto
,
propDescObj
?
)
Object
.
getPrototypeOf
(
obj
)
プロパティの反復と検出 (「プロパティの反復と検出」を参照)
Object
.
keys
(
obj
)
Object
.
getOwnPropertyNames
(
obj
)
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
propKey
)
propKey
in
obj
記述子によるプロパティの取得と定義 (「記述子によるプロパティの取得と定義」を参照)
Object
.
defineProperty
(
obj
,
propKey
,
propDesc
)
Object
.
defineProperties
(
obj
,
propDescObj
)
Object
.
getOwnPropertyDescriptor
(
obj
,
propKey
)
Object
.
create
(
proto
,
propDescObj
?
)
Object
.
preventExtensions
(
obj
)
Object
.
isExtensible
(
obj
)
Object
.
seal
(
obj
)
Object
.
isSealed
(
obj
)
Object
.
freeze
(
obj
)
Object
.
isFrozen
(
obj
)
すべてのオブジェクトのメソッド (「すべてのオブジェクトのメソッド」を参照)
Object
.
prototype
.
toString
()
Object
.
prototype
.
valueOf
()
Object
.
prototype
.
toLocaleString
()
Object
.
prototype
.
isPrototypeOf
(
obj
)
Object
.
prototype
.
hasOwnProperty
(
key
)
Object
.
prototype
.
propertyIsEnumerable
(
propKey
)