Overreacted

Reactはどうやって関数からクラスを見分けるているの?

2018 M12 2 • ☕️☕️ 9 min read

関数として定義されたGreetingコンポーネントについて考えてみましょう:

function Greeting() {
  return <p>Hello</p>;
}

Reactはclassとしての定義もサポートしています:

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

(最近までステートの機能を使うための唯一の方法でした。)

<Greeting />を描画するとき、どのように定義されたか気にする必要はありません。:

// クラスもしくは関数 — なんでも.
<Greeting />

しかしReact自身は違いを気にする必要があります!

Greetingが関数ならReactは下記のように呼ぶ必要があります

// あなたのコード
function Greeting() {
  return <p>Hello</p>;
}

// React内部
const result = Greeting(props); // <p>Hello</p>

しかし、もしGreetingがクラスの場合、Reactはnew演算子と作成したインスタンスに対してrender関数を呼ぶ必要があります。:

// あなたのコード
class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// React内部
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>

どちらのケースでもReactの目的は描画したノードを取得することです。(この例では<p>Hello</p>)しかし、実際のステップはどのようにGreetingが定義されたかということに依存しています。

Reactはどのようにしてクラスか関数か知るのでしょうか?

前の投稿のように、Reactを効率的に使うためにこれを知る必要はありません。 私はこれを何年間も知りませんでした。どうかこれを面接の質問にしないでください。実際、Reactについてというよりも、Javascriptについての投稿です。

このブログはReactがなぜこのように動いているのか知りたい好奇心の強い読者向けです。あなたはそのような人ですか?一緒に深掘りしてみましょう。

これは長い旅です。ベルトを締めてください。その投稿はReact自身についての十分な情報は扱っていません。しかし、Javascriptでnew, this, class, arrow functions, prototype, __proto__,instanceofのこれらがどのように機能するか説明します。幸運にもReactを使う時は、これらのことを考える必要がありませんでした。

(答えを知りたいだけなら最後までスクロールしてください。)


はじめに、私たちはなぜ関数とクラスの違いを扱うことが大切なのか理解する必要があります。Note: クラスを呼び出す時にnew演算子を使う方法:

// Greetingが関数なら
const result = Greeting(props); // <p>Hello</p>

// Greetingがクラスなら
const instance = new Greeting(props); // Greeting {}const result = instance.render(); // <p>Hello</p>

JavaScriptで new演算子がすることの大まかな動きを理解しましょう。


昔は、Javascriptはクラスを持っていませんでした。しかしながら普通の関数を使ってクラスと同じようなパターンを表現できます。 具体的には呼び出しの前にnewを追加することで任意の関数をクラスのコンストラクタに似た役割で使うことができます。 :

// 単なる関数
function Person(name) {
  this.name = name;
}

var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 動かない

今日でもこんなコードを書くことができます! DevToolsで試してみてください。

もし Person('Fred')newなしで呼び出したら、その中のthisはグローバルで無用なものを指すでしょう。(例えば windowsundefined)だから、そのコードはクラッシュしたり、window.nameに設定するような愚かなことをするでしょう。

呼び出しの前にnewを追加することで、私たちはこう言います。「やあJavascript、Personは単なる関数だってことは知っている。だけど、それをクラスコンストラクタのようなものにしよう。オブジェクト({})を作成し、Person関数内でthisはそのオブジェクトを指すようにして、this.nameに値を割り当てる。その後そのオブジェクトを返してほしいんだ。

これがnew演算子がすることです。

var fred = new Person('Fred'); // `Person`の中の`this`と同じオブジェクト

new演算子はPerson.prototypeに追加したもの全てをfredオブジェクトで使えるようにします。:

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {  alert('Hi, I am ' + this.name);}
var fred = new Person('Fred');
fred.sayHi();

これがJavascriptでクラスをエミュレートする方法です。


だからnewは結構前からJavascriptに登場しています。しかしながらクラスは最近です。最近のクラスはさらに直感的に上のコードを書き直すことができます。:

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    alert('Hi, I am ' + this.name);
  }
}

let fred = new Person('Fred');
fred.sayHi();

開発者の意図を捉えること は言語とAPI設計において重要です。

