第17章 オブジェクトと継承
目次
書籍を購入する
(広告です。ブロックしないでください。)

第17章 オブジェクトと継承

JavaScriptにおけるオブジェクト指向プログラミング(OOP)には、いくつかの階層があります。

新しい階層は前の階層にのみ依存しているため、JavaScript OOPを段階的に学習できます。階層1と2はシンプルなコアを形成しており、階層3と4が複雑になって混乱したときは、いつでもこのコアを参照できます。

第1層:単一オブジェクト

大まかに言えば、JavaScriptのすべてのオブジェクトは、文字列から値へのマップ(辞書)です。オブジェクト内の(キー、値)のエントリーは、プロパティと呼ばれます。プロパティのキーは常にテキスト文字列です。プロパティの値は、関数を含む任意のJavaScriptの値にすることができます。メソッドは、値が関数であるプロパティです。

オブジェクトリテラル

JavaScriptのオブジェクトリテラルを使用すると、プレーンオブジェクトObjectの直接インスタンス)を直接作成できます。次のコードでは、オブジェクトリテラルを使用して、オブジェクトを変数janeに割り当てています。オブジェクトには、namedescribeの2つのプロパティがあります。describeはメソッドです。

var jane = {
    name: 'Jane',

    describe: function () {
        return 'Person named '+this.name;  // (1)
    },  // (2)
};
  1. メソッド内でthisを使用して、現在のオブジェクトを参照します。(メソッド呼び出しのレシーバーとも呼ばれます)。
  2. ECMAScript 5では、オブジェクトリテラルで末尾のカンマ(最後のプロパティの後)を使用できます。残念ながら、古いブラウザではすべてがサポートされているわけではありません。末尾のカンマは、どのプロパティが最後であるかを気にせずにプロパティを並べ替えることができるため便利です。

オブジェクトは、文字列から値への単なるマップであるという印象を受けるかもしれません。しかし、オブジェクトはそれ以上のものです。オブジェクトは真の汎用オブジェクトです。たとえば、オブジェクト間で継承を使用したり(「第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の戻り値

プロパティが自身のプロパティであるが削除できない場合、deletefalseを返します。それ以外の場合はすべて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]

異常なプロパティキー

予約語(varfunctionなど)を変数名として使用することはできませんが、プロパティキーとして使用することはできます。

> 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()は、関数として(コンストラクターとしてではなく)使用すると、その機能を提供します。これにより、次の結果が生成されます。

結果

(パラメーターなしで呼び出された場合)

{}

undefined

{}

null

{}

ブール値bool

new Boolean(bool)

数値num

new Number(num)

文字列str

new String(str)

オブジェクトobj

obj(変更なし。変換するものがない)

次に例をいくつか示します。

> 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は常に(暗黙的な)パラメーターになります。

非厳格モードの通常の関数

通常の関数は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 の値は、メソッド呼び出しのレシーバーと呼ばれます。

this を設定しながら関数を呼び出す:call()、apply()、bind()

関数もオブジェクトであることを忘れないでください。したがって、各関数は独自のメソッドを持っています。このセクションでは、関数呼び出しを支援する3つのメソッドを紹介します。これらの3つのメソッドは、関数呼び出しのいくつかの落とし穴を回避するために、以下のセクションで使用されます。以下の例はすべて、次のオブジェクト jane を参照しています。

var jane = {
    name: 'Jane',
    sayHelloTo: function (otherName) {
        'use strict';
        console.log(this.name+' says hello to '+otherName);
    }
};

Function.prototype.call(thisValue, arg1?, arg2?, ...)

最初のパラメータは、呼び出される関数内で this が持つ値です。残りのパラメータは、呼び出される関数への引数として渡されます。次の3つの呼び出しは同等です。

jane.sayHelloTo('Tarzan');

jane.sayHelloTo.call(jane, 'Tarzan');

var func = jane.sayHelloTo;
func.call(jane, 'Tarzan');

2回目の呼び出しでは、call() は呼び出された関数をどのように取得したかを知らないため、jane を繰り返す必要があります。

Function.prototype.apply(thisValue, argArray)

最初のパラメータは、呼び出される関数内で this が持つ値です。2番目のパラメータは、呼び出しの引数を提供する配列です。次の3つの呼び出しは同等です。

