14. クラス以外の新しいOOP機能
目次
本書へのご支援をお願いします: 購入 (PDF, EPUB, MOBI) または 寄付
(広告です。ブロックしないでください。)

14. クラス以外の新しいOOP機能

クラス (については次の章で説明します) は、ECMAScript 6 における最も重要な新しいOOP機能です。しかし、オブジェクトリテラルのための新機能や、Object の新しいユーティリティメソッドも含まれています。この章では、それらについて説明します。



14.1 概要

14.1.1 新しいオブジェクトリテラル機能

メソッド定義

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

算出プロパティキーの主なユースケースは、シンボルをプロパティキーとして簡単に使用できるようにすることです。

14.1.2 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}

14.2 オブジェクトリテラルの新機能

14.2.1 メソッド定義

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* () {
        ···
    }
};

14.2.2 プロパティ値の省略記法

プロパティ値の省略記法を使用すると、オブジェクトリテラルでのプロパティの定義を省略できます。プロパティ値を指定する変数の名前がプロパティキーでもある場合は、キーを省略できます。これは次のようになります。

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つは、複数の戻り値です(これについては分割代入の章で説明しています)。

14.2.3 算出プロパティキー

プロパティを設定するときにキーを指定する方法は2つあることを覚えておいてください。

  1. 固定名を使用: obj.foo = true;
  2. 式を使用: 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行から始まるジェネレータメソッド定義のため、反復可能です。

14.3 Object の新しいメソッド

14.3.1 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() の仕組みを詳しく見てみましょう

14.3.1.1 すべての自身のプロパティのコピー

これは、ゲッターとセッターを正しく転送し、ターゲットでセッターを呼び出さずに、すべての自身のプロパティ(列挙可能なプロパティだけでなく)をコピーする方法です

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」の「プロパティ属性とプロパティ記述子」のセクションを参照してください。

14.3.1.2 注意: Object.assign() はメソッドの移動には適していません

一方では、super を使用するメソッドを移動することはできません。このようなメソッドには、それが作成されたオブジェクトに結び付ける内部スロット [[HomeObject]] があります。Object.assign() を介して移動した場合、元のオブジェクトのスーパープロパティを参照し続けます。詳細については、クラスの章のセクションで説明します。

他方では、オブジェクトリテラルで作成されたメソッドをクラスのプロトタイプに移動する場合、列挙可能性が正しくありません。前者のメソッドはすべて列挙可能です(そうでなければ、Object.assign() はそれらを認識しません)。しかし、プロトタイプには通常、列挙可能でないメソッドのみがあります。

14.3.1.3 Object.assign() のユースケース

いくつかのユースケースを見てみましょう。

14.3.1.3.1 this にプロパティを追加する

コンストラクターで Object.assign() を使用して this にプロパティを追加できます

class Point {
    constructor(x, y) {
        Object.assign(this, {x, y});
    }
}
14.3.1.3.2 オブジェクトプロパティのデフォルト値を提供する

Object.assign() は、不足しているプロパティのデフォルトを埋めるのにも役立ちます。次の例では、プロパティのデフォルト値を持つオブジェクト DEFAULTS と、データを持つオブジェクト options があります。

const DEFAULTS = {
    logLevel: 0,
    outputFormat: 'html'
};
function processContent(options) {
    options = Object.assign({}, DEFAULTS, options); // (A)
    ···
}

A行では、新しいオブジェクトを作成し、デフォルトをコピーしてから、options をコピーしてデフォルトをオーバーライドしました。Object.assign() はこれらの操作の結果を返し、それを options に代入します。

14.3.1.3.3 オブジェクトにメソッドを追加する

別のユースケースは、オブジェクトにメソッドを追加することです

Object.assign(SomeClass.prototype, {
    someMethod(arg1, arg2) {
        ···
    },
    anotherMethod() {
        ···
    }
});

関数を手動で代入することもできますが、その場合は、メソッド定義の優れた構文がなく、毎回 SomeClass.prototype に言及する必要があります

