JavaScriptのプロトタイプを深く浅く理解する

オブジェクトはキーと値のペアです。オブジェクトを作成する最も一般的な方法は中括弧{}を使用し、ドット表記法でプロパティとメソッドを追加することです。

let creature = {}
creature.name = 'Rex'
creature.stamina = 10

creature.feed = function (quantity) {
  console.log(`${this.name} が食べています。`)
  this.stamina += quantity
}

creature.rest = function (duration) {
  console.log(`${this.name} が休んでいます。`)
  this.stamina += duration
}

creature.exercise = function (duration) {
  console.log(`${this.name} が運動しています。`)
  this.stamina -= duration
}

現在、私たちのアプリケーションでは複数の creature を作成する必要があります。もちろん、次のステップはロジックをカプセル化することです。新しい creature を作成する必要があるときは、単に関数を呼び出すだけで済みます。このパターンを関数インスタンス化と呼び、関数自体を「コンストラクタ」と呼びます。なぜなら、それは新しいオブジェクトを「構築」する責任があるからです。

関数インスタンス化

function Creature (name, stamina) {
  let creature = {}
  creature.name = name
  creature.stamina = stamina

  creature.feed = function (quantity) {
    console.log(`${this.name} が食べています。`)
    this.stamina += quantity
  }

  creature.rest = function (duration) {
    console.log(`${this.name} が休んでいます。`)
    this.stamina += duration
  }

  creature.exercise = function (duration) {
    console.log(`${this.name} が運動しています。`)
    this.stamina -= duration
  }

  return creature
}

const rex = Creature('Rex', 7)
const sparky = Creature('Sparky', 10)

現在、いつでも新しい creature を作成する必要がある場合(あるいはより広く言えば、新しい「インスタンス」を作成する場合)、するべきことは Creature 関数を呼び出してパラメータ:namestamina を渡すことだけです。これは便利で非常にシンプルです。しかし、このパターンのどの欠点が考えられますか?

最大の問題点は、関数内の3つのメソッドに関係しています - feedrestexercise。これらのメソッドはそれぞれ動的に生成されるだけでなく、完全に汎用的でもあります。つまり、新しい creature を作成するたびにこれらのメソッドを再作成する理由はありません。メモリを無駄に使い、各新規オブジェクトを必要以上に大きくしているだけです。

解決策を思いつきますか?もし新しい動物を作成するたびにこれらのメソッドを再作成する代わりに、それらを独自のオブジェクトに移動して、各動物がそのオブジェクトを参照できるようにしたらどうでしょうか?このパターンを関数インスタンス化と共有メソッドと呼びましょう。

関数インスタンス化と共有メソッド

const creatureMethods = {
  feed(quantity) {
    console.log(`${this.name} が食べています。`)
    this.stamina += quantity
  },
  rest(duration) {
    console.log(`${this.name} が休んでいます。`)
    this.stamina += duration
  },
  exercise(duration) {
    console.log(`${this.name} が運動しています。`)
    this.stamina -= duration
  }
}

function Creature (name, stamina) {
  let creature = {}
  creature.name = name
  creature.stamina = stamina
  creature.feed = creatureMethods.feed
  creature.rest = creatureMethods.rest
  creature.exercise = creatureMethods.exercise

  return creature
}

const rex = Creature('Rex', 7)
const sparky = Creature('Sparky', 10)

共有メソッドを独自のオブジェクトに移動し、Creature 関数でそのオブジェクトを参照することで、メモリの無駄遣いとオブジェクトの過剰なサイズという問題を解決しました。

Object.create

もう一度 Object.create を使って例を改善しましょう。簡単に言うと、Object.create は、プロパティ検索が失敗した場合に別のオブジェクトに委任するオブジェクトを作成できます。言い換えれば、Object.create は、プロパティ検索が失敗した場合に別のオブジェクトを照会できるオブジェクトを作成できます。コードを見てみましょう:

const parent = {
  name: 'Stacey',
  age: 35,
  heritage: 'Irish'
}

const child = Object.create(parent)
child.name = 'Ryan'
child.age = 7

console.log(child.name) // Ryan
console.log(child.age) // 7
console.log(child.heritage) // Irish

