LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


JavaScriptのクロージャ入門とECMAScript 6のletキーワードによる変数宣言

こんにちはUITチームの手島です。

先日社内のセミナー(勉強会のようなもの)でJavaScriptのクロージャについてお話しさせていただいたので、その内容をblogでご紹介させていただきます。

クロージャ自体はJavaScriptだけが持つ機構ではなく、現在様々なプログラミング言語でサポートされています。最近ではSwiftやJava 8など、クロージャを使える言語が増えてきています。

本記事ではクロージャとは一体なんなのかをJavaScriptの具体的な例を交えて初心者向けに解説します。また、ECMAScript 6のドラフトにあるletキーワードによる変数宣言についても少しご紹介します。

クロージャの例

いきなりですが、クロージャのコード例をご紹介します。

function appendPrefix(x) {
  return function(y) {
    return x + "_" + y;
  }
}
var brown = appendPrefix("brown");
var cony = appendPrefix("cony");
alert(brown("stamp1")); // "brown_stamp1" と表示される
alert(cony("stamp1")); // "cony_stamp1" と表示される

この例では、初めにbrownを指定して返されるfunctionは値を受け取るとbrownをprefixとして追加します。conyを引数に指定した場合に返される関数は以降conyをprefixとして追加しています。
初めてクロージャのコードを見た方は少し不思議に思うかもしれませんが、同じ関数であっても違う引数(データ)を与えて返される関数は、以降それぞれ与えたデータに応じた振る舞いをします。

このようなクロージャの仕組みを利用することで、データ (環境) をそれを操作する関数と結びつける事ができるようになります。

実際に人気のJavaScriptのライブラリのコードを見てみると、無名関数等を用いたクロージャの例は数多く見られます。一般的には、クロージャをうまく利用することで効率的なコードを書くことができると言われています。(昔のブラウザはクロージャの多用によるメモリリークを引き起こすケースもありましたが、最近のブラウザでは特に問題は無いようです。)

クロージャとは

上述の例で、なんとなくクロージャがどんなものかわかっていただけたかもしれませんが、クロージャという単語の定義を見てみましょう。

Wikipediaより(2014年10月現在)