SomeClass.prototype.someMethod = function (arg1, arg2) {
    ···
};
SomeClass.prototype.anotherMethod = function () {
    ···
};
14.3.1.3.4 オブジェクトのクローンを作成する

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);
}

14.3.2 Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols(obj) は、obj のすべての自身の(継承されていない)シンボル値のプロパティキーを取得します。これは、すべての文字列値の自身のプロパティキーを取得する Object.getOwnPropertyNames() を補完します。プロパティの走査の詳細については、後のセクションを参照してください。

14.3.3 Object.is(value1, value2)

厳密等価演算子(===)は、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

その他はすべて === と同じように比較されます。

14.3.3.1 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 を見つけません。

14.3.4 Object.setPrototypeOf(obj, proto)

このメソッドは、obj のプロトタイプを proto に設定します。ECMAScript 5 での非標準的な方法は、多くのエンジンでサポートされている 特殊なプロパティ __proto__ への代入によるものです。プロトタイプを設定する推奨方法は、ECMAScript 5 と同じく、Object.create() を介してオブジェクトの作成時に行うことです。これは、最初にオブジェクトを作成してからプロトタイプを設定するよりも常に高速です。明らかに、既存のオブジェクトのプロトタイプを変更したい場合は機能しません。

14.4 ES6 でのプロパティのトラバース

14.4.1 プロパティをトラバースする 5 つの操作

ECMAScript 6 では、プロパティのキーは文字列またはシンボルのいずれかになります。以下は、オブジェクト obj のプロパティキーをトラバースする 5 つの操作です。

14.4.2 プロパティのトラバース順序

ES6 は、プロパティの 2 つのトラバース順序を定義しています。

自身のプロパティキー

列挙可能な自身の名前

for-in がプロパティをトラバースする順序は定義されていません。Allen Wirfs-Brock の引用

歴史的に、for-in の順序は定義されておらず、ブラウザの実装間では、生成する順序(およびその他の詳細)にばらつきがありました。ES5 では、Object.keys と、キーを for-in と同じ順序にする必要があるという要件が追加されました。ES5 と ES6 の両方の開発中、特定の for-in の順序を定義する可能性が検討されましたが、Web レガシーの互換性の懸念と、ブラウザが現在生成している順序を変更する意欲についての不確実性のために採用されませんでした。

14.4.2.1 整数インデックス

配列要素に整数インデックスを介してアクセスする場合でも、仕様ではこれらを通常の文字列プロパティキーとして扱います。

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 ビットの非負の整数に変換して戻すと、同じ値になる文字列のことです。したがって

さらに詳しく知るために

14.4.2.2

次のコードは、トラバース順序「自身のプロパティキー」を示しています。

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

解説

14.4.2.3 仕様がプロパティキーが返される順序を標準化する理由

Tab Atkins Jr. による回答

少なくともオブジェクトについては、すべての実装が(現在の仕様に一致する)ほぼ同じ順序を使用しており、多くのコードがその順序に依存して誤って記述されており、異なる順序で列挙すると壊れるからです。ブラウザはこの特定の順序を Web 互換にするために実装する必要があるため、要件として仕様化されました。

Maps/Sets でこの順序を破ることも議論されましたが、そうすると、コードが依存することが不可能な順序を指定する必要がありました。つまり、順序を単に未指定にするのではなく、ランダムにすることを義務付ける必要がありました。これは非常に手間がかかると判断され、作成順序はそれなりに価値があるため(たとえば、Python の OrderedDict を参照)、Maps と Sets は Objects に一致させることに決定しました。

14.4.2.4 仕様におけるプロパティの順序

このセクションに関連する仕様の部分を以下に示します。

14.5 プロパティの代入と定義

プロパティ prop をオブジェクト obj に追加する 2 つの似た方法があります。

代入によって、まだ存在しない場合でも、自身のプロパティ prop が作成されないケースが 3 つあります。

  1. プロトタイプチェーンに読み取り専用プロパティ prop が存在する場合。この場合、代入は厳格モードで TypeError を引き起こします。
  2. プロトタイプチェーンに prop のセッターが存在する場合。この場合、そのセッターが呼び出されます。
  3. プロトタイプチェーンに、セッターのない prop のゲッターが存在する場合。この場合、厳格モードで TypeError がスローされます。このケースは最初のケースに似ています。