したがって、上記の例では、childobject.create(parent) で作成されているため、child オブジェクト上のプロパティ検索が失敗するたびに、JavaScript はその検索を parent オブジェクトに委任します。つまり、childheritage プロパティがなくても、child.heritage を印刷すると、parent オブジェクトから heritage を見つけて印刷します。

では、Object.create を使って前の Creature コードをどのように簡素化できますか?animalMethods オブジェクトに委任する代わりに、Object.create を使って creatureMethods オブジェクトに委任できます。Bグレードにするために、これを共有メソッドと Object.create を使った関数インスタンス化と呼びましょう。

共有メソッドと Object.create を使った関数インスタンス化

const creatureMethods = {
  feed(quantity) {
    console.log(`${this.name} が食べています。`)
    this.stamina += quantity
  },
  rest(duration) {
    console.log(`${this.name} が休んでいます。`)
    this.stamina += duration
  },
  exercise(duration) {
    console.log(`${this.name} が運動しています。`)
    this.stamina -= duration
  }
}

function Creature (name, stamina) {
  let creature = Object.create(creatureMethods)
  creature.name = name
  creature.stamina = stamina

  return creature
}

const rex = Creature('Rex', 7)
const sparky = Creature('Sparky', 10)

rex.feed(10)
sparky.exercise(5)

したがって、rex.feed を呼び出すと、JavaScript は rex オブジェクト上で feed メソッドを検索します。rexfeed メソッドがないため、検索は失敗し、Object.create によって creatureMethods オブジェクトに委任されます。その結果、creatureMethods オブジェクトから feed メソッドが見つかります。

これまで順調です。それでも、いくつかの改善点があります。インスタンス間でメソッドを共有するために、別個のオブジェクト(creatureMethods)を管理する必要があるのは少し「面倒」です。私たちは言語自体に実装したい一般的な機能なので、次のプロパティ - prototype を導入する必要があります。

では、JavaScript の prototype とは何でしょうか?簡単に言うと、JavaScript の各関数にはオブジェクトへの参照を持つ prototype プロパティがあります。

function doAction () {}
console.log(doAction.prototype) // {}

もし、共有メソッドを管理するための別個のオブジェクト(上記の例の creatureMethods のように)を作成する代わりに、各メソッドを Creature 関数の prototype に配置したらどうでしょうか?その後、行うべきことは Object.create を使って creatureMethods に委任する代わりに、Animal.prototype に委任するだけです。このパターンをプロトタイプインスタンス化と呼びましょう。

プロトタイプインスタンス化

function Creature (name, stamina) {
  let creature = Object.create(Creature.prototype)
  creature.name = name
  creature.stamina = stamina

  return creature
}

Creature.prototype.feed = function (quantity) {
  console.log(`${this.name} が食べています。`)
  this.stamina += quantity
}

Creature.prototype.rest = function (duration) {
  console.log(`${this.name} が休んでいます。`)
  this.stamina += duration
}

Creature.prototype.exercise = function (duration) {
  console.log(`${this.name} が運動しています。`)
  this.stamina -= duration
}

const rex = Creature('Rex', 7)
const sparky = Creature('Sparky', 10)

rex.feed(10)
sparky.exercise(5)

同様に、prototype は JavaScript の各関数が持つプロパティであり、前述のように、関数のすべてのインスタンス間でメソッドを共有できるようにします。すべての機能は同じですが、すべてのメソッドを管理するための別個のオブジェクトを気にする必要はありません。代わりに、Creature 関数自体に組み込まれた別のオブジェクト Creature.prototype を使用するだけです。

さらに進む

今までに3つのポイントを学びました:

  1. コンストラクタを作成する方法。
  2. コンストラクタのプロトタイプにメソッドを追加する方法。
  3. Object.create を使って失敗した検索をコンストラクタのプロトタイプに委任する方法。

これらの3つのポイントは、どのプログラミング言語にとって非常に基本的です。JavaScript はそれほど悪い言語で、同じことを達成するより簡単な方法がないのでしょうか?お気づきかもしれませんが、すでにあります。それは new キーワードを使うことです。

Creature コンストラクタを振り返ると、最も重要な2つの部分はオブジェクトを作成して返すことです。Object.create を使わずにオブジェクトを作成しないと、失敗した検索でコンストラクタのプロトタイプに委任できません。return ステートメントがないと、作成したオブジェクトを返せません。

function Creature (name, stamina) {
  let creature = Object.create(Creature.prototype) // 1 
  creature.name = name
  creature.stamina = stamina

  return creature   // 2
}