関数を書いたら、Javascriptはそれがalert()みたいに呼ばれることを意図しているのか、それともnew Person()みたいにコンストラクタとして呼ばれるのか推測できない。

クラス構文は「これは関数じゃない、それはクラスでコンストラクタを持っている」と言ってくれる もしnewをつけ忘れて呼ぶとJavascriptはエラーを発生させる。:

let fred = new Person('Fred');
// ✅  もしPersonが関数なら: うまく動く
// ✅  もしPersonがクラスなら: これもうまく動く

let george = Person('George'); // We forgot `new`
// 😳  もしPersonがコンスラクタみたいな関数なら: 混乱した振る舞いになる
// 🔴  もしPersonがクラスなら: 即エラー

これは、this.namegeorge.nameではなくwindow.nameとして扱われるようなあいまいなバグのままにせず、早い段階でミスを見つけるのに役立ちます。

しかしながらそれはReactはどんなクラスでもnewを書かないといけないということを意味します。Javascriptはそれをエラーとして扱うので、普通の関数を単に呼び出せない!

class Counter extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// 🔴 React can't just do this:
const instance = Counter(props);

トラブルの種です。


Reactがこれをどうやって解決するかを見る前に、Reactを使うほとんどの人がBabelのようなコンパイラを使って古いブラウザのためにクラスのような機能をコンパイルしていることを覚えておくことが重要です。だから我々はReactを作る上での設計でコンパイラを考慮する必要があります。

Babelの初期のバージョンはクラスはnewなしで呼び出すことができました。しかし、これは下記のコードを生成することで修正されました。

function Person(name) {
  // Babelの出力から少し簡略化したもの:
  if (!(this instanceof Person)) {
    throw new TypeError("Cannot call a class as a function");
  }
  // 自分のコード:
  this.name = name;
}

new Person('Fred'); // ✅ OK
Person('George');   // 🔴 Cannot call a class as a function

もしかしたらバンドルされたコード中で_classCallCheckというコードをみたことがあるかもしれません。上記の例がそれです。 (ルーズモードのオプションでバンドルサイズを減らすことができますが、最終的にネイティブのクラスへの移行を複雑にするかもしれません。)


ここまでで、 newを付けて呼び出した場合とnewを付けずに呼び出した場合の違いをおおまかに理解できるはずです。

new Person() Person()
class this is a Person instance 🔴 TypeError
function this is a Person instance 😳 this is window or undefined

そのため、Reactがコンポーネントを正しく呼び出すことが重要です。 あなたのコンポーネントがクラスとして定義されている場合、Reactはそれを呼び出すときに newを使う必要があります。

それでReactは呼び出そうとしているコンポーネントがクラスであるかどうかを単にチェックすることができますか?

そう簡単ではありません!JavaScriptの関数からクラスを見分けることができたとしても、これはまだBabelのようなツールで処理されたクラスにはうまくいかないでしょう。ブラウザにとっては、それらは単なる普通の関数です。 Reactは頑張ってください。


OK,もしかしたらReactは全ての呼び出しにnewを使えばいいのでは?しかし残念なことに、それは常に正しく動くとは限りません。

通常の関数では、それらを newで呼び出すと、それらにthisとしてオブジェクトインスタンスが与えられます。これはコンストラクタとして書かれた関数(上記の Person)には望ましいですが、関数のコンポーネントには混乱を招くでしょう:

function Greeting() {
  // ここで `this`が他の種類のインスタンスであるとは思わないでしょう
  return <p>Hello</p>;
}

それは許容できるかもしれませんが、この考えをやめるのは他に2つの理由があります。


常にnewを使用してもうまくいかない最初の理由は、ネイティブのarrow関数(Babelによってコンパイルされたものではない)では、newを指定して呼び出すとエラーが発生するためです。:

const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor

この動作は意図的なもので、arrow関数の設計に基づいています。arrow関数の主な利点の1つは、それらが独自の this値を持たないということです - 代わりに、thisは最も近い通常の関数から解決されます。:

class Friends extends React.Component {
  render() {    const friends = this.props.friends;
    return friends.map(friend =>
      <Friend
        // `this`は`render`メソッドから解決されます        size={this.props.size}        name={friend.name}
        key={friend.id}
      />
    );
  }
}

