質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Q&A

解決済

1回答

2095閲覧

カウントダウンタイマー

am_765

総合スコア7

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

0グッド

0クリップ

投稿2018/05/12 18:22

前提・実現したいこと

JavaScriptで一日の残り時間を表示するカウントダウンタイマーを作成しています。
1秒毎のsetTimeout関数が上手く作動せず悩んでいます。
アドバイスいただければ幸いです。
よろしくお願いします。

発生している問題・エラーメッセージ

Uncaught TypeError: myself.countdown is not a function at recalc

該当のソースコード

JavaScript

1var indexFunc = { 2 init: function() { 3 this.param(); 4 this.countdown(); 5 this.recalc(); 6 }, 7 8 param: function() { 9 this.goal = new Date(); 10 this.goal.setHours(23); 11 this.goal.setMinutes(59); 12 this.goal.setSeconds(59); 13 }, 14 15 countdown: function() { 16 var myself = this; 17 var now = new Date(); 18 var rest = myself.goal.getTime() - now.getTime(); 19 var sec = Math.floor(rest / 1000) % 60, 20 min = Math.floor(rest / 1000 / 60) % 60, 21 hour = Math.floor(rest / 1000 / 60 / 60) % 24, 22 count = [hour, min, sec]; 23 return count; 24 }, 25 26 recalc: function() { 27 var myself = this; 28 var counter = myself.countdown(); 29 console.log(counter[0] + '時' + counter[1] + '分' + counter[2] + '秒'); 30 myself.refresh(); 31 }, 32 33 refresh: function() { 34 var myself = this; 35 setTimeout(myself.recalc, 1000); 36 } 37}; 38 39indexFunc.init();

試したこと

オブジェクトリテラルを使用しない記述方法だと成功しました。

補足情報(FW/ツールのバージョンなど)

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

m.ts10806

2018/05/12 20:34

エラーメッセージで調べてみたこと、やってみたことはありますか?
guest

回答1

0

ベストアンサー

最近見なくなった書き方なので、なかなかわかりにくいです。修正自体は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();

出力は次のようになります。(環境によってはエラーになる場合もあります)

1 undefined

JavaScriptのthisは通常のローカル変数にはない特殊な性質をもちます。それは、レキシカルに(文脈で)決定されないと言うことです。言い換えると、定義されている文ではなく、呼び出し側の文によって決定されます。

JavaScriptでメソッド呼出しの形レシーバー.メソッド(引数)で呼び出した場合、"メソッド"(JavaScriptのメソッドとは関数になっているプロパティのことです)内においてのthisは"レシーバー"になります。では、単にレシーバー.メソッドと呼出しではない形で書いた場合はどうなるのかというと、"メソッド"はどのようなプロパティに定義されていたかという情報が欠落したただの関数として扱われることになります。"メソッド"内のthisが何になるのかは、どのようにそれを呼び出すのかによって変わり、呼び出し側に依存するようになると言うことです。

上のコードではvar g = a.f;と呼出しではない形で書いてしまっています。この時点でaのプロパティとして定義されていた関数(つまり、aのメソッド)であるという情報は欠落しており、gにはaとは全く無関係な関数があることになります。そのため、g()と書いても、aの事など全く知らないので、thisaになることはありません。では、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の情報は欠落しています。ですので、これだけではthismyselfになることはなく、エラーになってしまうと言うのが今回のコードの問題の原因です。そこで、.bind(myself)とうしろにつけました。これは何を意味するのか言うと、このあとどんな呼び出しになろうがthisをそこでのmyselfになるように固定したと言うことです。指定時間後にsetTimeoutな内部で引数になっている関数が呼び出されますが、myself.recalc.bind(myself)としている場合は、どんなときでも、thismyselfになるため、問題は起きないということです。

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;

投稿2018/05/12 23:20

編集2018/05/12 23:21
raccy

総合スコア21735

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

am_765

2018/05/13 05:22

ご回答ありがとうございます。 また詳細な解説もありがとうございます、とても勉強になります。 「thisで問題がおきたら、別の変数に代入しておけばいい」 ご指摘いただいた通り、正にこの考え方でした。 thisの挙動がここまで奥が深いものだとは思いませんでした、今回のものを参考に勉強を続けたいです。ES2015での記述、クロージャーについても同様に学んでいこうと思います。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問