new について、すごいことがあります - 関数を new キーワードで呼び出すと、以下の番号付き12の2行のコードが暗黙的に(内部で)実行されます。作成されたオブジェクトは this と呼ばれます。

コメントを使って内部で何が起こっているかを示し、new キーワードで Creature コンストラクタが呼び出されたと仮定すると、次のように書き換えることができます。

function Creature (name, stamina) {
  // const this = Object.create(Creature.prototype)

  this.name = name
  this.stamina = stamina

  // return this
}

const rex = new Creature('Rex', 7)
const sparky = new Creature('Sparky', 10)

通常は次のようになります:

function Creature (name, stamina) {
  this.name = name
  this.stamina = stamina
}

Creature.prototype.feed = function (quantity) {
  console.log(`${this.name} が食べています。`)
  this.stamina += quantity
}

Creature.prototype.rest = function (duration) {
  console.log(`${this.name} が休んでいます。`)
  this.stamina += duration
}

Creature.prototype.exercise = function (duration) {
  console.log(`${this.name} が運動しています。`)
  this.stamina -= duration
}

const rex = new Creature('Rex', 7)
const sparky = new Creature('Sparky', 10)

再度説明すると、これができるのは、コンストラクタを new キーワードで呼び出したからです。関数を呼び出すときに new を省略すると、オブジェクトは作成されず、暗黙的に返されません。この問題は次の例で見ることができます。

function Creature (name, stamina) {
  this.name = name
  this.stamina = stamina
}

const rex = Creature('Rex', 7)
console.log(rex) // undefined

このパターンを擬似クラスインスタンス化と呼びます。

不慣れな人にとって、クラスはオブジェクトの青写真を作成するためのものです。その後、クラスのインスタンスを作成するたびに、そのオブジェクトで定義されたプロパティとメソッドにアクセスできます。

聞き覚えはありませんか?これは基本的に私たちが上記の Creature コンストラクタで行ったことです。しかし、class キーワードを使用する代わりに、通常の古い JavaScript 関数を使用して同じ機能を再作成しました。もちろん、いくつかの追加作業と JavaScript の「内部」で何が起こっているかの理解が必要ですが、結果は同じです。

これは良いニュースです。JavaScript は死んだ言語ではありません。TC-39委員会は継続的に改善と追加を行っています。つまり、JavaScript の初期バージョンがクラスをサポートしていなくても、それらを公式仕様に追加する理由はありません。実際、これが TC-39 委員会が行ったことです。2015年、EcmaScript(公式 JavaScript 仕様)6 がリリースされ、クラスと class キーワードがサポートされました。上記の Creature コンストラクタが新しいクラス構文を使ってどのように見えるかを見てみましょう。

class Creature {
  constructor(name, stamina) {
    this.name = name
    this.stamina = stamina
  }
  feed(quantity) {
    console.log(`${this.name} が食べています。`)
    this.stamina += quantity
  }
  rest(duration) {
    console.log(`${this.name} が休んでいます。`)
    this.stamina += duration
  }
  exercise(duration) {
    console.log(`${this.name} が運動しています。`)
    this.stamina -= duration
  }
}

const rex = new Creature('Rex', 7)
const sparky = new Creature('Sparky', 10)

これは前の例と比べて比較的簡単で明確です。

では、これがクラスを作成する新しい方法なのに、なぜ古い方法をこれだけ復習するのでしょうか?理由は、新しい方法(class キーワードを使用)が主に私たちが擬似クラスインスタンス化パターンと呼ぶ既存の方法の「シンタックスシュガー」だからです。ES6 クラスの便利な構文を完全に理解するには、まず擬似クラスインスタンス化パターンを理解する必要があります。

これで、JavaScript プロトタイプの基本原理をすべてカバーしました。この記事の残りの部分では、関連する他の良いトピックについて説明します。別の記事では、これらの基本原理を利用し、JavaScript での継承がどのように機能するかを理解する方法を説明します。

配列メソッド

上でインスタンス間でメソッドを共有する方法を深く掘り下げました。これらのメソッドをクラス(または関数)のプロトタイプに配置する必要があるはずです。Array クラスを見ると、同じパターンが見られます。

const items = []

これは new Array() の代わりに使用する構文糖衣です。