クロージャ(クロージャー、英: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。いくつかの言語ではラムダ式や無名関数で実現している。引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる。

。。。

ちょっと意味がよくわかりませんね…笑

初心者がはまる罠

クロージャという単語の意味を解説する前に、JavaScriptを書き始めたばかりの方がよく陥りがちなミスを題材として紹介します。

以下のJavaScriptを実行してみましょう。

<html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <input type="button" id="button_0" value="0" />
        <input type="button" id="button_1" value="1" />
        <input type="button" id="button_2" value="2" />
        <input type="button" id="button_3" value="3" />
        <input type="button" id="button_4" value="4" />
        <input type="button" id="button_5" value="5" />
        <input type="button" id="button_6" value="6" />
        <input type="button" id="button_7" value="7" />
        <input type="button" id="button_8" value="8" />
        <input type="button" id="button_9" value="9" />
        <script>
            var showAlert = function(i){
                alert(i);
            };
            for (var i = 0; i < 10; i++) {
                document.getElementById("button_" + i).onclick = function(){
                    showAlert(i);
                };
            }
        </script>
    </body>
</html>

これを実行するとどのボタンを押しても10と表示されてしまいます。(JavaScriptのエンジニアなら誰しも初めに通る道ではないかと思います。)

スクリーンショット 2014-10-22 11.57.17

実行時コンテキストと静的スコープ

前の例がなぜ思った通りに動かないかを解説する前に、JavaScriptの関数が実行される時の挙動について理解しておく必要があります。

実行時コンテキスト(Execution Context)と静的スコープ(Static Scope)です。

実行時コンテキストとは、関数実行時に変数の参照を解決するための環境で、関数が実行されたタイミングで生成されます。

以下のコード例では、関数が実行されていく途中での各行の実行時コンテキストの内容を表しています。

// globalスコープでの実行時コンテキスト: {hoge: undefined};
var hoge = "global";
// globalスコープでの実行時コンテキスト: {hoge: "global"};
function outerFunc() {
    // outetrFuncの実行時コンテキスト: {fuga: undefined} -> {hoge: "global"};
    var fuga = "outer";
    // outetrFuncの実行時コンテキスト: {fuga: "outer"} -> {hoge: "global"};
    function innerFunc() {
        // innerFuncの実行時コンテキスト: {piyo: undefined} -> {fuga: "outer"} -> {hoge: "global"};
        var piyo = "inner";
        // innerFuncの実行時コンテキスト: {piyo: "inner"} -> {fuga: "outer"} -> {hoge: "global"};
    }
}

上記のコード内のコメントは関数が実行された時に生成される実行時コンテキストのスナップショットを表しています。例えばouterFuncの部分ではhogeという変数を解決するために、globalスコープのhogeを探して変数の解決をします。

JavaScriptはスコープチェーンと呼ばれる性質があり、あるスコープで解決しない変数を実行時に参照した場合に、外側へスコープを参照して解決しようとします。
ここで重要なのは外側のコンテキストは現在実行中の関数が定義された時点のスコープを参照するということです。この性質を静的スコープと呼びます。

関数実行時の変数解決における重要なポイントをまとめます。

  1. 関数は実行されたタイミングでそれぞれが実行時の変数環境を作る (実行時コンテキスト)
  2. 自分のスコープで解決しない変数は外側を巡り参照する (スコープチェーン)
  3. スコープチェーンで辿る外側のコンテキストは、実行時ではなく、その関数自身が定義された時に決定(保持)されたスコープを参照する(静的スコープ

はまった例をクロージャで解決する

もう一度先ほどの期待通り動かなかった例を見てみましょう。

うまく行かない例

var showAlert = function(i){
    alert(i);
};
for (var i = 0; i < 10; i++) {
    document.getElementById("button_" + i).onclick = function(){
        // 実行時コンテキスト: {i: undefined} -> {i: 10};
        showAlert(i);
    };
}

上記の例ではユーザーのclickアクションに対するコールバックとして無名関数をfor文で定義しています。実行されるタイミングは定義される時のタイミングと違う点に注意が必要です。

onclickのfunctionの実行時に実行時コンテキストが作られ、その中で参照されるiは、自分自身が"定義された時に保持される外側のスコープ(変数環境)"を見ています。
このケースでは、実行時には定義時に作られていた外側のスコープではiが10となっているため、どのボタンを押しても10が表示されてしまいます。

この問題を解決する一つの方法として以下のようにクロージャを利用してみましょう。

クロージャを利用した例

for (var i = 0; i < 10; i++) {
    document.getElementById("button_" + i).onclick = (function(i){
        // 実行時コンテキスト (outer): {i: i};
        return function(){
            // 実行時コンテキスト (inner): {i: undefined} -> {i: i};
            showAlert(i);
        };
    })(i);
}

即時実行する関数(定義と実行が同時)でラップ(元の関数をreturn)すると、以下の仕組みで変数が解決されます。

  1. ループごとに外側の関数は新しい実行時コンテキストを作る(iの値はコンテキストごとに異なる)
  2. 内側の関数実行時には、定義された時のそれぞれの外側の関数のスコープを参照する

このような振る舞いをする仕組みがクロージャです。内側の関数の実行時に参照されるiは、forループのスコープではなく、ループごとに作られた外側の関数のスコープのものになります。

実際に実行してみると、期待通り動作しました!

スクリーンショット 2014-10-22 11.57.43

$.proxyやFunction.prototype.bindを利用した例

jQueryやZepto等のライブラリで提供されているスコープを制御する$.proxy等のユーティリティを用いても解決できます。

$.proxyを利用した例

for (var i = 0; i < 10; i++) {
    document.getElementById("button_" + i).onclick = $.proxy(function(i){
        showAlert(i);
    }, this, i);
}

$.proxyは本来は関数実行時のthisのコンテキストをセットするユーティリティですが、受け取った関数を即時実行する関数がラップし、クロージャとして機能します。
もちろんECMAScript 5から導入されている$.proxyと同等の機能であるFunction.prototype.bindを用いてライブラリの依存無しに記述することもできます。

Function.prototype.bindを利用した例

for (var i = 0; i < 10; i++) {
    document.getElementById("button_" + i).onclick = (function(i){
        showAlert(i);
    }).bind(this, i);
}

改めてクロージャとは

クロージャとはわかり易く言うと「自分が定義された時のスコープ(静的スコープ)を、実行時に参照・解決できる関数の仕組み」と言い換えられそうです。

Wikipediaより(再掲)

引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる。

云わんとしていることが少し理解できますね...!

ECMAScript 6のletキーワードによる変数宣言

実はECMAScript 6のドラフトにあるletキーワードをfor文で使用することで、上述のうまくいかない例を解決することができます。

Mozilla DeveloperサイトにJavaScript1.7のletの機能が紹介されています。

letはvarと異なり変数がそのブロックスコープレベルで制御されます。

Mozillaのサイトに掲載されている例を見てみましょう。

function varTest() {
  var x = 31;
  if (true) {
    var x = 71;  // same variable!
    console.log(x);  // 71
  }
  console.log(x);  // 71
}
function letTest() {
  let x = 31;
  if (true) {
    let x = 71;  // different variable
    console.log(x);  // 71
  }
  console.log(x);  // 31
}

letキーワードで宣言された変数はif文等のブロックレベルで別インスタンスとして定義されます。(varを用いた場合は同じインスタンスを参照します。)

今回の記事で紹介したfor文の中でletを用いて変数を宣言するとどうなるでしょうか。

letを使用した例 (Firefoxで動作確認推奨)

<html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <input type="button" id="button_0" value="0" />
        <input type="button" id="button_1" value="1" />
        <input type="button" id="button_2" value="2" />
        <input type="button" id="button_3" value="3" />
        <input type="button" id="button_4" value="4" />
        <input type="button" id="button_5" value="5" />
        <input type="button" id="button_6" value="6" />
        <input type="button" id="button_7" value="7" />
        <input type="button" id="button_8" value="8" />
        <input type="button" id="button_9" value="9" />
        <script type="application/javascript;version=1.7">
            "use strict";
            var showAlert = function(i){
                alert(i);
            };
            for (var i = 0; i < 10; i++) {
                let j = i;
                document.getElementById("button_" + i).onclick = function(){
                    showAlert(j);
                };
            }
        </script>
    </body>
</html>

letを用いて宣言されたjはvarとは性質が異なり、各forループ時の処理で別々のインスタンスとして定義されます。その性質によって、実行時にjを解決する時はそれぞれのインスタンスを参照するため、きちんと期待される動作が得られます。
この例を見てもわかるように、これまでfunctionをラップしてクロージャーを利用して解決していた手法よりも簡潔です。ECMAScript 6が導入された際に、主流の書き方となるかもしれません。

最後に

本記事では、初心者が陥り易いコード例を用いてJavaScriptのクロージャについてご紹介させていただきました。実行時コンテキストや静的スコープなどの仕組みがクロージャの機構を形成している事がお分かりいただけたかと思います。
ECMAScript 6で新たに導入されるletは従来のクロージャを利用して解決する方法よりも簡潔なアプローチができることもご紹介しました。今後のブラウザのECMAScript 6のサポート状況にも注目していきたいですね。