オブジェクトとその作成プロセス、ES6 の構文糖、プロトタイプパターン、コンストラクタ関数を理解する
プロトタイプチェーン、コンストラクタ関数の盗用、コンビネーション継承、ベストプラクティスなどを理解する
オブジェクトを理解する#
ECMA-262 はオブジェクトを属性の無秩序な集合として定義し、各属性またはメソッドには表すための名前があり、ハッシュテーブルのように考えることができ、値はデータ / 関数である。
- 例 1
// new Object()を使用して作成
let person = new Object();
person.name = "cosine";
person.age = 29
person.job = "ソフトウェアエンジニア"; 
person.sayName = function() { 
 console.log(this.name); 
}; 
// オブジェクトリテラルを使用して作成
let person = {
  name: 'cosine',
  age: 29, 
  job: "ソフトウェアエンジニア", 
  sayName() { 
    console.log(this.name); 
  }
}
上記の 2 つのオブジェクトは同等であり、その属性とメソッドは同じである。
考えてみてください: new のプロセスで何が行われたのか? 後で触れます。
属性の種類#
ECMA-262 は、属性の特性を記述するためにいくつかの内部特性を使用します。これらの特性は、JavaScript の実装エンジンの仕様によって定義されています。したがって、開発者は JavaScript 内でこれらの特性に直接アクセスすることはできません。特性を内部特性として識別するために、仕様は2 つの中括弧で特性の名前を囲みます。例えば、[[Enumerable]]
属性は 2 種類に分けられます:データ属性と アクセサ属性
データ属性#
データ値を保存する場所を含みます。値はこの場所から読み取られ、この場所に書き込まれます。データ属性には、その動作を記述する 4 つの特性があります。
- [[Configurable]]可配置- 属性がdeleteで削除され、再定義できるかどうかを示します。
- 特性を変更できるかどうか。
- アクセサ属性に変更できるかどうか。
- デフォルトでは、オブジェクト上に直接定義されたすべての属性のこの特性はtrueです。
 
- 属性が
- [[Enumerable]]可列挙- 属性がfor-inループで返されるかどうかを示します。
- デフォルト: true
 
- 属性が
- [[Writable]]可書き込み- 属性の値が変更可能かどうかを示します。
- デフォルト: true
 
- [[Value]]可書き込み- 属性の実際の値を含みます。
- これは前述の属性値の読み取りと書き込みの場所です。
- デフォルト: undefined
 
前の例のように属性をオブジェクトに明示的に追加した後、[[Configurable]]、[[Enumerable]]、および[[Writable]]はすべて true に設定され、[[Value]]特性は指定された値に設定されます。
Object.defineProperty () メソッド#
属性のデフォルト特性を変更するには、Object.defineProperty()メソッドを使用する必要があります。このメソッドは 3 つの引数を受け取ります:
- obj属性を追加するオブジェクト
- prop定義または変更する属性名または Symbol
- descriptor定義または変更する属性記述子
設定方法は以下の例のようになります。
// Object.defineProperty()で属性を設定
let person = {};
Object.defineProperty(person, "name", {
  writable: false,    // ここを見て! 変更不可
  value: "cosine"
});
console.log(person.name); // cosine
person.name = "NaHCOx";   // 変更を試みる 
console.log(person.name); // 変更無効 print: cosine 
nameという属性を作成し、読み取り専用の値を与えたため、この属性の値は変更できなくなりました。
- 非厳格モードでは、この属性に再度値を割り当てようとすると無視されます。
- 厳格モードでは、読み取り専用属性の値を変更しようとするとエラーが発生します。
同様のルールは、不可配置の属性を作成する場合にも適用されます。
configurableをfalseに設定すると、この属性はオブジェクトから削除できなくなります。
注意:属性が不可配置として定義された後は、再び可配置に戻すことはできません!!
Object.defineProperty()を再度呼び出して、writable以外の属性を変更しようとするとエラーが発生します。
Object.defineProperty()を呼び出す際に、configurable、enumerable、およびwritableの値が指定されていない場合、すべてデフォルトでfalseになります。
アクセサ属性#
アクセサ属性はデータ値を含まない。代わりに、取得(getter)関数と設定(setter)関数を含みますが、これらの関数は必須ではありません。
- アクセサ属性を読み取ると、getter関数が呼び出され、有効な値が返されます。
- アクセサ属性に書き込むと、setter関数が呼び出され、新しい値が渡され、この関数はデータに対してどのような変更を行うかを決定する必要があります。
アクセサ属性には、その動作を記述する 4 つの特性があります:
- [[Configurable]]:属性を示す- deleteで削除して再定義できるかどうか。
- 特性を変更できるかどうか。
- データ属性に変更できるかどうか。
- デフォルト:true
 
