イルカと泳ぎにいけない人のためのクロージャー入門

最近友達にクロージャーって何?という口頭試問質問を突然されるので僕が現時点でこうかな?と思っているクロージャーについてまとめてみました。 間違ってるところとかあったら教えてください。

具体的な例題を普通に書いたあと、クロージャーで書きなおしていきます。あークロージャーってこんな感じに使うんだなーと思ってもらえれば幸いです。

備考

  • 名前と言語を指定したら、その言語であいさつしてくれる関数を作成する。

version 1(普通に実装)

// まずは普通に実装
function greet(name, lang) {
    var greeting_message;

    if(lang == "English") {
        greeting_message = "Hello";
    }
    else if(lang == "Japanese"){
        greeting_message = "こんにちは";
    }
    else {
        greeting_message = null;
    }

    return greeting_message + "! " + name + "!";
}

greet("zerou", "English"); // => "Hello! zerou!"
greet("tarou", "Japanese"); // => "こんにちは! tarou!"

(指定言語が英語でも日本語でもないケースの処理については手抜きです。)

とりあえず普通に実装しましたが、挨拶の度に毎回langを指定するのは面倒なので、次の例以降では名前だけで挨拶関数を呼び出せるように仕様を変更します。

まずはぱっと思いつくグローバル変数を使った実装を試してみましょう。

version 2(グローバル変数に頼る)

// langを引数にしない以外はそのまま
function greetUseGlobalVariable(name) {
    var greeting_message = "";

    if(lang == "English") {
        greeting_message = "Hello";
    }
    else if(lang == "Japanese"){
        greeting_message = "こんにちは";
    }
    else {
        greeting_message = null;
    }

    return greeting_message + "! " + name + "!";
}

// グローバル変数としてlangを書き換える
lang = "English";
greetUseGlobalVariable("zerou"); // => "Hello! zerou!"

// 言語の切り替え
lang = "Japanese";
greetUseGlobalVariable("tarou"); // => "こんにちは! tarou!"

上記のようにlangをグローバル変数にした実装にすることで、色々な言語で挨拶をするという仕様を満足しながらも、毎回言語を指定せずに挨拶できるようになりました!

・・・これでいいのかというと、もちろん良くありません。

グローバル変数を使うと外部の状態に強く依存するので挙動が安定せず好ましくないというのはよく知られている通りで、この場合ではgreetUseGlobalVariable("tarou");の実行時に何が起こるかが曖昧になり、適当に呼び出した際にはめでたく意図しない結果が得られてストレスで死にます。

またグローバル名前空間を占有するため、他にlangという名前があると衝突が発生してストレスで死にます。

そうなると、コードのどこでlangが変更されているかを注意深く考えながら呼び出し、コードを追加する際にはlangという名前を決して使わないようにすれば良いのではないかという話になりますが、そういうことをしてるとストレスで死にます。

ストレス緩和のために、考えられる対策は二つです。

イルカと泳ぎにいく資金がないので、今回は後者の対策を考えていきます。

version 3(クロージャーを使う)

// クロージャーを使う

// 「関数を返す関数」(ここでは外側関数と呼ぶ)を定義
function getGreetingFunction(lang) {
    var greeting_message;

    if(lang == "English") {
        greeting_message = "Hello";
    }
    else if(lang == "Japanese"){
        greeting_message = "こんにちは";
    }
    else {
        greeting_message = null;
    }

    // 外側関数の中で新しい関数(ここでは内側関数と呼ぶ)を定義して返す。
    return function greetInSomeLanguage(name) {
        return greeting_message + "! " + name + "!";
    };
}

// 返された関数を受け取って変数に格納
var greetInEnglish = getGreetingFunction("English");
greetInEnglish("zerou"); // => "Hello! zerou!"

// 返された関数を受け取って変数に格納
var greetInJapanese = getGreetingFunction("Japanese");
greetInJapanese("tarou"); // => "こんにちは! tarou!"

greetInEnglishgreetInJapaneseの振る舞いは固定されているので、さっきのように外部でlangが書き換えられて意図しない出力が得られるということはありません。これでグローバル変数の魔の手から逃げつつも、langをいちいち指定しないでたくさんの人に挨拶できるようになりました。

ここで使ったクロージャーについて詳しく見ていきます。

クロージャーについて

上記の例のように関数の中で関数を定義して、状況に応じた関数*1を生成するテクニックがあります。このテクニックを使うと、外側関数を実行して内側関数が返り値として返却されるときに、「内側関数」と「外側関数(エンクロージャー)のスコープ内の変数への参照」からなる「新しい関数オブジェクト」が生成されます。この新しい関数オブジェクトのことを「クロージャー」と呼びます。

各用語と上の例での関数名を対応づけると下のようになります。

  • 外側関数:getGreetingFunction
  • 内側関数:greetInSomeLanguage
  • クロージャー:greetInEnglish, greetInJapanese

一見、内側関数が返された時点で外側関数のローカル変数は消えてしまいそうですが、クロージャーが参照しているために消えずにクロージャーを介してアクセスすることができます。

また外側関数のローカル変数は「クロージャーごと」に作成され、メモリ上に配置されます。上の例ではgreetInEnglish, greetInJapaneseという二つのクロージャーはそれぞれ別のgreeting_messageを持っています。

これらの性質を使えば上の例のように、振舞の異なる関数を同一の関数から生成することができるようになります。しかもクロージャーごとに振舞が固定されているので、振舞の変更を実現しているにも関わらず、グローバル変数を使ったときのような不安定性がありません。

何故クロージャーと呼ぶのか

内側関数であるgreetInSomeLanguage関数はgreeting_messageというローカル変数でも引数でもない変数(自由変数)に依存しているため、それ単体では"閉じた"関数ではありません。そしてgetGreetingFunction関数内にあるgreeting_messageがあって(自由変数が束縛されて)初めて関数は"閉じられる"ことになります。ということで、この閉じられた関数オブジェクトをクロージャーと呼びます。

あとがき

今回はクロージャーの原理とか由来を説明しました。実用的な使い方などの紹介まで入れるとさらに長くなってしまうので、今回はこの辺でいったん区切りたいと思います。

自分で分かってると思っていても文章にするとなると、長くなったりして捉えきれてないところがたくさんあるんだなーと思い知らされます。来年はもっと整理された説明を心がけたいと思います。ということで良いお年をお迎えください。

正確で分かりやすいクロージャーの解説はこちら→クロージャ - JavaScript | MDN

*1:より正確には、外側の関数の状態に依存した関数