const itemsWithSugar = []
const itemsWithoutSugar = new Array()

考えたこともないかもしれませんが、配列の各インスタンスがすべての組み込みメソッド(splice、slice、pop など)をどのように持っているのでしょうか?

今はご存知の通り、これらのメソッドは Array.prototype に存在するからです。新しい Array インスタンスを作成するとき、new キーワードを使用して、失敗した検索で Array.prototype に委任するように設定します。

Array.prototype にどのメソッドがあるかを印刷できます:

console.log(Array.prototype)

/*
  concat: ƒn concat()
  constructor: ƒn Array()
  copyWithin: ƒn copyWithin()
  entries: ƒn entries()
  every: ƒn every()
  fill: ƒn fill()
  filter: ƒn filter()
  find: ƒn find()
  findIndex: ƒn findIndex()
  forEach: ƒn forEach()
  includes: ƒn includes()
  indexOf: ƒn indexOf()
  join: ƒn join()
  keys: ƒn keys()
  lastIndexOf: ƒn lastIndexOf()
  length: 0n
  map: ƒn map()
  pop: ƒn pop()
  push: ƒn push()
  reduce: ƒn reduce()
  reduceRight: ƒn reduceRight()
  reverse: ƒn reverse()
  shift: ƒn shift()
  slice: ƒn slice()
  some: ƒn some()
  sort: ƒn sort()
  splice: ƒn splice()
  toLocaleString: ƒn toLocaleString()
  toString: ƒn toString()
  unshift: ƒn unshift()
  values: ƒn values()
*/

オブジェクトにもまったく同じロジックが存在します。すべてのオブジェクトは失敗した検索後に Object.prototype に委任するため、すべてのオブジェクトに toStringhasOwnProperty などのメソッドがある理由です。

静的メソッド

これまで、なぜインスタンス間でメソッドを共有する必要があるのか、そしてどのように共有するのかを説明してきました。しかし、クラスにとって重要だが、インスタンス間で共有する必要がないメソッドがある場合はどうでしょうか?たとえば、Creature インスタンスの配列を受け取り、次にどれを餌付けする必要があるかを判断する関数があるとします。このメソッドを nextToFeed と呼びましょう。

function nextToFeed (creatures) {
  const sortedByLeastStamina = creatures.sort((a,b) => {
    return a.stamina - b.stamina
  })

  return sortedByLeastStamina[0].name
}

nextToFeed をすべてのインスタンス間で共有する必要がないため、Creature.prototypenextToFeed を使用する意味はありません。代わりに、補助メソッドとして扱うことができます。

では、nextToFeedCreature.prototype に存在するべきではない場合、どこに置くべきでしょうか?明らかな答えは、nextToFeedCreature クラスと同じスコープに置き、必要に応じて通常のように参照することです。

class Creature {
  constructor(name, stamina) {
    this.name = name
    this.stamina = stamina
  }
  feed(quantity) {
    console.log(`${this.name} が食べています。`)
    this.stamina += quantity
  }
  rest(duration) {
    console.log(`${this.name} が休んでいます。`)
    this.stamina += duration
  }
  exercise(duration) {
    console.log(`${this.name} が運動しています。`)
    this.stamina -= duration
  }
}

function nextToFeed (creatures) {
  const sortedByLeastStamina = creatures.sort((a,b) => {
    return a.stamina - b.stamina
  })

  return sortedByLeastStamina[0].name
}

const rex = new Creature('Rex', 7)
const sparky = new Creature('Sparky', 10)

console.log(nextToFeed([rex, sparky])) // Rex

これは機能しますが、より良い方法があります。

クラス自体に固有のメソッドだが、そのクラスのインスタンス間で共有する必要がない場合は、それをクラスの静的プロパティとして定義できます。

class Creature {
  constructor(name, stamina) {
    this.name = name
    this.stamina = stamina
  }
  feed(quantity) {
    console.log(`${this.name} が食べています。`)
    this.stamina += quantity
  }
  rest(duration) {
    console.log(`${this.name} が休んでいます。`)
    this.stamina += duration
  }
  exercise(duration) {
    console.log(`${this.name} が運動しています。`)
    this.stamina -= duration
  }
  static nextToFeed(creatures) {
    const sortedByLeastStamina = creatures.sort((a,b) => {
      return a.stamina - b.stamina
    })

    return sortedByLeastStamina[0].name
  }
}

