読者です 読者をやめる 読者になる 読者になる

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

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

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

備考

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

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:より正確には、外側の関数の状態に依存した関数