jane.sayHelloTo('Tarzan');

jane.sayHelloTo.apply(jane, ['Tarzan']);

var func = jane.sayHelloTo;
func.apply(jane, ['Tarzan']);

2回目の呼び出しでは、apply() は呼び出された関数をどのように取得したかを知らないため、jane を繰り返す必要があります。

コンストラクターのためのapply()では、コンストラクターで apply() を使用する方法を説明します。

Function.prototype.bind(thisValue, arg1?, ..., argN?)

このメソッドは、部分関数適用を実行します。つまり、次の方法で 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();

コンストラクターのためのapply()

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() は機能しません。関数またはメソッドの呼び出しには役立ちますが、コンストラクターの呼び出しには役立たないためです。

コンストラクターのapply()を手動でシミュレートする

2つのステップapply() をシミュレートできます。

ステップ1

(まだ配列ではない)メソッド呼び出しを介して引数を Date に渡します。

new (Date.bind(null, 2011, 11, 24))

上記のコードでは、bind() を使用して、パラメーターなしでコンストラクターを作成し、new 経由で呼び出します。

ステップ2

apply() を使用して、配列を bind() に渡します。bind() はメソッド呼び出しであるため、apply() を使用できます。

new (Function.prototype.bind.apply(
         Date, [null, 2011, 11, 24]))

上記の配列には、arr の要素の後に null が含まれています。 concat() を使用して、nullarr の先頭に追加して作成できます。

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 を設定しません。

落とし穴:メソッドを抽出すると this が失われる

オブジェクトからメソッドを抽出すると、再び真の関数になります。オブジェクトとの接続が切断され、通常は正常に機能しなくなります。たとえば、次のオブジェクト 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 を呼び出すと、thisundefined になり、エラーが発生するためです。

メソッドを適切に抽出する方法

bind() のおかげで、inccounter との接続を失わないようにすることができます。

> 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() を呼び出すたびに、新しい関数が作成されます。これは、コールバックを登録および登録解除する場合(たとえば、イベント処理の場合)に影響します。登録した値をどこかに保存し、登録解除にも使用する必要があります。

落とし穴:メソッド内の関数が this をシャドウする

関数はパラメーター(例:コールバック)になり、関数式を介してインプレースで作成できるため、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つあります。

回避策1:that = this

ネストされた関数内でシャドウされない変数に 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

回避策2:bind()

bind() を使用して、コールバックthis の固定値、つまり、メソッドの this (行 (1)) を指定できます。

loop: function () {
    'use strict';
    this.friends.forEach(function (friend) {
        console.log(this.name+' knows '+friend);
    }.bind(this));  // (1)
}

回避策3:forEach() の thisValue

forEach()検査メソッドを参照)に固有の回避策は、コールバックの後にコールバックの this になる2番目のパラメーターを指定することです。

loop: function () {
    'use strict';
    this.friends.forEach(function (friend) {
        console.log(this.name+' knows '+friend);
    }, this);
}

レイヤー2:オブジェクト間のプロトタイプ関係

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() の内部では、thisobj になり、メソッドが obj.name にアクセスできるようになります。

オーバーライド

プロトタイプチェーンでは、オブジェクト内のプロパティは、後続のオブジェクトにある同じキーを持つプロパティを オーバーライド します。前者のプロパティが最初に見つかります。後者のプロパティは隠され、アクセスできなくなります。例として、obj 内でメソッド proto.describe() をオーバーライドしてみましょう。

> obj.describe = function () { return 'overridden' };
> obj.describe()
'overridden'

これは、クラスベースの言語でのメソッドのオーバーライドの動作と似ています。

プロトタイプを介したオブジェクト間でのデータ共有

プロトタイプは、オブジェクト間でデータを共有するのに最適です。複数のオブジェクトが同じプロトタイプを取得し、そのプロトタイプにすべての共有プロパティが保持されます。例を見てみましょう。オブジェクト janetarzan は両方とも同じメソッド 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 を共有するオブジェクト janetarzan を作成します。

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 を汎用的に呼び出しました(汎用メソッド:プロトタイプからメソッドを借りるを参照)。

特殊なプロパティ __proto__

一部の 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);

