JavaScriptでストップウォッチを作る
こんにちは。tushiko23です。
JavaScriptを学ぶために、「よしやるぞ!」って意気込んで、書籍やUdemyといった教材でインプットしました。
ですが、実際にストップウォッチや電卓を実装するってなると「あんなにインプットしたのに全然実装のイメージやコードがピンとこない」といったことが起こりました!
せっかくプログラミングを学習するからには、アプリやサービスといった動くものを作ってみたいと思いますよね。
ストップウォッチはシンプルに見えますが、「JavaScriptの基礎が全部詰まっている!」そう感じました。自分のようにコードの意味の理解も深めたいと思う人は多いはずだとも感じ、今回記事にしました。
完成イメージと必要な機能
デモ画像
(1)制御機能
- 時間経過が1秒ごとでわかるようにする
- 経過時間の画面表示は、時間:分:秒:ミリ秒(小数第1位まで)を表示する
- スタートボタンを押すと測定がスタート。
- ストップボタンで測定が一時停止。
- リセットボタンで測定を0に戻す。
(2)制御機能
- スタート・ストップが連続で押されないようにする。
- 測定前は、ストップとリセットボタンが押されないようにする。
- 測定中は、スタートが押されないようにする
- 一時停止中はストップが押されないようにする
- リセットを押したら、測定値が0に戻り、画面上も0に戻る
実装のポイント
完成したコード
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="main.css">
<title>StopWatch App</title>
</head>
<body>
<h1>ストップウォッチ</h1>
<div>
<div class="show_timer">0:0:0:0</div>
<button class="start">スタート</button>
<button class="stop">ストップ</button>
<button class="reset">リセット</button>
</div>
<div>
<script src="main.js"></script>
</div>
</body>
</html>
main.css
html, body {
font-family: 'Roboto mono';
font-size: 16px;
background-color: #1e90ff;
}
h1 {
color: #ffffff;
text-align: center;
font-size: 2rem;
margin: 3rem;
}
.container {
background-color: #ffffff;
width: 540px;
border-radius: 1rem;
margin: 0 auto;
padding: 1.5rem;
}
.time {
color: #1e90ff;
font-size: 3rem;
text-align: center;
padding: 1rem;
border-radius: 1rem;
box-shadow: 0 0 20px rgb(0 139 253 / 0.25);
}
.buttons {
font-size: 2rem;
text-align: center;
}
.start,.stop,.reset,.lap {
font-size: 1.5rem;
border-radius: .3rem;
box-shadow: 0 0 20px rgb(77, 78, 79 / 0.25);
}
// html id属性から要素を取得し、定数に代入
const time = document.querySelector('.time');
const startButton = document.querySelector('.start');
const stopButton = document.querySelector('.stop');
const resetButton = document.querySelector('.reset');
// 変数を定義
let startTime;
let stopTime = 0;
let timeoutID;
function displayTime () {
const elapsedTime = Date.now() - startTime + stopTime;
const h = String(Math.floor(elapsedTime / 3600000));
const m = String(Math.floor(elapsedTime / 60000) % 60);
const s = String(Math.floor(elapsedTime / 1000) % 60);
const ms = String(Math.floor((elapsedTime % 1000) / 100));
time.textContent = `${h}:${m}:${s}:${ms}`;
// 引数2を100にすることで、displayTime関数を0.1秒ごとに実行
timeoutID = setTimeout(displayTime, 100);
}
// 最初のボタン制御
stopButton.disabled = true;
resetButton.disabled = true;
startButton.addEventListener('click', () => {
startButton.disabled = true;
stopButton.disabled = false;
resetButton.disabled = false;
startTime = Date.now();
displayTime();
});
stopButton.addEventListener('click', () => {
startButton.disabled = false;
stopButton.disabled = true;
resetButton.disabled = false;
clearTimeout(timeoutID);
stopTime +=(Date.now() - startTime);
});
resetButton.addEventListener('click', () => {
startButton.disabled = false;
stopButton.disabled = true;
resetButton.disabled = true;
clearTimeout(timeoutID)
time.textContent = '0:0:0:0';
stopTime = 0;
});
JavaScript の基本設計
-
ボタンから要素を取得する動作は、同じ動作をし変更して欲しくないので、定数constとして定義します。
-
計測開始時間・停止時間・タイマーの ID は処理のたびに変化し再代入が必要なため、変数letを使います。
コードのポイント
1. 画面に表示する処理
const elapsedTime = Date.now() - startTime + stopTime;
まず、elapsedTimeという経過した時間には、Date .now()メソッドを使います。
Date.now() は「1970年1月1日からの経過時間を 1ミリ秒単位 で返す」メソッドです。
そこにスタートボタンを押した時に取得したDate.now()メソッドの値の差分で経過した時間を表示しています。
stopTimeを足すのは、一時停止した時に、停止した時に表示されている時間から再開するためです。
const h = String(Math.floor(elapsedTime / 3600000));
const m = String(Math.floor(elapsedTime / 60000) % 60);
const s = String(Math.floor(elapsedTime / 1000) % 60);
const ms = String(Math.floor((elapsedTime % 1000) / 100));
次に、Math.floorメソッドで取得した値の小数点を表示せずに表示する処理を加えます。
そしてここの計算が画面上の表示のミソとなります。
具体的に値を代入した方がわかりやすいのでここでは、elapsedTime= 1500という値用いて説明します。
時間を表す変数(h)で考えると、h = 1500 / 3600000 = 0.00041666... なので、0になります。なぜ3600000なのかというと、1000ms(ミリ秒)なので秒に換算するため、1000で割って、さらに、3600秒=1時間で秒を時間に換算します。
0なのでディスプレイには0が表示されます。
今度は、分を表す変数(m)で考えます。s = (1500 / 60000) % 60 = 0.025 % 60となりこのあまりは0になります。60000はミリ秒を秒に換算し、秒を分に換算しているためです。(1000*60)。最後に % 60しているのは、例えば71分なら、1時間11分になり、60で割ったあまりを表示しているためです。今回の場合は0です。なお、秒も同じ考え方です。
秒を計算すると 1500 / 1000 % 60 = あまりが1となるので、1を表示します
ミリ秒(ms)は、最初に秒で換算した時のあまりとして表示させるために、(elapsedTime % 1000)。をして500になります。ですが、このままでは、0:0:1:500と小数第3位まで表示されるので、/100をして0:0:1:5で小数第1位まで表示させます。
timeoutID = setTimeout(displayTime, 100);
setTimeoutメソッドは引数を2つ取り、1つ目に実行したい関数をかき、2つ目にそれを何ミリ秒間隔で実行したいのか書きます。今回は、時間の測定の値を計算し、html画面を更新する処理を0.1秒ごとに実行します。
-
スタートボタンを押した時の処理
ボタンの制御は、disabledメソッドを使用して、スタートボタン押下時にはストップとリセットを押せないようにします。
startTimeにDate.now();を入れて、displayTimeの関数を起こして、elapsedTimeの常に更新します。 -
ストップボタンを押した時の処理
clearTimeoutメソッドで、displayTime関数で起こしたsetTimeout関数のIDの値をクリアします。これにより、JavaScript内で時間の測定の処理を終了します。
その後、stopTimeに、Date.now()とstartTimeの差分と、事前に取得したstopTime(初期値は0)を足した値をstopTimeに再度代入します。
これで、JavaScriptの測定の処理は一旦終了しカウントが停止されます。画面上では、ストップを押した時間を表示されます。
これにより、再びスタートを押した時に画面上ではストップした時間から開始し、JavaScript側では新たにsetTimeoutメソッドが実行され測定を開始します。 -
リセットボタンを押した時の処理
リセットボタンの処理はシンプルです。ボタンを押されると、clearTimeoutが実行されJavaScriptの測定が一旦停止します。その後画面上では、初期値0:0:0:0が表示され、stopTime値が0に戻ります。
作成の段階でつまったこと
大きく分けると2つあります。
(1)1つ目が、リセットボタンを押して、画面上では0:0:0:0に戻ったはずなのに、再びスタートを押すと、0から始まらず、ストップリセットどちらも押せない状態になった。
(2)2つ目が、一時停止時に例えば、2.6秒でストップボタンを押した時に、次にスタートを押した時に、2.0秒からさかのぼって再開した。
この2点です。スタートを押した時からストップを押した時の経過時間の処理をどう実装するか、それを画面にどう反映させるかが難しかったです。
まず、1つ目の原因ですが、リセットをする処理の時に、
resetButton.addEventListener('click', () => {
startButton.disabled = false;
stopButton.disabled = true;
resetButton.disabled = true;
clearTimeout(timeoutID) // これがないと、リセットを押して再びスタートを押した時に0から始まらない。
time.textContent = '0:0:0:0';
stopTime = 0;
clearTimeout(timeoutID)を記述していなかったために、画面上ではtime.textContent = '0:0:0:0';で、0:0:0:0に戻りましたが、JavaScript側の測定の処理が続いていました。
なので、スタートボタンを押した時に0:0:0:0から始まらず、続いていたJavascriptの測定値が表示されてボタンの制御も効かなくなったということです。
2つ目の原因究明にはかなり時間がかかりました。
私は最初、小数第1位のミリ秒を表示するために Date.now() の値をそのまま %10 で処理していました。
// 誤った処理(桁が落ちる)
const ms = String(Math.floor(elapsedTime % 10));
しかし、この処理だと ミリ秒の桁が正しく表示されません。
実際に Date.now() は 1ミリ秒単位 で現在時刻を返します。
そのため、経過時間 elapsedTime が例えば 2600ms(2.6秒) だった場合、
2600 % 10 = 0
となり、本来「6(0.6秒部分)」が表示されるべきところで 0 に落ちてしまいます。
そこで、以下のように計算方法を変更しました。
// 正しく動作した処理(0.1秒単位で表示できる)
const ms = String(Math.floor((elapsedTime % 1000) / 100));
この式は、1000ms 未満の部分(0〜999ms)を切り出し100ms(0.1秒)単位に丸めるという処理になっています。
実際の結果は次の表の通りです。
| ms | 計算式 | 表示される桁 |
|---|---|---|
| 0〜99 | 0.x秒 → 0.x秒 / 100 = 0.x → 0 | 0 |
| 100〜199 | 0.1秒 | 1 |
| 200〜299 | 0.2秒 | 2 |
| 500ms | 0.5秒 | 5 |
このようにすると、0.1秒刻みの1桁のミリ秒を正確に表示することができるようになります。
作ってみて気づいたことや学んだこと
- 変数の使い方で、
console.log()でデバックしながら、この処理を実行後変数はどうなっているかを自分で仮説を立てて検証することの大切さを学びました。変数が複数あり、処理によって次々に値が変わるため混乱することもありましたが、デバックを粘り強く取り組むことができたのでそこはプラスになりました。 - つまづいたところの2番目は、コードレビューをしたらレビュワーから「この挙動を直してください」という内容だったので自分では気付けない挙動のエラーも修正することができてよかったです。
-
.querySelectorやaddEventListener、.textContentといったメソッドに触れ、基礎的な「DOM操作」を学ぶことができたのもとてもプラスとなりました。 - シンプルなコードですが、実際は「状態管理」「値の更新」「DOM操作」など学ぶことが多かったです。実装よりもまず仕様を文章化し、「JavaScript でどんな処理を行い、画面にどんな変化を起こすのか」を整理してからコメントを書くことの大切さを実感しました。次は、lap機能の実装にも挑戦してみたいと思います!
まとめ:
- まず初めに感じたことは、ただ読んだり動画を見たり、コピペではなく自分の手で動かして理解することで感情が動いて定着するんだと感じました。
- ストップウォッチのようなシンプルなアプリにも JavaScript の基礎が詰まっており、まずは小さなアプリを作ってみることの大切さを痛感しました。
- また学習中、実装中はスクールのメンターに毎日日報と進捗をSlackで報告するようにしました。おかげさまで、続けることができたので感謝です!一人で頑張らない、抱え込まずアウトプットを意識して誰かの役に立つように学習できるようにこれからも続けていきたいと思います!
- この記事が、未経験からエンジニア転職を目指す人や、駆け出しでJavaScriptを使って開発している人、現役のエンジニアの方々の学びになれば幸いです。
- 最後までお読みいただきありがとうございました!

