ツイート
シェア
LINEで送る
B! はてぶでブックマーク
Pocketでブックマーク
RSSフィード

クロージャってなんだ? JavaScript(TypeScript)以外で使ったことないけど。

プログラミング

『クロージャってJavaScriptで出てくる関数内の関数のことでしょ?』。これは100点満点で50点です。

JSが目立つだけで、JS固有の仕様でもなくプログラミングの処理の形態です。

たとえば、PHPにはClosureというクラスが用意されている。

クロージャについてのかんちがい

『クロージャってJavaScriptで出てくる関数内の関数のことでしょ?』。

さっきの50点の回答です。

ほかにも、こういう回答をする人もいるんじゃないでしょうか?

『クロージャって無名関数のことでしょ?』。

これは20点くらいかな?

クロージャのちゃんとした回答はこうです。

あるスコープ(関数とはかぎらない)内にある関数が、そのスコープ内の変数などを参照すること

または、その機能をもった関数のこと

関数内の関数で、定義されたスコープの変数などを利用してなければ、それはただの関数内の関数です。

たしかにクロージャは無名関数を使うんですが、無名関数はクロージャのためだけにあるんじゃありません。

関数のパラメータや戻り値にも使う。これはクロージャじゃなくてもいい。

クロージャのサンプル

言葉で説明するよりも百聞は一見にしかずなので、サンプルコードで見ていきましょう。

プログラム言語はクロージャをよく使うJavaScriptです。

クロージャのようでクロージャじゃない

let main = function() {
	let counter = 0;
	
	return function() {
		let result = 1;
		return result;
	}
}

let test = main();

console.log(test());
console.log(test());
console.log(test());
console.log(test());
console.log(test());
実行結果
1
1
1
1
1

これ、クロージャを使ってると思ってる人がいると思います。

(自分もそう思ってた。)

ただ、クロージャの定義からすると、戻り値の無名関数は main() の変数などは一切使ってません。

ということはこれはクロージャではない。ただ無名関数を返してるだけ。

これぞクロージャ

今度はさっきの関数を変更します。

let main = function() {
	let counter = 0;
	
	return function() {
		counter++;
		return counter;
	}
}

let test = main();

console.log(test());
console.log(test());
console.log(test());
console.log(test());
console.log(test());

return で返す無名関数内で、関数定義と同スコープの counter 変数を使いました。これぞクロージャです。

実行結果
1
2
3
4
5

カウンタが発動しました。でも気になるところありませんか?

counter = 0 で初期値を入れてるんだから、同じ1並びの結果になると予想できるのに、きちんとカウントされてる。

クロージャの真骨頂。クロージャオブジェクトは変更した変数値を保存する。

counter の初期値が無視される理由を探るために、さらにデバッグポイントを入れてみます。

let main = function() {
	let counter = 0;
	
	console.log('scope counter:' + counter);
	
	return function() {
		counter++;
		return counter;
	}
}

console.log('befoer set test');

let test = main();

console.log('after set test');

console.log(test());
console.log(test());
console.log(test());
console.log(test());
console.log(test());
実行結果
befoer set test
scope counter:0
after set test
1
2
3
4
5

counter の初期値セットは let test = main() を実行したときの1回こっきり。そして、counterの値は変更が保存されてます。

カウンタの発動。今度は、test変数がどういう値なのかデバッグを入れてみます。

// 同上

let test = main();
console.log('test:' + test);

// 同上
実行結果
test:function() {
		counter++;
		return counter;
	}

これで分かりましたね? test変数の中身は、main() が返すクロージャ関数のオブジェクトです。だから counter の初期値は実行されませんでした。

ここがクロージャの不思議。counterの値は前回の変更を保存しています。

最後にこういうサンプルを実行してみましょう。

let main = function() {
	let counter = 0;
	
	console.log('scope counter:' + counter);
	
	return function() {
		counter++;
		return counter;
	}
}


let test = main();
let test2 = main();
let test3 = main();
let test4 = main();
let test5 = main();