objproto から 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.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 には、自身のプロパティ protoEnumTrueprotoEnumFalse があり、obj には、自身のプロパティ objEnumTrueobjEnumFalse があります(また、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

オブジェクトの自身のプロパティの数を計算する

オブジェクトにはlengthsize のようなメソッドがないため、次の回避策を使用する必要があります。

Object.keys(obj).length

ベストプラクティス:自身のプロパティを反復処理する

プロパティキーを反復処理するには:

  • for-in で説明されているように、for-inhasOwnProperty() を組み合わせます。これは、古い 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);
    });

プロパティ値または(キー、値)ペアを反復処理するには

  • キーを反復処理し、各キーを使用して対応する値を取得します。他の言語ではこれがより簡単ですが、JavaScript ではそうではありません。

アクセサ(ゲッターとセッター)

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]]

undefined

[[Get]]

undefined

[[Set]]

undefined

[[Writable]]

false

[[Enumerable]]

false

[[Configurable]]

false

これらのデフォルトは、プロパティ記述子を使用してプロパティを作成している場合に重要です(次のセクションを参照)。

プロパティ記述子

プロパティ記述子は、属性をプログラムで操作するためのデータ構造です。これは、プロパティの属性をエンコードするオブジェクトです。記述子の各プロパティは、属性に対応します。たとえば、以下は、値が 123 の読み取り専用プロパティの記述子です。

{
    value: 123,
    writable: false,
    enumerable: true,
    configurable: false
}

アクセサを使用して同じ目標、つまり不変性を達成できます。この場合、記述子は次のようになります。

{
    get: function () { return 123 },
    enumerable: true,
    configurable: false
}

プロパティ記述子は、次の 2 種類の操作に使用されます。

プロパティの取得
プロパティのすべての属性が記述子として返されます。
プロパティの定義

プロパティを定義することは、プロパティが既に存在するかどうかによって意味が異なります。

  • プロパティが存在しない場合は、記述子で指定された属性を持つ新しいプロパティを作成します。属性に記述子に対応するプロパティがない場合は、デフォルト値を使用します。デフォルトは、属性名の意味によって決まります。これらは、代入によってプロパティを作成するときに使用される値の反対です(この場合、プロパティは書き込み可能、列挙可能、設定可能です)。 例:

    > 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 つのことを正しく行う必要があります。

  1. コピーは、元のオブジェクトと同じプロトタイプを持つ必要があります(レイヤー 2:オブジェクト間のプロトタイプ関係を参照)。
  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;
};

これらは、関連する手順です。

  1. source のすべての自身のプロパティのキーを持つ配列を取得します。
  2. それらのキーを反復処理します。
  3. プロパティ記述子を取得します。
  4. そのプロパティ記述子を使用して、target に自身のプロパティを作成します。

この関数は、Underscore.js ライブラリの関数 _.extend() に非常に似ていることに注意してください。

プロパティ:定義と代入の比較

次の 2 つの操作は非常に似ています。

ただし、いくつかの微妙な違いがあります。

継承された読み取り専用プロパティには代入できない

オブジェクト 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 ループは避ける必要があります(ベストプラクティス:配列の反復処理)。
  • 通常、組み込みプロトタイプとオブジェクトにプロパティを追加すべきではありません。しかし、もし追加する場合は、既存のコードを壊さないように、列挙不可にする必要があります。

オブジェクトの保護

オブジェクトを保護するには、次の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)

これにより、すべてのプロパティが書き込み不可になり、obj がシールされます。言い換えれば、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 を持っており、これも可変です。

レイヤー 3:コンストラクター—インスタンスのファクトリー

コンストラクター関数(略して コンストラクター)は、ある意味で類似したオブジェクトを生成するのに役立ちます。これは通常の関数ですが、名前が付けられ、セットアップされ、異なる方法で呼び出されます。このセクションでは、コンストラクターの仕組みについて説明します。これらは他の言語のクラスに対応します。

すでに、(プロトタイプを介したオブジェクト間でのデータの共有で)類似した2つのオブジェクトの例を見てきました。

var PersonProto = {
    describe: function () {
        return 'Person named '+this.name;
    }
};
var jane = {
    [[Prototype]]: PersonProto,
    name: 'Jane'
};
var tarzan = {
    [[Prototype]]: PersonProto,
    name: 'Tarzan'
};