これらのケースのいずれも、Object.defineProperty() が自身のプロパティを作成することを妨げません。次のセクションでは、ケース #3 について詳しく見ていきます。

14.5.1 継承された読み取り専用プロパティの上書き

オブジェクト 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

14.6 ECMAScript 6 の __proto__

プロパティ __proto__(「ダンダープロト」と発音)は、ほとんどの JavaScript エンジンでしばらく前から存在しています。このセクションでは、ECMAScript 6 以前の動作と、ECMAScript 6 での変更点について説明します。

このセクションでは、プロトタイプチェーンが何かを知っていると役立ちます。必要に応じて、「Speaking JavaScript」のセクション「レイヤー 2:オブジェクト間のプロトタイプ関係」を参照してください。

14.6.1 ECMAScript 6 以前の __proto__

14.6.1.1 プロトタイプ

JavaScript の各オブジェクトは、1 つ以上のオブジェクトのチェーン、いわゆるプロトタイプチェーンを開始します。各オブジェクトは、内部スロット [[Prototype]] を介して後続オブジェクト、つまりそのプロトタイプを指します(後続がない場合は null です)。このスロットは、言語仕様にのみ存在し、JavaScript から直接アクセスできないため、内部と呼ばれます。ECMAScript 5 では、オブジェクト obj のプロトタイプ p を取得する標準的な方法は次のとおりです。

var p = Object.getPrototypeOf(obj);

既存のオブジェクトのプロトタイプを変更する標準的な方法はありませんが、指定されたプロトタイプ p を持つ新しいオブジェクト obj を作成することはできます。

var obj = Object.create(p);
14.6.1.2 __proto__

昔々、Firefox は非標準のプロパティ __proto__ を取得しました。他のブラウザも、その人気のため、最終的にその機能をコピーしました。

ECMAScript 6 より前では、__proto__ はあいまいな方法で動作していました。

14.6.1.3 __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 とは異なり、組み込みのサブクラス化をサポートしています。

14.6.1.4 ES5 で __proto__ が問題となる理由

主な問題は、__proto__ がオブジェクトレベル(データを保持する通常のプロパティ)とメタレベルの2つのレベルを混在させていることです。

誤って __proto__ をデータ格納のために通常のプロパティ(オブジェクトレベル!)として使用すると、2つのレベルが衝突するため、問題が発生します。ES5 には、その目的のための組み込みのデータ構造がないため、オブジェクトをマップとして悪用する必要があるという事実によって状況が悪化します。マップは任意のキーを保持できる必要がありますが、オブジェクトをマップとして使用する場合、キー '__proto__' を使用することはできません。

理論的には、特殊な名前 __proto__ の代わりにシンボルを使用することで問題を解決できますが、(Object.getPrototypeOf() を介して行われるように)メタメカニズムを完全に分離することが最良のアプローチです。

14.6.2 ECMAScript 6 における 2 種類の __proto__

__proto__ が広くサポートされていたため、その動作を ECMAScript 6 で標準化することが決定されました。ただし、その問題の性質のため、非推奨機能として追加されました。これらの機能は、ECMAScript 仕様の Annex B にあり、次のように説明されています。

この付録で定義されている ECMAScript 言語の構文とセマンティクスは、ECMAScript ホストが Web ブラウザーの場合に必要です。この付録の内容は規範的ですが、ECMAScript ホストが Web ブラウザーでない場合はオプションです。

JavaScript には、Web 上の大量のコードで必要とされる、望ましくない機能がいくつかあります。したがって、Web ブラウザーはそれらを実装する必要がありますが、他の JavaScript エンジンは実装する必要はありません。

__proto__ の背後にある魔法を説明するために、ES6 では 2 つのメカニズムが導入されました。

14.6.2.1 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;
}
14.6.2.2 オブジェクトリテラルにおける演算子としてのプロパティキー __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__' ]