今、nextToFeed をクラスの静的プロパティとして追加したため、それは Creature クラス自体(そのプロトタイプではなく)に存在し、Creature.nextToFeed を使って呼び出すことができます。

const rex = new Creature('Rex', 7)
const sparky = new Creature('Sparky', 10)

console.log(Creature.nextToFeed([rex, sparky])) // Rex

この記事全体で同じパターンに従ったので、ES5 で同じことをどのように行うかを見てみましょう。上の例では、static キーワードを使ってメソッドをクラス自体に直接追加する方法を見ました。ES5 を使用すると、同じパターンは関数オブジェクトにメソッドを手動で追加するのと同じくらい簡単です。

function Creature (name, stamina) {
  this.name = name
  this.stamina = stamina
}

Creature.prototype.feed = function (quantity) {
  console.log(`${this.name} が食べています。`)
  this.stamina += quantity
}

Creature.prototype.rest = function (duration) {
  console.log(`${this.name} が休んでいます。`)
  this.stamina += duration
}

Creature.prototype.exercise = function (duration) {
  console.log(`${this.name} が運動しています。`)
  this.stamina -= duration
}

Creature.nextToFeed = function (creatures) {
  const sortedByLeastStamina = creatures.sort((a,b) => {
    return a.stamina - b.stamina
  })

  return sortedByLeastStamina[0].name
}

const rex = new Creature('Rex', 7)
const sparky = new Creature('Sparky', 10)

console.log(Creature.nextToFeed([rex, sparky])) // Rex

オブジェクトのプロトタイプを取得する

どのパターンを使用してオブジェクトを作成した場合でも、Object.getPrototypeOf メソッドを使用してそのオブジェクトのプロトタイプを取得できます。

function Creature (name, stamina) {
  this.name = name
  this.stamina = stamina
}

Creature.prototype.feed = function (quantity) {
  console.log(`${this.name} が食べています。`)
  this.stamina += quantity
}

Creature.prototype.rest = function (duration) {
  console.log(`${this.name} が休んでいます。`)
  this.stamina += duration
}

Creature.prototype.exercise = function (duration) {
  console.log(`${this.name} が運動しています。`)
  this.stamina -= duration
}

const rex = new Creature('Rex', 7)
const proto  = Object.getPrototypeOf(rex)

console.log(proto)
// {constructor: ƒ, feed: ƒ, rest: ƒ, exercise: ƒ}

proto === Creature.prototype // true

上記のコードには2つの重要なポイントがあります。

まずproto は4つのメソッドを持つオブジェクトであることに気づくでしょう:constructorfeedrestexercise。これは理にかなっています。getPrototypeOf を使用してインスタンスを渡し、インスタンスのプロトタイプを取得します。これがすべてのメソッドが存在する場所です。

これも prototype についての別の事実を教えてくれます。まだ説明していないことですが、デフォルトでは prototype オブジェクトは constructor プロパティを持っています。このプロパティは初期関数またはインスタンスを作成したクラスを指します。これはまた、JavaScript がデフォルトでプロトタイプにコンストラクタプロパティを配置するため、どのインスタンスでもコンストラクタプロパティにアクセスできることを意味します。

2番目の重要なポイントはObject.getPrototypeOf(rex) === Creature.prototype です。これは理にかなっています。Creature コンストラクタには prototype プロパティがあり、すべてのインスタンス間でメソッドを共有できます。getPrototypeOf はインスタンス自体のプロトタイプを見ることを許可します。

function Creature (name, stamina) {
  this.name = name
  this.stamina = stamina
}

const rex = new Creature('Rex', 7)
console.log(rex.constructor) // コンストラクタ関数をログ出力

これは、私たちが前に Object.create を使って議論した内容と一致しています。なぜなら、どの Creature インスタンスも失敗した検索で Creature.prototype に委任するからです。したがって、rex.constructor にアクセスしようとすると、rexconstructor プロパティがないため、検索は失敗し、Creature.prototype に委任されます。そして Creature.prototype は実際にコンストラクタプロパティを持っています。

以前 __proto__ を使ってインスタンスのプロトタイプを取得したことがあるかもしれません。これは過去の遺物です。代わりに、上記のように Object.getPrototypeOf(instance) を使用してください。

プロトタイプにプロパティが含まれているか確認する