オブジェクト janetarzan はどちらも「person」と見なされ、プロトタイプオブジェクト PersonProto を共有します。このプロトタイプを、janetarzan のようなオブジェクトを作成するコンストラクター Person に変換してみましょう。コンストラクターが作成するオブジェクトは、そのインスタンスと呼ばれます。このようなインスタンスは、janetarzan と同じ構造を持ち、次の2つの部分で構成されます。

  1. データはインスタンス固有であり、インスタンスオブジェクトの自身のプロパティに格納されます(前の例では janetarzan)。
  2. 動作はすべてのインスタンスで共有されます。つまり、メソッドを持つ共通のプロトタイプオブジェクト(前の例では 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

JavaScript で実装された new 演算子

手動で 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 演算子の結果にすることができます。これは、コンストラクターにサブコンストラクターのインスタンスを返させたい場合に便利です(コンストラクターから任意のオブジェクトを返すで例を示します)。

用語:2つのプロトタイプ

残念ながら、プロトタイプという用語は、JavaScript であいまいな方法で使用されています。

プロトタイプ1:プロトタイプの関係

オブジェクトは、別のオブジェクトのプロトタイプになることができます。

> var proto = {};
> var obj = Object.create(proto);
> Object.getPrototypeOf(obj) === proto
true

前の例では、protoobj のプロトタイプです。

プロトタイプ2:プロパティ prototype の値

各コンストラクター C には、オブジェクトを参照する prototype プロパティがあります。そのオブジェクトは、C のすべてのインスタンスのプロトタイプになります。

> function C() {}
> Object.getPrototypeOf(new C()) === C.prototype
true

通常、文脈からどちらのプロトタイプが意図されているかが明らかになります。あいまいさを解消する必要がある場合は、オブジェクト間の関係を記述するために、プロトタイプを使用する必要があります。これは、その名前が getPrototypeOfisPrototypeOf を介して標準ライブラリに入っているためです。したがって、prototype プロパティによって参照されるオブジェクトには別の名前を見つける必要があります。1つの可能性は コンストラクタープロトタイプ ですが、コンストラクターにもプロトタイプがあるため、問題があります。

> function Foo() {}
> Object.getPrototypeOf(Foo) === Function.prototype
true

したがって、インスタンスプロトタイプが最適なオプションです。

インスタンスの constructor プロパティ

デフォルトでは、各関数 C には、プロパティ constructorC を指すインスタンスプロトタイプオブジェクト C.prototype が含まれています。

> function C() {}
> C.prototype.constructor === C
true

constructor プロパティはプロトタイプから各インスタンスに継承されるため、それを使用してインスタンスのコンストラクターを取得できます。

> var o = new C();
> o.constructor
[Function: C]

constructor プロパティのユースケース

オブジェクトのコンストラクターを切り替える

次の 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)行の呼び出しが機能するのは、extendSub.superclassSuper.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演算子

instanceof演算子は

value instanceof Constr

valueがコンストラクタConstrまたはサブコンストラクタによって作成されたかどうかを判断します。これは、Constr.prototypevalueのプロトタイプチェーンにあるかどうかをチェックすることで行います。したがって、次の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のインスタンスです。これは、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になります。したがって、instanceofmyvarのプロトタイプチェーンに現在のレルムの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に属していることをマークします。これを行うにはいくつかの方法があります。valueTのインスタンスかどうかをチェックするには、次のようになります

    • value.isT()Tインスタンスのプロトタイプは、このメソッドからtrueを返す必要があります。共通のスーパーコンストラクタは、デフォルト値のfalseを返す必要があります。
    • 'T' in valueTインスタンスのプロトタイプに、キーが'T'(またはより固有のもの)であるプロパティでタグ付けする必要があります。
    • value.TYPE_NAME === 'T':関連するすべてのプロトタイプには、適切な値を持つTYPE_NAMEプロパティが必要です。

このセクションでは、コンストラクタを実装するためのいくつかのヒントを示します。

newを忘れることに対する保護:厳格モード

コンストラクタを使用するときに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

コンストラクタから任意のオブジェクトを返す