- [[Enumerable]]:属性が- for-inループで返されるかどうか。デフォルト- true
- [[Get]]:取得関数、属性を読み取るときに呼び出されます。デフォルト値は- undefined
- [[Set]]:設定関数、属性に書き込むときに呼び出されます。デフォルト値は- undefined
上記の属性のデフォルト値は、オブジェクトを直接定義したときのデフォルト値を示します。Object.defineProperty()を使用した場合、定義されていないものはすべてundefinedになります。
以下は例です。
// アクセサ属性の定義
// 擬似プライベートメンバーyear_と公共メンバーeditionを含むオブジェクトを定義
let book = { 
  year_: 2017, 
  edition: 1 
};
Object.defineProperty(book, "year", {
  get() {
    return this.year_;
  },
  set(newVal) {
    if(newVal > 2017) {
      this.year_ = newVal;
      this.edition += newVal - 2017;
    }
  }
});
book.year = 1999
console.log(book.year);   // 2017
console.log(book.edition);  // 1
book.year = 2018
console.log(book.year);   // 2018
console.log(book.edition);  // 2
オブジェクト book には 2 つのデフォルト属性があります:year_と edition。
year_の下線は、この属性がオブジェクトメソッドの外部からアクセスされることを望んでいないことを示すために一般的に使用されます。
別の属性 year はアクセサ属性として定義されており、その中で、
- getter は単に year_の値を返します。
- setter は正しいバージョン(edition)を決定するための計算を行います。
したがって、year 属性を 2018 に変更すると、year_は 2018 になり、edition は 2 になります。一方、1999 に変更しようとすると変化はありません。これはアクセサ属性の典型的な使用シーンであり、属性値を設定すると他の変化が発生します。
取得関数と設定関数は必ずしも両方を定義する必要はありません。
- getter関数のみを定義すると、属性は読み取り専用になり、属性を変更しようとすると無視されます。厳格モードでは、取得関数のみを定義した属性に書き込もうとするとエラーが発生します。
- 同様に、setterのみを定義した属性は読み取れません。非厳格モードでは読み取るとundefinedが返され、厳格モードではエラーが発生します。
その他の属性定義関数#
他にもObject.defineProperties()を使用して複数の属性を定義したり、Object.getOwnPropertyDescriptor()メソッドを使用して指定された属性の属性記述子を取得したり、Object.getOwnPropertyDescriptors()メソッドを使用して各自有属性の属性記述子を取得し、新しいオブジェクトに返すことができます。
オブジェクトのマージ#
ソースオブジェクトのすべての属性をターゲットオブジェクトにコピーする操作は「ミックスイン」とも呼ばれ、ターゲットオブジェクトはソースオブジェクトの属性をミックスインすることで強化されます。
Object.assign()メソッドは、1 つまたは複数のソースオブジェクトからすべての列挙可能な属性 / 自有属性の値をターゲットオブジェクトに割り当て、ターゲットオブジェクトを返します。
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);
// 期待される出力: Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget);
// 期待される出力: Object { a: 1, b: 4, c: 5 }
Object.assign()は実際には浅いコピーを実行し、オブジェクトの参照のみをコピーします。
- 複数のソースオブジェクトが同じ属性を持つ場合、最後にコピーされた値が使用されます。
- ソースオブジェクトからアクセサ属性の値を取得する場合、例えば取得関数は静的値としてターゲットオブジェクトに割り当てられます。つまり、2 つのオブジェクト間で取得関数と設定関数を移動することはできません。
- 割り当て中にエラーが発生した場合、操作は中止され、エラーがスローされます。「ロールバック」の概念はなく、これは部分的なコピーのみを完了する可能性があるメソッドです。
オブジェクトの識別と等価判定#
ES6 以前は、=== 演算子を使用して判断することができない場合がありました。例えば:
// これらの状況は異なるJavaScriptエンジンで異なる動作をしますが、依然として等しいと見なされます。
console.log(+0 === -0); // true 
console.log(+0 === 0); // true 
console.log(-0 === 0); // true 
// NaNの等価性を確認するには、非常に厄介なisNaN()を使用する必要があります。
console.log(NaN === NaN); // false 
console.log(isNaN(NaN)); // true 
ES6 仕様はこのような状況を改善し、Object.is()メソッドを新たに追加して、2 つの値が同じ値であるかどうかを判断します。
// 正しい0、-0、+0の等価/不等価判定
console.log(Object.is(+0, -0)); // false 
console.log(Object.is(+0, 0)); // true 
console.log(Object.is(-0, 0)); // false 
// 正しいNaNの等価判定
console.log(Object.is(NaN, NaN)); // true
2 つ以上の値をチェックするには、再帰的に等価性を伝播させることができます。
function recursivelyCheckEqual(x, ...rest) {
  return Object.is(x, rest[0]) && 
  (rest.length < 2 || recursivelyCheckEqual(...rest));
}
ES6 の構文糖#
ECMAScript 6 は、オブジェクトの定義と操作のために非常に便利な構文糖の特性を多く追加しました。これらの特性は、既存のエンジンの動作を変更することなく、オブジェクトの処理の便利さを大幅に向上させます。
属性名の簡略化#
オブジェクトに変数を追加する際、属性名と変数名が同じであることがよくあります。この場合、変数名を使用し、コロンを書く必要はありません。同名の変数が見つからない場合は、ReferenceErrorがスローされます。
例えば:
let name = 'cosine'; 
let person = { name: name }; 
console.log(person); // { name: 'cosine' }
// 構文糖を使用すると、上記と同等です。
let person = { name }; 
console.log(person); // { name: 'cosine' }
コード圧縮プログラムは、異なるスコープ間で属性名を保持して、参照が見つからないのを防ぎます。
計算可能な属性#
オブジェクトリテラル内で直接動的に属性を命名できます:
const nameKey = 'name'; 
const ageKey = 'age'; 
const jobKey = 'job'; 
let uniqueToken = 0; 
function getUniqueKey(key) { 
 return `${key}_${uniqueToken++}`; 
} 
let person = { 
 [nameKey]: 'cosine', 
 [ageKey]: 21, 
 [jobKey]: 'ソフトウェアエンジニア',
 // 式でも可能です!
 [getUniqueKey(jobKey+ageKey)]: 'test'
}; 
console.log(person); 
// { name: 'cosine', age: 21, job: 'ソフトウェアエンジニア', jobage_0: 'test' }
簡略化されたメソッド名#
直接見てみましょう:
let person = { 
    // sayName: function(name) { // 古い
    //     console.log(`私の名前は${name}です`); 
    // } 
    sayName(name) { // 新しい
        console.log(`私の名前は${name}です`); 
    } 
}; 
person.sayName('Matt'); // 私の名前はMattです 
簡略化されたメソッド名は、取得関数と設定関数にも適用され、簡略化されたメソッド名は計算可能な属性キーと互換性があり、後のクラスの基礎を築きます。
オブジェクトの分解#
// オブジェクトの分解
let person = { 
    name: 'cosine', 
    age: 21 
}; 
let { name: personName, age: personAge } = person; 
console.log(personName, personAge); // cosine 21 
// 変数が属性名を直接使用できるようにし、デフォルト値を定義します。未定義の場合、デフォルト値は存在しないため`undefined`になります。
let { name, age, job = 'test', score } = person; 
console.log(name, age, job, score); // cosine 21 test undefined
分解は内部で関数ToObject()(実行時環境で直接アクセスできません)を使用して、ソースデータ構造をオブジェクトに変換します。
これは、オブジェクトの分解の文脈で、原始値がオブジェクトとして扱われることを意味します。つまり、nullとundefinedは分解できず、そうするとエラーが発生します。
let { length } = 'foobar'; 
console.log(length); // 6 
let { constructor: c } = 4; 
console.log(c === Number); // true 
let { _ } = null; // TypeError 
let { _ } = undefined; // TypeError
事前に宣言された変数に分解代入を行う場合、代入式は括弧で囲む必要があります。
let personName, personAge; 
let person = { 
 name: 'cosine', 
 age: 21
}; 
({name: personName, age: personAge} = person); 
console.log(personName, personAge); // cosine 21
1、ネストされた分解#
分解は、ネストされた属性や代入対象に制限はありません。これにより、オブジェクト属性をコピーすることができます(浅いコピー)。
let person = { 
    name: 'cosine', 
    age: 21, 
    job: { 
        title: 'ソフトウェアエンジニア' 
    } 
}; 
// title変数を宣言し、person.job.titleの値をそれに割り当てます。
let { job: { title } } = person; 
console.log(title); // ソフトウェアエンジニア 
外側の属性が定義されていない場合、ネストされた分解を使用することはできません。ソースオブジェクトとターゲットオブジェクトの両方に当てはまります。
2、部分的な分解#
複数の属性を含む分解代入は、出力に依存しない順序化操作です。分解式が複数の代入を含む場合、最初の代入が成功し、後の代入がエラーになる(分解できない undefined || null に対して)と、全体の分解代入は一部のみが完了します。
3、パラメータの文脈マッチ#
関数のパラメータリストでも分解代入を行うことができます。パラメータの分解代入は arguments オブジェクトに影響を与えませんが、関数シグネチャ内で関数本体内で使用するローカル変数を宣言できます:
let person = { 
    name: 'cosine', 
    age: 21
}; 
function printPerson(foo, {name, age}, bar) { 
    console.log(arguments); 
    console.log(name, age); 
} 
function printPerson2(foo, {name: personName, age: personAge}, bar) { 
    console.log(arguments); 
    console.log(personName, personAge); 
} 
printPerson('1st', person, '2nd'); 
// ['1st', { name: 'cosine', age: 21 }, '2nd'] 
// 'cosine' 21 
printPerson2('1st', person, '2nd'); 
// ['1st', { name: 'cosine', age: 21 }, '2nd'] 
// 'cosine' 21 
オブジェクトの作成#
ES6 から、クラスと継承が正式にサポートされましたが、このサポートは実際には ES5.1 のコンストラクタ関数とプロトタイプ継承の構文糖を封装したものです。
ファクトリーパターン#
デザインパターンに関するブログでいくつかのデザインパターンについて言及しました(フロントエンドデザインパターン応用ノート)、ファクトリーパターンも広く使用されるデザインパターンの 1 つであり、オブジェクトを作成する最良の方法を提供します。ファクトリーパターンでは、オブジェクトを作成する際にクライアントに作成ロジックを公開せず、共通のインターフェースを使用して新しく作成されたオブジェクトを指し示します。
function createPerson(name, age, job) { 
    let o = new Object(); 
    o.name = name; 
    o.age = age; 
    o.job = job; 
    o.sayName = function() { 
        console.log(this.name); 
    }; 
    return o; 
} 
let person1 = createPerson("cosine", 21, "ソフトウェアエンジニア"); 
let person2 = createPerson("Greg", 27, "医者"); 
console.log(person1);   // { name: 'cosine', age: 21, job: 'ソフトウェアエンジニア', sayName: [Function (anonymous)] }
person1.sayName();  // cosine
console.log(person2); // { name: 'Greg', age: 27, job: '医者', sayName: [Function (anonymous)] }
person2.sayName();  // Greg
このパターンは、複数の類似オブジェクトを作成する問題を解決できますが、オブジェクトの識別問題(つまり、新しく作成されたオブジェクトがどのタイプであるか)は解決できません。
コンストラクタ関数パターン#
カスタムコンストラクタ関数を使用して、自分のオブジェクトタイプの属性とメソッドを定義します。
function Person(name, age, job){ 
    this.name = name; 
    this.age = age; 
    this.job = job; 
    this.sayName = function() { 
        console.log(this.name); 
    }; 
} 
let person1 = new Person("cosine", 21, "ソフトウェアエンジニア"); 
person1.sayName(); // cosine
- 明示的にオブジェクトを作成していません。
- 属性とメソッドは直接 this に割り当てられます。
- return はありません。
- Person のインスタンスを作成するには、new 演算子を使用する必要があります。
new のプロセスで何が起こるのか?#
重要なポイントは、new を使用してコンストラクタ関数を呼び出すと、以下の操作が実行されます:
- メモリ内に新しいオブジェクトを作成します。
- 新しいオブジェクトの内部[[Prototype]]をコンストラクタ関数の prototype 属性に設定します。
- コンストラクタ関数内のthisがこの新しいオブジェクトを指します。
- コンストラクタ関数内のコードを実行します(オブジェクトに属性を追加します)。
- コンストラクタ関数が非空のオブジェクトを返す場合、そのオブジェクトを返します。そうでなければ、作成された新しいオブジェクトを返します!
前の例の最後で、person1 には Person を指す constructor 属性があります。
console.log(person1.constructor)    // [Function: Person]
console.log(person1.constructor === Person)    // true
console.log(person1 instanceof Object); // true 
console.log(person1 instanceof Person); // true 
カスタムコンストラクタ関数を定義することで、インスタンスが特定のタイプとして識別されることを保証できます。ファクトリーパターンと比較して、これは大きな利点です。
person1 が Object のインスタンスと見なされるのは、すべてのカスタムオブジェクトが Object から継承されるためです(後で説明します)。
以下の点に注意してください:
- コンストラクタ関数も関数です:new 演算子を使用して呼び出される関数はすべてコンストラクタ関数であり、new 演算子を使用しない関数は通常の関数です。
- コンストラクタ関数の主な問題:定義されたメソッドは各インスタンスに対して 1 回ずつ作成されるため、異なるインスタンスの同名の関数は等しくありません。同じことをするために異なる Function インスタンスを定義する必要はありません。
this オブジェクトは、関数とオブジェクトのバインディングを実行時に遅延させることができるため、関数定義をコンストラクタ関数の外部に移動できます。
これにより、同じロジックの関数の重複定義の問題は解決されますが、グローバルスコープが混乱します。なぜなら、その関数は実際には 1 つのオブジェクトでのみ呼び出されるべきだからです。このオブジェクトが複数のメソッドを必要とする場合、グローバルスコープに複数の関数を定義する必要があります。これにより、カスタムタイプの参照コードがうまく集約できなくなります。この新しい問題は、プロトタイプパターンを使用して解決できます。
プロトタイプパターン#
- 各関数は、プロトタイプオブジェクトを指すprototype属性を作成します。
- プロトタイプオブジェクトに定義された属性とメソッドは、すべてのオブジェクトインスタンスで共有できます。
- コンストラクタ関数内でオブジェクトインスタンスに割り当てられた値は、直接プロトタイプに割り当てることができます。
1、プロトタイプを理解する#
- 関数を作成するたびに、特定のルールに従ってその関数にprototype属性が作成され、プロトタイプオブジェクトを指します。
- すべてのプロトタイプオブジェクトは、関連するコンストラクタ関数を指すconstructorという名前の属性を持ちます。- 例えば、Person.prototype.constructorはPersonを指します。
 
