http://konno-freesoftware.blogspot.com/2010/02/javascript_10.html より。

#JavaScript - 小学生のためのクロージャ

計算機科学におけるクロージャとは

クロージャ - Wikipedia
引数以外の変数を実行時の環境ではなく自身が定義された環境静的スコープにおいて解決する関数の一種

なのであるがさすがにこの説明だけではピンと来ないかと言って

function newCounter() {
     var i = 0;
     return function() { // 無名関数
         i = i + 1;
         return i;
     }
 }
 
 c1 = newCounter();
 alert(c1()); // 1
 alert(c1()); // 2
 alert(c1()); // 3
 alert(c1()); // 4
 alert(c1()); // 5

このコードはあまりにもひどいクロージャの説明によく出てくるカウンタの例であるが私はクロージャの説明以外でこれを見たことがない

クロージャとは - はてなキーワード
function makeCounter () {
  var count = 0;
  return f;
  
  function f () {
    return count++;
  }
}

var count = 10;
var counter = makeCounter();
document.writeln(counter());
document.writeln(counter());
document.writeln(counter());
document.writeln(counter());

Hatena::Keywordも同様functionを入れ子にして後方宣言は趣味の問題だとしてもdocument.writelnとはいまだにInternet ExplorerNetscape Navigatorが戦っているようなコードである

クロージャとは何か? 本来死んでいるはずの変数が生き埋めになっている関数である



function trim(str) {
   
return str.replace(/^\s+|\s+$/g, '');
}
alert( trim("   Hello, world!   ") );

このtrim関数は文字列の両端の空白を取り除いて返しますたとえば

"   Hello, world!   "



"Hello, world!"

になります余計なごみを取ってくれる大変便利な関数なのですがこの実装には1つ問題があります

function trim(str) {
   
return str.replace(/^\s+|\s+$/g, '');
}

/^\s+|\s+$/gという正規表現が関数を実行するたびに作り直されてしまっているのですこれはvar regexp = /^\s+|\s+$/g;のように変数に貯めておきそれを使い回すのが効率的です

function trim(str) {
   
var regexp = /^\s+|\s+$/g;
   
return str.replace(regexp, '');
}

これでは駄目です関数を実行するたびにvar regexp = /^\s+|\s+$/g;が評価されるのでやはり作り直されてしまいます

var regexp = /^\s+|\s+$/g;
function trim(str) {
   
return str.replace(regexp, '');
}

これが正解ですvar regexp = /^\s+|\s+$/g;をtrim関数の外に出したのでtrim関数を何回実行しても正規表現が作り直されることはありません

ところがこの実装にも1つ問題があります

regexp変数は関数の外で宣言されているのでグローバル変数ですつまりすべての関数から使うことができます

regexp変数を使っているのはtrim関数だけなのにこれは無駄です

var regexp = /^\s+|\s+$/g;
function trim(str) {
   
return str.replace(regexp, '');
}

function something() {
   
// ここでも regexp 変数が使える
}

function unrelated() {
   
// ここでも regexp 変数が使える
}

regexp変数をグローバル変数にせずにtrim関数でのみ使えるようにする方法はないのでしょうか? ありますJavaScriptでは

function trim(str) {
   
return str.replace(regexp, '');
}

という関数は

var trim = function(str) {
   
return str.replace(regexp, '');
};

と書くこともできますregexp変数と合わせると

var regexp = /^\s+|\s+$/g;
var trim   = function(str) {
   
return str.replace(regexp, '');
};

このようになります意味はまったく同じですまったく同じですからregexp変数はまだグローバル変数のままです

これを少し工夫してtrim関数を返す関数というものを作ってみましょう

var metatrim = function(){
   
var regexp = /^\s+|\s+$/g;
   
return function(str){ /* trim関数の体 */
       
return str.replace(regexp, '');
   
};
};
var trim = metatrim();
alert( trim("   Hello, world!   ") );

// ここでは regexp 変数は使えない

どうですか? metatrim関数を実行するとtrim関数だったものtrim関数の体だけが返されますそれをtrimという名前の変数に代入すれば元通りのtrim関数になります不思議ですね

そういえばregexp変数はどこへいったのでしょうか?

metatrim関数の中に入っているのでグローバル変数にはなっていません

trim関数の体の中ではregexp変数が使われていますがmetatrim関数を出ればもうregexp変数を使うことはできません

しかしmetatrim関数の外でtrim関数を実行してもエラーにはなりませんregexp変数はmetatrim関数の中で宣言されているのでmetatrim関数の中でしか使えないはずなのに一体なぜでしょうか?

このときtrim関数はクロージャになっています

trim関数の体を作った時点ではregexp変数が使えたのでそこを出てもtrim関数の中ではregexp変数が生きているのです

そういうものです難しく考える必要はありません本来死んでいるはずの変数が生き埋めになっている関数がクロージャです

最初のカウンタの例に戻りましょう

クロージャ - Wikipedia
function newCounter() {
     var i = 0;
     return function() { // 無名関数
         i = i + 1;
         return i;
     }
 }
 
 c1 = newCounter();
 alert(c1()); // 1
 alert(c1()); // 2
 alert(c1()); // 3
 alert(c1()); // 4
 alert(c1()); // 5

このときi変数はnewCounter関数の中で宣言されているのでnewCounter関数を出た時点で死んでいるはずですがc1関数 (無名関数) の中ではi変数が生きていますi変数が生き埋めになっているのでc1関数はクロージャです

クロージャとは - はてなキーワード
function makeCounter () {
  var count = 0;
  return f;
  
  function f () {
    return count++;
  }
}

var count = 10;
var counter = makeCounter();
document.writeln(counter());
document.writeln(counter());
document.writeln(counter());
document.writeln(counter());

やはりcount変数はmakeCounter関数の中で宣言されているのでmakeCounter関数を出た時点で死んでいるはずですがcounter関数 (f関数) の中で生き続けていますcounter関数はクロージャになっているのですね

var metatrim = function() {
   
var regexp = /^\s+|\s+$/g;
   
return function(str){
       
return str.replace(regexp, '');
   
};
};
var trim = metatrim();

これは通常

var trim = (function(){
   
var regexp = /^\s+|\s+$/g;
   
return function(str){
       
return str.replace(regexp, '');
   
};
})();

このように省略して書かれます関数を返す関数をmetatrimに代入してそれを実行して返された関数をtrimに代入してとやるのではなく一気に全部やってしまうのです

JavaScriptと言えば(function(){ /*  */ })()と言うほどこの書き方は一般的です(function(){ /*  */ })()さえマスターすればJavaScriptは全部マスターしたと言っても過言ではありません上級者になるとJavaScriptとは何か?と問われて(function(){ /*  */ })()と返します

さらに引数を利用して

var trim = (function(regexp){
   
return function(str){
       
return str.replace(regexp, '');
   
};
})(/^\s+|\s+$/g);

このように書くこともできますこれはKonno JavaScript Archive Network (KJSAN) でも実際に採用されているString.prototype.trimの実装とほぼ同じですここまで来ればJavaScriptライブラリとして出荷できるのですね

以上のようにクロージャは死んでいるように見える変数が実は生き埋めになって閉じ込められている関数なので閉包とも呼ばれます

Posted by Yuki Konno, Software Engineer

0 comments:

comments powered by Disqus