多くのオブジェクト指向言語では、コンストラクタは直接インスタンスのみを生成できます。例えば、Javaを考えてみましょう:AdditionMultiplicationのサブクラスを持つクラスExpressionを実装するとします。パースは後者の2つのクラスの直接インスタンスを生成します。これをExpressionのコンストラクタとして実装することはできません。そのコンストラクタはExpressionの直接インスタンスのみを生成できるためです。回避策として、Javaでは静的ファクトリメソッドが使用されます。

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を継承します。

このアプローチはほとんどの場合機能します。インスタンスnNamesを作成できます。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のおかげで、ConstrAConstrBのインスタンスを、それらがレルムをまたいでいる場合でも区別できます(この場合、instanceofは機能しません。 「落とし穴:レルム(フレームまたはウィンドウ)をまたぐ」を参照してください)。

データのプライベート化

JavaScriptには、オブジェクトのプライベートデータを管理するための専用の手段はありません。このセクションでは、その制限を回避するための3つの手法について説明します。

  • コンストラクタの環境におけるプライベートデータ
  • マークされたキーを持つプロパティにおけるプライベートデータ
  • 具象化されたキーを持つプロパティにおけるプライベートデータ

さらに、IIFEを介してグローバルデータをプライベートに保つ方法について説明します。

コンストラクタの環境におけるプライベートデータ(Crockfordプライバシーパターン)

コンストラクタが呼び出されると、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(...);
    }
    ...
}

以下は、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プライバシーパターンの長所と短所

Crockfordプライバシーパターンを使用する場合に検討すべきいくつかの点を示します。

あまりエレガントではない
特権メソッドを介してプライベートデータへのアクセスを仲介すると、不必要な間接性が導入されます。特権メソッドとプライベート関数はどちらも、コンストラクタ(インスタンスデータを設定する)とインスタンスプロトタイプ(メソッド)の間の関心の分離を破壊します。
完全に安全
外部から環境のデータにアクセスする方法はありません。そのため、このソリューションは、(たとえば、セキュリティクリティカルなコードの場合)必要な場合に安全です。一方で、プライベートデータが外部からアクセスできないことも不便になる可能性があります。場合によっては、プライベート機能を単体テストする必要があるでしょう。また、一時的なクイックフィックスの中には、プライベートデータへのアクセス機能に依存するものもあります。この種のクイックフィックスは予測できないため、設計がどれほど優れていても、その必要性が生じる可能性があります。
遅くなる可能性がある
プロトタイプチェーン内のプロパティへのアクセスは、現在のJavaScriptエンジンで高度に最適化されています。クロージャ内の値へのアクセスは遅くなる可能性があります。しかし、これらのことは常に変化しているため、コードにとって本当に重要である場合は、測定する必要があります。
メモリを多く消費する
環境を維持し、特権メソッドをインスタンスに配置すると、メモリが消費されます。繰り返しますが、コードにとって本当に重要であることを確認し、測定してください。

マークされたキーを持つプロパティにおけるプライベートデータ

ほとんどのセキュリティクリティカルでないアプリケーションの場合、プライバシーは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('');
    }
};

以下に、マークされたプロパティキーによるプライバシーの長所と短所をいくつか示します。

より自然なコーディングスタイルを提供する
プライベートデータとパブリックデータに同じ方法でアクセスできることは、プライバシーのために環境を使用するよりもエレガントです。
プロパティの名前空間を汚染する
マークされたキーを持つプロパティは、どこでも表示できます。より多くの人がIDEを使用するほど、表示されるべきではない場所でパブリックプロパティと一緒に表示されることが迷惑になります。理論的には、IDEは命名規則を認識し、可能な場合はプライベートプロパティを非表示にすることで適応できます。
プライベートプロパティは「外部」からアクセスできる
これは、単体テストやクイックフィックスに役立ちます。さらに、サブコンストラクタとヘルパー関数(いわゆる「フレンド関数」)は、プライベートデータへのアクセスが容易になるという利点があります。環境アプローチでは、この種の柔軟性は提供されません。プライベートデータはコンストラクタ内からのみアクセスできます。
キーの衝突につながる可能性がある
プライベートプロパティのキーが衝突する可能性があります。これはすでにサブコンストラクタの問題ですが、(一部のライブラリで有効になっている)多重継承を使用する場合はさらに問題になります。環境アプローチでは、衝突は決して発生しません。

