クラス [JavaScript]
もりけん塾(@terrace_tech)にて、JavaScriptを勉強しています。
今回は、クラスについて調べていきたいと思います。
クラスを学ぶきっかけ
前回までのJavaScript学習では、コンストラクタ関数やファクトリ関数について調べて分かってきたことをまとめてきました。prototypeチェーンの仕組みなど、基本的な部分については大分理解が進んできたように感じています。
クラスについては、ReactやTypeScriptを学んだ際に使う機会があったのですが、何となく写経する程度でちゃんとした仕組みは分かっていませんでした。
このまま分からないままだと他人に説明できないコーディングになってしまうので、一度調べてみる必要があると思い、クラスについて掘り下げてみることにしました。
クラスの学び方
私は今回、クラスについて説明しているページをひたすら読み漁ることで理解を深めていきました。何となく把握出来てきたら、実際にコーディングをしていくとさらに学習が進むと思います。
クラスを使った書き方は「JavaScript class use case」や「JavaScript MVC 」などと検索すると、色々出てきます。To Do リストや、簡単なゲームを作りながら学んでいけるチュートリアルなども見つかるのではないかと思います。
クラスとは
クラスは、コンストラクタ関数などと同じく、インスタンスを作る際のひな型となる関数です。ES6以降に導入された、新しいコンストラクタ関数の書き方です。
基本的な部分は従来のコンストラクタ関数と変わりませんが、prototypeメソッドがより分かりやすく書けるようになったり、classキーワードを使って関数を設定することで、より読みやすいコードが書けるようになりました。
クラス関数の設定方法
従来のコンストラクタ関数と違う点がいくつかあります。
その違いを見てみるために、同じ役割をするひな型を、クラスとコンストラクタ関数の両方で書いてみたいと思います。
ここでは例として、新しいbookオブジェクトを作り出す関数を作成します。
コンストラクタ関数の書き方
インスタンスに持たせたいプロパティを、thisキーワードを使って設定します。
function BookConstructor(title, author, year) { this.title = title; this.author = author; this.year = year; }
クラスの書き方
クラスの場合も同じく、thisキーワードを使用してプロパティを設定します。その際、それらのプロパティはconstructorの中に設定します。
class BookClass { constructor(title, author, year) { this.title = title this.author = author this.year = year } }
インスタンスを作る際は、どちらも同じくnewキーワードを付けて関数を呼び出します。
const book1 = new BookConstructor("人間失格", "太宰治", 1948);
const book2 = new BookClass("人間失格", "太宰治", 1948);
クラスの中に書いてある「constructor」とは
コンストラクタ(constructor)は、newキーワードを使用してインスタンスを作成する際に自動的に呼び出されるメソッドです。
呼び出された際、インスタンスにプロパティを割り当てるなどの初期設定を行います。
コンストラクタはクラスの中に1つだけ設定することが出来ます。
prototypeメソッドの定義の仕方
こちらも、従来のコンストラクタ関数でのやり方とクラスでのやり方を比較しながら見ていきます。
コンストラクタ関数の場合
まずコンストラクタ関数の場合、メソッドを関数内に定義すると、作られたインスタンスの中にもメソッドが付与されます。
function BookConstructor(title, author, year) { this.title = title; this.author = author; this.year = year; this.calculateYear = function() { return `この本は${2020 - this.year}年前に発行されました`; } } const book = new BookConstructor("羅生門", "芥川龍之介", 1915);
インスタンスを作成するたびにメソッドが付与されるとメモリを圧迫してしまうので、メソッドを定義する場合は、prototypeとして設定するのが望ましい方法です。
メソッドをprototypeとして設定するように書き直します。
function BookConstructor(title, author, year) { this.title = title; this.author = author; this.year = year; } BookConstructor.prototype.calculateYear = function() { this.calculateYear = function() { return `この本は${2020 - this.year}年前に発行されました`; } }
上記のコンストラクタ関数を使用し、新たにインスタンスを作成します。
const book = new BookConstructor("人間失格", "太宰治", 1948);
作成されたインスタンスを見てみると、上記で設定したメソッドcalculateYear
は、prototypeの中に入っていることが分かります。
クラスの場合
クラスの場合、クラス関数内に設定したメソッドは、自動的にprototypeとして設定されます。
なのでコンストラクタ関数の時のようにBookConstructor.prototype.calculateYear()
と、prototypeとして分けて設定する必要がありません。
下記の例では、BookClassクラスの中にメソッドcalculateYear()
が設定してあります。
class BookClass { constructor(title, author, year) { this.title = title; this.author = author; this.year = year; } // クラス関数内でメソッドを設定する calculateYear() { return `この本は${2020 - this.year}年前に発行されました`; } }
このクラスを使用し、インスタンスを作成します。
const book = new BookClass("人間失格", "太宰治", 1948);
インスタンスの詳細を見てみると、メソッドcalculateYear()
は、prototypeとして設定されていることが分かります。
extends と super
クラスを作った後に、サブクラス(子クラス)を設定したい場合は、extendsキーワードを使って新たにクラスを設定します。
サブクラスは、親クラスのプロパティやメソッドを継承することが出来ます。
例えば、親クラス Book
の下に、サブクラス Comic
を持たせる場合、下記のように設定します。
class Book { constructor(title, author, year) { this.title = title; this.author = author; this.year = year; } } class Comic extends Book { constructor(title, author, year, magazine) { super(title, author, year) this.magazine = magazine; } }
サブクラスの constructor の中では super というキーワードを使うことで、親クラスのプロパティを呼び出すことが出来ます。
この際、super は constructor の一番上に書く必要があります。一番上に書かない場合はエラーになってしまいます。
上記のサブクラスを元にインスタンスを作成し、詳細を確認してみます。
const comic = new Comic("美少女戦士セーラームーン","武内直子", 1992, "なかよし");
サブクラスのComicが、親クラスBookのプロパティtitle
、author
、year
を継承していることが確認できました。
getter と setter
getter と setterとは
getter と setterは、プロパティのように扱うことが出来るメソッドです。メソッドでありながらプロパティのような振舞いをするため、アクセッサプロパティと呼ばれています。
オブジェクトを扱う際、プロパティの値を直接変更することは一般的に望ましくないとされています。
そこで getter や setter を使うことで、実際にプロパティの値には手を加えないまま、あたかも変更されたかのように見せることが出来るのです。
例えるならば、Photoshopで言うところの「レイヤーマスク」をかけるイメージです。 Photoshopではレイヤーマスクを使用することにより、元の画像を切り取っていなくても、マスクした部分だけ 画像が切り取られたように見せることが出来ます。
getter や setter も同様に、値を返す前にメソッドのロジックを適用させ、そのカスタマイズされた値を、プロパティの値として扱います。
getterの使い方
getter を設定する際は、メソッド名の前にget
と付けます。
getterは、何かしらの値を返すように設定します。
getter について調べている時、このようなサンプルを何度も目にしました。
class Book { constructor(title, author, year) { this._title = title; this._author = author; this._year = year; } get title() { return this._title; } get author() { return this._author; } get year() { return this._year; } } const book = new Book("人間失格", "太宰治", 1948);
初めは、いったいなぜわざわざgetterを設定するのか分かりませんでした。
上記の書き方であれば、コンストラクタの中の this._title = title
と書いてあるところを this.title = title
に変更してしまえば、
わざわざgetterを設定しなくても book.title
で本のタイトルにアクセス出来ると思っていました。
JavaScriptでは、変数名の先頭に_を付けると、「変更してはいけない変数」であると明示することが出来るそうです。
なので上のコードの場合だと、実際のプロパティ名とは違った getter を設定し、その getter を使ってプロパティにアクセスすることで、元のプロパティが直接アクセスされることを防いでいます。
book.title
というのは、titleプロパティにアクセスしているのではなく、title
という名前の getter にアクセスしている状態です。
setter の使い方
setterを設定する際は、メソッド名の前に set
と付けます。
setter は、何らかの値を引数に取る必要があります。
インスタンスを作る場合は、最初は引数を設定せずに空のインスタンスを作成し、setter を介してプロパティの値を設定します。
setter を作ったら、getter も作ります。getter がないと、setter で登録したプロパティにアクセスすることが出来ません。
また setter と getter の名前は、プロパティの名前と違うものにしなければなりません。 もし同じにしてしまうと、Maximum Call stack size exceeded というエラーが出てきてしまいます。
下記の例の場合だと、setter と getter は title
、プロパティ名は _title
と区別しています。
class Book { constructor(title, author, year) { this._title = title; // title は getter の名前 this._author = author; // author は getter の名前 this._year = year; // year は getter の名前 } get title() { return this._title; } get author() { return this._author; } get year() { return this._year; } set title(newTitle) { this._title = newTitle; } set author(newAuthor) { this._author = newAuthor; } set year(newYear) { this._year = newYear; } }
インスタンス名.setter名
で setter メソッドを実行し、各プロパティを設定します。
const book = new Book(); book.title = "人間失格"; book.author = "太宰治"; book.year = 1948;
getter や setter に機能を持たせる
getter や setter には、条件分岐やバリデーション機能を持たせることも出来ます。
例えば
1.setter を使い、本のタイトルが1文字以下の場合は『短すぎるタイトル』というタイトルに設定する
2.getter を使い、作者が登録されていなかった場合は、作者名は「作者不明」と表示する
というような動作をさせたい場合、下記のように設定します。
class Book { constructor(title, author) { this._title = title; this._author = author; } get title() { return this._title; } get author() { if (this._author === undefined) { return '作者不明'; } else { return this._author; } } set title(newTitle) { if (newTitle.length < 3) { this._title = '短すぎるタイトル'; } else { this._title = newTitle; } } set author(newAuthor) { this._author = newAuthor; } }
インスタンスを作り、setter で下記のようにプロパティを登録します。
const book = new Book(); book.title = "本";
getter を使ってアクセスすると、条件分岐によって設定された値が取得されます。
メソッド vs getter
getter について調べていた時にさらに気になったのが、どういう時にgetterを使用し、どういう時にメソッドを使用するのかという点です。
2つの大まかな違いは、getter から返された値は「オブジェクトのデータ」として扱われ、通常のメソッドを実行した場合は「関数から返された値」として扱われるとのことです。
調べた印象では、2つの使い分けに厳密な決まりはなく、メソッドとして設定した方が分かりやすいという人や、getter でプロパティとして扱った方が無駄な括弧を書かなくて済むから楽だという人など、様々いるようでした。
2つの違いを見るために、本が発行されてからの経過年数を、メソッドを使った方法と getter を使った方法の両方で取得してみたいと思います。
メソッドの場合
メソッドを使う場合は、インスタンス名.メソッド名()
と書きます。
下記の場合だと、book.yearsFromPublication()
のように実行します。
class Book { constructor(title, year) { this._title = title; this._year = year; } get title() { return this._title; } get year() { return this._year; } set title(newTitle) { this._title = newTitle; } set year(newYear) { this._year = newYear; } // メソッド yearsFromPublication() { return `${2020 - this.year}`; } } const book = new Book("人間失格", 1948);
getter の場合
メソッドの先頭に get
と付けると、メソッドは getter に変わります。
getter の場合は、インスタンス名.getter名
でアクセスすることが出来ます。
下記の場合だと、book.yearsFromPublication
のようにアクセスします。
class Book { constructor(title, year) { this._title = title; this._year = year; } get title() { return this._title; } get year() { return this._year; } set title(newTitle) { this._title = newTitle; } set year(newYear) { this._year = newYear; } // getter get yearsFromPublication() { return `${2020 - this.year}`; } } const book = new Book("人間失格", 1948);
静的メソッド
静的メソッドは、オブジェクトを扱う際のユーティリティ関数(効用関数)として使用することができます。
静的メソッドは、メソッド名の前に static と付けて設定します。
下の例では、静的メソッドmyFav
を設定し、その後呼び出しています。
class Book { constructor() { } static myFav(genre) { return `私は${genre}が好きです。`; } }
静的メソッドはクラスのプロパティです。
getter や setter がインスタンスに属しているのに対し、静的メソッドはインスタンスには属していません。
なので、newキーワードを使用してインスタンスを作らなくても、クラス名.静的メソッド名()
と書くだけで実行することが出来ます。
先ほど作成したクラスからインスタンスを作成し、詳細を確認してみます。
class Book { constructor() { } static myFav(genre) { return `私は${genre}が好きです。`; } } const book = new Book();
インスタンスの中身を見てみても、静的メソッド myFav
は見当たりません。
Bookクラスの中身を確認してみると、こちらにmyFav
がありました。
静的メソッドの指すthis
静的メソッド内でthisキーワードを使った場合、thisはクラス自身を指します。静的メソッドのthisは、インスタンスのプロパティとは紐づいていません。
例として、静的メソッド内でthisを使い、 year プロパティの値を使おうとしてみます。
class Book { constructor(year) { this.year = year; } static calculateYearStatic() { return 2020 - this.year; } } const book = new Book("人間失格", 1948);
calculateYearを実行した場合、NaNになってしまいます。これは、calculateYear
内のthisが、インスタンスを向いておらず、yearプロパティの値にアクセス出来ないからです。
今度は、thisを表示させる静的メソッド printThis
を作って実行し、thisの正体を確認してみます。
class Book { constructor(year) { this.year = year; } static printThis() { console.log(this); } }
thisは、Bookクラスを指していることが確認できました。
クラス内で、thisを使って静的メソッドから別の静的メソッドにアクセスする
クラス内である静的メソッドから別の静的メソッドを呼び出したい場合は、thisを使って参照することが出来ます。
class StaticMethodDemo { static staticMethodOne() { return 'staticメソッド1 + '; } static staticMethodTwo() { return this.staticMethodOne() + 'staticメソッド2'; } }
静的メソッドを使う理由
静的メソッドはユーティリティ関数として使用できる、というような説明を何度も見かけたのですが、それだけでは静的メソッドを使う理由が分かりませんでした。
クラスの中にわざわざ静的メソッドを設定しなくても、普通にクラスの外に関数を作れば同じ処理が出来てしまうからです。
下記のコードでは、全く同じ処理を静的メソッドと通常の関数で実行しています。
class Book { constructor() { } static myFav(genre) { return `私は${genre}が好きです。`; } } function myFavFunc(genre) { return `私は${genre}が好きです。`; }
ではなぜ静的メソッドを使う理由があるかというと、関数をクラス内のスコープに入れておくことによって、クラスの外にある変数名との不要な衝突を防ぐことが出来るからということのようです。
クラス内に関数が入っていれば、例え同じ名前の関数をクラスの外に作ってしまってもエラーにならずに済みます。
下記の例では myFav
という変数名を2回使っていますが、1つはBookクラスの中に入っていて実行する際は Book.myFav()
と呼び出すため、エラーにはなりません。
class Book { constructor() { } static myFav(genre) { return `私は${genre}が好きです。`; } } function myFav(genre) { return `私は${genre}が好きです。`; }
まとめ
実は今回ちゃんと調べるまで、クラスのことをほぼ理解していませんでした。constructorって何だろう、superとは何だろう、そういうレベルでした。
クラスの書き方を学んだ後に、以前少しだけ勉強したTypeScriptのチュートリアルのメモを見返してみたのですが、自分がいかに理解もせずただ写経していたかを思い知らされました。今考えるとかなりの時間を無駄にしたと思います。ただクラスを学んだ今、もう一度やり直せば以前よりも理解出来るだろうという期待感は持てるようになりました。
今回は触りを少し勉強しただけですが、実際にコーディングをしながら色々調べ、クラスの書き方に慣れていきたいと思います。
参考にしたサイト
JavaScript Classes: An In-Depth look|Medium
非常に分かりやすい解説です。Part1からPart4までの解説に加え、応用編としてクラスを使用した非常にシンプルなゲームのコーディング方法を紹介しています。
クラス |JavaScript Primer
クラスについての詳しい解説が載っています。
Class basic syntax |The Modern JavaScript Tutorial
こちらもクラスについての解説が載っています。
How To Use Class in Javascript|AppDividend
コンストラクタ関数とクラスでの書き方を比較して説明しているページです。
JavaScriptを教えていただいている、もりた先生の詳細はこちらです。
[もりけん塾]
ブログ:http://kenjimorita.jp
Twitter:https://twitter.com/terrace_tech
ファクトリ関数 [JavaScript]
もりけん塾(@terrace_tech)にて、JavaScriptを勉強しています。
今回は、ファクトリ関数について調べていきたいと思います。
ファクトリ関数を学ぶ必要がある理由
私は以前、JavaScriptを新しく勉強している人の意見で「クラスによる新しい書き方があるから、ファクトリ関数などの古い書き方は覚えなくてもいいと思う」というものを目にしたことがあります。
その時感じたのは、果たして新しい書き方だけを学ぶだけで本当に充分なのだろうか、という気持ちでした。
きっと実際の現場に入っていくと、クラス以外にもこういった何年も前からある書き方のコードにも触れる機会があるのではないかということです。
実際MDNのJavaScriptのクラスの説明のページにも、クラスはJavaScript にすでにあるプロトタイプベース継承の糖衣構文であると明記してあります。
なので、これから先色んなコードを扱っていくためにも、まずは従来からあるファクトリ関数についてきちんと理解しておきたいと思うようになりました。
また今回調べていて分かったのですが、ファクトリ関数は多くのサイトで「thisやnewに依存しない、一番望ましいオブジェクトの生成の仕方」として紹介されています。
単純ではありますが、そこまで多くの人がそういうのであれば、きっとファクトリ関数には多くの利点があるのではないかと思うようになりました。
果たしてそれが本当に正しい情報であるかを自分自身で見極めるためにも、まずはよく調べてみる必要があるなと感じました。
ファクトリ関数とは
ファクトリ関数とは、オブジェクトを返す関数のことです。
コンストラクタ関数と同じように、オブジェクトを作成するためのひな型の役割をします。
ただコンストラクタ関数と違う点は、オブジェクト作成時にnewキーワードを付けて呼び出す必要がありません。
newを付けてコンストラクタ関数を呼び出すと、作られたインスタンスはコンストラクタ関数内のthisと関連付けられ、__proto__の中にprototypeを割り当てられます。
ファクトリ関数には、その動きはありません。なので、ファクトリ関数内でthisを設定する必要もありません。
ファクトリ関数の設定の仕方
コンストラクタ関数では、このように関数を設定しました。
function Book(title, author, year) { this.title = title; this.author = author; this.year = year; }
ファクトリ関数の場合は、このように設定します。
function createBook(title, author, year) { return { title: title, author: author, year: year }; }
ES6の書き方だと、こうなります。
function createBook(title, author, year) { return { title, author, year }; }
「ファクトリ関数とは」にも書いた通り、ファクトリ関数は、単純にオブジェクトを返す関数です。
return { }
の部分が、オブジェクトを返す動作です。
これがもし仮に配列を返すのであればreturn [ ]
と書かれます。
ファクトリ関数を使ってオブジェクトを作成する
オブジェクトを作成する際は、下記のように関数を呼び出します。コンストラクタ関数の時と似ていますが、ファクトリ関数の場合はnewを付けて呼び出す必要はありません。
const book1 = createBook("人間失格", "太宰治", 1948);
ファクトリ関数のメソッドの作り方
コンストラクタ関数の場合、コンストラクタ関数名.prototype.メソッド名
でメソッドを作成し、prototypeを設定します。そしてnewを付けてコンストラクタ関数を呼び出すことで、自動的に_proto_を介してprototypeにアクセスすることが出来ました。
ファクトリ関数の場合は、prototypeとして作成した関数ではなくても、手動でprototypeとして設定することが出来ます。
やり方は色々ありますが、今回は2つの方法を見ていきます。
Object.setPrototypeOf()
最初に、Object.setPrototypeOf()のやり方で書いてみます。
まず、prototypeとして使用したいメソッドのprotoBook()
を作成します。
次にcreateBook()
内で、setPrototype()
を使って、メソッドprotoBook()
をprototypeに設定します。
const protoBook = { calcYear: function () { return `この本は発行から${2020 - this.year}年経っています`; }, }; function createBook(title, author, year) { var newBook = {}; newBook.title = title; newBook.author = author; newBook.year = year; return Object.setPrototypeOf(newBook, protoBook); }
関数の設定が終わったら、ファクトリ関数createBook()
を呼び出し、オブジェクトを作成します。
const book1 = createBook("人間失格", "太宰治", 1948);
作成されたオブジェクト book の中身を見てみると、_proto_にcalcYear
が設定されていることが分かります。
メソッドcalcYear
を使うためには、下記のように実行します。
Object.create()
上で見たように、setPrototypeOf()でも、prototypeの設定は出来ました。
しかしMDNのドキュメントを読んでみると、setPrototypeOf()
は動作が遅いので、prototypeを設定する際にはObject.create()
を使う方が良いと書いてありました。
なので、setPrototypeOf で設定したやり方を、今度はObject.createの方法に書き直します。
Object.createもsetPrototypeOf同様、prototypeを指定してオブジェクトを生成する際に使われます。
下の例では、newBookオブジェクトを作る際に、protoBook
をprototypeとして指定しています。
const protoBook = { calcYear: function () { return `この本は発行から${2020 - this.year}年経っています`; }, }; function createBook(title, author, year) { const newBook = Object.create(protoBook); newBook.title = title; newBook.author = author; newBook.year = year; return newBook; }
同じくオブジェクトを作成し、_proto_の中身をチェックしてみます。
calcYear
が入っていました。実行してみます。
ファクトリ関数の this は何を指すか
ファクトリ関数内で変数を参照する際は、thisを使う必要はありません。 ですが、prototypeを別に設定する場合は、thisを使う必要があります。 その場合thisは、ファクトリ関数から返されたオブジェクトを指します。
const protoBook = { printThis: function () { console.log(this); }, }; function createBook(title, author, year) { const newBook = Object.create(protoBook); newBook.title = title; newBook.author = author; newBook.year = year; return newBook; }
オブジェクトを作り、this を表示させるメソッドprintThis
を実行してみます。
ファクトリ関数を使う利点
渡されるパラメーターによって、返すオブジェクトが変えられる
ファクトリ関数を使うと、関数を呼び出す際のパラメーターによって異なったオブジェクトを返す設定をすることが出来ます。
その特徴を使い、入金ボタンを押すと「入金オブジェクト」が生成され、出金ボタンを押すと「出金オブジェクト」が生成されるコードを書いてみます。
下記の例では、
入金のボタンにはカスタムデータ属性data-type="inc"
、出金のボタンにはカスタムデータ属性data-type="exp"
と設定してあります。
<input type="text" class="input__value" placeholder="金額を入力" /> <button class="btn" data-type="inc">INCOME</button> <button class="btn" data-type="exp">EXPENSE</button>
もし入金ボタンが押された場合は、ファクトリ関数budget内のcreateInc()
が実行される(=入金オブジェクトが返される)ようにし、
もし出金ボタンが押された場合は、ファクトリ関数budget内のcreateExp()
が実行される(=出金オブジェクトが返される)ように設定します。
const budget = function(type, price) { const createInc = function() { return { type: "入金", price }; }; const createExp = function() { return { type: "出金", price }; }; if (type === "inc") { return createInc(type, price); } if (type === "exp") { return createExp(type, price); } };
この2つのボタンにイベントリスナーを付け、カスタムデータ属性から読み込んだタイプinc
、exp
に応じて、budget関数内で別々の関数を呼び出させます。
document.querySelectorAll(".btn").forEach(function(target) { target.addEventListener("click", function(e) { const inputValue = document.querySelector(".input__value").value; const type = e.target.dataset.type; const item = budget(type, inputValue); console.log(item); }); });
console.logした結果がこちらです。(inputにそれぞれ1000と入力してからボタンを押しました。)
受け取ったtypeパラメーターごとに、それぞれ別々のオブジェクトを返すことが出来ました。
ファクトリ関数のクロージャのおかげで、変数を閉じ込められる
ファクトリ関数が実行された後、関数内のスコープは閉じられます。なのでファクトリ関数内で定義されたメソッドを実行した際には、スコープ内の変数が参照され続けます。
その動きを確かめるために、下記のようなコードを用意しました。
ファクトリ関数createBookの中のメソッドread
が実行されるたびに、1ずつ数字が増えていきます。
let pages = 10; function createBook(title) { let pages = 1; let read = function () { console.log(`${title}を${pages++}ページ読みました。`); }; return { title, read }; }
関数の外にも変数pages
がありますが、readメソッドを実行しても、そちらの変数は参照されません。
なぜなら、ファクトリ関数内createBook() が実行された後、createBook内のスコープは閉じられ、グローバルのpagesへの参照はもう残っていないからです。
book1.read()
を実行するたび、ファクトリ関数内のpages
が参照され、結果pages
は1ずつ増えていきます。
thisがundefinedになるトラブルが避けられる
コンストラクタ関数を使用した場合、関数の階層が深くなってくると、thisの値がundefinedになってしまうことがあります。
ファクトリ関数はそもそもthisを使用する必要がないため、そういったトラブルを回避することが出来ます。
どのような現象か、サンプルを見ながらチェックしていきます。
<コンストラクタ関数の場合>
関数の入れ子が深くなり、setTimeout関数の中の this が undefined になってしまっています。
function Book(title, author, year) { this.title = title; this.author = author; this.year = year; this.asyncTitle = function() { setTimeout(function() { console.log("この本のタイトル", this.title); }, 1000); }; }
<ファクトリ関数の場合>
ファクトリ関数の場合、変数名で参照できるので、コンストラクタ関数の時に起きたような「thisの値が不明」といったようなトラブルが起こりません。
function createBook(title, author, year) { const newBook= {}; newBook.title = title; newBook.author = author; newBook.year = year; const asyncTitle = function () { setTimeout(function () { console.log('この本のタイトル:', title); }, 1000); }; return { newBook, asyncTitle } }
変数名の衝突が防げる
ファクトリ関数ではreturnしたオブジェクトやメソッドだけが外部から参照出来ます。
なので、例えば同じ名前の変数名を各ファクトリ関数内で使用しても、returnしていないものに関しては外部からはアクセス出来ないので、名前の衝突が起きません。
下記の例ですと、createBook
とbookBought
はどちらもcount
という変数を使用していますが、returnしているのはあくまでメソッドだけなので、変数自体はファクトリ関数の中のスコープに守られています。
function createBook(title) { let count = 1; const read = function () { console.log(`${title}を${count++}ページ読みました。`); }; return { title, read }; } function bookBought() { let count = 1; const buy = function () { console.log(`今月${count++}冊目の本を買いました。`); }; return { buy }; }
ファクトリ関数内で加工したデータを返すことが出来る
ファクトリ関数を使うと、外部に返すデータを指定することが出来ます。
例えば何かの計算結果などのデータを使いたい場合は、計算するメソッドそのものではなく、メソッドで加工済みのデータを返すことが出来ます。
なので、関数内にメソッドを定義したとしても、オブジェクトを作成した時に毎回メソッドを付与しなくてもよくなります。
コンストラクタ関数で同じく、コンストラクタ関数内にメソッドを定義すると、インスタンスを作るごとにメソッドが付与されてしまいます。これだと望ましくないので、メソッドを使いたい場合は、prototypeを別途定義する必要が発生します。
実際に動きを見るために、「本が発行から何年経っているかを表示させるメソッド」を設定し、2つの関数の違いを確認してみます。
<コンストラクタ関数の場合>
コンストラクタ関数内でメソッドを設定してみます。
function Book(title, author, year) { this.title = title; this.author = author; this.year = year; this.published = function() { return `${2020 - year}年前`; } }
生成されたインスタンスには、publishedメソッドが付与されました。
publishedメソッドは下記のように実行することが出来ます。
ですが、インスタンス作成時に毎回メソッドが追加されてしまうのはよい状態ではありません。
なので「この本が発行から何年経っているかを計算する」といった関数を持たせたい場合は別途メソッドを作成し、インスタンスに持たせるのではなくprototypeとして設定します。
function Book(title, author, year) { this.title = title; this.author = author; this.year = year; } Book.prototype.published = function() { return `${2020 - this.year}年前`; }
インスタンスのprototypeに、publishedメソッドが設定されました。
以下のように書けば、publishedメソッドが実行出来ます。
<ファクトリ関数の場合>
同様に、ファクトリ関数内でメソッドの関数を設定してみます。
function createBook(title, author, year) { const howOld = function () { return `${2020 - year}年前`; }; const published = howOld(); return { title, author, year, published }; }
ファクトリ関数の場合だと、メソッドの実行結果のみを返すことが出来ます。なので、たとえファクトリ関数内にメソッドを設定したとしても、オブジェクトを作成するたびにメソッドをオブジェクトに付与させる必要がありません。
ファクトリ関数内createBookを呼び出し、オブジェクトを作成します。
publishedメソッドが付与されなくても、publishedメソッドの計算結果が得られています。
まとめ
ファクトリ関数について学んだことで、「すべてのものをオブジェクトとして捉えるコーディングの仕方」といったものへの理解が少し進んだように感じます。
そしてクロージャや即時関数など、まだまだ理解が不十分なトピックも洗い出されてきました。それらについても調べていきつつ、ファクトリ関数を使用した、オブジェクト単位にまとまったすっきりしたコーディングにもどんどん慣れていきたいと思いました。
参考にしたサイト
Factory Functions and the Module Pattern | The Odin Project
ファクトリ関数の概要が簡潔にまとめられています。
より堅牢なコードを作成するためのJavaScriptのベストプラクティス—オブジェクトの作成 |ICHI.PRO
こちらも、ファクトリ関数の概要と、実際のコーディングのサンプルが記載されています。
ライブラリなしのJavaScriptで作る、関数ファクトリを使ったCounterのサンプル | Crudzoo
ファクトリ関数でのコーディング方法を、簡単なカウンターを作成しながら解説しています。
JavaScriptを教えていただいている、もりた先生の詳細はこちらです。
[もりけん塾]
ブログ:http://kenjimorita.jp
Twitter:https://twitter.com/terrace_tech
コンストラクタ関数 [JavaScript]
もりけん塾(@terrace_tech)にて、JavaScriptを勉強しています。
今回は、コンストラクタ関数について調べていきたいと思います。
コンストラクタ関数の仕組みを理解する必要がある理由
コンストラクタ関数について学んでいくと、newキーワードやthis、プロトタイプなど様々な用語が出てきます。これらはすべて、コンストラクタ関数でインスタンスを作成する際に知っておくべきコンセプトです。
オブジェクトを作成する方法に関しては今後、classやファクトリ関数なども詳しく見ていく予定ですが、コンストラクタ関数の仕組みを理解することによって、別の方法と比較する際に、有利な点や不利な点を把握しておけると思いました。
コンストラクタ関数とは
コンストラクタ関数は、オブジェクトを作るひな型の関数です。
オブジェクトを作成するためには色々な方法がありますが、コンストラクタ関数はそのうちの一つです。
newキーワードを使用して、新しいオブジェクトを作成することが出来ます。
コンストラクタ関数はnewキーワードを使用して呼び出すと、通常の関数とは違った動きをします。
コンストラクタ関数以外でオブジェクトを作る場合
先ほど「オブジェクトを作成するためには色々な方法がある」と書きましたが、オブジェクトを作成する方法の一つとして、オブジェクトリテラルという書き方があります。
オブジェクトリテラルでオブジェクトを作る場合は、1個1個、オブジェクトのプロパティ(下記の例の場合、title / author / yearの部分)を書いていきます。
// 毎回すべてのプロパティを書く const book1 = { title: "人間失格", author: "太宰治", year: 1948 } const book2 = { title: "吾輩は猫である", author: "夏目漱石", year: 1905 } const book3 = { title: "羅生門", author: "芥川龍之介", year: 1915 }
コンストラクタ関数を使用してオブジェクトを作る場合
コンストラクタ関数を設定する
コンストラクタ関数を設定することにより、あらかじめオブジェクトが持つプロパティを設定することが出来ます。なのでオブジェクトを作るたびに毎回同じプロパティを書く必要がなくなります。
コンストラクタ関数を作成する場合、関数名の最初の文字は通常、大文字にします。例えばtitle、author、yearというプロパティを持たせたい場合は、下記のように設定します。
// コンストラクタ関数 function Book(title, author, year) { this.title = title; this.author = author; this.year = year; }
オブジェクトを作る
コンストラクタ関数を元にして作られたオブジェクトを、インスタンスと呼びます。
コンストラクタ関数でインスタンスを作成するには、newというキーワードを使い、関数を呼び出します。
// newキーワードを使用したインスタンスの作り方 const book1 = new Book("人間失格", "太宰治", 1948); const book2 = new Book("吾輩は猫である", "夏目漱石", 1905); const book3 = new Book("羅生門", "芥川龍之介", 1915);
newを付けてコンストラクタ関数を呼び出すと何が起こるのか
コンストラクタ関数自体は実は特別なものではなく、通常の関数と同じただの関数です。しかしnewキーワードを付けて呼び出されると、通常の関数とは違った動きをします。
例として、下記のようなコンストラクタ関数とそのプロトタイプを用意し、newキーワードがどんな動きをしているかを詳しく見ていきます。
newキーワードを付けてコンストラクタ関数を呼び出すと、下記の4つのことが自動で行われます。
1.
まず、プロパティが何も設定されていない状態の空のオブジェクトが作成されます。
(※あくまでプロセスであって、実際にconsole.logをしても空のオブジェクトが作られる瞬間が表示されたりはしません。)
2.
1で作成されたオブジェクトの_proto_ の中に、prototypeが設定されます。
例えば、下記のようなprototypeがコンストラクタ関数に設定されていたとすると、newキーワードで作られたインスタンスの _proto_ には、printTitle
が設定されます。
3.
コンストラクタ関数の中でthis
を使って書かれたプロパティがすべて、オブジェクトにセットされます。
ただし、もしもコンストラクタ関数の中に this が付いていないものがあれば、それに関してはオブジェクトの中に含まれません。
詳しく確認するために、コンストラクタ関数の中にvar local = 1;
と追加してみます。
そしてnewキーワードを付けて、上記のコンストラクタ関数を呼び出します。
作られたインスタンスを見てみると、変数localは含まれていないことが分かります。
これは変数localは、コンストラクタ関数内のスコープでのみ有効だからです。
もし変数localの値にアクセスしたい場合は、同じ関数内から、変数localにアクセスするメソッドを作ります。
book.printLocal()
を実行すると、変数localの値を表示させることが出来ました。
4.
新しいオブジェクトが返されます。
ただし、もしコンストラクタ関数内に return と設定されていると、オブジェクトを返してくれない場合があります。
3 で確認した通り、もしコンストラクタ関数がプリミティブ型の値をreturnしている場合は、その値は単純に無視され、オブジェクトが返されます。
ですが、もしプリミティブ型ではなくオブジェクトをreturnしている場合は、thisで設定した値は、新しく生成されたオブジェクトにはセットされません。
例えば、コンストラクタ関数の中に下記のようなオブジェクトをreturnする記述があったとします。
上記のコンストラクタ関数を呼び出してインスタンスを作ります。
作られたオブジェクトの詳細を見てみます。
this.title = title
などと設定したプロパティはすべて消え、returnされたオブジェクトのみが設定されていることが分かります。
コンストラクタ関数内のthisは何を指しているか
thisは、コンストラクタ関数を使って生み出されたインスタンスを指しています。
実際にthisがどのように表示されるか、コンソールに打ち出しながら確認していきます。
まずコンストラクタ関数に、this.title
が何を指しているかを表示出来るメソッドを持たせます。
上記のメソッドを作成した後、新しいインスタンスを作成します。
3つのオブジェクトでそれぞれprintTitle
メソッドを呼び出し、this.title
を表示させてみます。
3つのオブジェクトのthisがそれぞれ、インスタンス自身を指していることが明確になったと思います。
まとめ
コンストラクタ関数について学んでいく際、newキーワードやthis、プロトタイプなど色々な用語が出てきました。それらを言葉で説明しようとしても上手く出てこず、これまで漠然としか把握していなかったことに気が付きました。
そういった項目を細かく見ていくことによって、これまで少ししか理解出来ていなかったコンストラクタ関数への理解が今回より深まったように思います。
参考にしたサイトなど
JavaScript new Keyword|TutorialTeachers
newキーワードにより呼び出されたコンストラクタ関数がどのような動きをするのか、分かりやすい図を交えて説明しています。
new 演算子|MDN Web Docs
オブジェクトの概要や扱い方が載っています。
JavaScriptを教えていただいている、もりた先生の詳細はこちらです。
[もりけん塾]
ブログ:http://kenjimorita.jp
Twitter:https://twitter.com/terrace_tech
prototypeと__proto__ [JavaScript]
もりけん塾(@terrace_tech)にて、JavaScriptを勉強しています。
現在、これまで何となく書いてきたJavaScriptの基礎を見直すべく、まずは今自分の中で理解の浅い項目について調べています。
今回は、prototypeや__proto__について、分かってきたことをまとめていきます。
※本エントリーで扱うprototypeは、JavaScriptオブジェクトのインスタンスのプロパティのprototypeではなく、コンストラクタ関数のプロパティのprototypeです。
prototypeや_proto_を理解する必要がある理由
JavaScriptは、オブジェクトとプロトタイプチェーンによって成り立っています。この仕組みをちゃんと理解していないと、JavaScriptについての色々な説明を読んでも理解出来ないのだと痛感する出来事がありました。
JavaScriptの苦手な部分を1つずつ潰していくという学習の方向性が決まった時、私は最初にclassについて調べ始めました。深い理由はなく、たまたまリストアップしたJavaScript苦手項目の一番上に「classがどういうものか分からない」と書いてあったからです。
classのことを調べていくうちに、コンストラクタ関数やファクトリ関数と呼ばれるものも、class同様の機能を持っていることを知りました。なのでそれら2つについても並行して調べていくことにしました。
「class」について調べれば「コンストラクタ関数」や「ファクトリ関数」について調べる必要が発生し、「コンストラクタ関数」を調べれば「this」や「new」キーワードについて知る必要が発生し、調べる項目は増え続けました。しかし途中で、どうしても解説の意味が理解出来なくなり、前に進めなくなってしまいました。
上に挙げた項目はどれを取っても、prototypeの仕組みが分かっていないと、理解することが出来ないようになっていたのです。
この点に気付いた時、私はprototypeはJavaScriptを勉強する上で避けて通れない根幹の部分の知識なのだと痛感しました。そして、prototypeの仕組みを理解することから始めようと決意しました。
今後のJavaScriptの学習をスムーズに進めていくためにも、まずはベースとなるprototypeのことを最初の段階で理解しておくことが重要であると私は思います。
prototypeの他に _proto_ というものが存在する
prototypeについて調べようとした時に一緒に出てきたのが_proto_という言葉です。最初、私はprototypeと_proto_の違いがどうしても理解出来ませんでした。
漠然と、prototypeは継承を可能にするものだ、というようなことは分かっていたのですが、詳しい仕組みは理解していませんでした。何より_proto_が何者なのか、それは概念なのかオブジェクトなのかすら理解出来ずにいました。
prototypeと_proto_の違い
prototypeと_proto_の違いについては、色々と調べた結果、下記の通りに理解しました。
prototype
prototypeは、コンストラクタ関数を実行することによって生まれるプロパティ(※本ページ最初の注釈を参照)です。
コンストラクタ関数によって、インスタンスへの継承を実現させるために生み出されます。
_proto_
_proto_は、すべてのオブジェクトが持っているプロパティです。
コンストラクタ関数によって作られたインスタンス自体は、prototypeを持っていません。
しかし_proto_を持っていることによって、コンストラクタ関数のprototype(=自身が継承していて使うことが出来るprototype)への参照を持つことが出来ます。
prototypeと_proto_が作られていく過程
言葉で理解しようすると非常に難しかったのですが、もりた先生に「実際にコンソールで出力したものを1つ1つ画像化していくと、自分も納得するし説明にも説得力が生まれますよ」とアドバイスを頂きました。
なので実際にコンソールに出力した値を見ながら、prototypeと_proto_が作られていく過程を順を追ってチェックしていきます。
1.
まず、コンストラクタ関数Dogを作ります。
2.
コンストラクタ関数Dogの詳細をコンソールで見てみると、prototypeというプロパティがあるのが分かります。
3.
prototypeには、手動でメソッドを追加することができます。
次のように記述し、コンストラクタ関数Dogのprototypeにbarkメソッドを追加してみます。
4.
再びコンストラクタ関数Dogをコンソールで見てみると、prototypeにbarkメソッドが追加されているのが分かります。prototypeは、プロパティやメソッドを入れておくバケツのような役割を果たしています。
5.
コンストラクタ関数Dogを元に、インスタンス pochi
を作ります。
6.
上記で作られたインスタンス pochi
の中身を見てみると、_proto_というプロパティが作られています。
_proto_は、すべてのオブジェクトが持っているプロパティです。
_proto_は、prototypeへのプロパティアクセサーの役割を果たします。
(プロパティアクセサーの例:dog.nameの .name の部分など)
なのでpochiが継承しているprototype一覧は、pochi._proto_と書くと表示することが出来ます。
下の図はpochi._proto_と書いて出力された、pochiが継承しているprototype一覧です。先ほど作ったbarkメソッドも入っています。
7.
上記の通り、インスタンスpochi
はbark()メソッドを継承しているので、pochi.bark()
と書くと、メソッドを実行することができます。
(pochiは英語圏の犬なので "Woof" と鳴きます)
まとめ
prototypeや_proto_について理解すると、今後classやコンストラクタ関数、newなどオブジェクト関連のコンセプトを学ぶ際の下地が出来てきます。これからのJavaScriptの学習のためにも、是非抑えておくべきポイントだと思います。
参考にしたサイト
prototypeや_proto_について調べる際に参考にした、分かりやすい解説の載ったサイトです。
Object のプロトタイプ|MDN Web Docs
プロトタイプオブジェクトやprototypeプロパティのことについて詳しく載っています。
JavaScript Prototype|JavaScript Tutorial
prototypeがどのようにオブジェクトと繋がっているか等を、分かりやすい図で解説しています。
なるほど。。_proto_とprototypeの違い|武骨日記
もりた先生の記事です。コード付きで解説が載っています。
proto VS. prototype in JavaScript|Stack Overflow
_proto_ と prototypeの違いを、色々な人が分かりやすく解説しています。
JavaScriptを教えていただいている、もりた先生の詳細はこちらです。
[もりけん塾]
ブログ:http://kenjimorita.jp
Twitter:https://twitter.com/terrace_tech
React+Reduxで作るカウントアプリ
もりけん塾(@terrace_tech)にて、React+Reduxを勉強しています。
React+Reduxの基本的なデータの流れを理解するために、カウントアプリを作ってみようともりた先生に提案していただきました。
そこで今回は、カウントアプリを作りながら、Reduxがどのような働きをするかを把握していきたいと思います。
(まだまだ学習途中なので、理解が浅い部分が多々あります。)
アプリを作成するにあたり、下記のインストールを行います。
// Reactのインストール npx create-react-app PROJECT-NAME // Reduxのインストール npm install --save redux // React Reduxのインストール npm install --save react-redux
Reduxとは
Reduxは、アプリ全体のstate(UIの現在の状態)を管理するためのライブラリです。
React Reduxとして使用される機会は多いですが、Reactのためだけにあるものというわけではありません。
React Reduxの大まかな流れを、図に表してみました。
ここからは、実際にファイルを作成し、Reduxの動きを把握していきます。
必要なディレクトリとファイルの作成
上に上げたReduxの構成要素は、みんなそれぞれ別の役割をしているので、 1つのファイルにすべて書くのではなく、役割ごとにファイルを分けて作成した方が分かりやすいです。 なのではじめに、必要なディレクトリとファイルを作成しておきます。
/* * * src * / actions * + index.js * / components * + Counter.js * / constants * + action-types.js * / reducers * + index.js * / store * + index.js * */
また、Appコンポーネントに、Counterコンポーネントをインポートしておきます。 (App.cssの中身は、ページ最下部に記載してあります)
[src/App.js] import React from "react"; import Counter from "./components/Counter"; import "./App.css"; const App = () => { return <Counter />; }; export default App;
1. storeを作る
まずはアプリ全体のデータベースとなる、storeを作るところから始めます。
storeを使うためには、createStore()
という関数を呼び出す必要があります。
createStore()
は、第一引数にreducer
を受け取らせます。
この時点でreducerはまだ作られていませんが、後に作るので、まず reducer
を引数に入れておきます。
[src/store/index.js] import { createStore } from "redux"; import reducer from "../reducers"; const store = createStore(reducer); export default store;
2. reducerファイルを編集する
reducerは唯一stateとコミュニケーションの取れる大事な要素です。 現時点ではまだ具体的な処理は書きませんが、全く何も書かないとエラーになりアプリが動かなくなってしまうので、とりあえず、stateを返す処理を書いておくことにします。
今回は、stateは reducerファイルの中に書きます。 カウントアプリの場合だと、初期の数字は0なので、0を入れます。
[src/reducers/index.js] const initialState = { count: 0, }; function reducer(state = initialState, action) { return state // ここに後で実際の処理を書きます } export default reducer;
3. React Reduxをアプリ全体に認知させる
React Reduxをアプリ全体に認知させるために、index.js内ですべてのコンポーネントを<Provider>
で包みます。
[src/index.js] import React from "react"; import { render } from "react-dom"; import { Provider } from "react-redux"; import store from "./store/index"; import App from "./App"; render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
これでReact Reduxの下準備が整いました。
次は、実際にコンポーネントからstoreにアクセスしていきます。
4. 現在のカウントを表示させる
Counterコンポーネントに、下記のように記載します。
[src/components/Counter.js] import React from "react"; import { connect } from "react-redux"; // stateを受け取る const mapStateToProps= (state) => { return { count: state.count }; }; const Counter = ({ count }) => { return ( <div className="container"> <div className="box"> <span>{count}</span> <div className="control"> <button>+</button> <button>-</button> <button>RESET</button> </div> </div> </div> ); }; export default connect(mapStateToProps)(Counter);
[解説] mapStateToProps
const mapStateToProps= (state) => { return { count: state.count }; };
現在のカウントは、 count: 0
と設定してあります。
このstateの数字をCounterコンポーネントに表示させます。
stateをpropsとしてコンポーネントで受け取るためには、mapStateToProps
を使用します。
これでstateがpropsとしてコンポーネント内で使用できるようになったので、
Counterコンポーネントにconst Counter = ({ count }) => {
と記載し、propsを通してあげます。
[解説] connect
export default connect(mapStateToProps)(Counter);
connect()
は、stateが更新されるたびに呼び出される関数です。
実行されると、すべてのstateの値を受け取ります。
そこで、mapStateToPropsを引数に取ることで、コンポーネントが必要なstateの情報だけ(mapStateToPropsに記載したstate)を取り出してくれます。
これでstate.counterの数字が表示されるようになりました。
もし本当にstateの数字が表示されているか確かめたい場合は、試しにsrc/reducers/index.js
のinitialState
の数字を変更してみるといいかもしれません。
initialState = { count: 100 //ブラウザに 100 と表示されたらオッケーです }
今度は、このstate.countの数字を更新する処理を書いていきます。
5. ボタンを押したら数字が更新したい
実現したい処理は「ボタンを押したら、stateの数字が更新される」ことです。
しかし、コンポーネントからボタンを押しても、直接stateの数字を更新することはしてはいけません。 Reduxシステムの元では、コンポーネントはあくまでstateのデータを表示させることだけしか出来ないのです。 stateの数字を変更したい場合は、必ず一度、reducerを通して更新されないといけません。
では、コンポーネントがどうやってreducerとコミュニケーションを取るかというと、 actionをdispatch(伝達)することで実現出来ます。
6. actionの設定をactionファイルに記載する
actionは「stateに対して、どのような処理を行いたいか」という、動作の設定です。
今回のカウントアプリでは、各actionは下記のように設定しました。
- increment:state.counterの数字に+1する
- decrement:state.counterの数字から-1する
- reset:現在のstate.counterの数字を0にする
定数ファイルを作っておく
actionを作る前にまず、定数ファイルを作っておきます。 理由は、タイプミスを防ぐためです。 このactionは、reducer、コンポーネント内で何回も使います。 そのため、タイプミスを防ぐために、事前に定数を設定しておいた方がいいのです。
[src/constants/action-types.js] export const INCREMENT = "INCREMENT"; export const DECREMENT = "DECREMENT"; export const RESET = "RESET";
次に、actionファイルにactionを設定していきます。
[src/actions/index.js] import { INCREMENT, DECREMENT, RESET } from "../constants/action-types"; export function increment() { return { type: INCREMENT }; } export function decrement() { return { type: DECREMENT }; } export function reset() { return { type: RESET }; }
7. reducerの設定をreducerファイルに記載する
reducerに渡したいactionが決まったので、
次に、reducerに「このactionがdispatchされてきたら、こういう処理をする」という設定を記載します。
この「○○の場合は○○にする」を設定するには、switch文を使用します。
defaultは、必ず設定する必要があります。 なぜなら、アプリを立ち上げた一番最初の状態では、 何のactionもdispatchされていないため(このアプリの場合、ボタンをクリックして初めてactionがdispatchされます)、 3つのcaseのどれにも当てはまらず、何を返したらいいか分からなくなり、エラーを起こすからです。
[src/reducers/index.js] function reducer(state = initialState, action) { switch (action.type) { case INCREMENT: return { count: state.count + 1, }; case DECREMENT: return { count: state.count - 1, }; case RESET: return { count: 0, }; default: return state; } }
8. コンポーネント内で、actionをreducerに渡す
コンポーネントにmapToDispatch
を追加し、引数として connect()
に通します。
connect()でつながったコンポーネントは、 stateが更新される度に、更新されたstateをpropsとして受け取ることが出来るようになります。
[src/components/Counter.js] import React from "react"; import { connect } from "react-redux"; import { increment, decrement, reset } from "../actions"; const mapStateToProps = (state) => { return { count: state.count }; }; const mapDispatchToProps = { increment, decrement, reset }; const Counter = ({ count, increment, decrement, reset }) => { return ( <div className="container"> <div className="box"> <span>{count}</span> <div className="control"> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> <button onClick={reset}>RESET</button> </div> </div> </div> ); }; export default connect( mapStateToProps, mapDispatchToProps )(Counter);
これで、カウントアプリが動くようになりました。 実際に動いているサンプルは下記のリンク先で確認できます。
Codesandbox|React + Redux カウントアプリ
まとめ
今回のReact + Reduxによるカウントアプリで学んだ、React + Reduxの要点をまとめました。
- コンポーネントはstateの数字を変更できない。出来るのは、stateの中身を表示させることだけ。
- stateを更新できるのは、reducerだけ。
- コンポーネントがreducerにstateの更新を頼みたい場合は、reducerにactionをdispatchする
- reducerは、コンポーネントからdispatchされたactionを元に、必要な情報だけをstateから取り出し、propsとしてコンポーネントに渡す。
その他
今回使用したCSSです。
[src/App.css] .container { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 80vh; } .box { width: 300px; height: 300px; align-self: bottom; -webkit-box-shadow: 2px 2px 3px 1px rgba(0, 0, 0, 0.3); -moz-box-shadow: 2px 2px 3px 1px rgba(0, 0, 0, 0.3); box-shadow: 2px 2px 3px 1px rgba(0, 0, 0, 0.3); display: flex; flex-direction: column; justify-content: center; align-items: center; border: 5px solid #2e6da4; opacity: 0.8; } span { display: inline-block; text-align: center; font-size: 100px; } button { cursor: pointer; margin: 5px; color: #fff; background-color: #337ab7; border-color: #2e6da4; display: inline-block; margin-bottom: 0; font-weight: 400; text-align: center; white-space: nowrap; vertical-align: middle; -ms-touch-action: manipulation; touch-action: manipulation; cursor: pointer; background-image: none; border: 1px solid transparent; padding: 6px 12px; font-size: 20px; line-height: 1.42857143; border-radius: 4px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; min-width: 40px; outline: none; }
もりた先生の詳細情報はこちらです!
[もりけん塾]