Old-School Front-End / 2026 ・ No.07
いにしえのフロントエンド入門
第7回 JavaScript ① ── 基本:明細の合計を計算する関数を書く
公開
AI に頼まず、一行ずつ手で書く「いにしえの」フロントエンド入門。第7回から、いよいよ JavaScript です。
HTML が骨、CSS が服なら、JavaScript は筋肉。最初の仕事は、新井くんが電卓でやっていた明細の合計です。
値の種類、変数、関数、くり返し。そして、お金を扱うときに必ず踏む「足したのに、つながった」罠まで。
画面(DOM)にはまだ触らず、計算だけを担当する関数を作って、自分でテストします。
今回のゴール
新井先輩、合計の計算を AI に書いてもらいました! ……でも、大阪出張の合計が「0147201280014720 円」って出るんです。
古井147兆円の出張か。経理が腰を抜かすぞ。
新井数字が全部つながってるだけに見えますね……。
古井正解だ。JavaScript の「+」には顔が二つある。今日はそこから始めよう。
いまここ:第7回
今回作るのは js/app.js。第1回で決めたフォルダの形の、最後の空き部屋です。中身は、計算だけを担当する4つの関数。
- toAmount ── 入力欄の文字(「14,720」や全角の「14720」)を、金額の数に直す
- calcTotal ── 明細の一覧から合計を出す
- needsReceipt ── 合計が3万円以上なら「領収書が必須」と答える
- formatYen ── 42240 を「42,240 円」の形にする
画面の入力欄から値を取ったり、合計を画面に出したりするのは次回(第8回)。計算と画面を分けておくと、計算だけを取り出して確かめられます。今回はそれを、自分で書いたテストでやってみます。
JavaScript をどこに書くか
CSS を link で読み込んだように、JavaScript は script 要素で読み込みます。問題はどこに書くか。HTML は上から順に読まれるので、置き場所によって「実行される時点」が変わります。三通り試して、そのとき document.body(ページの本体)があるかを調べました。
script の置き場所と、実行される時点(実測)
head にそのまま書くと、まだ本体を読んでいない時点で実行されるので、画面の部品を探しても見つかりません。defer(「後回しにして」という意味)を付けると、HTML を読み終わってから実行されます。今回から、申請フォームの head にこう書きます。
<link rel="stylesheet" href="css/custom.css"> <script src="js/app.js" defer></script>
書いた JavaScript を試す場所は、開発者ツールのコンソールです。Chrome なら Windows は Ctrl+Shift+J、Mac は Cmd+Option+J で開きます。> の後に式を書いて Enter を押すと、すぐに結果が返ってきます。電卓としても使えるので、ぜひ手を動かしながら読んでください。
古井JavaScript は1995年に、たった10日ほどで試作された言語だ。
新井10日! それでこんなに使われてるんですか。
古井だから、急いで作ったなりのクセもある。今日はそのクセとの付き合い方を覚えるんだ。
値の種類
JavaScript の値には種類(型)があります。経費精算で出てくるものを並べました。
値の種類と、経費精算での例
大事なのは、同じ 14720 でも、数(number)と文字(string)は別物だということ。そして、HTML の入力欄から取り出した値は、たとえ type="number" の欄でも、必ず文字で届きます(第3回で見た name=value も、すべて文字でした)。値の種類は typeof で調べられます。
> typeof 14720 < 'number' > typeof '14720' < 'string'
変数:const と let
値に名前を付けて取っておくのが変数です。いまの JavaScript では、二つの書き方を使い分けます。
const RECEIPT_LIMIT = 30000; // const:あとから入れ替えない let total = 0; // let:あとから入れ替える total = total + 14720; // 入れ替えられる RECEIPT_LIMIT = 50000; // const は入れ替えられない → エラー
- まず const ── 入れ替える予定がないものは const。うっかり上書きすると、その場でエラーで教えてくれる
- 必要なときだけ let ── 合計のように、足しながら育てていくものは let
- var は使わない ── 古い書き方。ネットの古い記事や、それを学んだ AI がよく使う。見かけたら let か const に直す
名前は、何が入っているか分かる英語にします。x や data より、total や items。決まった値(3万円)は、RECEIPT_LIMIT のように大文字で書いておくと、「ここは規程の数字」とひと目で分かります。
「+」の二つの顔
冒頭の「147兆円」の正体です。
「+」は、足す? つなげる?(実測)
「+」は、両方が数なら足し算、どちらかが文字ならつなげるになります。入力欄の値は文字なので、そのまま足すと……。
合計を 0 から始めても、0 + '14720' の時点で文字の '014720' になり、あとは延々とつながっていきます。しかも、ややこしいことに「-」は文字を数に直してから引くので、引き算だけは正しく動いてしまいます。だから入力の値は、足す前に必ず数に直す。これが今回いちばんの教訓です。
ほかにも、金額を扱うと出会う罠を集めて、実際に実行してみました。
# Chromium 141 のコンソールで実行(Node.js 22 でも同じ結果) > '14720' + '12800' + '14720' → '147201280014720' > 0 + '14720' + '12800' + '14720' → '0147201280014720' > '14720' - '12800' → 1920 > Number('') → 0 > Number('1,000') → NaN > parseInt('1,000') → 1 > Number('14720') → NaN > '14720'.normalize('NFKC') → '14720' > 0.1 + 0.2 → 0.30000000000000004 > 42240 + NaN → NaN > NaN === NaN → false > typeof NaN → 'number' > typeof '14720' → 'string' > typeof 14720 → 'number' > typeof null → 'object' > typeof undefined → 'undefined' > typeof [] → 'object'
- Number('') が 0 ── 空欄が、黙って 0 円になる。「入力していない」と「0円」は区別したい
- parseInt('1,000') が 1 ── カンマの手前までで読むのをやめる。千円が1円になる
- 全角の数字は NaN ── 日本語の入力では、全角の「14720」がよく入ってくる
- NaN(Not a Number) ── 「数ではない」を表す特別な値。何を足しても NaN になり、合計を丸ごと台なしにする。しかも NaN === NaN は false なので、比べるときは Number.isNaN や Object.is を使う
- 0.1 + 0.2 ── コンピュータは小数をぴったり表せないことがある。お金は整数で扱う(円なら円、ドルならセント)のが鉄則
関数とくり返し
合計の計算に名前を付けて、何度でも使えるようにしたものが関数です。
関数の形
calcTotal が受け取る items は、明細1行を表すオブジェクト(名前付きの値のまとまり)を並べた配列です。
const items = [
{ date: '2026-10-15', item: '新幹線(東京→新大阪)', amount: 14720 },
{ date: '2026-10-15', item: 'ホテル1泊', amount: 12800 },
{ date: '2026-10-16', item: '新幹線(新大阪→東京)', amount: 14720 },
];
items.length // 3(行の数)
items[0].amount // 14720(1行目の金額。数えるのは 0 から)
for (const item of items) は、配列の中身を1つずつ取り出して、同じ処理をくり返す書き方です。実際に1回ごとの total を記録しました。
くり返しのたびに増えていく total(実行結果)
新井AI は items.reduce((a, b) => a + b.amount, 0) って1行で書いてきました。こっちのほうが格好よくないですか?
古井同じことをしている。ただ、いまのお前が「なぜ動くか」を説明できるのは for のほうだろう? 説明できない1行より、説明できる5行だ。reduce は、慣れてからで遅くない。
入力を金額に直す
「+」の罠を避けるために、入力欄の文字を金額に直す関数 toAmount を作ります。全角の空白・数字・カンマが混ざった、いかにも日本の現場らしい入力を通してみました。
toAmount の中で起きていること(実行結果)
文字列には、.normalize() や .trim() のような便利な道具(メソッド)が付いていて、点でつないで順に使えます。最後の /^[0-9]+$/ は「最初から最後まで数字だけ」を表す決まり文句(正規表現)で、詳しくは第9回で扱います。
もう一つの工夫は、返す値で「状態」を伝えることです。
- 数(例:14720)── 金額として読めた
- null ── 空欄だった。0円とは区別する
- NaN ── 金額として読めなかった(「約1万」「-500」「12.5」など)
受け取った側は、null なら「入力してください」、NaN なら「金額は数字で」と、違うメッセージを出し分けられます(第9回の入力チェックで使います)。
app.js の全体
// 経費精算システム:計算の関数(第7回)
// 画面(DOM)にはまだ触らない。「入力 → 計算 → 結果」だけを担当する
const RECEIPT_LIMIT = 30000; // 領収書が必須になる合計金額(円)
/**
* 入力欄の文字を、金額(円の整数)に変える
* 空欄なら null、金額として読めなければ NaN を返す
*/
function toAmount(text) {
const s = String(text)
.normalize('NFKC') // 全角の数字(123)を半角(123)に
.replaceAll(',', '') // 桁区切りのカンマを取る
.trim(); // 前後の空白を取る
if (s === '') return null;
if (!/^[0-9]+$/.test(s)) return NaN; // 数字だけでできているか
return Number(s);
}
/** 明細の配列から、合計金額を計算する */
function calcTotal(items) {
let total = 0;
for (const item of items) {
total += item.amount;
}
return total;
}
/** 領収書が必須か(合計 3万円以上) */
function needsReceipt(total) {
return total >= RECEIPT_LIMIT;
}
/** 金額を「42,240 円」の形にする */
function formatYen(amount) {
return amount.toLocaleString('ja-JP') + ' 円';
}
- 規程の数字は一か所に ── 3万円は RECEIPT_LIMIT に一度だけ書く。規程が変わっても直すのは1行
- 「以上」は >= ── 規程が「3万円以上」なら >=、「3万円を超える」なら >。1文字の違いで、ちょうど3万円の申請の扱いが変わる
- /** … */ の説明 ── 関数の前に、何を受け取り何を返すかを書いておく。エディタが関数にマウスを乗せたときに表示してくれる
- 表示用の形は最後に ── 計算は数のまま行い、「42,240 円」の文字にするのは画面に出す直前だけ。文字にしてから足すと、また「つながる」
手作りのテスト
関数が正しく動くかを、毎回コンソールで手打ちして確かめるのは大変です。そこで、確かめる手順そのものをコードにしておきます。これがテストです。専用の道具もありますが、いにしえ流に、10行ほどの手作りで始めます。
// テスト
let passed = 0, failed = 0;
function check(label, actual, expected) {
if (Object.is(actual, expected)) {
passed++;
console.log('✓ ' + label);
} else {
failed++;
console.error('✗ ' + label + ' → ' + actual + ' (≠ ' + expected + ')');
}
}
const items = [
{ date: '2026-10-15', item: '新幹線(東京→新大阪)', amount: 14720 },
{ date: '2026-10-15', item: 'ホテル1泊', amount: 12800 },
{ date: '2026-10-16', item: '新幹線(新大阪→東京)', amount: 14720 }
];
check('合計', calcTotal(items), 42240);
check('明細が0行', calcTotal([]), 0);
check('3万円以上', needsReceipt(42240), true);
check('ちょうど3万円', needsReceipt(30000), true);
check('29,999円', needsReceipt(29999), false);
check('カンマ付き', toAmount('14,720'), 14720);
check('全角の数字', toAmount('14720'), 14720);
check('空欄', toAmount(''), null);
check('数字以外', toAmount('約1万'), NaN);
check('マイナス', toAmount('-500'), NaN);
check('小数', toAmount('12.5'), NaN);
check('表示の形', formatYen(42240), '42,240 円');
console.log('テスト: ' + (passed + failed) + ' 件中 ' + passed + ' 件合格');
ブラウザで実行した結果です。
✓ 合計 ✓ 明細が0行 ✓ 3万円以上 ✓ ちょうど3万円 ✓ 29,999円 ✓ カンマ付き ✓ 全角の数字 ✓ 空欄 ✓ 数字以外 ✓ マイナス ✓ 小数 ✓ 表示の形 テスト: 12 件中 12 件合格
テストで大事なのは、境目を試すことです。3万円のルールなら、29,999円・30,000円・それ以上の3つ。「ちょうど」の扱いは、仕様書でもコードでも一番まちがえやすいところです。
AI にコードを書いてもらったら、テストも一緒に頼み、そのテストを自分で読むのがおすすめです。「29,999円は false、30,000円は true」のように、テストは仕様を言葉より正確に表します。テストが規程とずれていたら、それはコードより先に直すべき「理解のずれ」です。冒頭の 147兆円も、「合計は 42240」というテストが1行あれば、出荷前に捕まえられました。
結 — 一枚まとめと次回予告
- JavaScript は script 要素に defer を付けて読み込む。試すときは開発者ツールのコンソール。
- 値には種類がある。入力欄の値は必ず文字(string)。typeof で確かめられる。
- 変数は まず const、入れ替えるものだけ let。var は使わない。
- 「+」は 文字が混ざるとつなげる。入力は足す前に数に直す。空欄・全角・カンマ・NaN・小数に気を付け、お金は整数で。
- 計算は 画面から切り離した関数にし、境目を含むテストで確かめる。
新井42,240 円、ちゃんと出ました! ……でも、これってコンソールの中だけですよね。画面の合計の欄は、まだ空っぽです。
古井次回は DOM とイベント。入力したら合計が変わり、ボタンで明細の行が増える。いよいよ画面が動き出すぞ。
参考
- MDN — <script>: The Script element
- MDN — Grammar and types
- MDN — Addition (+)
- MDN — Number() constructor
- MDN — parseInt()
- MDN — NaN
- MDN — String.prototype.normalize()
- MDN — for...of
- MDN — Functions
- Chrome for Developers — Console overview
- Wirfs-Brock & Eich — JavaScript: The First 20 Years (HOPL IV)
ページの写真は、筆者が作ったデモページを2026年10月17日にクラウド上の Chromium(Playwright)で撮影したものです。コンソールの画面は、console.log の出力をページに書き出す手作りの再現で、本物の開発者ツールの画面ではありません。罠の式とテストは Chromium 141 と Node.js 22 で実行し、結果が一致することを確かめました。script の置き場所の結果も同じ Chromium で調べた値です。登場人物は架空の人物です。
いにしえのフロントエンド入門 第7回 — JavaScript ①:基本と合計の関数 / 2026.10