- 例えば、
- 次に、コンストラクタ関数を介してプロトタイプオブジェクトに他の属性やメソッドを追加できます。
- プロトタイプオブジェクトはデフォルトでconstructor属性のみを持ち、他のすべてのメソッドは Object から継承されます。
- コンストラクタ関数を呼び出して新しいインスタンスを作成するたびに、その内部[[Prototype]]ポインタはコンストラクタ関数のプロトタイプオブジェクトに設定されます。
- スクリプト内でこの[[Prototype]]特性にアクセスする標準的な方法はありませんが、Firefox、Safari、Chrome は各オブジェクトに__proto__属性を公開し、この属性を介してオブジェクトのプロトタイプにアクセスできます。
通常のプロトタイプチェーンは Object のプロトタイプオブジェクトで終了し、Objectプロトタイプのプロトタイプはnullです。
console.log(Person.prototype.__proto__ === Object.prototype); // true 
console.log(Person.prototype.__proto__.constructor === Object); // true 
console.log(Person.prototype.__proto__.__proto__ === null); // true 
コンストラクタ関数、プロトタイプオブジェクト、およびインスタンスは 3 つの完全に異なるオブジェクトです:
console.log(person1 !== Person); // true 
console.log(person1 !== Person.prototype); // true 
console.log(Person.prototype !== Person); // true
インスタンスは__proto__を介してプロトタイプオブジェクトにリンクされ、コンストラクタ関数はprototype属性を介してプロトタイプオブジェクトにリンクされています:
console.log(person1.__proto__ === Person.prototype); // true 
console.log(person1.__proto__.constructor === Person); // true 
同じコンストラクタ関数で作成されたインスタンスは同じプロトタイプオブジェクトを共有します。instanceofは、インスタンスのプロトタイプチェーンに指定されたコンストラクタ関数のプロトタイプが含まれているかどうかをチェックします:
console.log(person1.__proto__ === person2.__proto__); // true 
console.log(person1 instanceof Person); // true 
console.log(person1 instanceof Object); // true 
- isPrototypeOf()メソッドは、あるオブジェクトが別のオブジェクトのプロトタイプチェーンに存在するかどうかをテストするために使用されます。- instanceof演算子とは異なります。式- object instanceof AFunctionでは、- objectのプロトタイプチェーンは- AFunction.prototypeに対してチェックされ、- AFunction自体には対してチェックされません。
 
