映画「タイタニック2」の予告編
Google Docs in Plain English

Javascript、クロージャを使ったプライベート関数の隠蔽について

(このエントリーは「Javascriptクイズ:無名関数と実行効率の話」の続編。)

「???」と頭をかしげる太郎に、「じゃあ、これだったらどうかな?」と三郎はコードを書き始めます。

function code2name(code)
{
   var mapping = {
      'us': 'United States',
      'ja': 'Japan',
      'ko': 'Korea',
      'ru': 'Russa',
      'uk': 'United Kingdom',
      'fr': 'France',
      'cc': 'China',
      'gw': 'Germany'
   };
   return mapping[code] || '(unknown)';
}

「カントリーコードを国名に変換しているんですね。」と太郎。

「どこが問題だか分かる?」

「うーん、マッピングのためのオブジェクトを毎回作り直しているところかな。」

「そうだね。code2nameが呼ばれるために毎回同じオブジェクトを作り直すのは無駄。一度だけ作って使い回しをするべきだよね。インタープリタが賢ければある程度はオプティマイズしてくれるかも知れないけど、色々なブラウザーで走らせることを考えれば、そんなものに頼るのは良くないよね。ちなみに、確か太郎はC++には強かったけど、C++だったらこんなときどうする?」

「"const static"を付けて明示的に共有するようにコンパイラに指示する、かな」

「そうだね、C++の場合はstaticを指定することにより、同じ変数を使い回すようにコンパイラに指示ができるんだよね。Javascriptの場合、同じオブジェクトを使い回したい場合にはクロージャを使うんだ」とコードを書き直す三郎。

var code2name = (function(){
   var mapping = {
      'us': 'United States',
      'ja': 'Japan',
      'ko': 'Korea',
      'ru': 'Russa',
      'uk': 'United Kingdom',
      'fr': 'France',
      'cc': 'China',
      'gw': 'Germany'
   };
   return function(code) { return mapping[code] || '(unknown)';};
})();

「こうしておくと、一番外側の無名関数は一度だけ実行されて、その時にマッピングのためのオブジェクトが作られる。この時点ではローカル変数mappingだけがそれを参照しているんだけど、この関数がそれを参照する別の無名関数を作って返し、それへの参照がグローバル変数code2nameに格納されるため、結果的にはマッピング用のオブジェクトは外側の無名関数の終了後も生き延びることになるんだ。」

「ローカル変数が関数の終了後も生き残るんですか。クロージャってゾンビみたいですね。」と太郎。

「便利だろクロージャって。こんな風にクロージャを使えば、C++のstatic変数のようにオブジェクトの共有ができるようになるし、オブジェクト指向で言うところの『隠蔽』もちゃんとできるってわけさ。」と三郎。

「つまり、最初の例でもcapitalize関数をクロージャを使って隠蔽すれば良いってことですね。自分でやってみます!」と太郎が書いたのがこのコード。

var style2prop = (function(){
    function capitalize(str)
    {
        return str.charAt(1).toUpperCase();
    }

    return function(str) {
        return str.replace(/-[a-z]/g, capitalize);
    }
})();

「太郎もクロージャを理解しはじめたみたいだね。関数もオブジェクトだってことを理解していれば、毎回まったく同じものを作り直すのは無駄だって言うことは分かるよね。先の例と同じ様に、インタープリタがある程度はオプティマイズしてくれるかも知れないけど、可能であればこんな風に明示的に関数の共有を指示すべき。関数だってオブジェクトなんだから、毎回作り直すべきか使い回すべきかは常に意識してプログラムを書く習慣を持つべきだよね。」

「そうですね。でも、関数オブジェクトの場合にも『毎回作り直した方が良いケース』なんてあるんですか?」

「良い質問だね。クロージャを使うケースがそうだよ。タイマーを使ってオブジェクトのメソッドを呼び出したい時は、こんなコードを書くよね。」

    var obj = new ...; // local variable
    ...
    setInterval(function() {obj.callback();}, 33);

「ここでsetIntervalに渡すために作っている無名関数だけど、メソッドを呼びたいオブジェクトへの参照を関数オブエジェクトそのものに持たせる必要があるんで、実際に毎回違う関数オブジェクトを作る必要があるんだ。こういう時にこそ、動的に生成する無名関数オブジェクトが力を発揮するんだ。」

