banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

JavaScript学習の深い道(1)オブジェクト、クラスとオブジェクト指向プログラミング

オブジェクトとその作成プロセス、ES6 の構文糖、プロトタイプパターン、コンストラクタ関数を理解する

プロトタイプチェーン、コンストラクタ関数の盗用、コンビネーション継承、ベストプラクティスなどを理解する

オブジェクトを理解する#

ECMA-262 はオブジェクトを属性の無秩序な集合として定義し、各属性やメソッドには表すための名前があり、ハッシュテーブルのように考えることができ、値はデータ / 関数である

  • 例 1
// new Object()を使用して作成
let person = new Object();
person.name = "cosine";
person.age = 29
person.job = "Software Engineer"; 
person.sayName = function() { 
 console.log(this.name); 
}; 
// オブジェクトリテラルを使用して作成
let person = {
  name: 'cosine',
  age: 29, 
  job: "Software Engineer", 
  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という名前の属性を作成し、読み取り専用の値を与えた場合、この属性の値は変更できなくなります

  • 非厳格モードでは、この属性に再度値を割り当てようとすると無視されます。
  • 厳格モードでは、読み取り専用属性の値を変更しようとするとエラーが発生します

同様のルールは不可配置の属性の作成にも適用されます
configurablefalseに設定すると、この属性はオブジェクトから削除できなくなることを意味します。

注意:属性が不可配置として定義された後は、再び可配置に戻すことはできません!!

Object.defineProperty()を再度呼び出して、非writable属性を変更しようとするとエラーが発生します

Object.defineProperty()を呼び出す際に、configurableenumerable、および 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]: 'Software engineer',
 // 式も可能です!
 [getUniqueKey(jobKey+ageKey)]: 'test'
}; 
console.log(person); 
// { name: 'cosine', age: 21, job: 'Software engineer', jobage_0: 'test' }

簡略化されたメソッド名#

直接見てみましょう:

let person = { 
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()(実行時環境で直接アクセスできません)を使用して、ソースデータ構造をオブジェクトに変換します。
これは、オブジェクトの分解の文脈で、原始値がオブジェクトとして扱われることを意味します。つまり、nullundefinedは分解できず、そうするとエラーがスローされます。

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: 'Software engineer' 
    } 
}; 
// title変数を宣言し、person.job.titleの値をそれに割り当てます
let { job: { title } } = person; 
console.log(title); // Software engineer 

外側の属性が定義されていない場合、ネストされた分解を使用することはできません。ソースオブジェクトとターゲットオブジェクトの両方に当てはまります。

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 のコンストラクタ関数とプロトタイプ継承の構文糖を封装したものです。

ファクトリーパターン#

デザインパターンに関するブログでいくつかのデザインパターンについて触れました(フロントエンドデザインパターン応用ノート)、ファクトリーパターンも広く使用されるデザインパターンの一つであり、オブジェクトを作成する最良の方法を提供します。ファクトリーパターンでは、オブジェクトを作成する際にクライアントに作成ロジックを公開せず、共通のインターフェースを使用して新しく作成されたオブジェクトを指し示します。

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, "Software Engineer"); 
let person2 = createPerson("Greg", 27, "Doctor"); 
console.log(person1);   // { name: 'cosine', age: 21, job: 'Software Engineer', sayName: [Function (anonymous)] }
person1.sayName();  // cosine
console.log(person2); // { name: 'Greg', age: 27, job: 'Doctor', 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, "Software Engineer"); 
person1.sayName(); // cosine
  • 明示的にオブジェクトを作成していません
  • 属性とメソッドは直接 this に割り当てられます
  • return はありません
  • Person のインスタンスを作成するには、new 演算子を使用する必要があります

new のプロセスで何が起こるのか?#