14.6.3 __proto__ の魔法を回避する

14.6.3.1 __proto__ の定義(割り当てではない)

ECMAScript 6 では、独自のプロパティ __proto__定義した場合、特別な機能はトリガーされず、ゲッター/セッター Object.prototype.__proto__ がオーバーライドされます。

const obj = {};
Object.defineProperty(obj, '__proto__', { value: 123 })

Object.keys(obj); // [ '__proto__' ]
console.log(obj.__proto__); // 123
14.6.3.2 プロトタイプとして Object.prototype を持たないオブジェクト

__proto__ ゲッター/セッターは、Object.prototype を介して提供されます。したがって、プロトタイプチェーンに Object.prototype がないオブジェクトも、ゲッター/セッターを持っていません。次のコードでは、dict はそのようなオブジェクトの例です。プロトタイプを持ちません。その結果、__proto__ は他のプロパティと同様に機能します。

> const dict = Object.create(null);
> '__proto__' in dict
false
> dict.__proto__ = 'abc';
> dict.__proto__
'abc'
14.6.3.3 __proto__ と dict オブジェクト

オブジェクトを辞書として使用する場合は、プロトタイプがないことが最適です。そのため、プロトタイプのないオブジェクトは、dict オブジェクトとも呼ばれます。ES6 では、dict オブジェクトに対してプロパティキー '__proto__' をエスケープする必要さえありません。特別な機能がトリガーされないためです。

オブジェクトリテラル内の演算子としての __proto__ を使用すると、より簡潔に dict オブジェクトを作成できます。

const dictObj = {
    __proto__: null,
    yes: true,
    no: false,
};

ES6 では、特にキーが固定されていない場合は、dict オブジェクトよりも、組み込みのデータ構造 Map を優先して使用する必要があることに注意してください。

14.6.3.4 __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}'

14.6.4 ES6 スタイルの __proto__ のサポートを検出する

ES6 スタイルの __proto__ のサポートは、エンジンごとに異なります。現状については、kangax の ECMAScript 6 互換性表を参照してください。

次の 2 つのセクションでは、エンジンが 2 種類の __proto__ のいずれかをサポートしているかどうかをプログラムで検出する方法について説明します。

14.6.4.1 機能: ゲッター/セッターとしての __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'
);
14.6.4.2 機能: オブジェクトリテラル内の演算子としての __proto__

次のチェックを使用できます。

var supported = Object.getPrototypeOf({__proto__: null}) === null;

14.6.5 __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 が使用しています。

14.6.6 __proto__ の推奨事項

ES6 が __proto__ を、曖昧なものから理解しやすいものに変えたのは素晴らしいことです。

ただし、私はまだそれを使用しないことをお勧めします。事実上、非推奨の機能であり、コア標準の一部ではありません。すべてのエンジンで実行する必要があるコードでは、そこにあることを信頼できません。

その他の推奨事項

14.7 ECMAScript 6 における列挙可能性

列挙可能性は、オブジェクトプロパティの属性です。このセクションでは、ECMAScript 6 でどのように機能するかを説明します。まず、属性とは何かを探りましょう。

14.7.1 プロパティ属性

各オブジェクトは、0 個以上のプロパティを持っています。各プロパティにはキーと 3 つ以上の属性(プロパティのデータを格納する名前付きスロット。つまり、プロパティ自体が JavaScript オブジェクトや、データベースのフィールドを持つレコードによく似ています)があります。

ECMAScript 6 は、次の属性をサポートしています (ES5 も同様)。

プロパティの属性は、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」で説明しています。

14.7.2 列挙可能性の影響を受ける構造

ECMAScript 5

ECMAScript 6

for-in は、継承されたプロパティの列挙可能性が問題となる唯一の組み込み操作です。他のすべての操作は、自身のプロパティのみを対象とします。

14.7.3 列挙可能性のユースケース

残念ながら、列挙可能性は非常に特異な機能です。このセクションでは、そのいくつかのユースケースを紹介し、レガシーコードの破損を防ぐ以外には、その有用性が限定的であることを論じます。