console.log(test());
console.log(test2());
console.log(test3());
console.log(test4());
console.log(test5());
実行結果
scope counter:0
scope counter:0
scope counter:0
scope counter:0
scope counter:0
1
1
1
1
1

カウンタは発動しませんでした。当然ですね? main() を実行するたびに counter の初期化が行われてるんだから。

無名関数(クロージャ)の正体は関数オブジェクト

無名関数(クロージャも含む)は、変数に代入して使います。関数のパラメータや戻り値で使うときも変数として扱ってるからできること。

その変数に入っているデータのことを関数オブジェクトと言います。特殊なデータ型でクラスインスタンスの関数版と思えばいい。

new 演算子などを使いませんが、代入するだけで自動的にオブジェクトを生成します。

この特殊なデータ型の特長は、オブジェクト(だいたい変数になるが)の後ろに '()' を付けると、そのオブジェクトの型になっている関数を実行します。

これはプログラム言語を問わず同じ仕様。

といっても、JavaScript, PHP, Pythonぐらいしか知らないけど。

いつ変数定義が行われているのかを理解する。

counter 変数が初期化されるのは main() を実行したときです。

カウンタが発動したときは、main() を1回だけ実行し、その結果のクロージャ関数を test 変数へ保存しました。

ようは、main() の結果のクロージャオブジェクトの使い回しですね?

一方、最後のサンプルは5回、 main() を実行しました。なので、test, test2, test3, test4, test5 は同じクロージャでも、クロージャオブジェクトは別です。

クラスのインスタンスが別々だとイメージすると分かりやすい。使いまわしたときはひとつのインスタンスで実行したようなもの。

だから、counter が保存されました。

クラスインスタンスでイメージすると、counterはインスタンス内のプロパティに相当します。

最後にと言ってしまいましたが、ほんとの最後にこのサンプルを実行してみます。

// 同上

let test = main();
let test2 = test;
let test3 = test2;
let test4 = test3;
let test5 = test4;

console.log(test());
console.log(test2());
console.log(test3());
console.log(test4());
console.log(test5());

もう結果は分かりますね? カウンタが発動します。

test, test2, test3, test4, test5 のクロージャオブジェクトは変数はちがえど同じものだから。

実行結果
scope counter:0
1
2
3
4
5

話の途中から『クロージャオブジェクト』を連呼し始めたのに気づいたでしょうか?

そこにクロージャの真髄が詰まってるから。あえてしつこく言いました。

ちなみにオブジェクトのコピーはシャローコピーです。変数の見ている先の値は同じ。

クラスのインスタンスもオブジェクトなので考え方は同じです。

(だからクラスを使うものをオブジェクト指向という。)

クラスのインスタンスをイメージすると分かりやすいと言ったのは、無名関数(クロージャも含む)もオブジェクトの一種だから。

オブジェクトの構造がクラスか関数のちがいだけで。

無名関数(クロージャも)はオブジェクト。クラスもオブジェクト。

オブジェクト内の構造がちがうだけで、オブジェクトとして扱う挙動は同じ。

クロージャは真髄を使ってこそクロージャ。そうじゃないのはただの無名関数

ここで改めてクロージャの定義をもう1回おさらいしましょう。

あるスコープ(関数とはかぎらない)内にある関数が、そのスコープ内の変数などを参照すること

または、その機能をもった関数のこと

また同じことを言いますが、関数内の無名関数だけではクロージャとは言いません。

そもそもクロージャの和訳は関数閉包(かんすうへいほう)。関数をある領域(スコープ)で閉じて包むもの。

言いかえると、無名関数のオブジェクトをクラスのメソッドと想定して、上の層の変数をさもクラスのプロパティかのように扱うのがクロージャです。

クロージャの真髄を使わなかったら、なんで閉じて包むのか意味不明ですよね?

クロージャのスペルはClosureで『閉鎖』の意味があります。ただの無名関数だと、そもそも閉鎖しないので意味が分からない。

