クラス (については次の章で説明します) は、ECMAScript 6 における最も重要な新しいOOP機能です。しかし、オブジェクトリテラルのための新機能や、Object の新しいユーティリティメソッドも含まれています。この章では、それらについて説明します。
Object の新しいメソッドObject の新しいメソッドObject.assign(target, source_1, source_2, ···)Object.getOwnPropertySymbols(obj)Object.setPrototypeOf(obj, proto)__proto____proto____proto____proto__ のマジックを避ける__proto__ のサポート検出__proto__ は「ダンダープロト」と発音します__proto__ の推奨事項Symbol.hasInstance (メソッド)Symbol.toPrimitive (メソッド)Symbol.toStringTag (文字列)Symbol.unscopables (オブジェクト)super を使用できますか?メソッド定義
const obj = {
myMethod(x, y) {
···
}
};
プロパティ値の省略記法
const first = 'Jane';
const last = 'Doe';
const obj = { first, last };
// Same as:
const obj = { first: first, last: last };
算出プロパティキー
const propKey = 'foo';
const obj = {
[propKey]: true,
['b'+'ar']: 123
};
この新しい構文はメソッド定義にも使用できます
const obj = {
['h'+'ello']() {
return 'hi';
}
};
console.log(obj.hello()); // hi
算出プロパティキーの主なユースケースは、シンボルをプロパティキーとして簡単に使用できるようにすることです。
Object の新しいメソッド Object の最も重要な新しいメソッドは、assign() です。従来、この機能はJavaScriptの世界で extend() と呼ばれていました。この古典的な操作の動作とは対照的に、Object.assign() は自身の(継承されていない)プロパティのみを考慮します。
const obj = { foo: 123 };
Object.assign(obj, { bar: true });
console.log(JSON.stringify(obj));
// {"foo":123,"bar":true}
ECMAScript 5では、メソッドは値が関数のプロパティです
var obj = {
myMethod: function (x, y) {
···
}
};
ECMAScript 6では、メソッドは依然として関数値のプロパティですが、よりコンパクトな定義方法ができました
const obj = {
myMethod(x, y) {
···
}
};
ゲッターとセッターは、ECMAScript 5と同じように動作し続けます(構文的にメソッド定義とどれほど似ているかに注意してください)
const obj = {
get foo() {
console.log('GET foo');
return 123;
},
set bar(value) {
console.log('SET bar to '+value);
// return value is ignored
}
};
obj を使ってみましょう
> obj.foo
GET foo
123
> obj.bar = true
SET bar to true
true
値がジェネレータ関数であるプロパティを簡潔に定義する方法もあります
const obj = {
* myGeneratorMethod() {
···
}
};
このコードは以下と同等です
const obj = {
myGeneratorMethod: function* () {
···
}
};
プロパティ値の省略記法を使用すると、オブジェクトリテラルでのプロパティの定義を省略できます。プロパティ値を指定する変数の名前がプロパティキーでもある場合は、キーを省略できます。これは次のようになります。
const x = 4;
const y = 1;
const obj = { x, y };
最後の行は以下と同等です
const obj = { x: x, y: y };
プロパティ値の省略記法は、分割代入とうまく機能します
const obj = { x: 4, y: 1 };
const {x,y} = obj;
console.log(x); // 4
console.log(y); // 1
プロパティ値の省略記法のユースケースの1つは、複数の戻り値です(これについては分割代入の章で説明しています)。
プロパティを設定するときにキーを指定する方法は2つあることを覚えておいてください。
obj.foo = true;obj['b'+'ar'] = 123;オブジェクトリテラルでは、ECMAScript 5ではオプション1のみが可能です。ECMAScript 6では、オプション2(A行)が追加で提供されています
const obj = {
foo: true,
['b'+'ar']: 123
};
この新しい構文はメソッド定義にも使用できます
const obj = {
['h'+'ello']() {
return 'hi';
}
};
console.log(obj.hello()); // hi
算出プロパティキーの主なユースケースは、シンボルです。公開シンボルを定義して、常に一意である特別なプロパティキーとして使用できます。最も顕著な例の1つは、Symbol.iterator に格納されているシンボルです。オブジェクトにそのキーを持つメソッドがある場合、それは反復可能になります。このメソッドはイテレータを返す必要があり、for-of ループなどの構造によってオブジェクトを反復処理するために使用されます。次のコードは、その仕組みを示しています。
const obj = {
* [Symbol.iterator]() { // (A)
yield 'hello';
yield 'world';
}
};
for (const x of obj) {
console.log(x);
}
// Output:
// hello
// world
obj は、A行から始まるジェネレータメソッド定義のため、反復可能です。
Object の新しいメソッド Object.assign(target, source_1, source_2, ···) このメソッドは、ソースをターゲットにマージします。つまり、最初に source_1 のすべての列挙可能な自身の(継承されていない)プロパティをコピーし、次に source_2 のすべての自身のプロパティをコピーするなどして、target を変更します。最後に、ターゲットを返します。
const obj = { foo: 123 };
Object.assign(obj, { bar: true });
console.log(JSON.stringify(obj));
// {"foo":123,"bar":true}
Object.assign() の仕組みを詳しく見てみましょう
Object.assign() は、プロパティキーとして文字列とシンボルの両方を認識しています。Object.assign() は、継承されたプロパティと列挙可能でないプロパティを無視します。 const value = source[propKey];
つまり、ソースにキーが propKey のゲッターがある場合、それが呼び出されます。その結果、Object.assign() で作成されたプロパティはデータプロパティであり、ゲッターをターゲットに転送することはありません。
target[propKey] = value;
その結果、ターゲットにキーが propKey のセッターがある場合、それが value で呼び出されます。
これは、ゲッターとセッターを正しく転送し、ターゲットでセッターを呼び出さずに、すべての自身のプロパティ(列挙可能なプロパティだけでなく)をコピーする方法です
function copyAllOwnProperties(target, ...sources) {
for (const source of sources) {
for (const key of Reflect.ownKeys(source)) {
const desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
return target;
}
プロパティ記述子(Object.getOwnPropertyDescriptor() および Object.defineProperty() で使用される)の詳細については、「Speaking JavaScript」の「プロパティ属性とプロパティ記述子」のセクションを参照してください。
Object.assign() はメソッドの移動には適していません 一方では、super を使用するメソッドを移動することはできません。このようなメソッドには、それが作成されたオブジェクトに結び付ける内部スロット [[HomeObject]] があります。Object.assign() を介して移動した場合、元のオブジェクトのスーパープロパティを参照し続けます。詳細については、クラスの章のセクションで説明します。
他方では、オブジェクトリテラルで作成されたメソッドをクラスのプロトタイプに移動する場合、列挙可能性が正しくありません。前者のメソッドはすべて列挙可能です(そうでなければ、Object.assign() はそれらを認識しません)。しかし、プロトタイプには通常、列挙可能でないメソッドのみがあります。
Object.assign() のユースケース いくつかのユースケースを見てみましょう。
this にプロパティを追加する コンストラクターで Object.assign() を使用して this にプロパティを追加できます
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}
Object.assign() は、不足しているプロパティのデフォルトを埋めるのにも役立ちます。次の例では、プロパティのデフォルト値を持つオブジェクト DEFAULTS と、データを持つオブジェクト options があります。
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options); // (A)
···
}
A行では、新しいオブジェクトを作成し、デフォルトをコピーしてから、options をコピーしてデフォルトをオーバーライドしました。Object.assign() はこれらの操作の結果を返し、それを options に代入します。
別のユースケースは、オブジェクトにメソッドを追加することです
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
関数を手動で代入することもできますが、その場合は、メソッド定義の優れた構文がなく、毎回 SomeClass.prototype に言及する必要があります
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
Object.assign() の最後のユースケースは、オブジェクトをすばやくクローン作成することです
function clone(orig) {
return Object.assign({}, orig);
}
このクローン作成方法も、orig のプロパティ属性を保持しないため、やや不完全です。それが必要な場合は、copyAllOwnProperties() を実装したときのように、プロパティ記述子を使用する必要があります。
クローンを元のプロトタイプと同じプロトタイプにしたい場合は、Object.getPrototypeOf() と Object.create() を使用できます
function clone(orig) {
const origProto = Object.getPrototypeOf(orig);
return Object.assign(Object.create(origProto), orig);
}
Object.getOwnPropertySymbols(obj) Object.getOwnPropertySymbols(obj) は、obj のすべての自身の(継承されていない)シンボル値のプロパティキーを取得します。これは、すべての文字列値の自身のプロパティキーを取得する Object.getOwnPropertyNames() を補完します。プロパティの走査の詳細については、後のセクションを参照してください。
厳密等価演算子(===)は、2つの値を期待とは異なる方法で扱います。
まず、NaN はそれ自体と等しくありません。
> NaN === NaN
false
これは残念です。多くの場合、NaN を検出できなくなるためです
> [0,NaN,2].indexOf(NaN)
-1
次に、JavaScriptには2つのゼロがありますが、厳密等価はそれらを同じ値であるかのように扱います
> -0 === +0
true
これを行うことは通常は良いことです。
Object.is() は、=== よりも少し正確に値を比較する方法を提供します。これは次のように機能します
> Object.is(NaN, NaN)
true
> Object.is(-0, +0)
false
その他はすべて === と同じように比較されます。
Object.is() を使用して配列要素を検索する 次の関数 myIndexOf() では、Object.is() を新しいES6配列メソッドfindIndex()と組み合わせて、配列内の NaN を検索します。
function myIndexOf(arr, elem) {
return arr.findIndex(x => Object.is(x, elem));
}
const myArray = [0,NaN,2];
myIndexOf(myArray, NaN); // 1
myArray.indexOf(NaN); // -1
最後の行でわかるように、indexOf() は NaN を見つけません。
Object.setPrototypeOf(obj, proto) このメソッドは、obj のプロトタイプを proto に設定します。ECMAScript 5 での非標準的な方法は、多くのエンジンでサポートされている 特殊なプロパティ __proto__ への代入によるものです。プロトタイプを設定する推奨方法は、ECMAScript 5 と同じく、Object.create() を介してオブジェクトの作成時に行うことです。これは、最初にオブジェクトを作成してからプロトタイプを設定するよりも常に高速です。明らかに、既存のオブジェクトのプロトタイプを変更したい場合は機能しません。
ECMAScript 6 では、プロパティのキーは文字列またはシンボルのいずれかになります。以下は、オブジェクト obj のプロパティキーをトラバースする 5 つの操作です。
Object.keys(obj) : Array<string>Object.getOwnPropertyNames(obj) : Array<string>Object.getOwnPropertySymbols(obj) : Array<symbol>Reflect.ownKeys(obj) : Array<string|symbol>for (const key in obj)ES6 は、プロパティの 2 つのトラバース順序を定義しています。
自身のプロパティキー
Object.assign()、Object.defineProperties()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、Reflect.ownKeys()列挙可能な自身の名前
for-in がプロパティをトラバースする順序と同じである必要があります。JSON.parse()、JSON.stringify()、Object.keys()for-in がプロパティをトラバースする順序は定義されていません。Allen Wirfs-Brock の引用
歴史的に、
for-inの順序は定義されておらず、ブラウザの実装間では、生成する順序(およびその他の詳細)にばらつきがありました。ES5 では、Object.keysと、キーをfor-inと同じ順序にする必要があるという要件が追加されました。ES5 と ES6 の両方の開発中、特定のfor-inの順序を定義する可能性が検討されましたが、Web レガシーの互換性の懸念と、ブラウザが現在生成している順序を変更する意欲についての不確実性のために採用されませんでした。
配列要素に整数インデックスを介してアクセスする場合でも、仕様ではこれらを通常の文字列プロパティキーとして扱います。
const arr=['a', 'b', 'c'];
console.log(arr['0']); // 'a'
// Operand 0 of [] is coerced to string:
console.log(arr[0]); // 'a'
整数インデックスは、配列の length に影響を与える場合と、プロパティキーをリストするときに最初に表示される場合という 2 つの点でのみ特別です。
大まかに言えば、整数インデックスとは、53 ビットの非負の整数に変換して戻すと、同じ値になる文字列のことです。したがって
'10' と '2' は整数インデックスです。'02' は整数インデックスではありません。整数に変換して戻すと、異なる文字列 '2' になります。'3.141' は、3.141 が整数ではないため、整数インデックスではありません。さらに詳しく知るために
次のコードは、トラバース順序「自身のプロパティキー」を示しています。
const obj = {
[Symbol('first')]: true,
'02': true,
'10': true,
'01': true,
'2': true,
[Symbol('second')]: true,
};
Reflect.ownKeys(obj);
// [ '2', '10', '02', '01',
// Symbol('first'), Symbol('second') ]
解説
'2' と '10' は整数インデックスであり、最初に表示され、数値的にソートされます(辞書式順序ではありません)。'02' と '01' は通常の文字列キーであり、次に表示され、obj に追加された順序で表示されます。Symbol('first') と Symbol('second') はシンボルであり、最後に表示され、obj に追加された順序で表示されます。Tab Atkins Jr. による回答
少なくともオブジェクトについては、すべての実装が(現在の仕様に一致する)ほぼ同じ順序を使用しており、多くのコードがその順序に依存して誤って記述されており、異なる順序で列挙すると壊れるからです。ブラウザはこの特定の順序を Web 互換にするために実装する必要があるため、要件として仕様化されました。
Maps/Sets でこの順序を破ることも議論されましたが、そうすると、コードが依存することが不可能な順序を指定する必要がありました。つまり、順序を単に未指定にするのではなく、ランダムにすることを義務付ける必要がありました。これは非常に手間がかかると判断され、作成順序はそれなりに価値があるため(たとえば、Python の OrderedDict を参照)、Maps と Sets は Objects に一致させることに決定しました。
このセクションに関連する仕様の部分を以下に示します。
[[OwnPropertyKeys]] は、Reflect.ownKeys() などで使用されます。EnumerableOwnNames は、Object.keys() などで使用されます。[[Enumerate]] は、for-in で使用されます。プロパティ prop をオブジェクト obj に追加する 2 つの似た方法があります。
obj.prop = 123Object.defineProperty(obj, 'prop', { value: 123 })代入によって、まだ存在しない場合でも、自身のプロパティ prop が作成されないケースが 3 つあります。
prop が存在する場合。この場合、代入は厳格モードで TypeError を引き起こします。prop のセッターが存在する場合。この場合、そのセッターが呼び出されます。prop のゲッターが存在する場合。この場合、厳格モードで TypeError がスローされます。このケースは最初のケースに似ています。これらのケースのいずれも、Object.defineProperty() が自身のプロパティを作成することを妨げません。次のセクションでは、ケース #3 について詳しく見ていきます。
オブジェクト obj が読み取り専用であるプロパティ prop を継承している場合、そのプロパティに代入することはできません。
const proto = Object.defineProperty({}, 'prop', {
writable: false,
configurable: true,
value: 123,
});
const obj = Object.create(proto);
obj.prop = 456;
// TypeError: Cannot assign to read-only property
これは、ゲッターはあるがセッターがない継承されたプロパティの動作に似ています。これは、代入を継承されたプロパティの値を変更するものと見なすことと一致します。代入は非破壊的に行われます。つまり、元のプロパティは変更されず、新しく作成された自身のプロパティによって上書きされます。したがって、継承された読み取り専用プロパティと、継承されたセッターのないプロパティは、どちらも代入による変更を防ぎます。ただし、プロパティを定義することで、強制的に自身のプロパティを作成できます。
const proto = Object.defineProperty({}, 'prop', {
writable: false,
configurable: true,
value: 123,
});
const obj = Object.create(proto);
Object.defineProperty(obj, 'prop', {value: 456});
console.log(obj.prop); // 456
__proto__ プロパティ __proto__(「ダンダープロト」と発音)は、ほとんどの JavaScript エンジンでしばらく前から存在しています。このセクションでは、ECMAScript 6 以前の動作と、ECMAScript 6 での変更点について説明します。
このセクションでは、プロトタイプチェーンが何かを知っていると役立ちます。必要に応じて、「Speaking JavaScript」のセクション「レイヤー 2:オブジェクト間のプロトタイプ関係」を参照してください。
__proto__ JavaScript の各オブジェクトは、1 つ以上のオブジェクトのチェーン、いわゆるプロトタイプチェーンを開始します。各オブジェクトは、内部スロット [[Prototype]] を介して後続オブジェクト、つまりそのプロトタイプを指します(後続がない場合は null です)。このスロットは、言語仕様にのみ存在し、JavaScript から直接アクセスできないため、内部と呼ばれます。ECMAScript 5 では、オブジェクト obj のプロトタイプ p を取得する標準的な方法は次のとおりです。
var p = Object.getPrototypeOf(obj);
既存のオブジェクトのプロトタイプを変更する標準的な方法はありませんが、指定されたプロトタイプ p を持つ新しいオブジェクト obj を作成することはできます。
var obj = Object.create(p);
__proto__ 昔々、Firefox は非標準のプロパティ __proto__ を取得しました。他のブラウザも、その人気のため、最終的にその機能をコピーしました。
ECMAScript 6 より前では、__proto__ はあいまいな方法で動作していました。
var obj = {};
var p = {};
console.log(obj.__proto__ === p); // false
obj.__proto__ = p;
console.log(obj.__proto__ === p); // true
> var obj = {};
> '__proto__' in obj
false
__proto__ による Array のサブクラス化 __proto__ が普及した主な理由は、ES5 で Array のサブクラス MyArray を作成する唯一の方法だったからです。Array のインスタンスは、通常のコンストラクターでは作成できない特殊なオブジェクトでした。そのため、以下のトリックが使われました。
function MyArray() {
var instance = new Array(); // exotic object
instance.__proto__ = MyArray.prototype;
return instance;
}
MyArray.prototype = Object.create(Array.prototype);
MyArray.prototype.customMethod = function (···) { ··· };
ES6 におけるサブクラス化は、ES5 とは異なり、組み込みのサブクラス化をサポートしています。
__proto__ が問題となる理由 主な問題は、__proto__ がオブジェクトレベル(データを保持する通常のプロパティ)とメタレベルの2つのレベルを混在させていることです。
誤って __proto__ をデータ格納のために通常のプロパティ(オブジェクトレベル!)として使用すると、2つのレベルが衝突するため、問題が発生します。ES5 には、その目的のための組み込みのデータ構造がないため、オブジェクトをマップとして悪用する必要があるという事実によって状況が悪化します。マップは任意のキーを保持できる必要がありますが、オブジェクトをマップとして使用する場合、キー '__proto__' を使用することはできません。
理論的には、特殊な名前 __proto__ の代わりにシンボルを使用することで問題を解決できますが、(Object.getPrototypeOf() を介して行われるように)メタメカニズムを完全に分離することが最良のアプローチです。
__proto__ __proto__ が広くサポートされていたため、その動作を ECMAScript 6 で標準化することが決定されました。ただし、その問題の性質のため、非推奨機能として追加されました。これらの機能は、ECMAScript 仕様の Annex B にあり、次のように説明されています。
この付録で定義されている ECMAScript 言語の構文とセマンティクスは、ECMAScript ホストが Web ブラウザーの場合に必要です。この付録の内容は規範的ですが、ECMAScript ホストが Web ブラウザーでない場合はオプションです。
JavaScript には、Web 上の大量のコードで必要とされる、望ましくない機能がいくつかあります。したがって、Web ブラウザーはそれらを実装する必要がありますが、他の JavaScript エンジンは実装する必要はありません。
__proto__ の背後にある魔法を説明するために、ES6 では 2 つのメカニズムが導入されました。
Object.prototype.__proto__ を介して実装されたゲッターとセッター。'__proto__' を、作成されたオブジェクトのプロトタイプを指定するための特別な演算子と見なすことができます。Object.prototype.__proto__ ECMAScript 6 では、Object.prototype に格納されているゲッターとセッターを介して、プロパティ __proto__ の取得と設定が可能です。手動で実装すると、次のようになります。
Object.defineProperty(Object.prototype, '__proto__', {
get() {
const _thisObj = Object(this);
return Object.getPrototypeOf(_thisObj);
},
set(proto) {
if (this === undefined || this === null) {
throw new TypeError();
}
if (!isObject(this)) {
return undefined;
}
if (!isObject(proto)) {
return undefined;
}
const status = Reflect.setPrototypeOf(this, proto);
if (! status) {
throw new TypeError();
}
return undefined;
},
});
function isObject(value) {
return Object(value) === value;
}
__proto__ __proto__ がオブジェクトリテラルで引用符で囲まれていない、または引用符で囲まれたプロパティキーとして出現する場合、そのリテラルによって作成されたオブジェクトのプロトタイプはプロパティ値に設定されます。
> Object.getPrototypeOf({ __proto__: null })
null
> Object.getPrototypeOf({ '__proto__': null })
null
文字列値 '__proto__' を計算されたプロパティキーとして使用しても、プロトタイプは変更されず、独自のプロパティが作成されます。
> const obj = { ['__proto__']: null };
> Object.getPrototypeOf(obj) === Object.prototype
true
> Object.keys(obj)
[ '__proto__' ]
__proto__ の魔法を回避する __proto__ の定義(割り当てではない) ECMAScript 6 では、独自のプロパティ __proto__ を定義した場合、特別な機能はトリガーされず、ゲッター/セッター Object.prototype.__proto__ がオーバーライドされます。
const obj = {};
Object.defineProperty(obj, '__proto__', { value: 123 })
Object.keys(obj); // [ '__proto__' ]
console.log(obj.__proto__); // 123
Object.prototype を持たないオブジェクト __proto__ ゲッター/セッターは、Object.prototype を介して提供されます。したがって、プロトタイプチェーンに Object.prototype がないオブジェクトも、ゲッター/セッターを持っていません。次のコードでは、dict はそのようなオブジェクトの例です。プロトタイプを持ちません。その結果、__proto__ は他のプロパティと同様に機能します。
> const dict = Object.create(null);
> '__proto__' in dict
false
> dict.__proto__ = 'abc';
> dict.__proto__
'abc'
__proto__ と dict オブジェクト オブジェクトを辞書として使用する場合は、プロトタイプがないことが最適です。そのため、プロトタイプのないオブジェクトは、dict オブジェクトとも呼ばれます。ES6 では、dict オブジェクトに対してプロパティキー '__proto__' をエスケープする必要さえありません。特別な機能がトリガーされないためです。
オブジェクトリテラル内の演算子としての __proto__ を使用すると、より簡潔に dict オブジェクトを作成できます。
const dictObj = {
__proto__: null,
yes: true,
no: false,
};
ES6 では、特にキーが固定されていない場合は、dict オブジェクトよりも、組み込みのデータ構造 Map を優先して使用する必要があることに注意してください。
__proto__ と JSON ES6 より前は、JavaScript エンジンで次のことが起こる可能性がありました。
> JSON.parse('{"__proto__": []}') instanceof Array
true
ES6 では、__proto__ がゲッター/セッターであるため、JSON.parse() は問題なく動作します。プロパティを定義するだけで、割り当てないためです(正しく実装されていれば、古いバージョンの V8 は割り当てていました)。
JSON.stringify() も __proto__ の影響を受けません。独自のプロパティのみを考慮するためです。名前が __proto__ の独自のプロパティを持つオブジェクトは正常に機能します。
> JSON.stringify({['__proto__']: true})
'{"__proto__":true}'
__proto__ のサポートを検出する ES6 スタイルの __proto__ のサポートは、エンジンごとに異なります。現状については、kangax の ECMAScript 6 互換性表を参照してください。
次の 2 つのセクションでは、エンジンが 2 種類の __proto__ のいずれかをサポートしているかどうかをプログラムで検出する方法について説明します。
__proto__ ゲッター/セッターの簡単なチェック
var supported = {}.hasOwnProperty.call(Object.prototype, '__proto__');
より洗練されたチェック
var desc = Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
var supported = (
typeof desc.get === 'function' && typeof desc.set === 'function'
);
__proto__ 次のチェックを使用できます。
var supported = Object.getPrototypeOf({__proto__: null}) === null;
__proto__ は「dunder proto」と発音する 二重アンダースコアで名前を括ることは、メタデータ(__proto__ など)とデータ(ユーザー定義のプロパティ)間の名前の衝突を避けるために Python で一般的な慣習です。この慣習は、この目的のためにシンボルを持つようになったため、JavaScript では決して一般的になることはありません。ただし、二重アンダースコアの発音方法については、Python コミュニティにヒントを求めることができます。
次の発音は、Ned Batchelder によって提案されています。
Python でプログラミングする際の厄介な点は、二重アンダースコアがたくさんあることです。たとえば、構文糖の下にある標準のメソッド名には
__getattr__のような名前があり、コンストラクターは__init__、組み込みの演算子は__add__でオーバーロードできます。[…]二重アンダースコアに関する私の問題は、発音が難しいことです。
__init__はどう発音すればいいでしょうか。「アンダースコア アンダー スコア イニット アンダースコア アンダースコア」でしょうか。「アンダー アンダー イニット アンダー アンダー」でしょうか。単に「イニット」とするだけでは、何か重要なことを除外しているように思えます。解決策があります。二重アンダースコアは「dunder」と発音する必要があります。したがって、
__init__は「dunder init dunder」または単に「dunder init」となります。
したがって、__proto__ は「dunder proto」と発音します。この発音が定着する可能性は高く、JavaScript の作成者である Brendan Eich が使用しています。
__proto__ の推奨事項 ES6 が __proto__ を、曖昧なものから理解しやすいものに変えたのは素晴らしいことです。
ただし、私はまだそれを使用しないことをお勧めします。事実上、非推奨の機能であり、コア標準の一部ではありません。すべてのエンジンで実行する必要があるコードでは、そこにあることを信頼できません。
その他の推奨事項
Object.getPrototypeOf() を使用します。Object.create() を優先します。多くのエンジンでパフォーマンスが低下する Object.setPrototypeOf() は避けてください。__proto__ が実際に好きです。プロトタイプ継承を実証したり、dict オブジェクトを作成したりするのに役立ちます。ただし、前述の注意点が適用されます。列挙可能性は、オブジェクトプロパティの属性です。このセクションでは、ECMAScript 6 でどのように機能するかを説明します。まず、属性とは何かを探りましょう。
各オブジェクトは、0 個以上のプロパティを持っています。各プロパティにはキーと 3 つ以上の属性(プロパティのデータを格納する名前付きスロット。つまり、プロパティ自体が JavaScript オブジェクトや、データベースのフィールドを持つレコードによく似ています)があります。
ECMAScript 6 は、次の属性をサポートしています (ES5 も同様)。
enumerable: この属性を false に設定すると、一部の操作でプロパティが非表示になります。configurable: この属性を false に設定すると、プロパティへのいくつかの変更が防止されます (value を除く属性は変更できず、プロパティは削除できませんなど)。value: プロパティの値を保持します。writable: プロパティの値を変更できるかどうかを制御します。get: ゲッター (関数) を保持します。set: セッター (関数) を保持します。プロパティの属性は、Object.getOwnPropertyDescriptor() を介して取得できます。これは、属性を JavaScript オブジェクトとして返します。
> const obj = { foo: 123 };
> Object.getOwnPropertyDescriptor(obj, 'foo')
{ value: 123,
writable: true,
enumerable: true,
configurable: true }
このセクションでは、属性 enumerable が ES6 でどのように機能するかを説明します。他のすべての属性と属性の変更方法については、「Speaking JavaScript」の Sect.「Property Attributes and Property Descriptors」で説明しています。
ECMAScript 5
for-in ループ: 独自の列挙可能プロパティと継承された列挙可能プロパティの文字列キーを走査します。Object.keys(): 列挙可能な独自のプロパティの文字列キーを返します。JSON.stringify(): 文字列キーを持つ列挙可能な独自のプロパティのみを文字列化します。ECMAScript 6
Object.assign(): 列挙可能な自身のプロパティのみをコピーします(文字列キーとシンボルキーの両方が対象です)。for-in は、継承されたプロパティの列挙可能性が問題となる唯一の組み込み操作です。他のすべての操作は、自身のプロパティのみを対象とします。
残念ながら、列挙可能性は非常に特異な機能です。このセクションでは、そのいくつかのユースケースを紹介し、レガシーコードの破損を防ぐ以外には、その有用性が限定的であることを論じます。
for-in ループからプロパティを隠す for-in ループは、オブジェクトのすべての列挙可能なプロパティ(自身のプロパティと継承されたプロパティ)を走査します。そのため、enumerable 属性は、走査すべきでないプロパティを隠すために使用されます。これが、ECMAScript 1 で列挙可能性が導入された理由でした。
非列挙可能なプロパティは、言語の次の場所で発生します。
prototype プロパティは非列挙可能です。 > const desc = Object.getOwnPropertyDescriptor.bind(Object);
> desc(Object.prototype, 'toString').enumerable
false
prototype プロパティは非列挙可能です。 > desc(class {foo() {}}.prototype, 'foo').enumerable
false
length は列挙可能ではありません。つまり、for-in はインデックスのみを走査します。(ただし、代入によってプロパティを追加すると、簡単に列挙可能に変更できます。) > desc([], 'length').enumerable
false
> desc(['a'], '0').enumerable
true
これらのプロパティをすべて非列挙可能にする主な理由は、for-in ループや $.extend() (および継承されたプロパティと自身のプロパティの両方をコピーする同様の操作。次のセクションを参照)を使用するレガシーコードからそれらを隠すためです。両方の操作は ES6 では避けるべきです。それらを隠すことで、レガシーコードが壊れないようにします。
プロパティのコピーに関して、列挙可能性を考慮する2つの重要な歴史的な先例があります。
Object.extend(destination, source)
const obj1 = Object.create({ foo: 123 });
Object.extend({}, obj1); // { foo: 123 }
const obj2 = Object.defineProperty({}, 'foo', {
value: 123,
enumerable: false
});
Object.extend({}, obj2) // {}
$.extend(target, source1, source2, ···) は、source1 などのすべての列挙可能な自身のプロパティと継承されたプロパティを、target の自身のプロパティにコピーします。 const obj1 = Object.create({ foo: 123 });
$.extend({}, obj1); // { foo: 123 }
const obj2 = Object.defineProperty({}, 'foo', {
value: 123,
enumerable: false
});
$.extend({}, obj2) // {}
このプロパティのコピー方法の問題点
標準ライブラリで非列挙可能な唯一のインスタンスプロパティは、配列のプロパティ length です。ただし、そのプロパティは、他のプロパティを介して自動的に更新されるため、隠す必要があるだけです。独自のオブジェクトに対してそのような魔法のプロパティを作成することはできません(Proxyを使用する場合を除きます)。
Object.assign() ES6 では、Object.assign(target, source_1, source_2, ···) を使用して、ソースをターゲットにマージできます。ソースのすべての自身の列挙可能なプロパティが考慮されます(つまり、キーは文字列またはシンボルのいずれかになります)。 Object.assign() は、ソースから値を読み取るために「get」操作を使用し、ターゲットに値を書き込むために「set」操作を使用します。
列挙可能性に関して、Object.assign() は Object.extend() および $.extend() の伝統を引き継いでいます。Yehuda Katz の引用
Object.assign は、すでに流通しているすべての extend() API の轍(わだち)を舗装するでしょう。これらのケースで列挙可能なメソッドをコピーしないという先例が、Object.assign がこの動作をするのに十分な理由だと考えました。
言い換えれば、Object.assign() は、$.extend() (および類似のもの) からのアップグレードパスを念頭に置いて作成されました。そのアプローチは、継承されたプロパティを無視するため、$.extend のアプローチよりもクリーンです。
プロパティを非列挙可能にすると、Object.keys() や for-in ループでは表示できなくなります。これらのメカニズムに関して、プロパティはプライベートです。
ただし、このアプローチにはいくつかの問題があります。
JSON.stringify() から自身のプロパティを隠す JSON.stringify() は、非列挙可能なプロパティをその出力に含めません。したがって、列挙可能性を使用して、どの自身のプロパティを JSON にエクスポートすべきかを判断できます。このユースケースは、前のユースケースであるプロパティをプライベートとしてマークすることと似ています。しかし、JSONへのエクスポートに関するものであり、少し異なる考慮事項が適用されるため、異なっています。たとえば、オブジェクトを JSON から完全に再構築できますか?
オブジェクトを JSON に変換する方法を指定するための別の方法は、toJSON() を使用することです。
const obj = {
foo: 123,
toJSON() {
return { bar: 456 };
},
};
JSON.stringify(obj); // '{"bar":456}'
私は、現在のユースケースでは、列挙可能性よりも toJSON() の方がクリーンだと思います。また、オブジェクトに存在しないプロパティをエクスポートできるため、より多くの制御が可能になります。
一般的に、短い名前は、列挙可能なプロパティのみが考慮されることを意味します。
Object.keys() は非列挙可能なプロパティを無視します。Object.getOwnPropertyNames() はすべてのプロパティ名をリストします。ただし、Reflect.ownKeys() はその規則から逸脱しており、列挙可能性を無視し、すべてのプロパティのキーを返します。さらに、ES6 以降、次の区別がなされています。
したがって、Object.keys() のより適切な名前は Object.names() になります。
列挙可能性は、for-in ループと $.extend() (および同様の操作) からプロパティを隠す場合にのみ適しているように思えます。どちらもレガシー機能であり、新しいコードでは避けるべきです。他のユースケースに関しては
toJSON() メソッドは、オブジェクトを JSON に変換する方法を制御する場合、列挙可能性よりも強力で明示的です。今後の列挙可能性に対する最善の戦略が何であるかはわかりません。もし ES6 で、(古いコードが壊れないようにプロトタイププロパティを非列挙可能にすることを除いて) 存在しないふりをしていたら、最終的には列挙可能性を非推奨にできたかもしれません。ただし、列挙可能性を考慮する Object.assign() は、その戦略に反します(ただし、後方互換性のための正当な理由があります)。
私自身の ES6 コードでは、クラスの prototype メソッドが非列挙可能である場合を除いて、(暗黙的に) 列挙可能性を使用していません。
最後に、インタラクティブなコマンドラインを使用する場合、(Reflect.ownKeys だけでなく) オブジェクトの *すべての* プロパティキーを返す操作が時々ありません。そのような操作は、オブジェクトの内容の良い概要を提供してくれるでしょう。
このセクションでは、次の既知のシンボルをプロパティキーとして使用することで、基本的な言語操作をカスタマイズする方法について説明します。
Symbol.hasInstance (メソッド)C が x instanceof C の動作をカスタマイズできるようにします。Symbol.toPrimitive (メソッド)Symbol.toStringTag (文字列)obj のデフォルトの文字列記述を計算するために Object.prototype.toString() によって呼び出されます。 '[object ' + obj[Symbol.toStringTag] + ']'
Symbol.unscopables (オブジェクト)with ステートメントから一部のプロパティを隠すことができるようにします。Symbol.hasInstance (メソッド) オブジェクト C は、次のシグネチャを持つキー Symbol.hasInstance のメソッドを介して、instanceof 演算子の動作をカスタマイズできます。
[Symbol.hasInstance](potentialInstance : any)
x instanceof C は ES6 で次のように動作します。
C がオブジェクトでない場合は、TypeError をスローします。C[Symbol.hasInstance](x) を呼び出し、結果をブール値に強制変換して返します。C は呼び出し可能である必要があり、C.prototype は x のプロトタイプチェーンにある必要がありますなど)。このキーを持つ標準ライブラリ内の唯一のメソッドは次のとおりです。
Function.prototype[Symbol.hasInstance]()これは、すべての関数(クラスを含む)がデフォルトで使用する instanceof の実装です。仕様書からの引用
このプロパティは、バインドされた関数のターゲット関数をグローバルに公開するために使用できる改ざんを防ぐために、書き込み不可および構成不可です。
改ざんが可能なのは、従来の instanceof アルゴリズム、OrdinaryHasInstance() が、バインドされた関数を検出した場合、ターゲット関数に instanceof を適用するためです。
このプロパティは読み取り専用であるため、以前に説明したように、代入を使用して上書きすることはできません。
例として、その「インスタンス」が Object のインスタンス(したがって、プロトタイプチェーンに Object.prototype を持つ)であるオブジェクトだけでなく、すべてのオブジェクトであるオブジェクト ReferenceType を実装しましょう。
const ReferenceType = {
[Symbol.hasInstance](value) {
return (value !== null
&& (typeof value === 'object'
|| typeof value === 'function'));
}
};
const obj1 = {};
console.log(obj1 instanceof Object); // true
console.log(obj1 instanceof ReferenceType); // true
const obj2 = Object.create(null);
console.log(obj2 instanceof Object); // false
console.log(obj2 instanceof ReferenceType); // true
Symbol.toPrimitive (メソッド) Symbol.toPrimitive を使用すると、オブジェクトがプリミティブ値に *強制変換*(自動的に変換)される方法をカスタマイズできます。
多くの JavaScript 操作では、値を必要な型に強制変換します。
*) は、オペランドを数値に強制変換します。new Date(year, month, date) は、パラメータを数値に強制変換します。parseInt(string , radix) は、最初のパラメータを文字列に強制変換します。以下は、値が強制変換される最も一般的な型です。
trueを、falsyな値に対してはfalseを返します。オブジェクトは常にtruthyです(new Boolean(false)であっても)。null → 0, true → 1, '123' → 123, など)。null → 'null', true → 'true', 123 → '123', など)。b は new Boolean(b) を介して、数値 n は new Number(n) を介して、など)。したがって、数値と文字列の場合、最初のステップは、値が何らかの種類のプリミティブであることを確認することです。これは、仕様内部の操作であるToPrimitive()によって処理されます。これには3つのモードがあります。
デフォルトモードは、以下でのみ使用されます。
==)+)new Date(value) (パラメータが1つのみの場合!)値がプリミティブである場合、ToPrimitive()は既に完了しています。それ以外の場合、値はオブジェクトobjであり、以下のようにプリミティブに変換されます。
obj.valueOf()の結果がプリミティブであればそれを返します。それ以外の場合、obj.toString()の結果がプリミティブであればそれを返します。それ以外の場合は、TypeErrorをスローします。toString()が最初に呼び出され、valueOf()が2番目に呼び出されます。この通常のアルゴリズムは、オブジェクトに次のシグネチャを持つメソッドを与えることでオーバーライドできます。
[Symbol.toPrimitive](hint : 'default' | 'string' | 'number')
標準ライブラリには、そのようなメソッドが2つあります。
Symbol.prototype[Symbol.toPrimitive](hint) は、toString()が呼び出されるのを防ぎます(これにより例外がスローされます)。Date.prototype[Symbol.toPrimitive](hint) このメソッドは、デフォルトのアルゴリズムから逸脱した動作を実装します。仕様を引用すると、「Dateオブジェクトは、組み込みのECMAScriptオブジェクトの中で、'default'を'string'と同等に扱う点で独特です。他のすべての組み込みのECMAScriptオブジェクトは、'default'を'number'と同等に扱います。」次のコードは、強制変換がオブジェクトobjにどのように影響するかを示しています。
const obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};
console.log(2 * obj); // 246
console.log(3 + obj); // '3default'
console.log(obj == 'default'); // true
console.log(String(obj)); // 'str'
Symbol.toStringTag (文字列) ES5以前では、各オブジェクトは内部の所有プロパティ[[Class]]を持っており、その値がその型を示唆していました。直接アクセスすることはできませんでしたが、その値はObject.prototype.toString()によって返される文字列の一部でした。そのため、このメソッドはtypeofの代替として型チェックに使用されていました。
ES6では、内部スロット[[Class]]はもう存在せず、Object.prototype.toString()を型チェックに使用することは推奨されていません。このメソッドの後方互換性を確保するために、キーがSymbol.toStringTagであるパブリックプロパティが導入されました。これは[[Class]]の代わりと言えるでしょう。
Object.prototype.toString()は現在、次のように動作します。
thisをオブジェクトobjに変換します。objのtoString tag tstを決定します。'[object ' + tst + ']'を返します。さまざまな種類のオブジェクトのデフォルト値は、次の表に示されています。
| 値 | toStringタグ |
|---|---|
undefined |
'Undefined' |
null |
'Null' |
| Arrayオブジェクト | 'Array' |
| Stringオブジェクト | 'String' |
arguments |
'Arguments' |
| 呼び出し可能なもの | 'Function' |
| Errorオブジェクト | 'Error' |
| Booleanオブジェクト | 'Boolean' |
| Numberオブジェクト | 'Number' |
| Dateオブジェクト | 'Date' |
| RegExpオブジェクト | 'RegExp' |
| (その他) | 'Object' |
左側の列のチェックのほとんどは、内部スロットを調べることで実行されます。たとえば、オブジェクトが内部スロット[[Call]]を持っている場合、それは呼び出し可能です。
次のやり取りは、デフォルトのtoStringタグを示しています。
> Object.prototype.toString.call(null)
'[object Null]'
> Object.prototype.toString.call([])
'[object Array]'
> Object.prototype.toString.call({})
'[object Object]'
> Object.prototype.toString.call(Object.create(null))
'[object Object]'
オブジェクトが、キーがSymbol.toStringTagである(独自のまたは継承された)プロパティを持っている場合、その値はデフォルトのtoStringタグをオーバーライドします。例えば
> ({}.toString())
'[object Object]'
> ({[Symbol.toStringTag]: 'Foo'}.toString())
'[object Foo]'
ユーザー定義クラスのインスタンスは、デフォルトのtoStringタグ(オブジェクトの)を取得します
class Foo { }
console.log(new Foo().toString()); // [object Object]
デフォルトをオーバーライドする1つのオプションは、ゲッターを介することです
class Bar {
get [Symbol.toStringTag]() {
return 'Bar';
}
}
console.log(new Bar().toString()); // [object Bar]
JavaScript標準ライブラリには、次のカスタムtoStringタグがあります。グローバル名を持たないオブジェクトは、パーセント記号で囲まれて引用されます(例:%TypedArray%)。
JSON[Symbol.toStringTag] → 'JSON'Math[Symbol.toStringTag] → 'Math'M: M[Symbol.toStringTag] → 'Module'ArrayBuffer.prototype[Symbol.toStringTag] → 'ArrayBuffer'DataView.prototype[Symbol.toStringTag] → 'DataView'Map.prototype[Symbol.toStringTag] → 'Map'Promise.prototype[Symbol.toStringTag] → 'Promise'Set.prototype[Symbol.toStringTag] → 'Set'get %TypedArray%.prototype[Symbol.toStringTag] → 'Uint8Array' などWeakMap.prototype[Symbol.toStringTag] → 'WeakMap'WeakSet.prototype[Symbol.toStringTag] → 'WeakSet'%MapIteratorPrototype%[Symbol.toStringTag] → 'Map Iterator'%SetIteratorPrototype%[Symbol.toStringTag] → 'Set Iterator'%StringIteratorPrototype%[Symbol.toStringTag] → 'String Iterator'Symbol.prototype[Symbol.toStringTag] → 'Symbol'Generator.prototype[Symbol.toStringTag] → 'Generator'GeneratorFunction.prototype[Symbol.toStringTag] → 'GeneratorFunction'キーがSymbol.toStringTagである組み込みプロパティはすべて、次のプロパティ記述子を持っています
{
writable: false,
enumerable: false,
configurable: true,
}
前に述べたように、これらのプロパティは読み取り専用であるため、代入を使用してオーバーライドすることはできません。
Symbol.unscopables (Object) Symbol.unscopablesを使用すると、オブジェクトはwithステートメントから一部のプロパティを隠すことができます。
そうする理由は、TC39が古いコードを壊すことなくArray.prototypeに新しいメソッドを追加できるようにするためです。現在のコードでは、厳密モードで禁止されているため、withをほとんど使用しておらず、したがってES6モジュール(暗黙的に厳密モード)を使用していることに注意してください。
Array.prototypeにメソッドを追加すると、withを使用するコード(広く展開されているExt JS 4.2.1など)が壊れるのはなぜですか?次のコードを見てください。プロパティArray.prototype.valuesの存在は、Arrayで呼び出すとfoo()を壊します
function foo(values) {
with (values) {
console.log(values.length); // abc (*)
}
}
Array.prototype.values = { length: 'abc' };
foo([]);
withステートメント内では、valuesのすべてのプロパティがローカル変数になり、values自体もシャドウします。したがって、valuesにプロパティvaluesがある場合、*行のステートメントはvalues.lengthではなくvalues.values.lengthをログに記録します。
Symbol.unscopablesは標準ライブラリで一度だけ使用されます
Array.prototype[Symbol.unscopables]
withステートメントからは隠されています):copyWithin、entries、fill、find、findIndex、keys、valuessuperを使用できますか? はい、使用できます!詳細については、クラスに関する章で説明します。