というわけでarrow関数はそれ自身の thisを持っていません。 それはコンストラクタとして全く役に立たないことを意味します!

const Person = (name) => {
  // 🔴 これは意味がない!
  this.name = name;
}

そのため、JavaScriptでは newを使用してarrow関数を呼び出すことはできません。 これを実行した場合は、間違いを犯している可能性があります。これは、JavaScriptがクラスをnew無しで呼び出せないのと似ています。

これは素晴らしいことですが、 Reactはすべてのものに対して newを呼び出すだけでは不可能です。arrow関数が壊れるから!しかし、newをつけず、prototypeの欠如によってarrow関数を検出を試みることができます。:

(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}

しかしこれはBabelでコンパイルされた関数にはうまく動きません。 これは大したことではないかもしれませんが、このアプローチを行き止まりにするもう1つの理由があります。


常にnewを使うことができないもう一つの理由は、Reactが文字列や他のプリミティブ型を返すコンポーネントをサポートすることを妨げるということです。

function Greeting() {
  return 'Hello';
}

Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}

これもまた、new演算子の設計に関係しています。 前に見たように、 newはJavaScriptエンジンにオブジェクトを作成し、そのオブジェクトを関数の中でのthisにし、そして後で newの結果としてそのオブジェクトを渡すように伝えます。

しかしながら、JavaScriptでは、他のオブジェクトを返すことによって、newで呼び出された関数がnewの戻り値をオーバーライドすることもできます。おそらく、これはインスタンスを再利用したい場合のプーリングのようなパターンに役立つと考えられていました。

// 遅延作成var zeroVector = null;
function Vector(x, y) {
  if (x === 0 && y === 0) {
    if (zeroVector !== null) {
      // 同じインスタンスを再利用する      return zeroVector;    }
    zeroVector = this;
  }
  this.x = x;
  this.y = y;
}

var a = new Vector(1, 1);
var b = new Vector(0, 0);var c = new Vector(0, 0); // 😲 b === c

ただし、関数がオブジェクトではない場合、newは関数の戻り値を完全に無視します。 あなたが文字列や数字を返す場合、それは returnが全くなかったように振る舞います。

function Answer() {
  return 42;
}

Answer(); // ✅ 42
new Answer(); // 😳 Answer {}

newでそれを呼び出すときに、関数からプリミティブな戻り値(数字や文字列のような)を受け取る方法は全くありません。 そのため、Reactが常に newを使っていたら、文字列を返すサポートコンポーネントを追加することはできません。

それは受け入れられないので、諦める必要があります。


これまでに何を学びましたか? Reactは newを使ってクラス(Babel出力を含む)を呼び出す必要がありますが、newを使わずに通常の関数やarrow関数(Babel出力を含む)を呼び出す必要があります。 そしてそれらを区別する信頼できる方法はありません。

一般的な問題を解決できないなら、より具体的な問題なら解決できるかもしれません。

コンポーネントをクラスとして定義するとき、おそらく this.setState()のような組み込みメソッドのために React.Componentを拡張します。すべてのクラスを検出しようとするのではなく、 React.Componentの子孫だけを検出できますか?

ネタバレ:これはReactがすることです。


おそらく、 GreetingがReactコンポーネントクラスかどうかをチェックする慣用的な方法は、Greeting.prototype instanceof React.Componentかどうかをテストすることです。

class A {}
class B extends A {}

console.log(B.prototype instanceof A); // true

私はあなたが何を思っているかわかりますよ。 ここで何が起きたのですか? これに答えるためには、JavaScriptプロトタイプを理解する必要があります。

もしかしたらあなたは“prototype chain”に精通しているかもしれません。Javascriptでは全てのオブジェクトは“prototype”を持っています。fred.sayHi()を書いたときに、fredオブジェクトがsayHiプロパティを持っていなかったら、fredのプロトタイプでsayHiを探します。もしそこで見つからなかったら、チェーン内から次のプロトタイプであるfredのプロトタイプのプロトタイプを探します。

紛らわしいことに、クラスや関数の prototypeプロパティはその値のプロトタイプを指し示すわけではありません。 冗談じゃないよ。

function Person() {}

console.log(Person.prototype); // 🤪 Personのprototypeじゃない
console.log(Person.__proto__); // 😳 Personのprototype

「プロトタイプチェーン」は prototype.prototype.prototypeより__proto__.__proto__.__proto__ですね。 私はこれに何年も要しましたよ。

それでは、関数やクラスの prototypeプロパティは何ですか? それはそのクラスまたは関数で newされたすべてのオブジェクトに与えられた__proto__です!

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  alert('Hi, I am ' + this.name);
}

