使い古された話題ではありますけど、わかりやすく説明できそうな気がしたので書いてみたいと思います。
先に方針だけ伝えておくと、クラスとモジュールと関数は、変数のスコープを切ることができるという共通の性質を持っている、という切り口からクロージャについて説明していきたいと思います。
これだけ読んで何となく先が予想できてしまった人は読まなくても大丈夫かと思います。
それでも読んでくださるという方は、助言なり意見なりをくださるととても嬉しいです。
実行環境
言語はJavaScript(ES2015 or later)を使いますけど、別に知らなくてもなんとかなるんじゃないでしょうか。
何か他の言語をやっていれば大体読めるかと思います。
実行環境はNode.js(v6.9.0 or later)です。
なのでモジュールはCommonJSでやります。
まあわからない人は全然気にしなくても大丈夫です。
以下で出てくるコードを実際に動かしてみたいという人は、公式サイトからインストーラを落としてくるなりパッケージマネージャ使うなりしてNode.jsをインストールすれば問題はないかと思います。
関数が第一級オブジェクト(ファーストクラスオブジェクト)とは
クロージャは関数が第一級オブジェクトである言語でしか使えませんので、先に第一級オブジェクトについて説明します。
知っているという人は環境(状態)を持つというイメージに進んでもらえればと思います。
第一級オブジェクトとは、その言語で値として扱えるデータのことです。
オブジェクトとは言いますけど、オブジェクト指向のオブジェクトとはまた別の意味を持ちます。
まあ「対象物」とか「値」くらいのふわっとした意味でとらえていただければ大丈夫かと思います。
脱線しましたが、値として扱えるというのは、代入できたり関数の引数にできたり、戻り値にできたりするという意味です。
例えばCやPHPでは、関数は第一級オブジェクトではありません。
関数自体を関数の引数や戻り値にできませんからね。
しかし、PythonやJavaScriptでは関数を変数に代入したり、関数に渡したりすることができます。
代入したり引数にしたりできるということは、当然関数を値として生成する方法もあります。
以下はJavaScriptのコードです。
// 関数(のインスタンス)を生成し、変数に代入
const add1 = function(a) {
return a + 1
}
console.log(add(3)) // => 4
// 関数を引数に取る
function apply(a, f) {
return f(a)
}
console.log(apply(2, add1)) // => 3
node functions.js
で実行できます。
あと// => 4
には深い意味はなく、ただ実行結果のスクリーンショットを取るのが面倒だったので、実行したときに表示される結果をコメントに書いただけです。
console.log
はprint
みたいな標準出力に表示する関数ですね。
また、JavaScriptではfunction foo(a) {...}
とconst foo = function(a) {...}
がほぼ同じ意味になります。
// 以下の2つはほぼ同じ意味
function div(a, b) {
return a / b
}
const div = function(a, b) {
return a / b
}
関数が第一級オブジェクトであるJavaScriptでは、fooという名前の関数を定義することと関数をfooという名前の変数に代入することは同じようなことなんですね。
※厳密には違ったりするので「JavaScrpitでは」「同じような」と誤魔化してますけど許してください
あとなんとなくわかるとは思いますけど、上のコードを1つのファイルに書いて実行してしまうと「div
はすでに宣言されてる」って怒られますからね。
気を取り直してまして。
関数が第一級オブジェクトであるということのイメージは、なんとなくつかんでいただけたのではないでしょうか。
関数を変数に代入してみたり、引数として渡してみたり、関数の返り値にしてみたりといったことにはなれるまで時間がかかるかもしれませんけど。
環境(状態)を持つというイメージ
クロージャは英語ではClosure、日本語では関数閉包ですね。
イメージは自身が定義されたときの環境を持った関数です。
環境とは、変数などの状態のことでもありますね。
この環境を持つっていうのがなかなか想像がつかないポイントだと思うんですよ。
でもこれ、実は結構簡単なお話で、例えばオブジェクト指向の言語(JavaやPHPやC#など)のクラスは、フィールドやプロパティと呼ばれる内部状態(変数)を持っていますよね。
またクラスがない言語でも、モジュールが変数を持つことができたりします。
つまり、クラスやモジュールは環境(状態)を持つことができるのです。
それと同じで、関数も変数を持つことができるのです。
そして環境(変数)を持った関数のことをクロージャと呼びます。
ただ、関数が環境(変数)を持つというのは少しイメージがしづらいのではないでしょうか。
なので、実際にコードを見ていきたいと思います。
これもまた使い古された例なんですけど、数を数えるカウンターを題材にしてみたいと思います。
クラスが持つ環境
class Counter {
constructor() {
this.countNumber = 0
}
count() {
this.countNumber += 1
return this.countNumber
}
}
const counter = new Counter()
console.log(counter.count()) // => 1
console.log(counter.count()) // => 2
console.log(counter.count()) // => 3
// => 1
は、実行したときに表示される値をコメントとして書いているだけです。
また、JavaScriptではクラスのフィールドを宣言する方法がありませんので、コンストラクタの中でいきなり代入します。
それにだけ気を付ければ、オブジェクト指向をやったことがある人はたぶん簡単に読めますよね。
やったことのない人も、なんとなく意味はつかめるかと思います。
ちなみにconstructor
はnew
したときに実行される関数で、this
はそのクラス(のインスタンス)自身を指します。
どちらにせよ、クラスが持つ環境の意味はつかんでいただけたのではないでしょうか。
簡単にだけ説明しておきますと、クラスの外部に公開されたメソッドcount
から、クラス内部の環境であるフィールドcounterNumber
を変更することができています。
モジュールが持つ環境
let countNumber = 0
function count() {
countNumber += 1
return countNumber
}
module.exports = {
count: count
}
const counter = require('./counter.js')
console.log(counter.count()) // => 1
console.log(counter.count()) // => 2
console.log(counter.count()) // => 3
モジュールと言うと言語ごとに多少意味が異なってきますけど、ここでは関数や変数、クラスなどをまとめたものくらいのふわっとした理解で大丈夫かと思います。
module.exports
とかrequire
とかはCommonJSというモジュールシステムのお作法です。
あと、CommonJSではモジュールとファイルが深く結びついているので、上のコードだけはそれぞれ別のファイルに書いて実行する必要があります。
何をしているのかなんとなくはわかるのではないでしょうか。
先ほどのクラスがモジュールになっただけのような気がしますね。
モジュール内で宣言された変数が、モジュールが持つ環境となります。
これも一応説明しておきますと、モジュールの外部に公開された関数count
から、モジュール内部の環境である変数counterNumber
を変更することができています。
クラスが持つ環境とほとんど同じことを言っていますよね。
関数が持つ環境(クロージャ)
いよいよクロージャです。
クロージャとは環境(変数)を持った関数です。
ここで少し、クラスが持つ環境とモジュールが持つ環境を振り返ってみたいと思います。
どちらも、関数(メソッド)count
と変数counterNumber
がセットなっていますよね。
そして、それら2つをまとめているものはクラスが持つ環境ではクラスであり、モジュールが持つ環境ではモジュールとなっています。
ということはですよ。
関数が持つ環境では、関数count
と変数countNumber
のセットが関数によってまとめられると予想できるのではないでしょうか。
なので、そこらへんを意識しながら以下のコードを読んでみてくださいね。
function createCounter() {
let countNumber = 0
const count = function() {
countNumber += 1
return countNumber
}
return count
}
const counter = createCounter()
console.log(counter()) // => 1
console.log(counter()) // => 2
console.log(counter()) // => 3
どうでしょう、何となく読めたのではないでしょうか。
ただ関数を返す関数というのは、なじみのない人にはわかりにくいかもしれませんね。
それは少しおいておくとして。
これも同じく説明してみますと、関数が外部に公開した関数、つまり返り値にした関数count
から、関数内部の環境である変数counterNumber
を変更することができています。
少し変則的なのは、クラスとモジュールではそれぞれ環境を持つのはクラス/モジュールで、それを変更するのは外部に公開した関数(メソッド)count
であったのに対し、関数では環境を持っているのは関数自身であり、環境を変更するのもまたその関数自身である、というところですね。
環境を持っているもの | 環境を変更するもの | |
---|---|---|
クラス | クラス(のインスタンス)自身 | メソッド |
モジュール | モジュール自身 | 公開した関数 |
クロージャ | クロージャ自身 | クロージャ自身 |
まあでも、大体似たような説明ができましたよね。
さらに、今まで出てきたクラスとモジュール、そして関数にはある共通点があります。
それは、変数のスコープを切ることができるという点です。
それらを利用して内部に環境(変数)を持たせ、さらに外部に公開した関数から内部の環境(変数)にアクセスすることができるんですね。
そう考えると、クラスもモジュールも関数も似たようなもので、クロージャは至極当たり前のものなんじゃないかな、とちょっと思えないでしょうか。
そう思っていただけたのなら、今回の僕の目論見は成功したことになりますね。
よく紹介されるクロージャの不思議挙動
function createCounter() {
let countNumber = 0
const count = function() {
countNumber += 1
return countNumber
}
return count
}
const counter1 = createCounter()
const counter2 = createCounter()
console.log(counter1()) // => 1
console.log(counter2()) // => 1
console.log(counter1()) // => 2
console.log(counter2()) // => 2
「あれ、表示される数が増えてないところがある!」
ここまで読んでくださった方は、たぶんですけどそんなこと思いませんよね。
イメージがつかめていれば、そんなに難しいことではなく、当たり前の挙動に見えますよね。
わからないという人は、クラスの場合を考えてみてください。
class Counter {
constructor() {
this.countNumber = 0
}
count() {
this.countNumber += 1
return this.countNumber
}
}
const counter1 = new Counter()
const counter2 = new Counter()
console.log(counter1.count()) // => 1
console.log(counter2.count()) // => 1
console.log(counter1.count()) // => 2
console.log(counter2.count()) // => 2
オブジェクト指向をやっている人には当たり前すぎることですよね。
でもですね、ここで「あれ、モジュールは?」と疑問に思う方がいるかもしれません。
それか、もっと前から「これはおかしい」って怒り心頭だった人もいるかもしれませんね。
そうです、モジュールでは上のような挙動はできないのです。
オブジェクト指向をちゃんと理解している人向けに言うと、モジュールにはクラスにおけるインスタンス相当のものがないのです。
もちろん関数にはありますけど。
ですので、モジュールは厳密に言うと今回の説明に出すべきではありませんでした。
でも、クラスだけだとクロージャのイメージをつかみにくいかなあという苦渋の決断の元、イメージだからいいやという免罪符を手にモジュールも出すことにしました。
一応理由を説明したので許していただけるとありがたいです……
まとめ
クラスとモジュールと関数が、変数のスコープを切ることができるという似たような性質を持っている、という切り口からクロージャについて説明してみました。
クロージャのイメージをなんとなくでもつかんでいただけたのでしたら幸いです。
クロージャを理解していながらも読んでくださった方は、助言や意見をいただけるとなおのことありがたいです。
「これちゃんと理解して書いてるのかな」とか、「ここは意味を取りづらいんじゃないか」とか、「この表現は正確ではない」とか。
あんまり厳しい言い方だと心が折れそうなので、できれば優しく教えていただきたいですけど。