- getPrototypeOf()メソッドは、引数の内部特性- [[Prototype]]の値を返します。- これを使用すると、オブジェクトのプロトタイプを簡単に取得でき、プロトタイプを介して継承を実現する際に特に重要です。
 
- setPrototypeOf()メソッドは、インスタンスのプライベート特性- [[Prototype]]に新しい値を書き込むことができます。- これを使用すると、オブジェクトのプロトタイプ継承関係を書き換えることができます。
 
console.log(Person.prototype.isPrototypeOf(person1)); // true 
console.log(Person.prototype.isPrototypeOf(person2)); // true
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true 
console.log(Object.getPrototypeOf(person1).name); // "cosine"
Object.setPrototypeOf()はコードのパフォーマンスに深刻な影響を与える可能性があります。Mozilla のドキュメントは次のように明確に述べています。「すべてのブラウザと JavaScript エンジンにおいて、継承関係を変更する影響は微妙で深遠です。この影響は、Object.setPrototypeOf()文を実行するだけではなく、すべてのコードに影響を与えます。」
Object.setPrototypeOf()の使用によるパフォーマンスの低下を避けるために、Object.create()を使用して新しいオブジェクトを作成し、そのプロトタイプを指定することができます:
let biped = { 
 numLegs: 1
}; 
let person = Object.create(biped); 
person.name = 'cosine'; 
console.log(person.name); // cosine
console.log(person.numLegs); // 1
console.log(Object.getPrototypeOf(person) === biped); // true 
2、プロトタイプの階層#
- オブジェクトの属性にアクセスする際、属性名に従って検索が行われます。
- このインスタンスでその属性が見つかった場合、その属性に対応する値が返されます。
- インスタンスで見つからない場合、プロトタイプオブジェクトに移動し、プロトタイプオブジェクトで属性が見つかると、対応する値が返されます。
- プロトタイプオブジェクトでも見つからない場合、プロトタイプオブジェクトのプロトタイプオブジェクトに移動して探します…… このように繰り返し、見つかるまで続きます。
- これは、プロトタイプが複数のオブジェクトインスタンス間で属性とメソッドを共有する原理です。
以下の点に注意してください:
- インスタンスを介してプロトタイプオブジェクトの値を読み取ることはできますが、インスタンスを介してプロトタイプオブジェクトの値を上書きすることはできません。
- インスタンスにプロトタイプオブジェクトに同名の属性が追加されると、そのインスタンスにこの属性が作成され、この属性はプロトタイプオブジェクトの属性を隠します。
- delete演算子を使用すると、インスタンス上のこの属性を完全に削除でき、識別子の解析プロセスがプロトタイプオブジェクトを再検索できるようになります。
hasOwnProperty()#
hasOwnProperty()メソッドは、特定の属性がインスタンス上にあるかプロトタイプオブジェクト上にあるかを確認するために使用されます。このメソッドは、属性が呼び出したオブジェクトインスタンスに存在する場合、trueを返します。
function Person() {} 
Person.prototype.name = "cosine"; 
Person.prototype.age = 21; 
Person.prototype.job = "ソフトウェアエンジニア"; 
Person.prototype.sayName = function() { 
	console.log(this.name); 
}; 
let person1 = new Person(); 
let person2 = new Person(); 
console.log(person1.hasOwnProperty("name")); // false 
person1.name = "Khat"; 		// インスタンス上にnameを追加し、プロトタイプ上のnameを隠しました。
console.log(person1.name); // "Khat"、インスタンスから来ています。
console.log(person1.hasOwnProperty("name")); // true 
console.log(person2.name); // "cosine"、プロトタイプから来ています。
console.log(person2.hasOwnProperty("name")); // false 
delete person1.name; 
console.log(person1.name); // "cosine"、プロトタイプから来ています。
console.log(person1.hasOwnProperty("name")); // false
3、プロトタイプと in 演算子#
in演算子には以下の 2 つの使用方法があります:
- 単独でin演算子を使用する
- for-inループ内で使用する
単独で使用する場合、指定された属性にアクセスすることでtrueが返されます。その属性がインスタンス上にあるかプロトタイプ上にあるかに関係なく。
console.log(person1.hasOwnProperty("name")); // false 
console.log("name" in person1); // true 
person1.name = "cosine"; 
console.log(person1.name); // "Khat"、インスタンスから来ています。
console.log(person1.hasOwnProperty("name")); // true 
console.log("name" in person1); // true 
console.log(person2.name); // "cosine"、プロトタイプから来ています。
console.log(person2.hasOwnProperty("name")); // false 
console.log("name" in person2); // true 
delete person1.name; 
console.log(person1.name); // "cosine"、プロトタイプから来ています。
console.log(person1.hasOwnProperty("name")); // false 
console.log("name" in person1); // true 
特定の属性がプロトタイプ上に存在するかどうかを確認するには、hasOwnProperty()とin演算子を同時に使用できます。
function hasPrototypeProperty(object, name){ 
	return !object.hasOwnProperty(name) && (name in object); 
} ;
for-inループでin演算子を使用する場合、オブジェクトにアクセスできるかつ列挙可能な属性がすべて返されます。これには、列挙可能なインスタンス属性、プロトタイプ属性(隠されたプロトタイプ属性や非列挙属性は含まれません)も含まれます。さらに、Object.keys()メソッドを使用して、オブジェクト上のすべての列挙可能なインスタンス属性を取得できます。このメソッドは、そのオブジェクトのすべての列挙可能な属性名を含む文字列の配列を返します。
すべてのインスタンス属性(非列挙を含む)を取得したい場合は、Object.getOwnPropertyNames()を使用できます。
4、属性の列挙順序#
- 列挙順序は不確定です。
- for-inループ
- Object.keys()
- JavaScript エンジンによって異なり、ブラウザによって異なる場合があります。
 