「うーん、理解できたようなできないような」と太郎。

「関数オブジェクトとクロージャの概念は、C++から来た人たちにはなかなか直感的に理解できないから、太郎の悩みは当然だよ。でも、この障壁を乗り越えて、関数オブジェクトとクロージャを適切に使いこなせるようになることは、Javascriptにおけるオブジェクト指向プログラミングをする上で必須だっていうことを覚えておくと良いよ。C++だってstatic, private, constなどのキーワードがちゃんと使いこなせるかどうかが良いオブジェクト指向のプログラムを書けるかどうかの重要な鍵じゃないか。それと同じさ。」と三郎。

「そうだ、クロージャへの理解を深めるために一つ宿題をあげよう」と続ける三郎。「今の、"font-style"を"fontStyle"に変更する関数style2propに加えて、"font_style"を"fontStyle"に変更する別の関数hoge2propを同じライブラリの中に作りたいとき、その二つの関数でcapitalizeを共有しつつ、かつそれをクロージャを使って隠蔽するにはどうしたら良いか考えてみると良いよ。」と言って立ち去ってしまう三郎。

 ということで、今回の宿題は、一つのプライベートな関数を複数のパブリックな関数で共有しつつ、そのプライベート関数をクロージャを使って隠蔽するテクニック。さあ、太郎はどんな答えにたどりついたでしょう?

Comments

latchet

このようなのはどうでしょうか。
若干capitalizeに変更を加えました。

var make2prop = (function(){
  function capitalize(str){
    return str.charAt(str.length - 1).toUpperCase();
  };
  return function(delimiter){
    var regex = new RegExp(delimiter + '[a-z]', 'g');
    return function(str){
      return str.replace(regex, capitalize);
    };
  };
})();
var style2prop = make2prop('-');
var hoge2prop = make2prop('_');

alert('font-style ==> ' + style2prop('font-style'));
alert('font_style ==> ' + hoge2prop('font_style'));

latchet

半角スペースだとインデント表示が反映されないのかも・・?
読みづらくなってしまい、皆様すみません。

Satoshi Nakajima

早速コメントありがとうございます。全角に直しておきました。

latchet

中島さん、ありがとうございます!
お手数おかけしてしまい恐縮です。

tsacilppa

(function(ns) {
 function capitalize(str) {
  return str.charAt(1).toUpperCase();
 }

 ns.style2prop = function(str) {
  return str.replace(/-[a-z]/g, capitalize);
 }
 ns.hoge2prop = function(str) {
  return str.replace(/_[a-z]/g, capitalize);
 }
})(window);

latchet

少し、変えた場合を考えてみました。

(function(obj){
  var makeFunc = (function(){
    function capitalize(str){
      return str.charAt(str.length - 1).toUpperCase();
    };
    return function(delimiter){
      var regex = new RegExp(delimiter + '[a-z]', 'g');
      return function(str){
        return str.replace(regex, capitalize);
      };
    };
  })();
  for(var func in obj)
    window[func] = makeFunc(obj[func]);
})({'style2prop' : '-',
  'hoge2prop' : '_'});

alert('style2prop("font-style") : ' + style2prop('font-style'));
alert('hoge2prop("font_style") : ' + hoge2prop('font_style'));

latchet

(function(obj){
  var makeFunc = (function(){
    function capitalize(str){
      return str.charAt(str.length - 1).toUpperCase();
    };
    return function(delimiter){
      var regex = new RegExp(delimiter + '[a-z]', 'g');
      return function(str){
        return str.replace(regex, capitalize);
      };
    };
  })();
  for(var func in obj)
    window[func] = makeFunc(obj[func]);
})({'style2prop' : '-',
  'hoge2prop' : '_'});

Verify your Comment

Previewing your Comment

This is only a preview. Your comment has not yet been posted.

Working...
Your comment could not be posted. Error type:
Your comment has been posted. Post another comment

The letters and numbers you entered did not match the image. Please try again.

As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

Having trouble reading this image? View an alternate.

Working...

Post a comment

Your Information

(Name is required. Email address will not be displayed with the comment.)