無名関数は上位の変数などを使わなくても、その上位のスコープに制限される。

(関数が閉鎖される。)

そこから 無名関数をクロージャともいうのが広く使われている。

クラスでもプロパティがなくメソッドがひとつのものがありますからね?

これもクラスオブジェクトにちがいはない。

しかしそれは、さっきから言ってるようにクロージャの定義とは合わない。

クロージャは関数コールだってできる。

『最後のサンプルを...』と2回も言って申し訳ないですが、これがほんとに最後のサンプルです。

クロージャは、対象スコープ内の別の関数(クロージャを定義した同じ層の別の関数)をコールして処理を実行することもできます。

let main = function() {
	let counter = 0;
	
	let extra = function() {
		counter++;
	}
	
	function extra2() {
		counter++;
	}
	
	return function() {
		counter++;
		extra();
		extra2();
		return counter;
	}
}

let test = main();

console.log(test());
console.log(test());
console.log(test());
console.log(test());
console.log(test());

extraは分かるでしょう。無名関数を変数に入れてるので、できる匂いがプンプンします。

じつは extra2() も実行可能です。

関数は無名関数だろうが通常の関数だろうがそれ自体はオブジェクトです。だから通常の名前付き関数でも問題ありません。

実行結果
3
6
9
12
15

気づいているとは思いますが、extra(), extra2() は2つともクロージャです。クロージャは別のクロージャも呼べます。

クラス内でメソッドから別のメソッドを呼べるのと一緒。

クラスのメソッドはクロージャと呼べるのでは?

クロージャからクロージャを呼んだときに、クラスのメソッドと一緒と言いましたが、それならクラスのメソッドもクロージャじゃないか? と、ふと思いました。

あるスコープ(クラス)内にある関数(メソッド)が、そのスコープ内の変数(プロパティ)などを参照すること。

または、その機能をもった関数(メソッド)のこと。

(ボクだけ?)

クラスのメソッドは自クラス内のプロパティを参照しますし、private メソッドも実行可能です。プロパティ値を変更すればその値は保存される。

プログラム言語の種類にかぎらず、オブジェクト指向プログラミングができるものの共通仕様。

でもメソッドの、この当たり前のことについてクロージャとは言いません。オブジェクト指向言語の特長だよね? としか思わない。

またクラスには、匿名クラス・無名クラスという無名関数のクラス版みたいなものもあります。当然それはクロージャになり得る。

上位スコープの変数を参照すれば。

そういえば、15年以上前にJavaをメインに使ってたんですが、そのとき聞いた『クロージャってメソッドみたいなもんでしょ?』というのを、今思い出した。

PHPにはClosureクラスという、まんまのものがある

PHPはクロージャなんてあるの? と思うプログラム言語ですが、それもそう。クラスとして用意されクロージャはそこで行われるから。

Closureクラスは、無名関数やアロー関数の戻り値の型として使います。変数に代入したときのその値はClosureクラスのオブジェクトになる。

<?php

function test() {
	return function() {
		return 1;
	};
}

$func = test();
$func_result = $func();

var_dump($func);
var_dump($func_result);
実行結果
object(Closure)#1 (0) {
}
int(1)

ポイントは、内部関数を変数に代入するだけではその値はClosureオブジェクトで、関数の結果が欲しければ、Closureオブジェクトに()を付けて関数を実行します。

クロージャの処理を書くというより、Closureクラスのオブジェクトが作られるので、それを利用する感じ。

そりゃ、クロージャを使ってる気はしない。

オブジェクト指向プログラミングでスレッド処理を書いてる気がしないのと似ている。

これ、デジャブを感じませんか?

JavaScriptでクロージャ関数のオブジェクトでしていたことと同じ。そのオブジェクトがClosureクラスになったのがPHP。

PHP公式ドキュメント

Class - Closure

前の投稿
ファーストクラス, 第一級関数とか第一級オブジェクトってなんだ?

コメントを残す