すこしふしぎ.

VR/HI系院生による技術ブログ.まったりいきましょ.(友人ズとブログリレー中.さぼったら焼肉おごらなきゃいけない)

【coffeescript】関数の即時実行【javascript】

こんばんは.1000chです. 長かったようで短いTA生活も終わり,また日常に戻りつつあります.

今回のTA生活を経て,だんだんとjsが好きになってきました. (というか今まで自分は何もわかっていなかったことを実感させられました(´・ω・`)) あわせて久しぶりにcoffeescriptを触り,そのシンプルさにまた惹かれつつあります. coffeeちゃんかわいいよcoffeeちゃん.

coffeescript触ったのは去年DeNAインターン行ったとき以来なのですが,そのときは 「なんか楽っぽくjsかけるやつ」くらいの認識でした. まーその認識自体はあまり変わってないんですが,なんとなくjsの処理がわかってくると,「coffeescriptが裏でどんなコードを生み出しているのか?」が気になったりしてきます.

というわけで,coffeeちゃん独自の書式が,どのようなjsを生成して意図した挙動を実行しているのかを見てみようと思います.

関数の即時実行

coffeescriptの生成コードを読んでいく上でまずおぼえておかないと行けないのが,jsにおける関数の即時実行です. 即時関数とも言われるやつです.

まぁビギナー的には当然「即時実行?what?」という感じですね.自分にはこちらがわかりやすかったです.

初級者のためのJavaScriptで使う即時関数(function(){...})

通常jsにおける関数はこんな感じの使い方をします

// function式による 関数定義
var func = function(){
   console.log("func called");
};

// ()演算子による 関数実行
func(); // "func called"

これを即時実行で書くとこうなります.

// function式で定義 -> ()演算子で即実行
(function(){
    console.log("func called");
})();

//これもあり
(function(){
    console.log("func called");
}());

// これはダメ
function(){
    console.log("func called");
}();

要するに,

定義と同時に実行される関数

ってことですね.文字通りです.

じゃー関数を即実行するメリットとは何なのか?という話なのですが,これを考えるにはjsにおける変数スコープの概念を押さえる必要があります.

jsにおける変数スコープ

難しいことはなく,おぼえておくべきはこれだけ.

jsにはグローバルスコープと関数スコープしか無い (正確にはevalスコープもあるらしい)

ということ.言い方を変えると, ブロックスコープが存在しない ということです.

この仕様のため,javascriptでは意図しないところでグローバル汚染を引き起こす可能性があります.

たとえば以下の例.

//script1.js
var tmp_val = 1;
console.log(tmp_val); //1

まぁなんてこたないですね.ただ変数を定義してconsole.logしているだけです.

しかし問題は,変数がグローバル変数として定義されてるという点です. 同一スクリプト内では特に問題にならないかも知れませんが,script1.jsの次に,別ファイルscript2.jsが読み込まれ,そこに次のようなコードがあるとしましょう.

//script2.js
if(!tmp_val){
    var tmp_val = "hoge";
}

console.log(tmp_val); // "hoge" でなく 1が出力されてしまう.

script1.js内でtmp_valがグローバルに定義されているため,script2.js内でtmp_valが参照されてしまうんですね. tmp_valのような,よく使いそうな変数名がダブると危ないです.

このようなグローバル汚染を防ぐ為には,ローカルな変数スコープを作る必要があります. そこで利用されるのが関数の即時実行というわけです.script1.jsを以下のように書き換えてみましょう.

//script1.js
(function(){
var tmp_val=1;
console.log(tmp_val);
})()

この状態でscript2.jsを実行すると,"hoge"が出力されます.

これは,script1.js内のtmp_valが即時実行される関数のスコープの中で定義されていることに起因します. tmp_valはグローバルに存在しないため,script2.jsからはtmp_valが参照できず,tmp_val="hoge"が実行されるということですね.

このように,即時関数によってローカルな変数スコープを作成することができるようになります.

即時関数に引数を渡す

即時関数の別メリットとして,こんな使い方もできます

(function(w,$){
console.log(w);
console.log($("a").text());
})(window,jQuery);

即時実行時の引数としてwindowオブジェクトや$オブジェクトを明示的に渡すことで,実行関数内から外部変数を参照しないようにすることができます.上記例では外部スコープでwオブジェクトが定義されていても,即時実行時にwindowオブジェクトを参照できます.

また上記例では,引数のjQueryオブジェクトの代わりにzeptoオブジェクトを渡すことで$オブジェクトの挙動を変更できたりもできますね.

本題:coffeescriptで即時関数

では,coffeescriptで即時関数を利用するにはどうすればよいでしょうか.

その前に,まずはcoffeescriptで定義した関数はどのようにjsに変換されるのかを見てみましょう.

# test.coffee
#いわゆる関数定義
f1 = () ->
    console.log "hoge"

f2 = (text) ->
    cosole.log text

こんなかんじでコンパイルします.

$ coffee -c test.coffee
// Generated by CoffeeScript 1.7.1
(function() {
  var f1, f2;

  f1 = function() {
    return console.log("hoge");
  };

  f2 = function(text) {
    return cosole.log(text);
  };

}).call(this);

おおう...なかなかごてっとしてますね.

最外部の(function(){,,,}).call(this)は,コンテクストをグローバルのthisにした上で内部のfunctionを実行しているようです. そしてその内部でf1,f2を定義しています.各関数内のreturn句は,coffeescriptのデフォルト挙動である「最後に評価された式をreturnする」というやつですね.今回の場合はあまり深く考えないで良いと思います.

ココで注意すべきは,先程みたようにf1,f2は関数スコープの中で定義されているため,外部からは参照できないということです. ...なんとまぁ,デフォルトで即時実行っぽいことしてくれているんですね.

なお,コンパイル時に-b(--bare)オブションをつけると,最外部の(function(){,,,}).call(this)を省くことができます.

$ coffee -cb test.coffee
// Generated by CoffeeScript 1.7.1
var f1, f2;

f1 = function() {
  return console.log("hoge");
};

f2 = function(text) {
  return cosole.log(text);
};

こちらの場合,f1およびf2はグローバルスコープで定義されるため,外部からも参照できますね.

--bareオプションを付けない場合でも,f1,f2をthisのプロパティとして定義することはできるっちゃできます.

#いわゆる関数定義
@f1 = () ->
    console.log "hoge"

@f2 = (text) ->
    cosole.log text
// Generated by CoffeeScript 1.7.1
(function() {
  this.f1 = function() {
    return console.log("hoge");
  };

  this.f2 = function(text) {
    return cosole.log(text);
  };

}).call(this);

でもまぁ,あんま意味は無い,というかせっかくスコープ区切ったのが台無しなのでやめた方が良いと思います.

jsっぽい形で即時実行する

普通にcoffeeでコンパイルするだけでグローバル変数を汚さないコードが出力されることはわかりましたが,先程jsで記述したような関数の即時実行をさせるにはどうすれば良いのでしょう?

ということでググると.こんな情報がでてきました.

The Little Book on CoffeeScript - Idioms

最下段のプライベート変数の項を参考ください.

doキーワードが関数を即時実行する...?

これは試してみる価値がありそうです. 以下のようなコードを書いてみました.

do (window,$) ->
    a = 10
    console.log "hoge"

通常コンパイルしてみます.

// Generated by CoffeeScript 1.7.1
(function() {
  (function(window, $) {
    var a;
    a = 10;
    return console.log("hoge");
  })(window, $);

}).call(this);

call(this)の中で即時実行のパターンが見えますね. このコードだと,call(this)の部分が冗長に見えてしまいます.

そこで--bareオプションでコンパイルするとこうなります.

// Generated by CoffeeScript 1.7.1
(function(window, $) {
  var a;
  a = 10;
  return console.log("hoge");
})(window, $);

これはまさに,生のjsで書いた即時実行のパターンそのものですね!

しかし,即時実行する関数の引数と,実際に与える引数の名前が両方windowなのはどうにかならないか?

と思ってやってみたらすんなり行けました.

do (w = window, $ = jQuery) ->
    a = 10
    console.log "hoge"
    return
// Generated by CoffeeScript 1.7.1
(function(w, $) {
  var a;
  a = 10;
  console.log("hoge");
})(window, jQuery);

おお,見覚えのあるjsになりました!やったね!

まとめ

以上見てきたように,最も馴染み深い形で関数の即時実行をしたい場合,関数定義時にdoキーワードをつけて,--bareオプションでコンパイルするのが最も良さそうです.

coffeescriptは人間が読みにくいjsを吐く」と聞くことがありますが,このコードならスッキリしていてjs側の可読性も高いのではないでしょうか. いずれにせよ,coffeescriptを利用する際は「どのようなコードを出力するか」をきちんと押さえておくのが重要です.

そしてなにより,出力コードをちゃんと読むとjsの勉強になりますねw