最近見なくなった書き方なので、なかなかわかりにくいです。修正自体はrefresh
の所で
JavaScript
1...(略)...
2 refresh: function() {
3 var myself = this;
4 setTimeout(myself.recalc.bind(myself), 1000);
5 }
6...(略)...
または
JavaScript
1...(略)...
2 refresh: function() {
3 var myself = this;
4 setTimeout(function(){myself.recalc();}, 1000);
5 }
6...(略)...
と書き直せば、うまくいくでしょう。
まず、次のコードの結果を予測できますか?
JavaScript
1var a = {
2 x: 1,
3 f: function() { console.log(this.x); }
4};
5a.f();
6var g = a.f;
7g();
出力は次のようになります。(環境によってはエラーになる場合もあります)
JavaScriptのthis
は通常のローカル変数にはない特殊な性質をもちます。それは、レキシカルに(文脈で)決定されないと言うことです。言い換えると、定義されている文ではなく、呼び出し側の文によって決定されます。
JavaScriptでメソッド呼出しの形レシーバー.メソッド(引数)
で呼び出した場合、"メソッド"(JavaScriptのメソッドとは関数になっているプロパティのことです)内においてのthis
は"レシーバー"になります。では、単にレシーバー.メソッド
と呼出しではない形で書いた場合はどうなるのかというと、"メソッド"はどのようなプロパティに定義されていたかという情報が欠落したただの関数として扱われることになります。"メソッド"内のthis
が何になるのかは、どのようにそれを呼び出すのかによって変わり、呼び出し側に依存するようになると言うことです。
上のコードではvar g = a.f;
と呼出しではない形で書いてしまっています。この時点でa
のプロパティとして定義されていた関数(つまり、a
のメソッド)であるという情報は欠落しており、g
にはa
とは全く無関係な関数があることになります。そのため、g()
と書いても、a
の事など全く知らないので、this
がa
になることはありません。では、this
が何になるのかは実行環境(ブラウザ、Node.js、モジュール等)や厳格モード(strict mode)の有り無しによって異なります。参考: this - JavaScript | MDN
これがいわゆる「this問題」というもので、JavaScriptがわかりにくい、難しい、初心者には向いていないと言われる理由の一つです。仕組みさえ理解していればそれほど難しくないのですが…。
さて、この「this問題」を解決する方法は色々あります。ただ、適材適所に対応しなければなりません。
1. this
を別の変数に代入する。(ES2015+では非推奨)
コードでvar myself = this;
としている部分がありますので、この方法をたかってみたのだと思います。「thisで問題がおきたら、別の変数に代入しておけばいい」などというものをどこで読んだか、誰かに聞いたのでしょう。
この方法はクロージャーでthis
を使いたい場合のみ有効な方法です。先ほど述べたとおりthis
はレキシカルに決定されません。つまりは、クロージャーの対象外になっていると言うことです。関数の中のthis
と外のthis
は同じとは限らないとなります。そこで、this
をあらかじめ別のローカル変数に代入して、その変数を関数内で使うようにします。その別のローカル変数はクロージャーの大将がですので、別の何かに変わったりはしないと言うことです。
今回のコードではクロージャーを使っていません。ですので、この対策では対策にはなりません。this
がクロージャーの対象にならないという問題ではないからです。
なお、ES2015+ではこの方法は非推奨です。後述のアロー関数を使用してください。
2. クロージャーとなる関数にアロー関数を使用する。(ES2015+)
ES2015からアロー関数(=>
)が追加されました。このアロー関数で定義した関数ではthis
もレキシカルに決定され、クロージャーの対象になります。つまり、1.の方法の代替であり、よりスマートに解決できるようになります。
これは1.と同じくクロージャーでの問題しか解決できません。ですので、アロー関数を使っても、今回のコードの問題を解決することは出来ません。
なお、ES2015+に対応していないIE(Internet Explorer)等のクラシックなブラウザでは動作しません。IEを捨てるか、BabelでES5に変換する必要があるでしょう。実際に、Babelで変換すると1.の対策をしたコードに置き換わることになります。
3. bind()
でthis
が何かを固定する。
this
は呼び出し依存と言いましたが、呼び出しに依存しないようにする方法があります。それがbind()
を使ってthis
が何であるのかを固定(束縛)することです。
修正したmyself.recalc.bind(myself)
をみてみましょう。myself.recalc
だけではただの関数であり、この時点でmyself
の情報は欠落しています。ですので、これだけではthis
がmyself
になることはなく、エラーになってしまうと言うのが今回のコードの問題の原因です。そこで、.bind(myself)
とうしろにつけました。これは何を意味するのか言うと、このあとどんな呼び出しになろうがthis
をそこでのmyself
になるように固定したと言うことです。指定時間後にsetTimeout
な内部で引数になっている関数が呼び出されますが、myself.recalc.bind(myself)
としている場合は、どんなときでも、this
はmyself
になるため、問題は起きないということです。
※ bind()
を付けなかった場合、this
が何になるのかは実行環境(ブラウザ、Node.js、モジュール等)や厳格モード(strict mode)の有り無しによって異なります。参考: WindowOrWorkerGlobalScope.setTimeout() - Web API インターフェイス | MDN
なお、bind()
によるthis
の固定をあらかじめ行っていく方法もあります。何度も呼び出す物はこちらの方が速度面で有利な場合があります。今回のコードでは、下記のようにindexFunc.init()
の前に一行追加することで可能にナルでしょう。
JavaScript
1indexFunc.recalc = indexFunc.recalc.bind(indexFunc);
2indexFunc.init();
4. 関数式で囲む。(ただし、関数式内部でthis
は使用できないため、場合によっては1.と併用が必要)
メソッド呼び出しの形を維持できれば問題は起きません。myself.recalc()
とい()
がある形になれば良いのです。しかし、これをそのまま書いてしまってはそこで実行されてしまいます。そこで関数式(function(){...}
で関数をその場で定義する式)で囲む、つまり、function(){myself.recalc();}
書いてしまえばいいということです。この関数式が呼び出されて実行された場合、その中身が実行されます。そして、その中身はメソッド呼び出しの形を維持していますので、this
はただしくmyself
になっています。
おっと待ってください。実はこの方法、this
をレシーバーにしたい場合は、1.と併用しなければうまくいきません。つまり、function(){this.recalc();}
と書いても1.に書いてある理由でうまく動作しないということです。どうしてもthis.recalc();
という形で書きたい場合は次に書く方法を使う必要があります。
5. アロー関数で囲む。(ES2015+)
this
もレキシカルに決定したい場合は、関数式の代わりにアロー関数を使うしかありません。つまり、()=>{this.recalc();}
のように書く必要があると言うことです。
6. クラスフィールドでアロー関数を使う。(ES2019+予定)
クラスフィールド(Class field declarations)とアロー関数を使うという方法があります。ES2015+のクラス構文を使った場合にのみ使える方法ですので、今回のコードは大幅に書き換えないと難しいでしょう。
参考例: Handling Events - React
この機能は2018年5月13日現在仕様候補(stage 3)です。**JavaScriptの仕様として正式に採用されている物ではありません。**ただし、Babelでの変換は既に可能になっていますので、Babelの使用を前提にすれば、使用できない訳ではありません。
7. this
を別途指定する。
Array.prototype.map()
のように呼び出す時のthis
を別途指定できる関数もあります。しかし、全ての関数で指定できるとは限りません。setTimeout()
はそのような指定はできないため、今回の方法には使えません。
以上になります。最後に、ES2018+stage3(Babelによる変換必須)を使ってモダンなコードにするとどうなるのかを書いておきます。
JavaScript
1class indexFunc {
2 constructor() {
3 this.param();
4 this.countdown();
5 this.recalc();
6 }
7
8 param() {
9 this.goal = new Date();
10 this.goal.setHours(23);
11 this.goal.setMinutes(59);
12 this.goal.setSeconds(59);
13 }
14
15 countdown() {
16 const now = new Date();
17 const rest = this.goal.getTime() - now.getTime();
18 const [sec, min, hour] = [
19 Math.floor(rest / 1000) % 60,
20 Math.floor(rest / 1000 / 60) % 60,
21 Math.floor(rest / 1000 / 60 / 60) % 24
22 ]
23 const count = [hour, min, sec];
24 return count;
25 }
26
27 recalc = () => {
28 const counter = this.countdown();
29 console.log(`${counter[0]}時${counter[1]}分${counter[2]}秒`);
30 this.refresh();
31 };
32
33 refresh() {
34 setTimeout(this.recalc, 1000);
35 }
36}
37
38new indexFunc;