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

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

ただいまの
回答率

90.76%

  • JavaScript

    15363questions

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

カウントダウンタイマー

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 439

am_765

score 1

 前提・実現したいこと

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

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

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

 該当のソースコード

var indexFunc = {
  init: function() {
    this.param();
    this.countdown();
    this.recalc();
  },

  param: function() {
    this.goal = new Date();
    this.goal.setHours(23);
    this.goal.setMinutes(59);
    this.goal.setSeconds(59);
  },

  countdown: function() {
    var myself = this;
    var now = new Date();
    var rest = myself.goal.getTime() - now.getTime();
    var sec = Math.floor(rest / 1000) % 60,
        min = Math.floor(rest / 1000 / 60) % 60,
        hour = Math.floor(rest / 1000 / 60 / 60) % 24,
        count = [hour, min, sec];
    return count;
  },

  recalc: function() {
    var myself = this;
    var counter = myself.countdown();
    console.log(counter[0] + '時' + counter[1] + '分' + counter[2] + '秒');
    myself.refresh();
  },

  refresh: function() {
    var myself = this;
    setTimeout(myself.recalc, 1000);
  }
};

indexFunc.init();

 試したこと

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

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

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • mts10806

    2018/05/13 05:34

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

    キャンセル

回答 1

checkベストアンサー

+5

最近見なくなった書き方なので、なかなかわかりにくいです。修正自体はrefreshの所で

...(略)...
  refresh: function() {
    var myself = this;
    setTimeout(myself.recalc.bind(myself), 1000);
  }
...(略)...

または

...(略)...
  refresh: function() {
    var myself = this;
    setTimeout(function(){myself.recalc();}, 1000);
  }
...(略)...

と書き直せば、うまくいくでしょう。


まず、次のコードの結果を予測できますか?

var a = {
  x: 1,
  f: function() { console.log(this.x); }
};
a.f();
var g = a.f;
g();

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

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()の前に一行追加することで可能にナルでしょう。

indexFunc.recalc = indexFunc.recalc.bind(indexFunc);
indexFunc.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による変換必須)を使ってモダンなコードにするとどうなるのかを書いておきます。

class indexFunc {
  constructor() {
    this.param();
    this.countdown();
    this.recalc();
  }

  param() {
    this.goal = new Date();
    this.goal.setHours(23);
    this.goal.setMinutes(59);
    this.goal.setSeconds(59);
  }

  countdown() {
    const now = new Date();
    const rest = this.goal.getTime() - now.getTime();
    const [sec, min, hour] = [
      Math.floor(rest / 1000) % 60,
      Math.floor(rest / 1000 / 60) % 60,
      Math.floor(rest / 1000 / 60 / 60) % 24
    ]
    const count = [hour, min, sec];
    return count;
  }

  recalc = () => {
    const counter = this.countdown();
    console.log(`${counter[0]}${counter[1]}${counter[2]}秒`);
    this.refresh();
  };

  refresh() {
    setTimeout(this.recalc, 1000);
  }
}

new indexFunc;

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/05/13 14:22

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

    キャンセル

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

  • ただいまの回答率 90.76%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • JavaScript

    15363questions

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