場合によっては、プロパティがインスタンス自体にあるのか、それともオブジェクトが委任するプロトタイプにあるのかを知る必要があります。作成した rex オブジェクトをループして印刷することでこれを見ることができます。for in ループ方式は次のとおりです:

function Creature (name, stamina) {
  this.name = name
  this.stamina = stamina
}

Creature.prototype.feed = function (quantity) {
  console.log(`${this.name} が食べています。`)
  this.stamina += quantity
}

Creature.prototype.rest = function (duration) {
  console.log(`${this.name} が休んでいます。`)
  this.stamina += duration
}

Creature.prototype.exercise = function (duration) {
  console.log(`${this.name} が運動しています。`)
  this.stamina -= duration
}

const rex = new Creature('Rex', 7)

for(let key in rex) {
  console.log(`キー: ${key}. 値: ${rex[key]}`)
}

予想される印刷結果は次のようになるかもしれません:

キー: name. 値: Rex
キー: stamina. 値: 7

しかし、コードを実行すると、次のようになります:

キー: name. 値: Rex
キー: stamina. 値: 7
キー: feed. 値: function (quantity) {
  console.log(`${this.name} が食べています。`)
  this.stamina += quantity
}
キー: rest. 値: function (duration) {
  console.log(`${this.name} が休んでいます。`)
  this.stamina += duration
}
キー: exercise. 値: function (duration) {
  console.log(`${this.name} が運動しています。`)
  this.stamina -= duration
}

なぜでしょうか?for in ループにとって、ループはオブジェクト自体とそれが委任するプロトタイプのすべての列挙可能なプロパティを反復処理します。なぜなら、デフォルトでは関数のプロトタイプに追加するプロパティはすべて列挙可能であるため、namestamina だけでなく、プロトタイプ上のすべてのメソッド - feedrestexercise も見ることになります。

この問題を解決するには、すべてのプロトタイプメソッドを列挙不可にするか、rex オブジェクト自体にあるプロパティのみを印刷し、rex が失敗した検索のために委任するプロトタイプではないようにする必要があります。これは hasOwnProperty が助けになる場所です。

...

const rex = new Creature('Rex', 7)

for(let key in rex) {
  if (rex.hasOwnProperty(key)) {
    console.log(`キー: ${key}. 値: ${rex[key]}`)
  }
}

今、rex オブジェクト自体のプロパティのみが表示され、rex が委任するプロトタイプではありません。

キー: name. 値: Rex
キー: stamina. 値: 7

hasOwnProperty がまだ混乱している場合は、より良く理解するのに役立つコードをいくつか示します。

function Creature (name, stamina) {
  this.name = name
  this.stamina = stamina
}

Creature.prototype.feed = function (quantity) {
  console.log(`${this.name} が食べています。`)
  this.stamina += quantity
}

Creature.prototype.rest = function (duration) {
  console.log(`${this.name} が休んでいます。`)
  this.stamina += duration
}

Creature.prototype.exercise = function (duration) {
  console.log(`${this.name} が運動しています。`)
  this.stamina -= duration
}

const rex = new Creature('Rex', 7)

rex.hasOwnProperty('name') // true
rex.hasOwnProperty('stamina') // true
rex.hasOwnProperty('feed') // false
rex.hasOwnProperty('rest') // false
rex.hasOwnProperty('exercise') // false

オブジェクトがクラスのインスタンスかどうかを確認する

オブジェクトが特定のクラスのインスタンスかどうかを知りたい場合があります。これには instanceof 演算子を使用できます。ユースケースは非常に単純ですが、以前に見たことがない場合は、実際の構文は少し奇妙に見えるかもしれません。それは次のように機能します:

object instanceof Class

上記のステートメントは、objectClass のインスタンスの場合に true を返し、そうでない場合は false を返します。Creature の例に戻ると、次のようなものがあります:

function Creature (name, stamina) {
  this.name = name
  this.stamina = stamina
}

function User () {}

const rex = new Creature('Rex', 7)

rex instanceof Creature // true
rex instanceof User // false

instanceof の動作方法は、オブジェクトのプロトタイプチェーンに constructor.prototype が存在するかどうかをチェックすることです。上の例では、rex instanceof Creature が true なのは、Object.getPrototypeOf(rex) === Creature.prototype だからです。また、rex instanceof User が false なのは、Object.getPrototypeOf(rex) !== User.prototype だからです。

