※クロージャと変数のスコープ(追記)を追記しました。
※コメント欄で環境という用語について議論がありましたが、そもそも環境は専門用語として存在します。詳しくは環境を持つというイメージに追記しました。
使い古された話題ではありますけど、わかりやすく説明できそうな気がしたので書いてみたいと思います。
先に方針だけ伝えておくと、クラスとモジュールと関数は、変数のスコープを切ることができるという共通の性質を持っている、という切り口からクロージャについて説明していきたいと思います。
これだけ読んで何となく先が予想できてしまった人は読まなくても大丈夫かと思います。
それでも読んでくださるという方は、助言なり意見なりをくださるととても嬉しいです。
実行環境
言語はJavaScript(ES2015 or later)を使いますけど、別に知らなくてもなんとかなるんじゃないでしょうか。
何か他の言語をやっていれば大体読めるかと思います。
実行環境はNode.js(v6.9.0 or later)です。
なのでモジュールはCommonJSでやります。
まあわからない人は全然気にしなくても大丈夫です。
以下で出てくるコードを実際に動かしてみたいという人は、公式サイトからインストーラを落としてくるなりパッケージマネージャ使うなりしてNode.jsをインストールすれば問題はないかと思います。
関数が第一級オブジェクト(ファーストクラスオブジェクト)とは
クロージャは関数が第一級オブジェクトである言語でしか使えませんので、先に第一級オブジェクトについて説明します。
クロージャは関数が第一級オブジェクトでない言語でも存在します。
勉強不足な僕の勝手な思い込みでクロージャは関数で作るものと思っていたんですけど、そうでもないようです。
関数はないけどクロージャはある例としてRubyなどが挙げられるみたいですね。
詳しくはコメント欄を見ていただければと思いますけど、指摘していただいた @raccy さんに感謝です。
ただ関数でクロージャを表現している言語は多いと思いますので、以下の内容は知っておいた方がいいとは思います。
知っているという人は環境を持つというイメージに進んでもらえればと思います。
第一級オブジェクトとは、その言語で値として扱えるデータのことです。
オブジェクトとは言いますけど、オブジェクト指向のオブジェクトとはまた別の意味を持ちます。
まあ「対象物」とか「値」くらいのふわっとした意味でとらえていただければ大丈夫かと思います。
脱線しましたが、値として扱えるというのは、代入できたり関数の引数にできたり、戻り値にできたりするという意味です。
例えばCやPHPでは、関数は第一級オブジェクトではありません。
Cでは関数は第一級オブジェクトではないようですが、PHPには無名関数があるため関数が第一級オブジェクトとなっているようでした。
話を戻しまして、Cは関数自体を関数の引数や戻り値にできません。
しかし、PythonやJavaScriptでは関数を変数に代入したり、関数に渡したりすることができます。
代入したり引数にしたりできるということは、当然関数を値として生成する方法もあります。
以下はJavaScriptのコードです。
// 関数(のインスタンス)を生成し、変数に代入
const add1 = function(a) {
return a + 1
}
console.log(add1(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という名前の変数に代入することは同じようなことなんですね。
※厳密には違ったりするので「JavaScriptでは」「同じような」と誤魔化してますけど許してください
あとなんとなくわかるとは思いますけど、上のコードを1つのファイルに書いて実行してしまうと「div
はすでに宣言されてる」って怒られますからね。
気を取り直しまして。
関数が第一級オブジェクトであるということのイメージは、なんとなくつかんでいただけたのではないでしょうか。
関数を変数に代入してみたり、引数として渡してみたり、関数の返り値にしてみたりといったことにはなれるまで時間がかかるかもしれませんけど。
環境を持つというイメージ
クロージャは英語ではClosure、日本語では関数閉包ですね。
イメージは自身が定義されたときの環境を持った関数1です。
環境とは、変数などの状態のことでもありますね。
環境とは、名前(変数名)と値のペアのリストです。
[(x, 2), (y, 4), (z, 8)]
大体こんな感じのものです。
この関数が環境を持つっていうのがなかなか想像がつかないポイントだと思うんですよ。
でもこれ、実は結構簡単なお話で、例えばオブジェクト指向の言語(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
はそのクラス(のインスタンス)自身を指します。
インスタンスはnew
でクラスから実際に生成され変数に代入される値のことですね。
どちらにせよ、クラス(のインスタンス)が持つ環境の意味はつかんでいただけたのではないでしょうか。
簡単にだけ説明しておきますと、クラスの外部に公開されたメソッド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
であったのに対し、関数では環境を持っているのは関数自身であり、環境を変更するのもまたその関数自身である、というところですね。
| 環境を持っているもの | 環境を変更するもの
------- | ---------------------- | ---------------
クラス | クラス(のインスタンス)自身 | メソッド
モジュール | モジュール自身 | 公開した関数
クロージャ | クロージャ自身 | クロージャ自身
まあでも、大体似たような説明ができましたよね。
さらに、今まで出てきたクラスとモジュール、そして関数にはある共通点があります。
それは、変数のスコープを切ることができるという点です。
それらを利用して内部に環境(変数)を持たせ、さらに外部に公開した関数から内部の環境(変数)にアクセスすることができるんですね。
そう考えると、クラスもモジュールも関数も似たようなもので、クロージャは至極当たり前のものなんじゃないかな、とちょっと思えないでしょうか。
そう思っていただけたのなら、今回の僕の目論見は成功したことになりますね。
クロージャと変数のスコープ(追記)
もう一度関数が持つ環境(クロージャ)のclosure.jsのコードを見てほしいんですけど、変数count
に代入されている関数(この関数はクロージャ)がありますよね。
その関数の中で使用されている変数countNumber
は、この関数の中だけ見れば存在しないはずの変数です。
このような、関数の中だけ見たときに引数にもないし宣言もされていないのに突然現れる変数を自由変数と言います。
closure.jsのコードを読んでいたときは、その関数(クロージャ)の外で変数countNumber
が宣言されているのでそれを使っているのかなとなんとなく思っていただけていたのではないでしょうか。
こういう関数の自由変数が、その関数の定義時の環境で解決されることをクロージャが変数を捕捉するという言い方をします。
なんとなく、関数がその外の変数を持ってきて、自分のものにしているように見えますよね。
function createCounter() {
let countNumber = 0
const count = function() {
countNumber += 1
return countNumber
}
return count
}
そう考えてみると、「捕捉する」という言い方にも納得できそうな気がします。
この変数を捕捉するという言い回しはクロージャの絡む話ではたびたび見かけますので、ぜひ覚えておいた方がいいかと思います。
というわけで、JavaScriptでは関数内の自由変数はその関数定義時の環境で解決されるので変数の捕捉が起こっていたわけなんですけど、実はそうではない言語もあるんですよ。
以下はJavaScriptっぽく書いていますが、ある仮想言語のコードです。
function addx(y) {
return y + x
}
let x = 3
let z = addx(4)
z // => 7
先ほどまでのJavaScriptのコードとはかなり違った動きをしているように見えます。
JavaScriptでは、関数内の自由変数はその関数の定義時の環境で解決されるんでしたよね。
でもこの仮想言語では、関数内の自由変数はその関数の実行時の環境で解決されているんですよ。
少しイメージしづらいかもしれませんけど、関数は呼び出された場所にそのまま展開されると考えれば思ったより簡単に理解できるかと思います。
function addx(y) {
return y + x
}
let x = 3
let z = x + 4
z // => 7
これなら読めますよね。
でもこれ、変数の名前解決の方法としては、不思議なというか、なんだか少し危なそうな感じがしませんか(これは僕の偏見です)。
それは置いておくとして。
JavaScriptのように関数内の自由変数がその関数の定義時の環境で解決される変数スコープのことを静的スコープと言います。
同じように、上の仮想言語のように関数内の自由変数がその関数の実行時の環境で解決される変数スコープのことを動的スコープと言います。
動的スコープと静的スコープ、聞いたことはあるという人が多いのではないでしょうか。
自由変数をどう解決するかという違いだったんですね。
またクロージャでは、自由変数が静的スコープで解決される必要があります。
なぜでしょうか。
それはJavaScriptでクロージャを書いたコードを、先程の動的スコープを持つ仮想言語で書いてみた場合の実行結果を見ればわかるのではないかと思います。
function createCounter() {
let countNumber = 0
const count = function() {
countNumber += 1
return countNumber
}
return count
}
const counter = createCounter()
let countNumber = 10
const result1 = counter()
console.log(result1) // => 11
const result2 = counter()
console.log(result2) // => 12
countNumber = 0
const result3 = counter()
console.log(result3) // => 1
どうでしょう、読めましたでしょうか。
全然クロージャしていませんよね(これは勝手な造語です)。
関数createCounter
内の変数宣言let countNumber = 0
が見事に無視されています。
これは自由変数が動的スコープで解決されるため、定義時の環境は関係ないからです。
同じ理由で、let counterNumber = 10
を書かないと、関数counter
の実行時に「変数countNumber
がないよ」と怒られてしまいます。
関数counter
の実行時の環境に変数countNumber
が宣言されている必要があるからですね。
上のコードだけではわかりにくいという人は、先ほどの関数が呼び出された場所にそのまま展開されるというイメージを利用すると読めるようになるかと思います。
function createCounter() {
let countNumber = 0
const count = function() {
countNumber += 1
return countNumber
}
return count
}
const counter = createCounter()
let countNumber = 10
const result1 = {
countNumber += 1
return countNumber
}
console.log(result1) // => 11
const result2 = {
countNumber += 1
return countNumber
}
console.log(result2) // => 12
countNumber = 0
const result3 = {
countNumber += 1
return countNumber
}
console.log(result3) // => 1
少し特殊な書き方をしていますけど、大体意味はわかりますよね。
関数createCounter
は、定義時に変数countNumber
を捕捉しない関数count
を返します。
そして変数counter
に代入された関数count
の実行時の環境で、その中の変数countNumber
が解決されるんですね。
これで動的スコープではクロージャが成り立たないことがわかっていただけたかと思います。
以上、クロージャと変数スコープについての追記でした。
よく紹介されるクロージャの不思議挙動
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
オブジェクト指向をやっている人には当たり前すぎることですよね。
でもですね、ここで「あれ、モジュールは?」と疑問に思う方がいるかもしれません。
それか、もっと前から「これはおかしい」って怒り心頭だった人もいるかもしれませんね。
そうです、モジュールでは上のような挙動はできないのです。
オブジェクト指向をちゃんと理解している人向けに言うと、モジュールにはクラスにおけるインスタンス相当のものがないのです。
あってもシングルトンになっているのではないかと思います。
モジュールの例のカウンタは、require
したすべてのファイルで共通のカウンタ変数を使ってしまうことになります。
ですので、モジュールは厳密に言うと今回の説明に出すべきではありませんでした。
でも、クラスだけだとクロージャのイメージをつかみにくいかなあという苦渋の決断の元、イメージだからいいやという免罪符を手にモジュールも出すことにしました。
一応理由を説明したので許していただけるとありがたいです……
まとめ
クラスとモジュールと関数が、変数のスコープを切ることができるという似たような性質を持っている、という切り口からクロージャについて説明してみました。
クロージャのイメージをなんとなくでもつかんでいただけたのでしたら幸いです。
クロージャを理解していながらも読んでくださった方は、助言や意見をいただけるとなおのことありがたいです。
「これちゃんと理解して書いてるのかな」とか、「ここは意味を取りづらいんじゃないか」とか、「この表現は正確ではない」とか。
あんまり厳しい言い方だと心が折れそうなので、できれば優しく教えていただきたいですけど。
-
これはクロージャによって補足された変数が静的スコープで解決されるということでもあります。 ↩