重要なポイントは、new を使用してコンストラクタ関数を呼び出すと、次の操作が実行されます:

  1. メモリ内に新しいオブジェクトを作成します
  2. 新しいオブジェクトの内部[[Prototype]]をコンストラクタ関数の prototype 属性に設定します。
  3. コンストラクタ関数内部のthisがこの新しいオブジェクトを指します
  4. コンストラクタ関数内部のコードを実行します(オブジェクトに属性を追加します)
  5. コンストラクタ関数が非空オブジェクトを返す場合、そのオブジェクトを返します。そうでない場合は、作成した新しいオブジェクトを返します!

前の例の最後で、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 から継承されるためです(後で説明します)。

以下の点に注意してください:

  1. コンストラクタ関数も関数です :new 演算子を使用して呼び出される関数はすべてコンストラクタ関数であり、new 演算子を使用せずに呼び出される関数は通常の関数です。
  2. コンストラクタ関数の主な問題:定義されたメソッドは各インスタンスで作成されるため、異なるインスタンスの同名の関数は等しくありませんが、同じことをするため、2 つの異なる Function インスタンスを定義する必要はありません。

this オブジェクトは関数とオブジェクトのバインディングを実行時に遅延させることができるため、関数定義をコンストラクタ関数の外部に移動できます。
これにより、同じロジックの関数の重複定義の問題は解決されますが、グローバルスコープが混乱します。なぜなら、その関数は実際には 1 つのオブジェクトでのみ呼び出すことができるからです。このオブジェクトが複数のメソッドを必要とする場合、グローバルスコープに複数の関数を定義する必要があります。これにより、カスタムタイプの参照コードがうまく集約されなくなります。この新しい問題は、プロトタイプパターンによって解決できます。

プロトタイプパターン#

  • 各関数はprototype属性を持ち、プロトタイプオブジェクトを指します
  • プロトタイプオブジェクト上に定義された属性とメソッドは、すべてのオブジェクトインスタンスで共有できます
  • コンストラクタ関数内でオブジェクトインスタンスに割り当てられた値は、直接プロトタイプに割り当てることができます

1、プロトタイプを理解する#

  • いつでも関数を作成すると、特定のルールに従ってその関数にprototype属性が作成され、プロトタイプオブジェクトを指します
  • すべてのプロトタイプオブジェクトは、関連するコンストラクタ関数を指すconstructorという名前の属性を持ちます
    • 例えばPerson.prototype.constructorPersonを指します
  • 次に、コンストラクタ関数を使用してプロトタイプオブジェクトに他の属性やメソッドを追加できます
  • プロトタイプオブジェクトはデフォルトで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()文を実行するだけではなく、すべてのコードがその[[Prototype]]を変更したオブジェクトにアクセスすることに関係しています。」

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 = "Software Engineer"; 
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: "Software Engineer", 
	sayName() { 
	console.log(this.name); 
	} 
}; 

しかし、1 つの問題があります:このように再定義すると、Person.prototype の constructor 属性は Person を指さなくなります。関数を作成すると、その prototype オブジェクトも自動的に作成され、そのプロトタイプの constructor 属性に自動的に値が設定されます。したがって、constructor の値を設定する必要があります。

Person.prototype = { 
	constructor: Person, // 値を設定
	name: "cosine", 
	age: 21, 
	job: "Software Engineer", 
	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: "Software Engineer", 
	sayName() { 
		console.log(this.name); 
	} 
}; 
friend.sayName(); // エラー

3、組み込みオブジェクトのプロトタイプ#

  • プロトタイプパターンはすべての組み込み参照型を実現するためのパターンです。
  • すべての組み込み参照型のコンストラクタ関数(ObjectArrayStringなどを含む)は、プロトタイプ上にインスタンスメソッドを定義しています。
    • 配列インスタンスの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演算子を使用します。インスタンスのプロトタイプチェーンに対応するコンストラクタ関数が存在する場合instanceoftrueを返します。
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" 

SubTypeSuperTypeをプロトタイプ継承すると、SubType.prototypeSuperTypeのインスタンスになり、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); 
}; 

これにより、不要な親クラスのコンストラクタ関数の多重呼び出しを回避し、プロトタイプチェーンを保持することができ、引用型の継承の最良の方法と見なされます。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。