すこしふしぎ.

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

【coffeescript】ファットアローでコンテクスト保持した関数定義【javascript】

こんばんは.1000chです.

coffeescriptの機能の中で好きなものに,「ファットアローによる関数定義」があります. 一言で言うと「定義する関数内で現在thisコンテキストを維持」することができるようになる機能です.

javascriptにおけるthisトラップはよくある話ですが,coffeescriptがどのようにこの問題を解決しているのか見ていきます.

javascriptにおける関数定義

javascriptにおける関数定義は,

がありますね.それぞれこんな感じです.

//関数式
var func1 = function(name){
    console.log(name)
}

// 関数宣言
function func2(name){
    console.log(name)
}

// Functionコンストラクタ
var func3 = new Function('name','console.log(name)')

//実行
func1("alice") // alice
func2("bob") // bob
func3("cate") // cate

どれもちゃんと動きますが,基本的には関数式が使われることが多いですね.

coffeescriptに置ける関数宣言

coffeescriptにおける関数宣言は->によって行います.

最も簡単な例を見てみましょう.

# 定義
func1 = -> console.log "hello"

# 呼び出し
func1() # hello

引数を与えるときは() ->です.

# 定義
func2 = (name) -> console.log name

# 呼び出し
func2("alice") # alice

# 引数ある場合は,スペース入れれば()省略可能
func2 "alice" # alice

# 複数行での定義もできる
# ブロックはインデントで表現
func3 = (name) ->
    text = "my name is " + name
    console.log text

さて,このときどのようなコードが生成されているでしょうか.

// Generated by CoffeeScript 1.7.1
// --bareオプションでコンパイル

var func1,func2,func3;

func1 = function() {
  return console.log("hello");
};

func2 = function(name) {
  return console.log(name);
};

func3 = function(name) {
  var text;
  text = "my name is " + name;
  return console.log(text);
};

パターン的には,関数式を用いた関数定義が行われているようですね. 関数を格納する変数名は冒頭で一括定義するようです.

なお,coffeescriptでは明示的にreturnを書かない場合に最後に評価された内容が自動的にreturnされます. 今回の例だとconsole.log()の行がreturnされています. なにもreturnさせたくない場合は,関数定義の最後にreturnとだけ書けばok.

func4 = (name) ->
    text = "my name is " + name
    console.log text
    return

コンパイルするとこんな感じ.

func4 = function(name) {
  var text;
  text = "my name is " + name;
  console.log(text);
};

さらにはデフォルト引数や可変引数も使えます.便利!

func5 = (name = "alice") ->
    console.log name
    return

func6 = (names...) ->
    for name in names
        console.log name
    return

コンパイル

// デフォルト値
func5 = function(name) {
  if (name == null) {
    name = "alice";
  }
  console.log(name);
};

// 可変引数
func6 = function() {
  var name, names, _i, _len;
  names = 1 <= arguments.length ? __slice.call(arguments, 0) : []; //argumentsを配列化
  for (_i = 0, _len = names.length; _i < _len; _i++) {
    name = names[_i];
    console.log(name);
  }
};

javascriptにおけるthisコンテキストの書き換え

javascriptでは,関数の中で関数を定義すると,内側のthisコンテキストがグローバルオブジェクトを指すという仕様があります.

var myObj = {
    func1: function(){
        console.log(this); //thisはmyObj オブジェクト
        (function(){
            console.log(this); // thisはwindow オブジェクト
        })()
    }
}
myObj.func1();

これ,例えばコールバック関数を使って元オブジェクトのプロパティをいじったりするときに不都合がありますね.

そこで通常はこんな感じでコンテキストを引き渡したりします.

var myObj = {
    func1: function(){
        console.log(this); // myObj
        (function(that){
            console.log(that); //myObj
        })(this)
    }
}
myObj.func1();

本題:ファットアローでの関数定義

coffeescriptでは,コンテキストを保持した状態での関数定義が可能です. やり方は簡単で,関数定義時に->ではなく=>(ファットアロー)を使うだけです. これを用いることで,thatにthisを格納する,というような手間をかけずにコンテキスト保持ができます.

myObj =
    func1: () ->
        console.log @
        do () -> #アローで関数定義,即時実行
            console.log @
            return
        do () => #ファットアローで関数定義,即時実行
            console.log @
            return

myObj.func1()

実行結果(chrome

Object {func1: function} 
Window {top: Window, window: Window, location: Location, external: Object, chrome: Object}
Object {func1: function} 

coffeescriptでは@コンパイル時にthisに変換されます. また,関数定義前にdoキーワードを置くことで,その関数を即時実行することができます.

これらを用い,関数内で->を使った即時関数と,=>をつかった即時関数でthisの内容を確認しているのが上記スクリプトになります.

実行結果として,ファットアローでの即時実行ではmyObj自体を参照できていることがわかります.

ファットアロー使った場合の出力コード

さて,ではファットアローを使ったコードはどのようにコンパイルされているのでしょうか. シンプルにこんな感じで試してみましょう.

func1 = () =>
    console.log @
    return

コンパイルするとこうなります.

func1 = (function(_this) {
  return function() {
    console.log(_this);
  };
})(this);

ちょっとぱっと見わかりづらいかも?

まず,即時関数でthisコンテキストを_thisとして渡しています.先程のthatみたいなものですね.

即時関数の内部では無名関数を定義し,これをreturnしています.

ここで定義している無名関数こそがcoffeescriptで書いた部分であり.中では_thisを通じて 外部のthisにアクセスしています.

要するに,やってることは先程のthatthisを格納するパターンと一緒ですね. coffee側で記述しているのは内部の無名関数部分だけですが,コンパイラ_thisを渡す即時関数部分を自動で付け加えてくれるという感覚でしょうか.

まとめ

javascriptcoffeescriptにおける関数定義の方法,コンパイル後のコードの動作を確認しました. また,javascript特有のコンテキスト保持の方法を確認し,coffeesriptではファットアローを使うことで簡単に同じことができることを確認しました.

ファットアローを活用すると,ajaxのコールバック関数でスマートにコンテキスト保持ができるようになります.

# Vue.jsで応用した例
do (window,$,_,Vue) ->
    
    vm = new Vue

        el: "#main"

        data:
            my_list: []

        ready:
           @func1()

        methods:
            func1: ->
                jqxhr = $.ajax("/api/my_list")
                    .done (res) =>
                        @my_list = res.data
                return jqxhr

スマートに書けてコーディングが楽しくなりますね.

裏側で何をしているか,を意識しつつ,楽しいcoffeeライフをすごしましょう!