14.7.3.1 ユースケース: for-in ループからプロパティを隠す

for-in ループは、オブジェクトのすべての列挙可能なプロパティ(自身のプロパティと継承されたプロパティ)を走査します。そのため、enumerable 属性は、走査すべきでないプロパティを隠すために使用されます。これが、ECMAScript 1 で列挙可能性が導入された理由でした。

14.7.3.1.1 言語における非列挙可能性

非列挙可能なプロパティは、言語の次の場所で発生します。

これらのプロパティをすべて非列挙可能にする主な理由は、for-in ループや $.extend() (および継承されたプロパティと自身のプロパティの両方をコピーする同様の操作。次のセクションを参照)を使用するレガシーコードからそれらを隠すためです。両方の操作は ES6 では避けるべきです。それらを隠すことで、レガシーコードが壊れないようにします。

14.7.3.2 ユースケース: コピーしないプロパティとしてマークする
14.7.3.2.1 歴史的な先例

プロパティのコピーに関して、列挙可能性を考慮する2つの重要な歴史的な先例があります。

このプロパティのコピー方法の問題点

標準ライブラリで非列挙可能な唯一のインスタンスプロパティは、配列のプロパティ length です。ただし、そのプロパティは、他のプロパティを介して自動的に更新されるため、隠す必要があるだけです。独自のオブジェクトに対してそのような魔法のプロパティを作成することはできません(Proxyを使用する場合を除きます)。

14.7.3.2.2 ES6: 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 のアプローチよりもクリーンです。

14.7.3.3 プロパティをプライベートとしてマークする

プロパティを非列挙可能にすると、Object.keys()for-in ループでは表示できなくなります。これらのメカニズムに関して、プロパティはプライベートです。

ただし、このアプローチにはいくつかの問題があります。

14.7.3.4 JSON.stringify() から自身のプロパティを隠す

JSON.stringify() は、非列挙可能なプロパティをその出力に含めません。したがって、列挙可能性を使用して、どの自身のプロパティを JSON にエクスポートすべきかを判断できます。このユースケースは、前のユースケースであるプロパティをプライベートとしてマークすることと似ています。しかし、JSONへのエクスポートに関するものであり、少し異なる考慮事項が適用されるため、異なっています。たとえば、オブジェクトを JSON から完全に再構築できますか?

オブジェクトを JSON に変換する方法を指定するための別の方法は、toJSON() を使用することです。

const obj = {
    foo: 123,
    toJSON() {
        return { bar: 456 };
    },
};
JSON.stringify(obj); // '{"bar":456}'

私は、現在のユースケースでは、列挙可能性よりも toJSON() の方がクリーンだと思います。また、オブジェクトに存在しないプロパティをエクスポートできるため、より多くの制御が可能になります。

14.7.4 命名の不一致

一般的に、短い名前は、列挙可能なプロパティのみが考慮されることを意味します。

ただし、Reflect.ownKeys() はその規則から逸脱しており、列挙可能性を無視し、すべてのプロパティのキーを返します。さらに、ES6 以降、次の区別がなされています。

したがって、Object.keys() のより適切な名前は Object.names() になります。

14.7.5 今後の展望

列挙可能性は、for-in ループと $.extend() (および同様の操作) からプロパティを隠す場合にのみ適しているように思えます。どちらもレガシー機能であり、新しいコードでは避けるべきです。他のユースケースに関しては

今後の列挙可能性に対する最善の戦略が何であるかはわかりません。もし ES6 で、(古いコードが壊れないようにプロトタイププロパティを非列挙可能にすることを除いて) 存在しないふりをしていたら、最終的には列挙可能性を非推奨にできたかもしれません。ただし、列挙可能性を考慮する Object.assign() は、その戦略に反します(ただし、後方互換性のための正当な理由があります)。

私自身の ES6 コードでは、クラスの prototype メソッドが非列挙可能である場合を除いて、(暗黙的に) 列挙可能性を使用していません。

最後に、インタラクティブなコマンドラインを使用する場合、(Reflect.ownKeys だけでなく) オブジェクトの *すべての* プロパティキーを返す操作が時々ありません。そのような操作は、オブジェクトの内容の良い概要を提供してくれるでしょう。