新しいコンストラクタを作成する

以下のコードのエラーを見つけられますか?

function Creature (name, stamina) {
  this.name = name
  this.stamina = stamina
}

const rex = Creature('Rex', 7)

経験豊富な JavaScript 開発者でさえ、時々上記の例でつまずきます。なぜなら、私たちが前に学んだ擬似クラスインスタンスパターンを使用しているため、Creature コンストラクタを呼び出すときに new キーワードを使用する必要があるからです。これをしないと、this キーワードは作成されず、暗黙的に返されません。

復習のために、コメントされた行は、new キーワードで関数を呼び出したときに背後で発生するものです。

function Creature (name, stamina) {
  // const this = Object.create(Creature.prototype)

  this.name = name
  this.stamina = stamina

  // return this
}

他の開発者が new キーワードを使うことを忘れないようにするのは、重要な詳細のようです。他の開発者と協力していると仮定すると、私たちの Creature コンストラクタが常に new キーワードで呼び出されるようにする方法はありますか?実は、前に学んだ instanceof 演算子を使って実現できます。

コンストラクタが new キーワードで呼び出された場合、コンストラクタ本体の内部 this はコンストラクタ自体のインスタンスになります。

function Creature (name, stamina) {
  if (this instanceof Creature === false) {
     console.warn('new キーワードで Creature を呼び出すのを忘れました')
  }

  this.name = name
  this.stamina = stamina
}

では、関数を再び呼び出すが、今回は new キーワードを使用し、単に呼び出し元に警告を印刷する代わりに、どうでしょうか?

function Creature (name, stamina) {
  if (this instanceof Creature === false) {
    return new Creature(name, stamina)
  }

  this.name = name
  this.stamina = stamina
}

これで、new キーワードを使用するかどうかに関係なく、Creature は正常に機能します。

Object.create を再実装する

この記事では、失敗した検索でコンストラクタのプロトタイプに委任するオブジェクトを作成するために Object.create に非常に依存しています。この時点で、コードで Object.create を使用する方法は知っているはずですが、Object.create が実際にどのように機能するかを考えることはありませんでした。Object.create の動作を本当に理解するために、自分で再実装してみましょう。まず、Object.create の動作についてどれだけ知っていますか?

  1. オブジェクトのパラメータを受け取ります。
  2. 失敗した検索で委任するオブジェクトを作成します
  3. 作成されたオブジェクトを返します。
Object.create = function (objToDelegateTo) {

}

今、失敗した検索で委任するオブジェクトを作成する必要があります。これは少しトリッキーです。これを行うために、new キーワードに関する知識を使用します。

まず、Object.create 本体内で空の関数を作成します。次に、空の関数の prototype を渡されたパラメータオブジェクトに等しく設定します。次に、new キーワードで空の関数を呼び出して返します。

Object.create = function (objToDelegateTo) {
  function Fn(){}
  Fn.prototype = objToDelegateTo
  return new Fn()
}

上記のコードで新しい関数 Fn を作成すると、prototype プロパティが付いた関数が作成されます。new キーワードで呼び出すと、失敗した検索で関数のプロトタイプに委任されるオブジェクトが得られることがわかっています。

関数のプロトタイプを上書きすると、失敗した検索でどのオブジェクトに委任するかを決定できます。したがって、上の例では、Object.create を呼び出すときに渡されたオブジェクトで Fn のプロトタイプを上書きし、objToDelegateTo と呼びます。

注意:Object.create の単一パラメータのみをサポートしています。公式実装は、作成されたオブジェクトに追加のプロパティを追加できる2番目のオプションパラメータもサポートしています。

アロー関数

アロー関数は独自の this キーワードを持っていません。したがって、アロー関数はコンストラクタにはできず、new キーワードで呼び出そうとするとエラーが発生します。

const Creature = () => {}

const rex = new Creature() // Error: Creature is not a constructor

また、上で説明したように擬似クラスインスタンスパターンはアロー関数と一緒に使用できないため、アロー関数にはプロトタイププロパティもありません。

const Creature = () => {}
console.log(Creature.prototype) // undefined

一如前端深似海,从此时间是路人。 作者:incess

原文地址:https://my.oschina.net/incess/blog/3049294

喜欢 0

タグ: javascript prototype class Object.create instanceof

6月28日 19:21 投稿