はじめに
概要
このページではJavaScriptを使ってカウントダウンタイマーを実装します。
※この章はJavaScript「超」入門 第2版
のchapter5-1「カウントダウンタイマー 時間の計算とタイマー」の内容をさらに噛み砕いた解説ページです。chapter4までの内容を理解している前提で進めますので、用語などわからない箇所があれば入門本を振り返りましょう。
※コードはシンプルさを追求し独自のものを用意しているので、本ページの通りに進めてください。
本章の目的
- Dateオブジェクトを理解する
- 時刻の表示方法を理解する
- 1秒ごとに再計算する処理をマスターする
テキスト
残り時間を計算するファンクションを作る
カウントダウンタイマーを作るには未来の時刻から現在の時刻を引いて残りの時間を算出する
というファンクションを書いてあげる必要があります。
次のテンプレートを用意して一つ一つ実装していきましょう。
新規ディレクトリ・ファイルを作成
monigate/javascript/lesson1
∟ index.html
∟ style.css
∟ countdown.js
<html>
<head>
<meta charset="UTF-8">
<title>JS:カウントダウンタイマー</title>
<link rel="stylesheet" href="style.css">
<script src="countdown.js"></script>
</head>
<body>
<header>
<h1>カウントダウンタイマー</h1>
</header>
<main>
<p>本日の残り時間は〇〇秒です</p>
</main>
<footer>
<p>Monigate</p>
</footer>
</body>
</html>
h1 {
text-decoration: underline;
}
'use strict';
このように表示されれば準備完了です。
未来時刻から現在時刻を引く
まず未来時刻から現在時刻を引くというファンクションを作ります。
次のコードを記述しましょう。
'use strict';
function countdown(due){
const now = new Date();
const rest = due.getTime() - now.getTime();
const sec = Math.floor(rest/1000);
return sec;
}
解説
function名をcountdown、要求するパラメーター(引数)をdueと置きました。
次に時刻の計算をするための記述をします。
function countdown(due){
// 定数nowに現在時刻を代入
const now = new Date();
// 定数restに未来時刻-現在時刻の計算結果を代入
const rest = due.getTime() - now.getTime();
}
due.getTime() - now.getTime()
の計算結果は1/1000秒単位になっているので、次の行で秒単位に変換します。(Dateオブジェクトの計算はミリ秒(1/1000秒)単位になります)
// 定数secに残り時間rest(ミリ秒)を秒単位にして代入
const sec = Math.floor(rest/1000);
最後に残り時間(秒)を計算結果として返します。
return sec;
ここまでが`残り時間を計算する`の骨格になる部分です。 JavaScriptに慣れていないうちはファンクション全体を見ると難解に思えるかもしれません。 しかし1行ずつ分解していくと、さほど難しいことはしていないことがわかります。
では次に、実際に未来時刻を設定して残り時間を画面に表示させていきましょう。
未来時刻(ゴール)を設定してコンソールに出力してみよう
ここまで書いたのはファンクションなので、呼び出さなければ何も実行されません。
さっそく呼び出して記述が正しいか確認したいところですが、countdownファンクションを呼び出すためにはパラメータ(引数)としてゴール時間を渡す必要がありました。
そこで、ここでは同日の23時59分59秒をゴールに設定し、パラメータとして渡すことにします。
次のコードをcountdownファンクションの次に追加してください
// 中略
// 以下追記
let goal = new Date();
goal.setHours(23);
goal.setMinutes(59);
goal.setSeconds(59);
console.log(countdown(goal));
では、ブラウザを開きコンソールを確認してみましょう。
今日が終わるまでの時間が秒単位で表示されているはずです。
その数字を60で割れば分単位に、さらに60で割れば時間単位になります。
雑談
実際に作業する時間によって数字が変わってくるので何とも言えませんが、だいたい数千〜数万秒になったかと思います。
1日って、長くても数万秒しかないんですね…このカリキュラムを作りながら1日1日を大切にしたいなと思いました。
Dateオブジェクトを理解しよう
ここでちょっと寄り道をしてDateオブジェクトの理解を深めたいと思います。
そもそも、Dateオブジェクトにはどんなデータが含まれているのでしょう?
次の一行を追加して、Dateオブジェクトの中身を見てみます。
// 中略
let goal = new Date();
// 次の一行を追加
console.log(goal)
// ここまで
goal.setHours(23);
goal.setMinutes(59);
goal.setSeconds(59);
console.log(countdown(goal));
コンソールを確認します。
現在時刻が表示されていますね。
Dateオブジェクトはnew Date()
の記述をすることで初期化されます。
初期化とは現在時刻を記憶した状態にすることを意味します。
つまり
- 「日時を出力」と指示すれば現在日時を出力する
- 「10日後が何日か出力」と指示すれば10日後の日付を出力する
こういった時間の計算を正しく処理できるのはnew Date()の記述によりDateオブジェクトが初期化された結果、現在日時を基準に計算できているから
と言えます。
したがってDateオブジェクトを使用する際はnew Date()
で初期化してから使用することを覚えておきましょう。
おまけ
おまけにもうひとつ。
さらに次のコードを足してコンソールで確認してみてください。
// 中略
let goal = new Date();
// さっき追加した行
console.log(goal)
// ここまで
goal.setHours(23);
goal.setMinutes(59);
goal.setSeconds(59);
// さらに追加
console.log(goal)
// ここまで
console.log(countdown(goal));
コンソール上には
- 現在時刻
- 今日の23時59分59秒の時刻
- 残り時間
の3つが順番に表示されていると思います。
これはつまりJavaScriptが上の行から順番に処理されている
ことを意味します。
これからコードを書くことはもちろん、長いコードが何の処理をしているかを読み解く場面が出てきます。
その際には「上から順番に処理されている」というプログラミングの基本を忘れずに読み解いてください。
残り時間を「○時○分○秒」の形に整える
ここまで
- 残り時間の計算
- Dateオブジェクトの概念
を解説しました。
しかし、今のままでは秒単位の出力しかできません。
コードを次のように書き直し、秒単位の値を「時/分/秒」の形に整形します。
これまでのコード
'use strict';
function countdown(due){
const now = new Date();
const rest = due.getTime() - now.getTime();
const sec = Math.floor(rest/1000);
return sec;
}
let goal = new Date();
goal.setHours(23);
goal.setMinutes(59);
goal.setSeconds(59);
console.log(countdown(goal));
「時/分/秒」の形に整形したコード
'use strict';
function countdown(due){
const now = new Date();
// 以下を編集
const rest = due.getTime() - now.getTime();
const sec = Math.floor(rest/1000) % 60;
const min = Math.floor(rest/1000/60) % 60;
const hours = Math.floor(rest/1000/60/60) % 24;
const count = [hours, min, sec];
return count;
// ここまで
}
let goal = new Date();
goal.setHours(23);
goal.setMinutes(59);
goal.setSeconds(59);
console.log(countdown(goal));
解説
まずsecについて解説します。
// これまでのコード
const sec = Math.floor(rest/1000);
// 「時/分/秒」の形に整形したコード
const sec = Math.floor(rest/1000) % 60;
これまでのコードは残り時間を秒単位で表していました。
例えば、残り時間が10,000秒であれば「10,000」と出力されます。
しかし、今回出力したいのは「時/分/秒」の形にしたときの「秒」です。
そのためには10,000秒を60で割ったときの余りを求める必要があります。
(例)10,000 / 60 = 166(余り40)
つまり、10,000秒のときの「時/分/秒」表記における秒は「40」となります。
これと同じことを「時」と「分」でも行います。
// 「分単位の時間」を60で割ったときの余り = 「時/分/秒」表記における「分」
const min = Math.floor(rest/1000/60) % 60;
// (例)
// 10,000秒/60 = 166分(floorにより小数点以下切り捨て)
// 166 / 60 = 2(余り46)
// ∴ 46分
// 「1時間単位の時間」を24で割ったときの余り = 「時/分/秒」表記における「時」
const hours = Math.floor(rest/1000/60/60) % 24;
// (例)
// 10,000秒/60/60 = 2時間(floorにより小数点以下切り捨て)
// 2 / 24 = 0(余り2)
// ∴ 2時間
これにより10,000秒を「時/分/秒」表記で表すと
2時間46分40秒
となることがわかりました。
また、そのための計算式も書けるようになりました。
最後に、sec、min、hours の値をブラウザに表示させるために、それぞれの値をcountdownファンクションの戻り値として返してあげます。
まとまった情報があるときは配列にして返すと記述がシンプルになります。
// 定数countに配列として定義する
const count = [hours, min, sec];
// 配列で返す
return count;
これで「時/分/秒」の形でデータを返すファンクションの完成です。
あとは返ってきたデータ(配列)をブラウザに反映させれば終わりです。
配列データをブラウザに反映させる
次のコードを追記して配列のデータを文章化しましょう。
// 中略
console.log(countdown(goal));
// 以下追記
const counter = countdown(goal);
const time = `${counter[0]}時間${counter[1]}分${counter[2]}秒`;
console.log(time);
コンソールを確認します。
今日の残り時間が「○時間○分○秒」の形で表示されていればOKです。
※配列は先頭から[0,1,2...]と続きます。
コンソールでの動作確認ができたのでHTMLへ表示します。
まずはHTMLを編集します。
<html>
<head>
<meta charset="UTF-8">
<title>JS:カウントダウンタイマー</title>
<link rel="stylesheet" href="style.css">
<script src="countdown.js"></script>
</head>
<body>
<header>
<h1>カウントダウンタイマー</h1>
</header>
<main>
<!-- 次の一行を編集 -->
<p>本日の残り時間は<span id="timer"></span>です</p>
</main>
<footer>
<p>Monigate</p>
</footer>
</body>
</html>
次に<span id="timer"></span>
内に残り時間が表示されるようにjsファイルを編集します。
// 中略
console.log(time);
// 以下追記
document.getElementById('timer').textContent = time;
ブラウザを確認してみましょう。
・・・ちゃんと表示されません。
では、コンソールを見てみましょう。
textContentプロパティをセットできない、ということが書いてあります。
また、nullという文字に注目します。
nullとは対象がない状態を指します。
参照
「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
問題
これはどういうことでしょうか。このエラーの原因を、少し考えてみてください。
では解説します。
textContent
はgetElementById
で取得した"timer"
というidを持つ要素に対して発動します。
しかし**「取得する対象がない」**と言っているわけです。
考えられる原因は
- id名を間違えている
- 同一ページ内でid名が重複している
- document.getElementByIdでの読込みがうまくいっていない
などが挙げられます。
仮説検証
- id名の誤記はやりがちなケースなので一番初めに疑いましょう。しかし今回は関係ありません。
- id名が重複している場合、先に記述されている要素のみ取得するため、2個目以降の要素にはメソッドが発動されないという状態になります。このとき、コンソールにエラーは表示されません。
- 何らかの理由でhtmlの取得がうまく出来ていないケースが考えられます。
今回のケースでは1,2は関係ないのでjsファイルのdocument〜の行が実行されるタイミングでid="timer"が読み込めていない
ことを疑います。
回答
HTMLのjsファイルの読込位置をbody要素の最下部に変更しましょう。
<html>
<head>
<meta charset="UTF-8">
<title>JS:カウントダウンタイマー</title>
<link rel="stylesheet" href="style.css">
<!-- 次の一行を切り取り -->
<script src="countdown.js"></script>
</head>
<body>
<header>
<h1>カウントダウンタイマー</h1>
</header>
<main>
<p>本日の残り時間は<span id="timer"></span>です</p>
</main>
<footer>
<p>Monigate</p>
</footer>
<!-- ここに移動 -->
</body>
</html>
ブラウザを確認してみてください。
エラーが解消され、残り時間が正しく表示されていると思います。
解説
HTMLは上から順に読み取られます。これまでの記述では、body要素を読み込む前にjsファイルを読み込んでいたため、idを取得せずにjsファイルの処理が終了してしまい、エラーとなっていました。
この他にもjsファイルを読み込むタイミングを指定する方法はあるのですが、まず基本形としてこの形を覚えておいてください。
☕️カリキュラム制作の裏話
実はこの一連のエラー、まったく予期せぬエラーでして、筆者自身「jsファイルはどこに書いてもHTMLが読まれた後に読まれる」ものだと思ってました(笑)せっかく調べたし、エラー対処の例題としてちょうどいいかなと思いカリキュラムに入れてみました。
1秒ごとに再計算する
最後に、1秒ごとに残り時間が変化するように処理を記述しましょう。
これでカウントダウンタイマーの完成です。
まず「再計算」という意味で「recalc」ファンクションを最終行に追加しましょう。(recalcは任意の名前です)
// 最終行に追加
function recalc() {
}
recalcのファンクションが呼び出されるたびに「時/分/秒」の形でブラウザに表示させたいわけなので、recalcファンクション内に処理を記述します。つまり、先ほど書いた処理内容をコピペすればOKです。
// 中略
// ここから
console.log(countdown(goal));
const counter = countdown(goal);
const time = `${counter[0]}時間${counter[1]}分${counter[2]}秒`;
console.log(time);
document.getElementById('timer').textContent = time;
// ここまでを切り取り
function recalc() {
// ここに貼り付け
}
これだけではfunctionを作っただけなので実行されません。
1秒ごとに実行されるように次の記述を追加します。
// 中略
function recalc() {
console.log(countdown(goal));
const counter = countdown(goal);
const time = `${counter[0]}時間${counter[1]}分${counter[2]}秒`
console.log(time);
document.getElementById('timer').textContent = time;
// ※次の一行を忘れずに追加!!
refresh();
}
// ここから
function refresh() {
setTimeout(recalc, 1000);
}
recalc();
// ここまでを追加
解説
新たに"refresh"ファンクション
を定義しました。
これは1秒おきに"recalc"ファンクション
を呼び出して残り時間の処理をさせるための記述です。
また、recalcファンクションの処理が終わったらrefreshファンクションを実行してもらわないと連続性が成り立たなくなります。
recalcファンクションの最終行にrefresh();
を追加し、2つのファンクションを繰り返し行き来させます。
setTimeoutメソッド
setTimeoutは「待ち時間」後にファンクションを一度だけ実行するメソッドです。
function refresh() {
// recalcファンクションを1000ミリ秒(1秒)後に実行
setTimeout(recalc, 1000);
}
以上で、カウントダウンタイマーの完成です。
なぜsetTimeoutメソッド内のファンクションには()をつけないのか
こちらの解説はJS入門本に記載されてますので、そちらを参照してください。
完成コード
次が完成コードです。確認用のconsole.logも必要ないので削除しています。
<html>
<head>
<meta charset="UTF-8">
<title>JS:カウントダウンタイマー</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>カウントダウンタイマー</h1>
</header>
<main>
<!-- 次の一行を編集 -->
<p>本日の残り時間は<span id="timer"></span>です</p>
</main>
<footer>
<p>Monigate</p>
</footer>
<script src="countdown.js"></script>
</body>
</html>
'use strict';
function countdown(due){
const now = new Date();
const rest = due.getTime() - now.getTime();
const sec = Math.floor(rest/1000) % 60;
const min = Math.floor(rest/1000/60) % 60;
const hours = Math.floor(rest/1000/60/60) % 24;
const count = [hours, min, sec];
return count;
}
let goal = new Date();
goal.setHours(23);
goal.setMinutes(59);
goal.setSeconds(59);
function recalc() {
const counter = countdown(goal);
const time = `${counter[0]}時間${counter[1]}分${counter[2]}秒`;
document.getElementById('timer').textContent = time;
refresh();
}
function refresh() {
setTimeout(recalc, 1000);
}
recalc();
※CSSは変更なし
おわりに
次はカウントダウンタイマーの発展系を解説したいと思います。