- 列挙順序は確定しています。
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.assign()
- まず数値キーを昇順で列挙し、その後文字列とシンボルキーを挿入順序で列挙します。
 
オブジェクトの反復#
ECMAScript 2017 では、オブジェクトの内容をシリアライズされた(反復可能な)形式に変換するための 2 つの静的メソッドが追加されました。これらの 2 つの静的メソッドObject.values()とObject.entries()は、オブジェクトを受け取り、その内容の配列を返します。
- Object.values()はオブジェクトの値の配列を返します。
- Object.entries()はキー - 値ペアの配列を返します。
- 非文字列属性は文字列に変換されて出力され、シンボル属性は無視されます。これらの 2 つのメソッドはオブジェクトの浅いコピーを実行します。
1、他のプロトタイプ構文#
コードの冗長性を減らし、視覚的にプロトタイプ機能をより良くカプセル化するために、すべての属性とメソッドを含むオブジェクトリテラルを使用してプロトタイプを再定義することが一般的な手法になっています。
function Person() {} 
Person.prototype = {
	name: "cosine", 
	age: 21, 
	job: "ソフトウェアエンジニア", 
	sayName() { 
	console.log(this.name); 
	} 
}; 
ただし、1 つの問題があります:このように再定義すると、Person.prototype の constructor 属性が Person を指さなくなります。関数を作成すると、その prototype オブジェクトも自動的に作成され、そのプロトタイプの constructor 属性に自動的に値が設定されます。したがって、constructor の値を特別に設定する必要があります。
Person.prototype = { 
	constructor: Person, // 値を設定
	name: "cosine", 
	age: 21, 
	job: "ソフトウェアエンジニア", 
	sayName() { 
		console.log(this.name); 
	} 
}; 
この方法でconstructor属性を復元すると、[[Enumerable]]がtrueの属性が作成されます。一方、元のconstructor属性はデフォルトで列挙不可です。したがって、ECMAScript に準拠した JavaScript エンジンを使用している場合は、Object.defineProperty()メソッドを使用してconstructor属性を定義する必要があります。
2、プロトタイプの動的性#
プロトタイプから値を検索するプロセスは動的であるため、インスタンスがプロトタイプを変更する前に存在していても、プロトタイプオブジェクトに対する変更はいつでもインスタンスに反映されます:
function Person() {} 
let friend = new Person(); 
Person.prototype = { 
	constructor: Person, 
	name: "cosine", 
	age: 21, 
	job: "ソフトウェアエンジニア", 
	sayName() { 
		console.log(this.name); 
	} 
}; 
friend.sayName(); // エラー
3、ネイティブオブジェクトのプロトタイプ#
- プロトタイプパターンは、すべてのネイティブ参照型を実現するためのパターンです。
- すべてのネイティブ参照型のコンストラクタ関数(Object、Array、Stringなどを含む)は、プロトタイプにインスタンスメソッドを定義しています。- 配列インスタンスのsort()などのメソッドはArray.prototypeに定義されています。
- 文字列ラッパーオブジェクトのsubstring()などのメソッドもString.prototypeに定義されています。
 