var fred = new Person('Fred'); // `fred.__proto__`に`Person.prototype`を設定

そしてその __proto__チェーンがJavaScriptがプロパティを調べる方法です。:

fred.sayHi();
// 1. Does fred have a sayHi property? No.
// 2. Does fred.__proto__ have a sayHi property? Yes. Call it!

fred.toString();
// 1. Does fred have a toString property? No.
// 2. Does fred.__proto__ have a toString property? No.
// 3. Does fred.__proto__.__proto__ have a toString property? Yes. Call it!

実際には、プロトタイプチェーンに関連するものをデバッグしているのでなければ、コードから直接 __proto__を直接触る必要はないはずです。fred.__proto__で利用可能にしたい場合は、それをPerson.prototypeに置くことになっています。少なくともそれはもともと設計された方法です。

プロトタイプチェーンは内部概念と考えられていたため、 __proto__プロパティは最初はブラウザによって公開されることさえ想定されていませんでした。しかし、いくつかのブラウザは __proto__を追加し、結局それはひどく標準化されました(しかしObject.getPrototypeOf()を支持して推奨されなくなりました)。

それでもなお、 prototypeと呼ばれるプロパティが値のプロトタイプを与えないことは非常に混乱します。 (例えば、fredは関数ではないので fred.prototypeは未定義です。)個人的には、これが経験豊富な開発者でさえJavaScriptプロトタイプを誤解しがちな最大の理由だと思います。


これは長い記事ですね。 現在80%くらいの場所にいると思います。 あとちょっと。

obj.fooを実行したとき、JavaScriptは実際にはobjfooを探し、 obj.__proto__obj.__proto__.__proto__などのように続きます。

クラスでは、このメカニズムに直接さらされることはありませんが、 extendsは古き良きプロトタイプチェーンの上でも機能します。 それが私たちのReactクラスインスタンスが setStateのようなメソッドにアクセスする方法です:

class Greeting extends React.Component {  render() {
    return <p>Hello</p>;
  }
}

let c = new Greeting();
console.log(c.__proto__); // Greeting.prototype
console.log(c.__proto__.__proto__); // React.Component.prototypeconsole.log(c.__proto__.__proto__.__proto__); // Object.prototype

c.render();      // Found on c.__proto__ (Greeting.prototype)
c.setState();    // Found on c.__proto__.__proto__ (React.Component.prototype)c.toString();    // Found on c.__proto__.__proto__.__proto__ (Object.prototype)

言い換えれば、クラスを使うとき、インスタンスの __proto__チェーンはクラス階層を反映しています。:

// `extends` chain
Greeting
  → React.Component
    → Object (implicitly)

// `__proto__` chain
new Greeting()
  → Greeting.prototype
    → React.Component.prototype
      → Object.prototype

__proto__チェーンはクラス階層を反映しているので、Greeting.prototypeから始めて、その__proto__チェーンをたどることでGreetingReact.Componentを拡張しているかどうかをチェックすることができます。:

// `__proto__` chain
new Greeting()
  → Greeting.prototype // 🕵️ ここから始める    → React.Component.prototype // ✅ Found it!      → Object.prototype

便利なことに、x instanceof Yはまさにこの検索を行います。 それはx.__proto__チェーンで Y.prototypeを探します。

通常は、何かがクラスのインスタンスであるかどうかを判断するために使用されます。:

let greeting = new Greeting();

console.log(greeting instanceof Greeting); // true
// greeting (🕵️‍ ここから始める)
//   .__proto__ → Greeting.prototype (✅ 見つけた!)
//     .__proto__ → React.Component.prototype 
//       .__proto__ → Object.prototype