具象化されたキーを持つプロパティにおけるプライベートデータ

プライベートプロパティの命名規則の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は新しい環境を作成し(「環境:変数の管理」を参照してください)、そこにプライベートデータを配置します。

プライベートグローバルデータをシングルトンオブジェクトにアタッチする

環境内のプライベートデータをオブジェクトに関連付けるために、コンストラクタは必要ありません。次の例は、同じ目的で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'

レイヤー4:コンストラクタ間の継承

このセクションでは、コンストラクタがどのように継承されるかを調べます。コンストラクタSuperがある場合、Superのすべての機能に加えて独自の機能も持つ新しいコンストラクタSubをどのように記述できるでしょうか? 残念ながら、JavaScriptにはこのタスクを実行するための組み込みメカニズムがありません。したがって、手動で作業を行う必要があります。

図17-5は、その考えを示しています。サブコンストラクタSubは、独自のプロパティに加えて、Superのすべてのプロパティ(プロトタイププロパティとインスタンスプロパティの両方)を持つ必要があります。したがって、Subがどのようなものになるかの大まかなアイデアはありますが、そこに到達する方法がわかりません。次に説明するいくつかのことを理解する必要があります。

  • インスタンスプロパティの継承。
  • プロトタイププロパティの継承。
  • instanceofが機能することを確認する:subSubのインスタンスである場合、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)
}

Subnewを介して呼び出されると、その暗黙的なパラメータthisは新しいインスタンスを参照します。最初に、そのインスタンスをSuper(1)に渡し、インスタンスプロパティを追加します。その後、Subは独自のインスタンスプロパティを設定します(2,3)。コツは、Supernewを介して呼び出さないことです。これは、新しいスーパーインスタンスを作成するためです。代わりに、Superを関数として呼び出し、現在の(サブ)インスタンスをthisの値として渡します。

プロトタイププロパティの継承

メソッドなどの共有プロパティは、インスタンスプロトタイプに保持されます。 したがって、Sub.prototypeSuper.prototypeのすべてのプロパティを継承する方法を見つける必要があります。解決策は、Sub.prototypeにプロトタイプSuper.prototypeを与えることです。

2種類のプロトタイプに混乱していませんか?

はい、ここでの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は、SubSuperがどのように関連付けられているかを示しています。Subの構造は、図17-5でスケッチしたものに似ています。図にはインスタンスプロパティは表示されていません。これらは図で説明されている関数呼び出しによって設定されます。

instanceofが機能することを確認する

instanceofが機能することを確認する」とは、SubのすべてのインスタンスがSuperのインスタンスでもある必要があることを意味します。図17-7は、SubのインスタンスであるsubInstanceのプロトタイプチェーンがどのように見えるかを示しています。その最初のプロトタイプはSub.prototypeであり、その2番目のプロトタイプはSuper.prototypeです。

まず、より簡単な質問から始めましょう。subInstanceSubのインスタンスですか?はい、そうです。これは、次の2つのアサーションが同等であるためです(後者は前者の定義と見なすことができます)。

subInstance instanceof Sub
Sub.prototype.isPrototypeOf(subInstance)

前述のように、Sub.prototypesubInstanceのプロトタイプの1つであるため、どちらのアサーションもtrueです。同様に、subInstanceSuperのインスタンスでもあります。これは、次の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つのステップが含まれます。

  1. 現在のメソッドのホームオブジェクトの「後」(プロトタイプ内)から検索を開始します。
  2. 名前がfooのメソッドを探します。
  3. 現在の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つの部分に分割すると、前述の手順が見つかります

  1. Super.prototypeSub.prototypeのプロトタイプ(現在のメソッドSub.prototype.methodBのホームオブジェクト)であるSuper.prototypeで検索を開始します。
  2. methodB:名前がmethodBのメソッドを探します。
  3. 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);

例:組み込みコンストラクタの継承階層

組み込みコンストラクタは、このセクションで説明したのと同じサブクラス化アプローチを使用します。 たとえば、ArrayObjectのサブコンストラクタです。したがって、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とみなされます(ブール値への変換を参照)。

