...
) [ES2018]Object.assign()
[ES6]this
this
.call()
.bind()
this
の落とし穴:メソッドの抽出this
の落とし穴:this
の意図しないシャドウイングthis
の値(上級)in
演算子:指定されたキーを持つプロパティが存在するか?Object.keys()
などによるプロパティキーの列挙Object.values()
によるプロパティ値の列挙Object.entries()
によるプロパティエントリの列挙 [ES2017]Object.fromEntries()
によるオブジェクトの構築 [ES2019]Object.hasOwn()
:指定されたプロパティが固有(非継承)か? [ES2022]本書では、JavaScriptのオブジェクト指向プログラミング(OOP)のスタイルを4つのステップで紹介します。この章ではステップ1と2を、次の章ではステップ3と4を扱います。(図8参照)
オブジェクトリテラルによるオブジェクトの作成(波括弧で始まり、波括弧で終わる)
const myObject = { // object literal
myProperty: 1,
myMethod() {
return 2;
, // comma!
}myAccessor() {
get return this.myProperty;
, // comma!
}myAccessor(value) {
set this.myProperty = value;
, // last comma is optional
};
}
.equal(
assert.myProperty, 1
myObject;
).equal(
assert.myMethod(), 2
myObject;
).equal(
assert.myAccessor, 1
myObject;
).myAccessor = 3;
myObject.equal(
assert.myProperty, 3
myObject; )
クラスなしで直接オブジェクトを作成できることは、JavaScriptのハイライトの1つです。
オブジェクトへのスプレッド構文
const original = {
a: 1,
b: {
c: 3,
,
};
}
// Spreading (...) copies one object “into” another one:
const modifiedCopy = {
...original, // spreading
d: 4,
;
}
.deepEqual(
assert,
modifiedCopy
{a: 1,
b: {
c: 3,
,
}d: 4,
};
)
// Caveat: spreading copies shallowly (property values are shared)
.a = 5; // does not affect `original`
modifiedCopy.b.c = 6; // affects `original`
modifiedCopy.deepEqual(
assert,
original
{a: 1, // unchanged
b: {
c: 6, // changed
,
},
}; )
スプレッド構文を使用して、オブジェクトの変更されていない(シャロー)コピーを作成することもできます。
const exactCopy = {...obj};
プロトタイプはJavaScriptの基本的な継承メカニズムです。クラスもこれに基づいています。各オブジェクトは、null
またはオブジェクトをプロトタイプとして持ちます。後者のオブジェクトもプロトタイプを持つことができます。一般的に、プロトタイプのチェーンが得られます。
プロトタイプは次のように管理されます。
// `obj1` has no prototype (its prototype is `null`)
const obj1 = Object.create(null); // (A)
.equal(
assertObject.getPrototypeOf(obj1), null // (B)
;
)
// `obj2` has the prototype `proto`
const proto = {
protoProp: 'protoProp',
;
}const obj2 = {
__proto__: proto, // (C)
objProp: 'objProp',
}.equal(
assertObject.getPrototypeOf(obj2), proto
; )
注記
各オブジェクトは、そのプロトタイプのすべてのプロパティを継承します。
// `obj2` inherits .protoProp from `proto`
.equal(
assert.protoProp, 'protoProp'
obj2;
).deepEqual(
assertReflect.ownKeys(obj2),
'objProp'] // own properties of `obj2`
[; )
オブジェクトの非継承プロパティは、固有のプロパティと呼ばれます。
プロトタイプのもっとも重要なユースケースは、複数のオブジェクトが共通のプロトタイプから継承することでメソッドを共有できることです。
JavaScriptにおけるオブジェクト
JavaScriptでは、オブジェクトを2つの方法で使用できます。
固定レイアウトオブジェクト:この方法で使用すると、オブジェクトはデータベースのレコードのように機能します。固定数のプロパティを持ち、そのキーは開発時にわかっています。その値は一般的に異なる型を持ちます。
const fixedLayoutObject = {
product: 'carrot',
quantity: 4,
; }
ディクショナリオブジェクト:この方法で使用すると、オブジェクトはルックアップテーブルまたはマップのように機能します。可変数のプロパティを持ち、そのキーは開発時にわかっていません。その値はすべて同じ型を持ちます。
const dictionaryObject = {
'one']: 1,
['two']: 2,
[; }
2つの方法は混合することもできます。一部のオブジェクトは、固定レイアウトオブジェクトとディクショナリオブジェクトの両方です。
オブジェクトの使用方法はこの章での説明方法に影響します。
まず、固定レイアウトオブジェクトを調べましょう。
オブジェクトリテラルは、固定レイアウトオブジェクトを作成する1つの方法です。これはJavaScriptの際立った機能です。クラスなしで直接オブジェクトを作成できます!例を以下に示します。
const jane = {
first: 'Jane',
last: 'Doe', // optional trailing comma
; }
この例では、波括弧{}
で始まり、波括弧で終わるオブジェクトリテラルを使用してオブジェクトを作成しました。その中に、2つのプロパティ(キーと値のペア)を定義しました。
first
と値'Jane'
を持ちます。last
と値'Doe'
を持ちます。ES5以降、オブジェクトリテラルでは末尾のカンマが許可されています。
後でプロパティキーを指定する他の方法も見ますが、この方法では、JavaScriptの変数名の規則に従う必要があります。たとえば、first_name
をプロパティキーとして使用できますが、first-name
は使用できません。ただし、予約語は使用できます。
const obj = {
if: true,
const: true,
; }
オブジェクトに対する様々な操作の効果を確認するために、この章の後半ではObject.keys()
を時々使用します。これはプロパティキーを列挙します。
> Object.keys({a:1, b:2})[ 'a', 'b' ]
プロパティの値が、キーと同じ名前の変数によって定義されている場合は、キーを省略できます。
function createPoint(x, y) {
return {x, y}; // Same as: {x: x, y: y}
}.deepEqual(
assertcreatePoint(9, 2),
x: 9, y: 2 }
{ ; )
プロパティを取得(読み取る)する方法は次のとおりです(A行)。
const jane = {
first: 'Jane',
last: 'Doe',
;
}
// Get property .first
.equal(jane.first, 'Jane'); // (A) assert
不明なプロパティを取得するとundefined
になります。
.equal(jane.unknownProperty, undefined); assert
プロパティを設定(書き込む)する方法は次のとおりです(A行)。
const obj = {
prop: 1,
;
}.equal(obj.prop, 1);
assert.prop = 2; // (A)
obj.equal(obj.prop, 2); assert
設定によって既存のプロパティを変更しました。不明なプロパティを設定すると、新しいエントリが作成されます。
const obj = {}; // empty object
.deepEqual(
assertObject.keys(obj), []);
.unknownProperty = 'abc';
obj.deepEqual(
assertObject.keys(obj), ['unknownProperty']);
次のコードは、オブジェクトリテラルを使用してメソッド.says()
を作成する方法を示しています。
const jane = {
first: 'Jane', // value property
says(text) { // method
return `${this.first} says “${text}”`; // (A)
, // comma as separator (optional at end)
};
}.equal(jane.says('hello'), 'Jane says “hello”'); assert
メソッド呼び出しjane.says('hello')
の間、jane
はメソッド呼び出しのレシーバと呼ばれ、特別な変数this
に割り当てられます(this
の詳細については、§28.5 「メソッドと特別な変数this
」を参照)。これにより、メソッド.says()
はA行で兄弟プロパティ.first
にアクセスできます。
アクセサーは、メソッドのように見えるオブジェクトリテラル内の構文によって定義されます。ゲッターと/またはセッターです。(つまり、各アクセサーには、これらの一方または両方が含まれます)。
アクセサーの呼び出しは、値プロパティへのアクセスのように見えます。
ゲッターは、メソッド定義の前に修飾子get
を付けることで作成されます。
const jane = {
first: 'Jane',
last: 'Doe',
full() {
get return `${this.first} ${this.last}`;
,
};
}
.equal(jane.full, 'Jane Doe');
assert.first = 'John';
jane.equal(jane.full, 'John Doe'); assert
セッターは、メソッド定義の前に修飾子set
を付けることで作成されます。
const jane = {
first: 'Jane',
last: 'Doe',
full(fullName) {
set const parts = fullName.split(' ');
this.first = parts[0];
this.last = parts[1];
,
};
}
.full = 'Richard Roe';
jane.equal(jane.first, 'Richard');
assert.equal(jane.last, 'Roe'); assert
演習:オブジェクトリテラルによるオブジェクトの作成
exercises/objects/color_point_object_test.mjs
...
) [ES2018]オブジェクトリテラル内では、スプレッドプロパティによって別のオブジェクトのプロパティを現在のオブジェクトに追加します。
> const obj = {one: 1, two: 2};
> {...obj, three: 3}{ one: 1, two: 2, three: 3 }
const obj1 = {one: 1, two: 2};
const obj2 = {three: 3};
.deepEqual(
assert...obj1, ...obj2, four: 4},
{one: 1, two: 2, three: 3, four: 4}
{; )
プロパティキーが衝突する場合は、最後に記載されているプロパティが「勝ちます」。
> const obj = {one: 1, two: 2, three: 3};
> {...obj, one: true}{ one: true, two: 2, three: 3 }
> {one: true, ...obj}{ one: 1, two: 2, three: 3 }
undefined
やnull
も含め、すべての値はスプレッド可能です。
> {...undefined}{}
> {...null}{}
> {...123}{}
> {...'abc'}{ '0': 'a', '1': 'b', '2': 'c' }
> {...['a', 'b']}{ '0': 'a', '1': 'b' }
文字列と配列の.length
プロパティはこの種の操作からは隠されています(列挙可能ではありません。詳細は§28.8.1 「プロパティ属性とプロパティ記述子 [ES5]」を参照)。
スプレッドには、キーがシンボルであるプロパティも含まれます(Object.keys()
、Object.values()
、Object.entries()
では無視されます)。
const symbolKey = Symbol('symbolKey');
const obj = {
stringKey: 1,
: 2,
[symbolKey];
}.deepEqual(
assert...obj, anotherStringKey: 3},
{
{stringKey: 1,
: 2,
[symbolKey]anotherStringKey: 3,
}; )
スプレッド構文を使用して、オブジェクトoriginal
のコピーを作成できます。
const copy = {...original};
注意点 - コピーはシャローコピーです。copy
は、original
のすべてのプロパティ(キーと値のペア)の複製を持つ新しいオブジェクトです。しかし、プロパティ値がオブジェクトの場合は、それらはそれ自体コピーされません。original
とcopy
の間で共有されます。例を見てみましょう。
const original = { a: 1, b: {prop: true} };
const copy = {...original};
copy
の最初のレベルは実際のコピーです。そのレベルでプロパティを変更しても、オリジナルには影響しません。
.a = 2;
copy.deepEqual(
assert, { a: 1, b: {prop: true} }); // no change original
しかし、より深いレベルはコピーされません。例えば、.b
の値はオリジナルとコピーの間で共有されます。コピー内の.b
を変更すると、オリジナルも変更されます。
.b.prop = false;
copy.deepEqual(
assert, { a: 1, b: {prop: false} }); original
JavaScriptには、深層コピーをサポートする組み込み機能はありません
オブジェクトの深層コピー(すべてのレベルがコピーされる)は、一般的に非常に困難です。そのため、JavaScriptには(今のところ)それに対する組み込みの操作がありません。そのような操作が必要な場合は、自分で実装する必要があります。
コードの入力の1つがデータを含むオブジェクトの場合、プロパティが欠落している場合に使用されるデフォルト値を指定することで、プロパティをオプションにすることができます。そのための一つのテクニックは、プロパティにデフォルト値を含むオブジェクトを使用することです。次の例では、そのオブジェクトがDEFAULTS
です。
const DEFAULTS = {alpha: 'a', beta: 'b'};
const providedData = {alpha: 1};
const allData = {...DEFAULTS, ...providedData};
.deepEqual(allData, {alpha: 1, beta: 'b'}); assert
結果、オブジェクトallData
は、DEFAULTS
をコピーし、そのプロパティをprovidedData
のプロパティで上書きすることによって作成されます。
しかし、デフォルト値を指定するためにオブジェクトは必要ありません。個別にオブジェクトリテラル内で指定することもできます。
const providedData = {alpha: 1};
const allData = {alpha: 'a', beta: 'b', ...providedData};
.deepEqual(allData, {alpha: 1, beta: 'b'}); assert
これまで、オブジェクトのプロパティ.alpha
を変更する方法は1つしか見ていませんでした。それは、(A行)で設定してオブジェクトを変化させる方法です。つまり、このプロパティの変更方法は破壊的です。
const obj = {alpha: 'a', beta: 'b'};
.alpha = 1; // (A)
obj.deepEqual(obj, {alpha: 1, beta: 'b'}); assert
スプレッド構文を使用すると、.alpha
を非破壊的に変更できます。つまり、.alpha
が異なる値を持つobj
のコピーを作成します。
const obj = {alpha: 'a', beta: 'b'};
const updatedObj = {...obj, alpha: 1};
.deepEqual(updatedObj, {alpha: 1, beta: 'b'}); assert
演習:スプレッド構文によるプロパティの非破壊的な更新(固定キー)
exercises/objects/update_name_test.mjs
Object.assign()
[ES6]Object.assign()
はツールメソッドです。
Object.assign(target, source_1, source_2, ···)
この式は、source_1
のすべてのプロパティをtarget
に、次にsource_2
のすべてのプロパティなどを代入します。最後に、target
を返します。例えば以下のように。
const target = { a: 1 };
const result = Object.assign(
,
targetb: 2},
{c: 3, b: true});
{
.deepEqual(
assert, { a: 1, b: true, c: 3 });
result// target was modified and returned:
.equal(result, target); assert
Object.assign()
のユースケースは、スプレッドプロパティのユースケースと似ています。ある意味、破壊的にスプレッドします。
this
メソッドを紹介するために使用された例を再訪してみましょう。
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
,
}; }
やや驚くべきことに、メソッドは関数です。
.equal(typeof jane.says, 'function'); assert
なぜそうなのでしょうか?呼び出し可能な値に関する章で学習したように、通常の関数はいくつかの役割を果たします。メソッドはその役割の1つです。そのため、内部的には、jane
はおおよそ次のように見えます。
const jane = {
first: 'Jane',
says: function (text) {
return `${this.first} says “${text}”`;
,
}; }
this
次のコードを考えてみましょう。
const obj = {
someMethod(x, y) {
.equal(this, obj); // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');
assert
};
}.someMethod('a', 'b'); // (B) obj
B行で、obj
はメソッド呼び出しのレシーバです。それは、this
という名前の暗黙的な(隠れた)パラメータを介して、obj.someMethod
に格納されている関数に渡されます(A行)。
this
の理解方法
this
を理解する最良の方法は、通常の関数(そしてメソッドも)の暗黙的なパラメータとして考えることです。
.call()
メソッドは関数であり、関数はそれ自体がメソッドを持っています。そのメソッドの1つは.call()
です。このメソッドの動作を理解するために、例を見てみましょう。
前のセクションでは、このメソッド呼び出しがありました。
.someMethod('a', 'b') obj
この呼び出しは、次と同等です。
.someMethod.call(obj, 'a', 'b'); obj
これも次と同等です。
const func = obj.someMethod;
.call(obj, 'a', 'b'); func
.call()
は、通常は暗黙的なパラメータthis
を明示的にします。.call()
を介して関数を呼び出す場合、最初の引数はthis
であり、その後に通常の(明示的な)関数パラメータが続きます。
余談ですが、これは実際には2つの異なるドット演算子があることを意味します。
obj.prop
obj.prop()
(2)は(1)に関数呼び出し演算子()
が続くだけではありません。代わりに、(2)はthis
の値も追加で提供します。
.bind()
.bind()
は、関数オブジェクトの別のメソッドです。次のコードでは、.bind()
を使用して、メソッド.says()
をスタンドアロン関数func()
に変換します。
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`; // (A)
,
};
}
const func = jane.says.bind(jane, 'hello');
.equal(func(), 'Jane says “hello”'); assert
.bind()
を介してthis
をjane
に設定することは、ここで非常に重要です。そうでなければ、this
はA行で使用されているため、func()
は正しく動作しません。次のセクションでは、その理由を調べます。
this
の落とし穴:メソッドの抽出関数とメソッドについてかなり理解したので、メソッドとthis
に関する最大の落とし穴を見ていきましょう。オブジェクトから抽出されたメソッドを関数呼び出しすると、注意しないと失敗する可能性があります。
次の例では、メソッドjane.says()
を抽出して変数func
に格納し、func
を関数呼び出しすると失敗します。
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
,
};
}const func = jane.says; // extract the method
.throws(
assert=> func('hello'), // (A)
()
{name: 'TypeError',
message: "Cannot read properties of undefined (reading 'first')",
; })
A行では、通常の関数呼び出しを行っています。通常の関数呼び出しでは、this
はundefined
です(厳格モードが有効な場合、ほぼ常に有効です)。したがって、A行は次と同等です。
.throws(
assert=> jane.says.call(undefined, 'hello'), // `this` is undefined!
()
{name: 'TypeError',
message: "Cannot read properties of undefined (reading 'first')",
}; )
これを修正するにはどうすればよいでしょうか?メソッド.says()
を抽出するには、.bind()
を使用する必要があります。
const func2 = jane.says.bind(jane);
.equal(func2('hello'), 'Jane says “hello”'); assert
.bind()
は、func()
を呼び出すとき、this
が常にjane
であることを保証します。
アロー関数を使用してメソッドを抽出することもできます。
const func3 = text => jane.says(text);
.equal(func3('hello'), 'Jane says “hello”'); assert
以下は、実際のWeb開発で見られる可能性のあるコードの簡略版です。
class ClickHandler {
constructor(id, elem) {
this.id = id;
.addEventListener('click', this.handleClick); // (A)
elem
}handleClick(event) {
alert('Clicked ' + this.id);
} }
A行では、メソッド.handleClick()
を正しく抽出していません。代わりに、次のようにする必要があります。
const listener = this.handleClick.bind(this);
.addEventListener('click', listener);
elem
// Later, possibly:
.removeEventListener('click', listener); elem
.bind()
の各呼び出しは、新しい関数を生成します。そのため、後で削除したい場合は、その結果をどこかに保存する必要があります。
残念ながら、メソッドの抽出の落とし穴を回避する簡単な方法はありません。メソッドを抽出するたびに、注意深く適切に行う必要があります。たとえば、this
をバインドするか、アロー関数を使用します。
演習:メソッドの抽出
exercises/objects/method_extraction_exrc.mjs
this
の落とし穴:this
の意図しないシャドウイング
this
の意図しないシャドウイングは、通常の関数でのみ問題になります
アロー関数はthis
をシャドウイングしません。
次の問題を考えてみましょう。通常の関数内では、その通常の関数自体が独自のthis
を持っているため、周囲のスコープのthis
にアクセスできません。言い換えれば、内部スコープの変数は、外部スコープの変数を隠します。これはシャドウイングと呼ばれます。次のコードはその例です。
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x; // (A)
;
}),
};
}.throws(
assert=> prefixer.prefixStringArray(['a', 'b']),
()
{name: 'TypeError',
message: "Cannot read properties of undefined (reading 'prefix')",
}; )
A行では、.prefixStringArray()
のthis
にアクセスしたいと考えています。しかし、周囲の通常の関数には独自のthis
があり、this
をシャドウイングして(そしてアクセスをブロックして)いるため、アクセスできません。コールバックが関数呼び出しされているため、前者のthis
の値はundefined
です。これがエラーメッセージの説明です。
この問題を解決する最も簡単な方法は、アロー関数を使用することです。アロー関数には独自のthis
がなく、そのため何もシャドウイングしません。
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
=> {
(x) return this.prefix + x;
;
}),
};
}.deepEqual(
assert.prefixStringArray(['a', 'b']),
prefixer'==> a', '==> b']); [
別の変数(A行)にthis
を保存して、シャドウイングされないようにすることもできます。
prefixStringArray(stringArray) {
const that = this; // (A)
return stringArray.map(
function (x) {
return that.prefix + x;
;
}), }
別のオプションは、.bind()
(A行)を介してコールバックに固定されたthis
を指定することです。
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
.bind(this)); // (A)
}, }
最後に、.map()
を使用すると、コールバックの呼び出し時に使用するthis
(A行)の値を指定できます。
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
,
}this); // (A)
, }
this
の意図しないシャドウイングの落とし穴を回避する§25.3.4「推奨事項:通常の関数よりも特殊化された関数を使用する」のアドバイスに従うと、this
の意図しないシャドウイングの落とし穴を回避できます。これは要約です。
匿名のインライン関数としてアロー関数を使用します。暗黙のパラメータとしてthis
を持たず、シャドウイングしません。
名前付きのスタンドアロン関数宣言の場合、アロー関数または関数宣言のいずれかを使用できます。後者を使用する場合は、本体にthis
が含まれていないことを確認する必要があります。
this
の値(上級)さまざまなコンテキストにおけるthis
の値は何ですか?
呼び出し可能なエンティティ内では、this
の値は、呼び出し可能なエンティティがどのように呼び出され、どのような種類の呼び出し可能なエンティティであるかに依存します。
this === undefined
(厳格モードの場合)this
は周囲のスコープと同じです(レキシカルthis
)this
は呼び出しのレシーバですnew
:this
は新しく作成されたインスタンスを参照します一般的なすべてのトップレベルスコープでthis
にアクセスすることもできます。
<script>
要素:this === globalThis
this === undefined
this === module.exports
ヒント:トップレベルスコープでは
this
が存在しないと仮定する
トップレベルのthis
は混乱を招きやすく、その(少ない)ユースケースにはより良い代替手段があるため、私はそうしています。
次の種類のオプションチェーン操作が存在します。
?.prop // optional fixed property getting
obj?.[«expr»] // optional dynamic property getting
obj?.(«arg0», «arg1») // optional function or method call func
大まかな考え方は次のとおりです。
undefined
でもnull
でもない場合、疑問符の後の操作を実行します。undefined
を返します。3つの構文のそれぞれについては、後で詳しく説明します。以下は最初のいくつかの例です。
> null?.propundefined
> {prop: 1}?.prop1
> null?.(123)undefined
> String?.(123)'123'
次のデータを考えてみましょう。
const persons = [
{surname: 'Zoe',
address: {
street: {
name: 'Sesame Street',
number: '123',
,
},
},
}
{surname: 'Mariner',
,
}
{surname: 'Carmen',
address: {
,
},
}; ]
オプションチェーンを使用して、安全に通りの名前を抽出できます。
const streetNames = persons.map(
=> p.address?.street?.name);
p .deepEqual(
assert, ['Sesame Street', undefined, undefined]
streetNames; )
nullish 合体演算子を使用すると、undefined
の代わりにデフォルト値'(no name)'
を使用できます。
const streetNames = persons.map(
=> p.address?.street?.name ?? '(no name)');
p .deepEqual(
assert, ['Sesame Street', '(no name)', '(no name)']
streetNames; )
次の2つの式は同等です。
?.prop
o!== undefined && o !== null) ? o.prop : undefined (o
例
.equal(undefined?.prop, undefined);
assert.equal(null?.prop, undefined);
assert.equal({prop:1}?.prop, 1); assert
次の2つの式は同等です。
?.[«expr»]
o!== undefined && o !== null) ? o[«expr»] : undefined (o
例
const key = 'prop';
.equal(undefined?.[key], undefined);
assert.equal(null?.[key], undefined);
assert.equal({prop:1}?.[key], 1); assert
次の2つの式は同等です。
?.(arg0, arg1)
f!== undefined && f !== null) ? f(arg0, arg1) : undefined (f
例
.equal(undefined?.(123), undefined);
assert.equal(null?.(123), undefined);
assert.equal(String?.(123), '123'); assert
この演算子の左辺が呼び出し可能でない場合、この演算子はエラーを生成することに注意してください。
.throws(
assert=> true?.(123),
() TypeError);
なぜでしょうか?この考え方は、この演算子は意図的な省略のみを許容するというものです。undefined
とnull
以外の呼び出し不可能な値はおそらくエラーであり、回避するのではなく、報告する必要があります。
プロパティ取得とメソッド呼び出しのチェーンでは、最初のオプション演算子が左辺でundefined
またはnull
に遭遇すると、評価が停止します。
function invokeM(value) {
return value?.a.b.m(); // (A)
}
const obj = {
a: {
b: {
m() { return 'result' }
}
};
}.equal(
assertinvokeM(obj), 'result'
;
).equal(
assertinvokeM(undefined), undefined // (B)
; )
B行のinvokeM(undefined)
を考えてみましょう。undefined?.a
はundefined
です。したがって、A行で.b
が失敗することが予想されます。しかし、そうではありません。?.
演算子は値undefined
に遭遇し、式全体の評価はすぐにundefined
を返します。
この動作は、JavaScriptが演算子を評価する前に常にすべてのオペランドを評価する通常の演算子とは異なります。これは短絡評価と呼ばれます。他の短絡評価演算子は次のとおりです。
(a && b)
:a
が真の場合にのみb
が評価されます。(a || b)
:a
が偽の場合にのみb
が評価されます。(c ? t : e)
: c
が真の場合、t
が評価されます。それ以外の場合は、e
が評価されます。オプショナルチェイニングにも欠点があります。
オプショナルチェイニングの代替案として、情報を一度に単一の位置で抽出する方法があります。
どちらのアプローチでも、チェックを実行し、問題があれば早期に失敗させることが可能です。
さらに読む
?.
)の良いニーモニックとは?オプショナルチェイニング演算子がドット(.?
)で始まるのか、疑問符(?.
)で始まるのか、時々迷うことはありませんか?その場合、このニーモニックが役立つかもしれません。
?
).
)。o?.[x]
とf?.()
にドットがあるのですか?次の2つのオプショナル演算子の構文は理想的ではありません。
?.[«expr»] // better: obj?[«expr»]
obj?.(«arg0», «arg1») // better: func?(«arg0», «arg1») func
残念ながら、理想的な構文(最初の式)と条件演算子(2番目の式)を区別することが複雑すぎるため、エレガントではない構文が必要になります。
?['a', 'b', 'c'].map(x => x+x)
obj? ['a', 'b', 'c'].map(x => x+x) : [] obj
null?.prop
はnull
ではなくundefined
を評価するのですか??.
演算子は主に右辺に関するものです。プロパティ.prop
が存在しますか?存在しない場合、早期に停止します。したがって、左辺に関する情報を保持することはめったに役に立ちません。しかし、単一の「早期終了」値のみを使用すると、ことが単純化されます。
オブジェクトは固定レイアウトオブジェクトとして最適に機能します。しかし、ES6以前のJavaScriptには、ディクショナリ用のデータ構造がありませんでした(ES6でMapが導入されました)。そのため、オブジェクトをディクショナリとして使用しなければならず、重要な制約がありました。ディクショナリキーは文字列でなければなりませんでした(シンボルもES6で導入されました)。
まず、ディクショナリに関連するが、固定レイアウトオブジェクトにも役立つオブジェクトの機能を見ていきます。このセクションは、オブジェクトをディクショナリとして実際に使用するヒントで締めくくります。(ネタバレ:可能であれば、Mapを使用する方が良いでしょう)。
これまで、常に固定レイアウトオブジェクトを使用してきました。プロパティキーは、有効な識別子でなければならない固定トークンであり、内部的には文字列になりました。
const obj = {
mustBeAnIdentifier: 123,
;
}
// Get property
.equal(obj.mustBeAnIdentifier, 123);
assert
// Set property
.mustBeAnIdentifier = 'abc';
obj.equal(obj.mustBeAnIdentifier, 'abc'); assert
次のステップとして、プロパティキーに関するこの制限を超えます。この小節では、任意の固定文字列をキーとして使用します。次の小節では、キーを動的に計算します。
2つの構文により、任意の文字列をプロパティキーとして使用できます。
まず、オブジェクトリテラルを介してプロパティキーを作成する場合、プロパティキーを引用符(シングルクォートまたはダブルクォート)で囲むことができます。
const obj = {
'Can be any string!': 123,
; }
次に、プロパティを取得または設定する場合、内部に文字列を含む角括弧を使用できます。
// Get property
.equal(obj['Can be any string!'], 123);
assert
// Set property
'Can be any string!'] = 'abc';
obj[.equal(obj['Can be any string!'], 'abc'); assert
これらの構文はメソッドにも使用できます。
const obj = {
'A nice method'() {
return 'Yes!';
,
};
}
.equal(obj['A nice method'](), 'Yes!'); assert
前節では、オブジェクトリテラル内で固定文字列を使用してプロパティキーを指定しました。この節では、プロパティキーを動的に計算する方法を学習します。これにより、任意の文字列またはシンボルを使用できます。
オブジェクトリテラルにおける動的に計算されたプロパティキーの構文は、プロパティへの動的なアクセスに触発されています。つまり、角括弧を使用して式を囲むことができます。
const obj = {
'Hello world!']: true,
['p'+'r'+'o'+'p']: 123,
[Symbol.toStringTag]: 'Goodbye', // (A)
[;
}
.equal(obj['Hello world!'], true);
assert.equal(obj.prop, 123);
assert.equal(obj[Symbol.toStringTag], 'Goodbye'); assert
計算されたキーの主なユースケースは、プロパティキーとしてシンボルを使用することです(A行)。
プロパティを取得および設定するための角括弧演算子は、任意の式で機能することに注意してください。
.equal(obj['p'+'r'+'o'+'p'], 123);
assert.equal(obj['==> prop'.slice(4)], 123); assert
メソッドにも計算されたプロパティキーを持つことができます。
const methodKey = Symbol();
const obj = {
[methodKey]() {return 'Yes!';
,
};
}
.equal(obj[methodKey](), 'Yes!'); assert
この章の残りの部分では、主に固定プロパティキーを再び使用します(構文的にもっと便利であるため)。しかし、すべての機能は任意の文字列とシンボルにも使用できます。
演習:スプレッド(計算されたキー)を使用してプロパティを非破壊的に更新する
exercises/objects/update_property_test.mjs
in
演算子:指定されたキーを持つプロパティが存在するか?in
演算子は、オブジェクトに指定されたキーを持つプロパティがあるかどうかをチェックします。
const obj = {
alpha: 'abc',
beta: false,
;
}
.equal('alpha' in obj, true);
assert.equal('beta' in obj, true);
assert.equal('unknownKey' in obj, false); assert
真偽値チェックを使用して、プロパティが存在するかどうかを判断することもできます。
.equal(
assert.alpha ? 'exists' : 'does not exist',
obj'exists');
.equal(
assert.unknownKey ? 'exists' : 'does not exist',
obj'does not exist');
前のチェックは、obj.alpha
が真であり、存在しないプロパティを読み取るとundefined
(偽)が返されるため機能します。
ただし、重要な注意点が1つあります。プロパティが存在するが、偽の値(undefined
、null
、false
、0
、""
など)を持つ場合、真偽値チェックは失敗します。
.equal(
assert.beta ? 'exists' : 'does not exist',
obj'does not exist'); // should be: 'exists'
delete
演算子を使用してプロパティを削除できます。
const obj = {
myProp: 123,
;
}
.deepEqual(Object.keys(obj), ['myProp']);
assertdelete obj.myProp;
.deepEqual(Object.keys(obj), []); assert
列挙可能性は、プロパティの属性です。列挙できないプロパティは、一部の操作(たとえば、Object.keys()
やプロパティのスプレッドなど)では無視されます。デフォルトでは、ほとんどのプロパティは列挙可能です。次の例は、列挙可能性を変更する方法とそのスプレッドへの影響を示しています。
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
// We create enumerable properties via an object literal
const obj = {
enumerableStringKey: 1,
: 2,
[enumerableSymbolKey]
}
// For non-enumerable properties, we need a more powerful tool
Object.defineProperties(obj, {
nonEnumStringKey: {
value: 3,
enumerable: false,
,
}: {
[nonEnumSymbolKey]value: 4,
enumerable: false,
,
};
})
// Non-enumerable properties are ignored by spreading:
.deepEqual(
assert...obj},
{
{enumerableStringKey: 1,
: 2,
[enumerableSymbolKey]
}; )
Object.defineProperties()
についてはこの章の後半で説明します。次の小節では、これらの操作が列挙可能性によってどのように影響を受けるかを示します。
Object.keys()
などによるプロパティキーの列挙列挙可能 | 非列挙 | 文字列 | シンボル | |
---|---|---|---|---|
Object.keys() |
✔ |
✔ |
||
Object.getOwnPropertyNames() |
✔ |
✔ |
✔ |
|
Object.getOwnPropertySymbols() |
✔ |
✔ |
✔ |
|
Reflect.ownKeys() |
✔ |
✔ |
✔ |
✔ |
表19の各メソッドは、パラメーターの独自のプロパティキーを含む配列を返します。メソッドの名前から、以下の区別が行われていることがわかります。
4つの操作を示すために、前節の例を再訪します。
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
const obj = {
enumerableStringKey: 1,
: 2,
[enumerableSymbolKey]
}Object.defineProperties(obj, {
nonEnumStringKey: {
value: 3,
enumerable: false,
,
}: {
[nonEnumSymbolKey]value: 4,
enumerable: false,
,
};
})
.deepEqual(
assertObject.keys(obj),
'enumerableStringKey']
[;
).deepEqual(
assertObject.getOwnPropertyNames(obj),
'enumerableStringKey', 'nonEnumStringKey']
[;
).deepEqual(
assertObject.getOwnPropertySymbols(obj),
, nonEnumSymbolKey]
[enumerableSymbolKey;
).deepEqual(
assertReflect.ownKeys(obj),
['enumerableStringKey', 'nonEnumStringKey',
, nonEnumSymbolKey,
enumerableSymbolKey
]; )
Object.values()
によるプロパティ値の列挙Object.values()
は、オブジェクトのすべての列挙可能な文字列キーのプロパティの値を列挙します。
const firstName = Symbol('firstName');
const obj = {
: 'Jane',
[firstName]lastName: 'Doe',
;
}.deepEqual(
assertObject.values(obj),
'Doe']); [
Object.entries()
によるプロパティエントリの列挙 [ES2017]Object.entries()
は、すべての列挙可能な文字列キーのプロパティをキーバリューペアとして列挙します。各ペアは2要素の配列としてエンコードされます。
const firstName = Symbol('firstName');
const obj = {
: 'Jane',
[firstName]lastName: 'Doe',
;
}.deepEqual(
assertObject.entries(obj),
['lastName', 'Doe'],
[; ])
Object.entries()
の簡単な実装次の関数は、Object.entries()
の簡略版です。
function entries(obj) {
return Object.keys(obj)
.map(key => [key, obj[key]]);
}
演習:
Object.entries()
exercises/objects/find_key_test.mjs
オブジェクトの独自の(継承されていない)プロパティは、常に次の順序で列挙されます。
次の例は、プロパティキーがこれらのルールに従ってどのようにソートされるかを示しています。
> Object.keys({b:0,a:0, 10:0,2:0})[ '2', '10', 'b', 'a' ]
プロパティの順序
ECMAScript仕様では、プロパティの順序付けについて詳しく説明されています。
Object.fromEntries()
によるオブジェクトのアセンブル [ES2019][キー、値]ペアのイテラブルが与えられると、Object.fromEntries()
はオブジェクトを作成します。
const symbolKey = Symbol('symbolKey');
.deepEqual(
assertObject.fromEntries(
['stringKey', 1],
[, 2],
[symbolKey
],
)
{stringKey: 1,
: 2,
[symbolKey]
}; )
Object.fromEntries()
はObject.entries()
の反対を行います。ただし、Object.entries()
はシンボルキーのプロパティを無視しますが、Object.fromEntries()
は無視しません(前の例を参照)。
両方を示すために、次の小節で、ライブラリUnderscoreの2つのツール関数をそれらを使用して実装します。
pick()
Underscore関数pick()
は、次のシグネチャを持っています。
pick(object, ...keys)
これは、末尾の引数に記載されているキーを持つプロパティのみを持つobject
のコピーを返します。
const address = {
street: 'Evergreen Terrace',
number: '742',
city: 'Springfield',
state: 'NT',
zip: '49007',
;
}.deepEqual(
assertpick(address, 'street', 'number'),
{street: 'Evergreen Terrace',
number: '742',
}; )
pick()
は次のように実装できます。
function pick(object, ...keys) {
const filteredEntries = Object.entries(object)
.filter(([key, _value]) => keys.includes(key));
return Object.fromEntries(filteredEntries);
}
invert()
Underscore関数invert()
は、次のシグネチャを持っています。
invert(object)
これは、すべてのプロパティのキーと値が入れ替えられたobject
のコピーを返します。
.deepEqual(
assertinvert({a: 1, b: 2, c: 3}),
1: 'a', 2: 'b', 3: 'c'}
{; )
invert()
は次のように実装できます。
function invert(object) {
const reversedEntries = Object.entries(object)
.map(([key, value]) => [value, key]);
return Object.fromEntries(reversedEntries);
}
Object.fromEntries()
の簡単な実装次の関数は、Object.fromEntries()
の簡略版です。
function fromEntries(iterable) {
const result = {};
for (const [key, value] of iterable) {
let coercedKey;
if (typeof key === 'string' || typeof key === 'symbol') {
= key;
coercedKey else {
} = String(key);
coercedKey
}= value;
result[coercedKey]
}return result;
}
演習:
Object.entries()
とObject.fromEntries()
を使用する
exercises/objects/omit_properties_test.mjs
プレーンオブジェクト(オブジェクトリテラルを介して作成)をディクショナリとして使用する場合、2つの落とし穴に注意する必要があります。
最初の落とし穴は、in
演算子が継承されたプロパティも見つけることです。
const dict = {};
.equal('toString' in dict, true); assert
dict
は空として扱われることを望んでいますが、in
演算子は、そのプロトタイプであるObject.prototype
から継承するプロパティを検出します。
2番目の落とし穴は、特別な権限を持つため(オブジェクトのプロトタイプを設定するため)、プロパティキー__proto__
を使用できないことです。
const dict = {};
'__proto__'] = 123;
dict[// No property was added to dict:
.deepEqual(Object.keys(dict), []); assert
では、どのようにして2つの落とし穴を回避するのでしょうか?
次のコードは、プロトタイプのないオブジェクトをディクショナリとして使用する方法を示しています。
const dict = Object.create(null); // prototype is `null`
.equal('toString' in dict, false); // (A)
assert
'__proto__'] = 123;
dict[.deepEqual(Object.keys(dict), ['__proto__']); assert
2つの落とし穴を回避しました。
__proto__
はObject.prototype
を介して実装されています。つまり、Object.prototype
がプロトタイプチェーンにない場合はオフになります。 演習:オブジェクトをディクショナリとして使用
exercises/objects/simple_dict_test.mjs
オブジェクトがプロパティで構成されているのと同様に、プロパティは属性で構成されています。プロパティの値は、いくつかの属性の1つにすぎません。その他には、以下が含まれます。
writable
: プロパティの値を変更できますか?enumerable
: プロパティはObject.keys()
、スプレッド構文などで考慮されますか?プロパティ属性を操作する演算子のいずれかを使用する場合、属性はプロパティ記述子を介して指定されます。プロパティ記述子とは、各プロパティが1つの属性を表すオブジェクトです。たとえば、obj.myProp
のプロパティの属性は次のように読み取ります。
const obj = { myProp: 123 };
.deepEqual(
assertObject.getOwnPropertyDescriptor(obj, 'myProp'),
{value: 123,
writable: true,
enumerable: true,
configurable: true,
; })
そして、obj.myProp
の属性は次のように変更します。
.deepEqual(Object.keys(obj), ['myProp']);
assert
// Hide property `myProp` from Object.keys()
// by making it non-enumerable
Object.defineProperty(obj, 'myProp', {
enumerable: false,
;
})
.deepEqual(Object.keys(obj), []); assert
さらに読む
Object.freeze(obj)
は、obj
を完全に不変にします。プロパティの変更、プロパティの追加、プロトタイプの変更はできません。たとえば、
const frozen = Object.freeze({ x: 2, y: 5 });
.throws(
assert=> { frozen.x = 7 },
()
{name: 'TypeError',
message: /^Cannot assign to read only property 'x'/,
; })
内部的には、Object.freeze()
はプロパティの属性(例:書き込み不可にする)とオブジェクトの属性(例:拡張不可にする、つまりプロパティを追加できなくなる)を変更します。
ただし、1つの注意点があります。Object.freeze(obj)
は浅い凍結を行います。つまり、obj
のプロパティのみが凍結され、プロパティに格納されているオブジェクトは凍結されません。
詳細情報
オブジェクトの凍結とオブジェクトのロックダウンのその他の方法の詳細については、Deep JavaScriptを参照してください。
プロトタイプはJavaScript唯一の継承メカニズムです。各オブジェクトには、null
かオブジェクトのいずれかのプロトタイプがあります。後者の場合、オブジェクトはそのプロトタイプのすべてのプロパティを継承します。
オブジェクトリテラルでは、特別なプロパティ__proto__
を使用してプロトタイプを設定できます。
const proto = {
protoProp: 'a',
;
}const obj = {
__proto__: proto,
objProp: 'b',
;
}
// obj inherits .protoProp:
.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true); assert
プロトタイプオブジェクト自体にプロトタイプを持つことができるため、オブジェクトのチェーン(いわゆるプロトタイプチェーン)が作成されます。継承により、単一のオブジェクトを扱っているように見えますが、実際にはオブジェクトのチェーンを扱っています。
図9は、obj
のプロトタイプチェーンを示しています。
継承されていないプロパティは、固有プロパティと呼ばれます。obj
には、.objProp
という1つの固有プロパティがあります。
一部の演算子は、すべてのプロパティ(固有プロパティと継承プロパティ)を考慮します。たとえば、プロパティの取得などです。
> const obj = { one: 1 };
> typeof obj.one // own'number'
> typeof obj.toString // inherited'function'
他の演算子は、固有プロパティのみを考慮します。たとえば、Object.keys()
などです。
> Object.keys(obj)[ 'one' ]
固有プロパティのみを考慮する別の演算子について、以下で説明します。それはプロパティの設定です。
プロトタイプオブジェクトのチェーンを持つオブジェクトobj
の場合、obj
の固有プロパティを設定すると、obj
のみが変更されるのは理にかなっています。ただし、obj
を介して継承プロパティを設定した場合も、obj
のみが変更されます。継承されたプロパティを上書きするobj
に新しい固有プロパティが作成されます。次のオブジェクトを使用して、その動作を見てみましょう。
const proto = {
protoProp: 'a',
;
}const obj = {
__proto__: proto,
objProp: 'b',
; }
次のコードスニペットでは、継承プロパティobj.protoProp
(A行)を設定します。これは、固有プロパティを作成することで「変更」されます。obj.protoProp
を読み取ると、まず固有プロパティが見つかり、その値が継承プロパティの値を上書きします。
// In the beginning, obj has one own property
.deepEqual(Object.keys(obj), ['objProp']);
assert
.protoProp = 'x'; // (A)
obj
// We created a new own property:
.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);
assert
// The inherited property itself is unchanged:
.equal(proto.protoProp, 'a');
assert
// The own property overrides the inherited property:
.equal(obj.protoProp, 'x'); assert
obj
のプロトタイプチェーンは、図10に示されています。
__proto__
に関する推奨事項
__proto__
を擬似プロパティ(Object
のすべてのインスタンスのセッター)として使用しないでください。
Object
のインスタンスでないオブジェクト)で使用できるわけではありません。この機能の詳細については、§29.8.7「Object.prototype.__proto__
(アクセサ)」を参照してください。
オブジェクトリテラルで__proto__
を使用してプロトタイプを設定することは異なります。これは、欠点のないオブジェクトリテラルの機能です。
プロトタイプの取得と設定の推奨方法は次のとおりです。
オブジェクトのプロトタイプの取得
Object.getPrototypeOf(obj: Object) : Object
オブジェクトのプロトタイプを設定する最適なタイミングは、オブジェクトを作成するときです。オブジェクトリテラルで__proto__
を使用するか、または次の方法を使用することで、これを行うことができます。
Object.create(proto: Object) : Object
必要に応じて、Object.setPrototypeOf()
を使用して既存オブジェクトのプロトタイプを変更できます。ただし、これによりパフォーマンスが低下する可能性があります。
これらの機能の使用方法を次に示します。
const proto1 = {};
const proto2a = {};
const proto2b = {};
const obj1 = {
__proto__: proto1,
a: 1,
b: 2,
;
}.equal(Object.getPrototypeOf(obj1), proto1);
assert
const obj2 = Object.create(
,
proto2a
{a: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
,
}b: {
value: 2,
writable: true,
enumerable: true,
configurable: true,
,
}
};
).equal(Object.getPrototypeOf(obj2), proto2a);
assert
Object.setPrototypeOf(obj2, proto2b);
.equal(Object.getPrototypeOf(obj2), proto2b); assert
これまで「proto
はobj
のプロトタイプである」とは常に「proto
はobj
の直接プロトタイプである」ことを意味していました。しかし、これはより緩やかに使用され、「proto
はobj
のプロトタイプチェーン内にある」ことを意味することもできます。このより緩やかな関係は、.isPrototypeOf()
を使用して確認できます。
例:
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert
.equal(c.isPrototypeOf(a), false);
assert.equal(a.isPrototypeOf(a), false); assert
このメソッドの詳細については、§29.8.5「Object.prototype.isPrototypeOf()
」を参照してください。
Object.hasOwn()
: 指定されたプロパティは固有(非継承)ですか? [ES2022]in
演算子(A行)は、オブジェクトに指定されたプロパティがあるかどうかを確認します。これに対して、Object.hasOwn()
(B行とC行)は、プロパティが固有かどうかを確認します。
const proto = {
protoProp: 'protoProp',
;
}const obj = {
__proto__: proto,
objProp: 'objProp',
}.equal('protoProp' in obj, true); // (A)
assert.equal(Object.hasOwn(obj, 'protoProp'), false); // (B)
assert.equal(Object.hasOwn(proto, 'protoProp'), true); // (C) assert
ES2022以前の代替手段:
.hasOwnProperty()
ES2022以前は、別の機能を使用できます。§29.8.8「Object.prototype.hasOwnProperty()
」。この機能には欠点がありますが、参照されているセクションでは、それらの回避方法について説明しています。
次のコードを考えてみましょう。
const jane = {
firstName: 'Jane',
describe() {
return 'Person named '+this.firstName;
,
};
}const tarzan = {
firstName: 'Tarzan',
describe() {
return 'Person named '+this.firstName;
,
};
}
.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan'); assert
非常に似た2つのオブジェクトがあるとします。どちらも名前が.firstName
と.describe
である2つのプロパティを持っています。さらに、メソッド.describe()
は同じです。このメソッドの重複を避けるにはどうすればよいでしょうか?
オブジェクトPersonProto
に移動し、そのオブジェクトをjane
とtarzan
の両方のプロトタイプにすることができます。
const PersonProto = {
describe() {
return 'Person named ' + this.firstName;
,
};
}const jane = {
__proto__: PersonProto,
firstName: 'Jane',
;
}const tarzan = {
__proto__: PersonProto,
firstName: 'Tarzan',
; }
プロトタイプの名前は、jane
とtarzan
の両方が人であることを反映しています。
図11は、3つのオブジェクトがどのように接続されているかを示しています。下部のオブジェクトには、jane
とtarzan
に固有のプロパティが含まれています。上部のオブジェクトには、それらで共有されるプロパティが含まれています。
メソッド呼び出しjane.describe()
を行うと、this
はメソッド呼び出しのレシーバーであるjane
(図の左下隅)を指します。そのため、メソッドは引き続き機能します。tarzan.describe()
も同様に機能します。
.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan'); assert
クラスに関する次の章を先取りすると、クラスは内部的にこのように構成されています。
§29.3「クラスの内部構造」でより詳細に説明されています。
原則として、オブジェクトは順序付けられていません。プロパティを順序付ける主な理由は、エントリ、キー、または値をリストする演算子を決定論的にするためにあります。これにより、たとえばテストが容易になります。
クイズ
クイズアプリを参照してください。