setIntervalとsetTimeoutの罠。

eyecatchImg

setIntervalとsetTimeoutで関数を渡す時に起こる素敵な罠と対処法についてのメモです。

thisの行方。

setInterval( 'func', time )

アニメーションや動作のウェイトなどで活躍するsetIntervalとsetTimeoutが、言うまでもなく繰り返す事が前提なため、いか軽く組めるかが重要になる関数ですが、そうなると必ず通る罠がthisの行方です。setIntervalを例に、関数を組んでいきます。

var constructor = function(){  
    this.loop();  
};  
constructor.prototype = {  
    loop : function(){  
        setInterval( this.func, 1000 );  
    }  
    ,dateNow : function(){  
        return new Date()  
    }  
    ,func : function(){  
        console.log( this.dateNow() );  
    }  
};  

new constructor;

同じ事を繰り返すだけなのでメモリ領域は節約したい所です。インスタンス、もしくはプロトタイプで定義してしまいたいですが、これだとfunc内のthisが参照出来ずに未定義が発生します。なぜか。実はこの場合のthisはグローバル、つまりwindowを参照しているんです。なぜそうなるのか、全く意味不明な訳ですが、答えはこの二つの関数の書き方に明らかな答えがありましたね。

この子たちevalしてた。

setInterval( 'func', time )

そう、なぜかsetIntervalにはリテラルで関数が指定出来ます。この形は明らかにあれですね。

eval( 'func' )

evalさんです。evalは指定された文字列をjavascriptとしてグローバル領域で評価します。つまり、グローバルで評価されたthisはwindowを参照してしまう訳です。

回避するためには。

いくつか回避方法はありますが、やはり引数で渡すのが一番無難かもしれません。

引数で渡す。

setInterval( 'func', time, this )

この方法でfunc 側に引数としてthisを渡す事が出来ます。

var constructor = function(){  
    this.loop();  
};  
constructor.prototype = {  
    loop : function(){  
        setInterval( this.func, 1000, this );  
    }  
    ,dateNow : function(){  
        return new Date()  
    }  
    ,func : function(that){  
        console.log( that.dateNow() );  
    }  
};  

new constructor;

と書く事で解決出来ます。そして少し目的と外れてしまいますが、もう一つ可能な方法があります。

関数内のthisをオブジェクトとして渡してしまう。

var constructor = function(){  
    this.loop();  
};  
constructor.prototype = {  
    loop : function(){  
        var self = this;  
        setInterval(  
            function(){  
                 console.log( self.dateNow() );  
            }, 1000 );  
    }  
    ,dateNow : function(){  
        return new Date()  
    }  
};  

new constructor;

直接動作を埋め込める場合は、this自体をオブジェクトとして変数に格納して渡せば、ちゃんと同スコープ内で参照してくれます。未だに少し違和感を覚えますが…eval仕様です。

なんと言うか、javascriptにもwait関数欲しいところです。