- 配列インスタンスの
- ネイティブオブジェクトのプロトタイプを介して、すべてのデフォルトメソッドの参照を取得でき、ネイティブ型のインスタンスに新しいメソッドを定義することもできます(ただし、これは推奨されません)。
4、プロトタイプの問題#
- コンストラクタ関数に初期化パラメータを渡す能力が弱まり、すべてのインスタンスが同じ属性値を取得することになります。
- 主な問題は、その共有特性から生じます。一般的に、異なるインスタンスはそれぞれの属性のコピーを持つべきですが、プロトタイプパターンで追加された属性は異なるインスタンスに反映されます。
継承の実現#
プロトタイプチェーン#
- ECMA-262 はプロトタイプチェーンを ECMAScript の主要な継承方法として定義しています。
- 基本的な考え方:プロトタイプを介して、複数の参照型の属性とメソッドを継承します。
振り返ってみると、各コンストラクタ関数にはプロトタイプオブジェクトがあり、prototypeがプロトタイプオブジェクトを指し、プロトタイプにはconstructor属性があり、コンストラクタ関数を指します。インスタンスには内部ポインタ__proto__があり、プロトタイプを指します。
プロトタイプが別の型のインスタンスである場合はどうでしょうか?それは、プロトタイプ自体が別のプロトタイプオブジェクトを指す内部ポインタ__proto__を持つことを意味します。対応する別のプロトタイプも別のポインタconstructorを持ち、別のコンストラクタ関数を指します。これにより、インスタンスとプロトタイプの間にプロトタイプチェーンが構築されます。
プロトタイプチェーンは、前述のプロトタイプ検索メカニズムを拡張します。インスタンス上の属性を読み取るとき、最初にインスタンス上でその属性を検索します。見つからない場合は、インスタンスのプロトタイプを検索します。プロトタイプチェーンを介して継承を実現すると、検索は上に向かって行われ、プロトタイプのプロトタイプを検索します。プロトタイプチェーンの末端に達するまで続きます。
1、デフォルトプロトタイプ#
デフォルトでは、すべての参照型はObjectから継承されます。これはプロトタイプチェーンを介して実現されます。任意の関数のデフォルトプロトタイプはObjectのインスタンスであり、このインスタンスには内部ポインタがObject.prototypeを指します。これが、カスタムタイプがtoString()、valueOf()などのすべてのデフォルトメソッドを継承できる理由です。
2、プロトタイプと継承関係#
プロトタイプとインスタンスの関係は、2 つの方法で確認できます。
- instanceof演算子を使用します。インスタンスのプロトタイプチェーンに対応するコンストラクタ関数が存在する場合、- instanceofは- trueを返します。
console.log(instance instanceof Object); // true 
console.log(instance instanceof SuperType); // true 
console.log(instance instanceof SubType); // true
- isPrototypeOf()メソッドを使用します。そのインスタンスのプロトタイプチェーンにこのプロトタイプが含まれている場合、このメソッドは- trueを返します。
console.log(Object.prototype.isPrototypeOf(instance)); // true 
console.log(SuperType.prototype.isPrototypeOf(instance)); // true 
console.log(SubType.prototype.isPrototypeOf(instance)); // true
3、プロトタイプチェーンの問題#
- プロトタイプの問題について言及した際に述べたように、プロトタイプに含まれる参照値はすべてのインスタンス間で共有されます。これが、属性を通常コンストラクタ関数内で定義し、プロトタイプ上には定義しない理由です。
- プロトタイプを使用して継承を実現する場合、プロトタイプは実際には別の型のインスタンスになります。これにより、元のインスタンス属性が影響を受けます。
function SuperType() { 
	this.colors = ["red", "blue", "green"]; 
} 
function SubType() {} 
// SuperTypeを継承 
SubType.prototype = new SuperType(); 
let instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
let instance2 = new SubType(); 
console.log(instance2.colors); // "red,blue,green,black" 
SubTypeがSuperTypeをプロトタイプ継承すると、SubType.prototypeはSuperTypeのインスタンスになり、独自のcolors属性を持つことになります。これは、SubType.prototype.colors属性が作成されたようなものです。最終的な結果は、SubTypeのすべてのインスタンスがこのcolors属性を共有し、instance1.colorsの変更がinstance2.colorsに反映されることです。
- 2 つ目の問題は、子タイプがインスタンス化されるときに、親タイプのコンストラクタ関数にパラメータを渡すことができないことです。実際には、親クラスのコンストラクタ関数にパラメータを渡すことはできません。前述のプロトタイプに含まれる参照値の問題に加えて、プロトタイプチェーンは基本的に単独では使用されません。
コンストラクタ関数の盗用#
基本的な考え方:子クラスのコンストラクタ関数内で親クラスのコンストラクタ関数を呼び出す。
関数は特定のコンテキストでコードを実行する単純なオブジェクトであるため、apply()やcall()メソッドを使用して新しく作成されたオブジェクトをコンテキストとしてコンストラクタ関数を実行できます。
function SuperType() { 
	this.colors = ["red", "blue", "green"]; 
} 
function SubType() { 
	// SuperTypeを継承!!
	SuperType.call(this); 
} 
let instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
let instance2 = new SubType(); 
console.log(instance2.colors); // "red,blue,green"
call()(またはapply())メソッドを使用することで、SuperTypeコンストラクタ関数はSubTypeのインスタンスを作成する新しいオブジェクトのコンテキストで実行されます。これは、SubTypeオブジェクト上でSuperType()関数内のすべての初期化コードが実行されることを意味します。これにより、各インスタンスは独自の属性を持つことができます。
1、パラメータの渡し方#
コンストラクタ関数の盗用を使用すると、子クラスのコンストラクタ関数内で親クラスのコンストラクタ関数にパラメータを渡すことができます。
function SuperType(name){ 
	this.name = name; 
} 
function SubType() { 
	// SuperTypeを継承し、パラメータを渡す
	SuperType.call(this, "cosine"); 
	// インスタンス属性
	this.age = 21; 
} 
let instance = new SubType(); 
console.log(instance.name); // "cosine"; 
console.log(instance.age); // 21
2、主な問題#
コンストラクタ関数の盗用の主な欠点は、コンストラクタ関数モードでカスタムタイプを定義する際の問題と同様です:
- メソッドをコンストラクタ関数内で定義する必要があります。したがって、関数は再利用できません。
- 子クラスは親クラスのプロトタイプに定義されたメソッドにアクセスできません。したがって、すべてのタイプはコンストラクタ関数モードのみを使用できます。
これらの問題があるため、コンストラクタ関数の盗用は基本的に単独では使用されません。
コンビネーション継承#
コンビネーション継承は、プロトタイプチェーンとコンストラクタ関数の盗用の両方の利点を組み合わせたものです。
- 基本的な考え方は、プロトタイプチェーンを使用してプロトタイプ上の属性とメソッドを継承し、コンストラクタ関数の盗用を使用してインスタンス属性を継承することです。
- メソッドをプロトタイプに定義して再利用を実現し、各インスタンスが独自の属性を持つことができます。
function SuperType(name){ 
	this.name = name; 
	this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function() { 
	console.log(this.name); 
}; 
function SubType(name, age){ 
	// 属性を継承
	SuperType.call(this, name); 
	this.age = age; 
} 
// メソッドを継承
SubType.prototype = new SuperType(); 
SubType.prototype.sayAge = function() { 
	console.log(this.age); 
}; 
let instance1 = new SubType("cosine", 21); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
instance1.sayName(); // "cosine"; 
instance1.sayAge(); // 21 
let instance2 = new SubType("NaHCOx", 22); 
console.log(instance2.colors); // "red,blue,green" 
instance2.sayName(); // "NaHCOx"; 
instance2.sayAge(); // 22
プロトタイプ式継承#
適用シナリオ:既にオブジェクトがあり、それを基に新しいオブジェクトを作成したい場合。まずそのオブジェクトを object () に渡し、次に返されたオブジェクトを適切に修正します。
let person = { 
	name: "Nicholas", 
	friends: ["Shelby", "Court", "Van"] 
}; 
let anotherPerson = Object.create(person, { 
	name: { 
		value: "Greg" 
	} 
}); 
console.log(anotherPerson.name); // "Greg"
これは以下のような状況に非常に適しています:
- 個別にコンストラクタ関数を作成する必要がない。
- オブジェクト間で情報を共有する必要がある場合。
- 注意:属性に含まれる参照値は常に関連するオブジェクト間で共有されます。
寄生式継承#
プロトタイプ式継承に非常に近い別の継承方法は寄生式継承(parasitic inheritance)です。
継承を実現する関数を作成し、何らかの方法でオブジェクトを強化し、そのオブジェクトを返します。
基本的な寄生継承パターンは以下の通りです。
function createAnother(original) {
	let clone = Object.create(original);   // コンストラクタ関数を呼び出して新しいオブジェクトを作成
	clone.sayHi = function() {
        console.log(`こんにちは!私は${this.name}です`);
    };
    return clone;   // このオブジェクトを返す
}
let person = {
    name: "cosine",
    friends: ['NaHCOx', 'Khat']
};
let person2 = createAnother(person);
person2.name = 'CHxCOOH';
person2.sayHi();    // こんにちは!私はCHxCOOHです
この例では、person をソースオブジェクトとして使用し、sayHi 関数を追加した新しいオブジェクトを返します(強化を行います)。これは、オブジェクトに関心があり、コンストラクタ関数やタイプを気にしないシナリオに適しています。
注意すべき点は以下の通りです:
- 寄生式継承を介してオブジェクトに関数を追加すると、関数が再利用しにくくなります。これはコンストラクタ関数モードと同様です。
寄生式コンビネーション継承#
コンビネーション継承には効率の問題もあります。親クラスのコンストラクタ関数が常に 2 回呼び出されます。
- 子クラスのプロトタイプを作成する際に呼び出されます。
- 子クラスのコンストラクタ関数内で呼び出されます。
function SuperType(name){ 
	this.name = name; 
	this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function() { 
	console.log(this.name); 
}; 
function SubType(name, age){ 
	// 属性を継承
	SuperType.call(this, name); // 親クラスのコンストラクタ関数を2回呼び出します!
	this.age = age; 
} 
// メソッドを継承
SubType.prototype = new SuperType(); // 親クラスのコンストラクタ関数を1回呼び出します!
SubType.prototype.sayAge = function() { 
	console.log(this.age); 
}; 
本質的に、子クラスのプロトタイプは親クラスオブジェクトのすべてのインスタンス属性を含む必要があるため、子クラスのコンストラクタ関数は実行時に自分のプロトタイプを再設定すれば十分です。
寄生式コンビネーション継承の主な考え方は以下の通りです:
- 盗用コンストラクタ関数を使用して属性を継承します。
- 混合プロトタイプチェーンを使用してメソッドを継承します。
 つまり、親クラスのプロトタイプを取得し、子クラスを指す constructor で元の constructor を隠します。
function inheritPrototype(subType, superType) {
    let prototype = Object.create(superType.prototype); // 親クラスのプロトタイプの副本を作成
    prototype.constructor = subType;    // 再設定されたプロトタイプによって失われたconstructorを取り戻す
    subType.prototype = prototype;      // オブジェクトを割り当てる
}
function SuperType(name){ 
	this.name = name; 
	this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function() { 
	console.log(this.name); 
}; 
function SubType(name, age){ 
	// 属性を継承
	SuperType.call(this, name); // 親クラスのコンストラクタ関数を2回呼び出します!
	this.age = age; 
} 
// メソッドを継承
- SubType.prototype = new SuperType(); // 親クラスのコンストラクタ関数を1回呼び出します!
+ inheritPrototype(SubType, SuperType);	// この関数を呼び出すように変更
SubType.prototype.sayAge = function() { 
	console.log(this.age); 
}; 
これにより、親クラスのコンストラクタ関数を不必要に複数回呼び出すことを避け、プロトタイプチェーンを維持することができ、引用型の継承の最良のパターンと見なされます。