4日目のゴールと今日やること
4日目のテーマは
「ストップウォッチとカウントダウンの“共通部分”を整理して、きれいな設計にする」
ことです。
ここまでであなたは、
- ストップウォッチ(経過時間)
- カウントダウン(残り時間)
- setInterval と Date.now() を使った時間管理
- 開始 / 停止 / 一時停止 / 再開 / リセット
- ミリ秒表示
- 多重起動防止(状態管理)
を一通り体験しました。
4日目はここから一歩進んで、
- 共通ロジックを「部品」として切り出す
- 時間フォーマット関数を共通化する
- setTimeout を“補助的な演出”に使う
- 「状態 × 条件分岐」をコードとして整理する
という、“中級者らしい整理の仕方”をやっていきます。
時間フォーマットを「共通関数」にする
なぜ共通化するのか
ストップウォッチでもカウントダウンでも、
「ミリ秒 → 分:秒.ミリ秒」の変換は同じです。
同じロジックをあちこちにコピペすると、
- 修正が大変
- どこかだけ仕様がズレる
- バグの原因になる
ので、「時間フォーマット専用の関数」に切り出します。
共通フォーマット関数
function formatTime(ms) {
if (ms < 0) ms = 0;
const milliseconds = ms % 1000;
const totalSeconds = Math.floor(ms / 1000);
const seconds = totalSeconds % 60;
const minutes = Math.floor(totalSeconds / 60);
const mm = String(minutes).padStart(2, "0");
const ss = String(seconds).padStart(2, "0");
const mmm = String(milliseconds).padStart(3, "0");
return `${mm}:${ss}.${mmm}`;
}
JavaScript深掘りポイント
ここでやっていることはシンプルですが、とても重要です。
- 「時間の見た目」はこの関数だけが知っている
- 呼び出し側は「ミリ秒を渡して文字列を受け取る」だけ
- ストップウォッチもカウントダウンも同じ関数を使える
これで、「時間の表示仕様」を一箇所で管理できるようになります。
表示処理も「役割ごと」に分ける
DOM 更新を一箇所にまとめる
時間表示の更新も、関数にしておきます。
const timeDisplay = document.getElementById("time");
function renderTime(ms) {
timeDisplay.textContent = formatTime(ms);
}
JavaScriptストップウォッチ側もカウントダウン側も、
「画面に時間を出したいときは renderTime を呼ぶ」
というルールにしておくと、コードがかなり読みやすくなります。
深掘りポイント
ここまでで、
- 時間の計算(ロジック)
- 時間のフォーマット(見た目の文字列)
- DOM への反映(画面更新)
がきれいに分かれました。
この「役割の分離」が、中級以降のコードではとても大事です。
タイマーの「エンジン部分」を抽象化してみる
共通しているのは「一定間隔で関数を呼ぶこと」
ストップウォッチもカウントダウンも、
本質的にはこうです。
- setInterval で一定間隔ごとに update を呼ぶ
- update の中で「今の時間」を計算する
- 計算結果を表示する
この「setInterval の管理」を、
小さな“エンジン”としてまとめてみます。
シンプルなタイマーエンジン
function createTimerEngine(tickIntervalMs, onTick) {
let timerId = null;
let running = false;
function start() {
if (running) return;
running = true;
timerId = setInterval(() => {
onTick();
}, tickIntervalMs);
}
function stop() {
if (!running) return;
clearInterval(timerId);
timerId = null;
running = false;
}
return { start, stop, isRunning: () => running };
}
JavaScriptこのエンジンは、
- 何ミリ秒ごとに
- どの関数を呼ぶか
だけを知っていて、
「時間の意味(経過か残りか)」は知りません。
深掘りポイント
この設計にすると、
- ストップウォッチ用のエンジン
- カウントダウン用のエンジン
を同じ仕組みで作れます。
「時間の意味」は外側のロジックが決める。
「一定間隔で呼ぶ」はエンジンが担当する。
という分担ができているのがポイントです。
ストップウォッチをエンジンに乗せて書き直す
ストップウォッチ用のロジック
let swStartTime = 0;
let swElapsedBefore = 0;
let swState = "stopped"; // "stopped" | "running" | "paused"
const stopwatchEngine = createTimerEngine(10, () => {
const now = Date.now();
const elapsed = swElapsedBefore + (now - swStartTime);
renderTime(elapsed);
});
JavaScript開始・停止・リセット
function startStopwatch() {
if (swState === "running") return;
if (swState === "stopped") {
swElapsedBefore = 0;
}
swStartTime = Date.now();
stopwatchEngine.start();
swState = "running";
}
function pauseStopwatch() {
if (swState !== "running") return;
const now = Date.now();
swElapsedBefore += now - swStartTime;
stopwatchEngine.stop();
swState = "paused";
}
function resetStopwatch() {
stopwatchEngine.stop();
swStartTime = 0;
swElapsedBefore = 0;
swState = "stopped";
renderTime(0);
}
JavaScript深掘りポイント
ここで注目してほしいのは、
- setInterval / clearInterval は createTimerEngine の中だけ
- ストップウォッチ側は「時間の計算」と「状態管理」だけに集中している
という構造です。
「タイマーの仕組み」と「時間の意味」を分けることで、
コードの見通しが一気によくなります。
カウントダウンも同じエンジンに乗せる
カウントダウン用のロジック
let cdEndTime = 0;
let cdRemainingMs = 0;
let cdState = "stopped"; // "stopped" | "running" | "paused"
const countdownEngine = createTimerEngine(10, () => {
const now = Date.now();
cdRemainingMs = cdEndTime - now;
if (cdRemainingMs <= 0) {
cdRemainingMs = 0;
renderTime(cdRemainingMs);
finishCountdown();
return;
}
renderTime(cdRemainingMs);
});
JavaScript開始・一時停止・再開・リセット
function startCountdown() {
if (cdState !== "stopped") return;
const inputSeconds = Number(secondsInput.value);
if (Number.isNaN(inputSeconds) || inputSeconds <= 0) {
showError("1秒以上を入力してください。");
return;
}
clearError();
cdRemainingMs = inputSeconds * 1000;
cdEndTime = Date.now() + cdRemainingMs;
countdownEngine.start();
cdState = "running";
}
function pauseCountdown() {
if (cdState !== "running") return;
const now = Date.now();
cdRemainingMs = cdEndTime - now;
if (cdRemainingMs < 0) cdRemainingMs = 0;
countdownEngine.stop();
cdState = "paused";
}
function resumeCountdown() {
if (cdState !== "paused") return;
cdEndTime = Date.now() + cdRemainingMs;
countdownEngine.start();
cdState = "running";
}
function resetCountdown() {
countdownEngine.stop();
cdRemainingMs = 0;
cdEndTime = 0;
cdState = "stopped";
renderTime(0);
}
function finishCountdown() {
countdownEngine.stop();
cdState = "stopped";
// ここでアラームやメッセージを出せる
}
JavaScript深掘りポイント
ストップウォッチとカウントダウンで、
- 状態の種類
- 時間の意味(経過 vs 残り)
は違いますが、
- createTimerEngine を使っている
- Date.now() で差分を計算している
- 状態文字列で操作を制御している
という「骨格」は完全に共通です。
setTimeout を“演出”に使ってみる
例:カウントダウン終了時に点滅させる
setTimeout は「単発の遅延」に向いています。
例えば、
カウントダウンが 0 になったときに
表示を数回点滅させる演出を入れてみます。
function blinkDisplay(times) {
if (times <= 0) return;
timeDisplay.classList.toggle("blink");
setTimeout(() => {
blinkDisplay(times - 1);
}, 200);
}
function finishCountdown() {
countdownEngine.stop();
cdState = "stopped";
blinkDisplay(6); // 6回点滅
}
JavaScriptCSS 側で blink クラスを定義しておきます。
.blink {
visibility: hidden;
}
深掘りポイント
ここでのポイントは、
- setInterval は「ずっと繰り返す」
- setTimeout は「一回だけ遅らせる」
- 再帰的に setTimeout を呼ぶことで「回数付きの繰り返し」が作れる
というところです。
時間アプリの“本体ロジック”は setInterval、
“演出や一時的な動き”は setTimeout、
という役割分担ができると、
アプリの表現力がぐっと上がります。
多重起動防止を「エンジン+状態」で二重に守る
エンジン側の防御
createTimerEngine の中で、
running フラグを見て start / stop を制御しています。
function start() {
if (running) return;
running = true;
timerId = setInterval(onTick, tickIntervalMs);
}
function stop() {
if (!running) return;
clearInterval(timerId);
timerId = null;
running = false;
}
JavaScriptこれで「同じエンジンを二重起動」は防げます。
ロジック側の防御
さらに、ストップウォッチやカウントダウン側でも
state を見て操作を制御しています。
function startStopwatch() {
if (swState === "running") return;
// …
}
JavaScript深掘りポイント
多重起動防止を、
- エンジンレベル(running フラグ)
- アプリロジックレベル(state 文字列)
の二段構えでやっているのが、
「中級者らしい設計」です。
今日いちばん深く理解してほしいこと
4日目で一番大事なのは、
- 時間フォーマットを共通化する
- setInterval の管理を「エンジン」として切り出す
- ストップウォッチとカウントダウンは“同じ骨格”で書ける
- setTimeout は「演出」や「一時的な動き」に向いている
- 多重起動防止は「状態管理 × エンジン」で守る
という、“設計の視点”です。
ここまで来ると、
もう「とりあえず動くコード」ではなく、
“自分で育てていけるタイマーアプリ”になっています。
5日目以降は、
この設計をベースにして、
- UI の改善
- 複数タイマーの同時管理
- プリセット時間(25分タイマーなど)
といった方向に広げていけます。