Object.prototype.toLocaleString()

このメソッドは、オブジェクトのロケール固有の文字列表現を返します。デフォルトの実装では、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つの引数があります。

  1. chablisはメソッド呼び出しのレシーバーであり、this経由でincAgeに渡されます。
  2. 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とArray.prototypeへのアクセス

ジェネリックにメソッドを呼び出すのは非常に冗長です。

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()pushconcat()など)は持っていません。これは残念なことですが、後で説明するように、ジェネリック配列メソッドを使用すると回避策が可能になります。配列のようなオブジェクトの例には、次のようなものがあります。

  • 特別な変数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'

    これは、arrayLikeArrayのインスタンスではないためです(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言語仕様で言及されているすべてのジェネリックメソッドが含まれています。

落とし穴: オブジェクトをマップとして使用する

JavaScript にはマップのための組み込みデータ構造がないため、オブジェクトが文字列から値へのマップとしてよく使用されます。残念ながら、それは見た目よりもエラーが発生しやすいものです。このセクションでは、このタスクに関わる3つの落とし穴について説明します。

落とし穴1: 継承がプロパティの読み取りに影響する

プロパティを読み取る操作は、2種類に分けられます。

  • 一部の操作はプロトタイプチェーン全体を考慮し、継承されたプロパティを参照します。
  • 他の操作は、オブジェクトの自身の (継承されていない) プロパティのみにアクセスします。

オブジェクトをマップとして解釈してエントリを読み取る際には、これらの操作の種類を慎重に選択する必要があります。理由を見るために、次の例を考えてみましょう。

var proto = { protoProp: 'a' };
var obj = Object.create(proto);
obj.ownProp = 'b';

obj は1つの自身のプロパティを持つオブジェクトで、そのプロトタイプは proto です。 proto も1つの自身のプロパティを持っています。proto は、オブジェクトリテラルで作成されたすべてのオブジェクトと同様に、プロトタイプ Object.prototype を持ちます。したがって、objprotoObject.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

落とし穴2: オーバーライドがメソッドの呼び出しに影響する

関数 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() をジェネリックに呼び出しました (「ジェネリックメソッド: プロトタイプからメソッドを借りる」を参照)。

落とし穴3: 特殊なプロパティ __proto__

多くの 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 ドキュメントがハングしていました。これは、オブジェクトを文字列マップとして使用するこのようなバグのある使用法に起因していました。

dict パターン: プロトタイプのないオブジェクトはより優れたマップ

次のようにして、プロトタイプのないオブジェクトを作成します。

var dict = Object.create(null);

このようなオブジェクトは、通常のオブジェクトよりも優れたマップ (辞書) であり、そのため、このパターンはdict パターン (辞書のためのdict) と呼ばれることもあります。まず、通常のオブジェクトを調べてから、プロトタイプのないオブジェクトがより優れたマップである理由を見つけてみましょう。

通常のオブジェクト

通常、JavaScript で作成する各オブジェクトは、少なくともプロトタイプチェーンに Object.prototype を持っています。Object.prototype のプロトタイプは null であるため、それがほとんどのプロトタイプチェーンが終わる場所です。

> Object.getPrototypeOf({}) === Object.prototype
true
> Object.getPrototypeOf(Object.prototype)
null

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

プロトタイプのないオブジェクトには、マップとして2つの利点があります。

唯一の欠点は、Object.prototype によって提供されるサービスが失われることです。たとえば、dict オブジェクトは自動的に文字列に変換できなくなります。

> console.log('Result: '+obj)
TypeError: Cannot convert object to primitive value

しかし、それは実際には欠点ではありません。dict オブジェクトでメソッドを直接呼び出すのはいずれにしても安全ではないからです。

推奨事項

クイックハックやライブラリの基礎として、dict パターンを使用します。(非ライブラリ) 本番コードでは、すべての落とし穴を回避できるため、ライブラリが推奨されます。次のセクションでは、そのようなライブラリをいくつか紹介します。

チートシート: オブジェクトの操作

このセクションは、より詳しい説明へのポインターを含む、クイックリファレンスです。



[17] この方法で map() を使用することは、Brandon Benvie (@benvie) によるヒントです。

次へ: 18. 配列