console.log(greeting instanceof React.Component); // true
// greeting (🕵️‍ ここから始める)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype (✅ 見つけた!)
//       .__proto__ → Object.prototype

console.log(greeting instanceof Object); // true
// greeting (🕵️‍ ここから始める)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype
//       .__proto__ → Object.prototype (✅ 見つけた!)

console.log(greeting instanceof Banana); // false
// greeting (🕵️‍ ここから始める)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype 
//       .__proto__ → Object.prototype (🙅‍ 見つからなかった!)

しかし、あるクラスが別のクラスを継承しているかどうかを判断するのにも使えます。

console.log(Greeting.prototype instanceof React.Component);
// greeting
//   .__proto__ → Greeting.prototype (🕵️‍ ここから始める)
//     .__proto__ → React.Component.prototype (✅ 見つけた!)
//       .__proto__ → Object.prototype

そしてこのチェックは、Reactコンポーネントクラスなのか通常の関数なのかを判断する方法です。


しかしこれはReactがすることではありません。 😳

instanceofソリューションの注意点の1つは、ページ上にReactのコピーが複数ある場合、それが機能しないこと、そしてチェックしているコンポーネントが別のReactコピーのReact.Componentから継承されることです。1つのプロジェクトにReactの複数のコピーを混在させるのは、いくつかの理由で好ましくありませんが、私たちはこれまで可能な限り問題を避けるようにしてきました。(Hooksの場合、重複排除を強制する必要があるかもしれません。)

もう1つの可能性のある発見的方法は、プロトタイプ上の renderメソッドの存在をチェックすることです。ただし、その当時は、コンポーネントAPIがどのように進化するのか明確ではありませんでした。すべてのチェックにはコストがかかるため、複数を追加することは望ましくありません。 クラスプロパティ構文のように renderがインスタンスメソッドとして定義されている場合もこれは機能しません。

その代わりに、コンポーネントに特別なフラグをReactに追加しました。Reactはそのフラグの存在をチェックし、それがReactコンポーネントクラスであるかどうかを知る方法です。

もともとフラグはReact.Componentクラス自体にありました:

// Inside React
class Component {}
Component.isReactClass = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ Yes

しかし、私たちがターゲットにしたかったクラス実装の中には静的プロパティをコピーしない(あるいは非標準の__proto__を設定する)ものがあったので、フラグは失われていました。

これが、ReactがこのフラグをReact.Component.prototype移動した理由です。

// React内部
class Component {}
Component.prototype.isReactComponent = {};

// こんな感じでチェックできます。
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // ✅ Yes

そしてこれは文字通りすべてです。

なぜそれが単なるブール値ではなくオブジェクトであるのか疑問に思うかもしれません。実際にはそれほど重要ではありませんが、Jestの初期のバージョン(JestがGood™️以前のバージョン)では、デフォルトで自動モックが有効になっていました。生成されたモックはプリミティブプロパティを省略し、チェックを破りました。 ありがとう、Jest。

isReactComponentチェックは今日Reactで使われています。

React.Componentを継承しないのであれば、Reactはプロトタイプ上でisReactComponentを見つけることができず、コンポーネントをクラスとして扱うこともできません。今、あなたはCannot call a class as a functionのエラーに対する最も支持された答えextends React.Componentを追加することである理由はわかりますね。最後に、prototype.renderが存在するがprototype.isReactComponentが存在しない場合に警告するというのも追加されました。


もしかしたらあなたはこの話が引っ掛けだと言うかもしれません。実際の解決策は非常に単純ですが、Reactがこの解決策を採用した理由とその代替案について説明するために、話を大きく脱線しました。

私の経験では、ライブラリのAPIの場合、APIを使いやすくするためには、言語のセマンティクス(将来の方向性を含むいくつかの言語について)、実行時のパフォーマンス、コンパイルの手順、エコシステムの状態、およびパッケージソリューション、早期警告など、多くのことを考慮する必要があります。最終的な結果は必ずしも最も洗練されたものではないかもしれませんが、それは実用的でなければなりません。

最終的なAPIが成功した場合、そのユーザーはこのプロセスについて考える必要はありません。 代わりに、彼らはアプリの作成に集中することができます。

しかし、あなたも興味があればそれがどのように動くのか知っているのはいいことです。