14.8 既知のシンボルによる基本的な言語操作のカスタマイズ

このセクションでは、次の既知のシンボルをプロパティキーとして使用することで、基本的な言語操作をカスタマイズする方法について説明します。

14.8.1 プロパティキー Symbol.hasInstance (メソッド)

オブジェクト C は、次のシグネチャを持つキー Symbol.hasInstance のメソッドを介して、instanceof 演算子の動作をカスタマイズできます。

[Symbol.hasInstance](potentialInstance : any)

x instanceof C は ES6 で次のように動作します。

14.8.1.1 標準ライブラリでの使用

このキーを持つ標準ライブラリ内の唯一のメソッドは次のとおりです。

これは、すべての関数(クラスを含む)がデフォルトで使用する instanceof の実装です。仕様書からの引用

このプロパティは、バインドされた関数のターゲット関数をグローバルに公開するために使用できる改ざんを防ぐために、書き込み不可および構成不可です。

改ざんが可能なのは、従来の instanceof アルゴリズム、OrdinaryHasInstance() が、バインドされた関数を検出した場合、ターゲット関数に instanceof を適用するためです。

このプロパティは読み取り専用であるため、以前に説明したように、代入を使用して上書きすることはできません。

14.8.1.2 例: 値がオブジェクトかどうかをチェックする

例として、その「インスタンス」が 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

14.8.2 プロパティキー Symbol.toPrimitive (メソッド)

Symbol.toPrimitive を使用すると、オブジェクトがプリミティブ値に *強制変換*(自動的に変換)される方法をカスタマイズできます。

多くの JavaScript 操作では、値を必要な型に強制変換します。

以下は、値が強制変換される最も一般的な型です。

したがって、数値と文字列の場合、最初のステップは、値が何らかの種類のプリミティブであることを確認することです。これは、仕様内部の操作であるToPrimitive()によって処理されます。これには3つのモードがあります。

デフォルトモードは、以下でのみ使用されます。

値がプリミティブである場合、ToPrimitive()は既に完了しています。それ以外の場合、値はオブジェクトobjであり、以下のようにプリミティブに変換されます。

この通常のアルゴリズムは、オブジェクトに次のシグネチャを持つメソッドを与えることでオーバーライドできます。

[Symbol.toPrimitive](hint : 'default' | 'string' | 'number')

標準ライブラリには、そのようなメソッドが2つあります。

14.8.2.1

次のコードは、強制変換がオブジェクト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'

14.8.3 プロパティキー Symbol.toStringTag (文字列)

ES5以前では、各オブジェクトは内部の所有プロパティ[[Class]]を持っており、その値がその型を示唆していました。直接アクセスすることはできませんでしたが、その値はObject.prototype.toString()によって返される文字列の一部でした。そのため、このメソッドはtypeofの代替として型チェックに使用されていました。

ES6では、内部スロット[[Class]]はもう存在せず、Object.prototype.toString()を型チェックに使用することは推奨されていません。このメソッドの後方互換性を確保するために、キーがSymbol.toStringTagであるパブリックプロパティが導入されました。これは[[Class]]の代わりと言えるでしょう。

Object.prototype.toString()は現在、次のように動作します。

14.8.3.1 デフォルトのtoStringタグ

さまざまな種類のオブジェクトのデフォルト値は、次の表に示されています。

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]'
14.8.3.2 デフォルトのtoStringタグのオーバーライド

オブジェクトが、キーが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%)。

キーがSymbol.toStringTagである組み込みプロパティはすべて、次のプロパティ記述子を持っています

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

前に述べたように、これらのプロパティは読み取り専用であるため、代入を使用してオーバーライドすることはできません。

14.8.4 プロパティキー 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は標準ライブラリで一度だけ使用されます

14.9 FAQ:オブジェクトリテラル

14.9.1 オブジェクトリテラルでsuperを使用できますか?

はい、使用できます!詳細については、クラスに関する章で説明します。

次へ:15. クラス