Old-School Front-End / 2026 ・ No.07

Old-School Front-End

Part 7: JavaScript basics, writing a function that totals the line items

Published

js/app.js on the left and a console on the right. The code holds four functions: toAmount, calcTotal, needsReceipt and formatYen. The console shows 12 passing tests, then calcTotal(items) giving 42240, formatYen giving '¥42,240' and needsReceipt giving true
This part's goal: 1 write four functions that only calculate, and 2 check them with home-made tests. (The author's demo pages, captured in Chromium in the cloud on 17 October 2026. The console screen is a home-made imitation that writes console.log output onto the page, not the real DevTools. The browser frame was drawn in for illustration.)

An "old-school" front-end course: no AI doing it for you, every line typed by hand. Part 7 is where JavaScript finally arrives.
If HTML is the bones and CSS the clothes, JavaScript is the muscle. Its first job is the line-item total Arai has been doing on a calculator.
Kinds of value, variables, functions and loops, plus the trap everyone hits with money: "I added them, and they got glued together".
We won't touch the page (the DOM) yet. We'll write functions that only calculate, and test them ourselves.

1

This part's goal

AraiFurui, I had the AI write the total! …But the Osaka trip comes out as ¥0147201280014720.

FuruiA 147-trillion-yen business trip. Accounts will faint.

AraiIt looks like all the numbers just got stuck together…

FuruiExactly. JavaScript's + has two faces. That's where we start today.

You are here: part 7

1 Setup 2 HTML 3 Forms 4 CSS basics 5 Layout 6 Bootstrap 7 JS basics 8 DOM, events 9 Validation 10 Fetch 11 Sign-in 12 Approvals 13 Charts, print 14 jQuery 15 Finish You are here Setup HTML CSS JavaScript Real app Finish

This time we write js/app.js, the last empty room in the folder layout from part 1. It holds four functions that only calculate.

  • toAmount: turns the text in an input ("14,720", or full-width "14720") into an amount
  • calcTotal: adds up the list of line items
  • needsReceipt: answers "receipt required" when the total is ¥30,000 or more
  • formatYen: turns 42240 into "¥42,240"

Reading values from the page's inputs and showing the total on the page is next time (part 8). Keeping calculation apart from the page means you can lift the calculation out and check it on its own. This time we do that with tests we write ourselves.

2

Where JavaScript goes

Just as CSS is loaded with link, JavaScript is loaded with a script element. The question is where to put it. HTML is read top to bottom, so the position changes when the script runs. I tried three positions and checked whether document.body (the page's body) existed at that moment.

Where the script goes, and when it runs (measured)

the HTML is read top to bottom <script> in head runs before reading ends <head> … <body> <h1> … document.body → null <script defer> runs after reading <head> … <body> <h1> … document.body → body just before </body> runs last <head> … <body> <h1> … document.body → body The vertical line marks when it runs. We use defer in the head

Put plainly in the head, it runs before the body has been read, so it can't find anything on the page. Add defer ("put it off") and it runs once the HTML has been read. From now on, the claim form's head says:

<link rel="stylesheet" href="css/custom.css">
<script src="js/app.js" defer></script>

The place to try out JavaScript is the console in DevTools. In Chrome, press Ctrl+Shift+J on Windows or Cmd+Option+J on Mac. Type an expression after >, press Enter, and the result comes straight back. It works as a calculator too, so do try things as you read.

FuruiJavaScript was prototyped in 1995 in about ten days.

AraiTen days! And now it's everywhere?

FuruiSo it has the quirks of something made in a hurry. Today you learn to live with them.

3

Kinds of value

JavaScript values come in kinds (types). Here are the ones an expense app meets.

Kinds of value, with expense-app examples

number 14720 A number you can do sums with e.g. an amount string '14720' Text, in quotes e.g. every input's value boolean true Either true or false e.g. is a receipt required? null null Deliberately empty e.g. an empty amount field undefined undefined Nothing set yet e.g. a forgotten variable object { amount: 14720 } A bundle of named values an array [ ] lists them 14720 as a number and as a string behave completely differently

The key point: 14720 as a number and 14720 as a string are different things. And a value read from an HTML input is always a string, even from a type="number" field (the name=value pairs we saw in part 3 were all strings too). typeof tells you which kind a value is.

> typeof 14720
< 'number'
> typeof '14720'
< 'string'
4

Variables: const and let

A variable gives a value a name so you can keep it. Modern JavaScript has two ways to declare one.

const RECEIPT_LIMIT = 30000;   // const: never reassigned
let total = 0;                 // let: will be reassigned
total = total + 14720;         // fine
RECEIPT_LIMIT = 50000;         // a const can't be reassigned: error
  • const first: anything you don't plan to reassign is const. Overwrite it by accident and you get an error on the spot
  • let only when needed: for things that grow as you go, like a total
  • no var: the old way. Old articles online, and AI that learned from them, still use it. Change it to let or const when you see it

Use English names that say what's inside: total and items rather than x or data. Write fixed values (¥30,000) in capitals, like RECEIPT_LIMIT, so it's obvious that this is the number from the policy.

5

The two faces of +

Here is where the 147 trillion yen came from.

Does + add or join? (measured)

14720 + 12800 → 27520 adds number + number '14720' + '12800' → '1472012800' joins string + string 0 + '14720' → '014720' joins if either is a string '14720' - '12800' → 1920 subtracts! - converts to numbers + both adds and joins. Inputs are strings: convert first

If both sides are numbers, + adds; if either is a string, it joins. Input values are strings, so adding them as they are…

A console. Adding the string array ['14720', '12800', '14720'] in a for loop as it is gives the string '0147201280014720', and typeof says 'string'. Converting with toAmount first gives 42240
A bug AI often writes: adding input values (strings) as they are joins them instead of adding. (The author's demo pages, captured in Chromium in the cloud on 17 October 2026. The console screen is a home-made imitation that writes console.log output onto the page, not the real DevTools. The browser frame was drawn in for illustration.)

Even starting the total at 0, the moment 0 + '14720' happens it becomes the string '014720', and from then on everything gets glued on. To make it more confusing, - converts strings to numbers before subtracting, so subtraction alone works. So always convert input values to numbers before adding. That's the biggest lesson of this part.

I gathered more traps you meet with money and actually ran them.

A console in two columns: adding strings, Number('') giving 0, Number('1,000') giving NaN, parseInt('1,000') giving 1, full-width digits giving NaN, 0.1 + 0.2 giving 0.30000000000000004, NaN === NaN giving false, typeof null giving 'object' and more, 17 expressions with results
The traps you meet when handling money. Every result comes from actually running it in a browser. (The author's demo pages, captured in Chromium in the cloud on 17 October 2026. The console screen is a home-made imitation that writes console.log output onto the page, not the real DevTools. The browser frame was drawn in for illustration.)
# Run in the Chromium 141 console (Node.js 22 gives the same results)
> '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('') is 0: an empty field quietly becomes ¥0. "Not filled in" and "zero" should be told apart
  • parseInt('1,000') is 1: it stops reading at the comma, so a thousand yen becomes one
  • Full-width digits are NaN: Japanese and Chinese input often produces full-width "14720"
  • NaN (Not a Number): a special value meaning "not a number". Add anything to it and you get NaN, ruining the whole total. And NaN === NaN is false, so compare with Number.isNaN or Object.is
  • 0.1 + 0.2: computers can't always represent decimals exactly. The golden rule: handle money in whole numbers (yen as yen, dollars as cents)
6

Functions and loops

Giving the total calculation a name so it can be reused makes it a function.

The shape of a function

function calcTotal(items) {   let total = 0;   for (const item of items) {     total += item.amount;   }   return total; } name says what it does parameter items what it receives (line items) body adds one row at a time return hands back the result calcTotal(items) 42240 call it and the result comes back Input in, result out. It needn't know about the page

The items that calcTotal receives is an array of objects (bundles of named values), one per line item.

const items = [
  { date: '2026-10-15', item: 'Shinkansen, Tokyo to Osaka', amount: 14720 },
  { date: '2026-10-15', item: 'Hotel, 1 night', amount: 12800 },
  { date: '2026-10-16', item: 'Shinkansen, Osaka to Tokyo', amount: 14720 },
];
items.length       // 3 (the number of rows)
items[0].amount    // 14720 (row 1's amount; counting starts at 0)

for (const item of items) takes the array's contents one at a time and repeats the same work. I recorded total on every round.

total growing on each round (actual run)

round item item.amount total 0 — — 0 1 Shinkansen, Tokyo to Osaka 14720 14720 2 Hotel, 1 night 12800 27520 3 Shinkansen, Osaka to Tokyo 14720 42240 for (const item of items) puts each element in item and repeats the body

AraiThe AI wrote it in one line: items.reduce((a, b) => a + b.amount, 0). Isn't that cooler?

FuruiIt does the same thing. But right now, which one can you explain? The for loop. Five lines you can explain beat one you can't. reduce can wait until you're comfortable.

7

Turning input into an amount

To dodge the + trap, we write toAmount, which turns an input's text into an amount. I fed it the kind of input you really get on a Japanese keyboard, full-width spaces, digits and comma included.

What happens inside toAmount (actual run)

' 14,720 ' full-width spaces, digits, comma .normalize('NFKC') → ' 14,720 ' full-width to normal .replaceAll(',', '') → ' 14720 ' drop the comma .trim() → '14720' trim the spaces /^[0-9]+$/.test(…) → true digits only? Number(…) → 14720 turn into a number Why check digits? Number('0x10') is 16, Number('1e3') is 1000

Strings come with handy tools (methods) such as .normalize() and .trim() that you can chain with dots. The last one, /^[0-9]+$/, is a stock phrase (a regular expression) meaning "digits from start to end"; more on those in part 9.

The other trick is using the return value to report a state.

  • a number (e.g. 14720): it read as an amount
  • null: the field was empty. Kept apart from ¥0
  • NaN: it couldn't be read as an amount ("about 10k", "-500", "12.5" and so on)

The caller can then show different messages: "please fill this in" for null, "use digits for the amount" for NaN. We'll use that in part 9, on validation.

8

The whole of app.js

// Expense System: calculation functions (part 7)
// No touching the page (DOM) yet. Just input -> calculate -> result

const RECEIPT_LIMIT = 30000; // Total (yen) from which a receipt is required

/**
 * Turn the text in an input into an amount (whole yen)
 * Returns null for an empty field, NaN if it can't be read as an amount
 */
function toAmount(text) {
  const s = String(text)
    .normalize('NFKC')     // full-width digits (123) to normal ones (123)
    .replaceAll(',', '')   // remove thousands separators
    .trim();               // remove spaces at both ends
  if (s === '') return null;
  if (!/^[0-9]+$/.test(s)) return NaN; // digits only?
  return Number(s);
}

/** Add up the line items and return the total */
function calcTotal(items) {
  let total = 0;
  for (const item of items) {
    total += item.amount;
  }
  return total;
}

/** Is a receipt required? (total of ¥30,000 or more) */
function needsReceipt(total) {
  return total >= RECEIPT_LIMIT;
}

/** Format an amount as ¥42,240 */
function formatYen(amount) {
  return '¥' + amount.toLocaleString('en-US');
}
  • Policy numbers in one place: ¥30,000 is written once, in RECEIPT_LIMIT. If the policy changes, you edit one line
  • "Or more" is >=: if the policy says "¥30,000 or more", use >=; if it says "over ¥30,000", use >. One character decides what happens to a claim of exactly ¥30,000
  • /** … */ notes: say above each function what it takes and what it returns. Editors show it when you hover over the function
  • Display format last: calculate with numbers, and turn them into "¥42,240" text only just before showing them. Add after turning them into text and you're back to gluing
9

Home-made tests

Typing into the console every time to check the functions gets tiring. So we turn the checking itself into code. That's a test. There are dedicated tools for this, but in the old-school spirit we start with ten home-made lines.

// Tests
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: 'Shinkansen, Tokyo to Osaka', amount: 14720 },
  { date: '2026-10-15', item: 'Hotel, 1 night', amount: 12800 },
  { date: '2026-10-16', item: 'Shinkansen, Osaka to Tokyo', amount: 14720 }
];

check('total', calcTotal(items), 42240);
check('no line items', calcTotal([]), 0);
check('30,000 or more', needsReceipt(42240), true);
check('exactly 30,000', needsReceipt(30000), true);
check('29,999', needsReceipt(29999), false);
check('with commas', toAmount('14,720'), 14720);
check('full-width digits', toAmount('14720'), 14720);
check('empty', toAmount(''), null);
check('not a number', toAmount('約1万'), NaN);
check('negative', toAmount('-500'), NaN);
check('decimal', toAmount('12.5'), NaN);
check('display format', formatYen(42240), '¥42,240');

console.log('Tests: ' + (passed + failed) + ' of ' + passed + ' passed');

The result of running it in the browser:

✓ total
✓ no line items
✓ 30,000 or more
✓ exactly 30,000
✓ 29,999
✓ with commas
✓ full-width digits
✓ empty
✓ not a number
✓ negative
✓ decimal
✓ display format
Tests: 12 of 12 passed

The key to testing is testing the boundaries. For the ¥30,000 rule, that's 29,999, 30,000 and above. The "exactly" case is where both specs and code most often go wrong.

POINT

When an AI writes code for you, ask for tests too, and read them yourself. "29,999 is false, 30,000 is true" states the rule more precisely than words. If a test disagrees with the policy, that's a misunderstanding to fix before the code. The 147 trillion yen in the opening scene would have been caught before release by a single line saying "the total is 42240".

§ — On one page, and what is next

  1. Load JavaScript with a script element with defer. Try things in the DevTools console.
  2. Values have kinds. An input's value is always a string; typeof will tell you.
  3. Variables: const first, let only for what you reassign. No var.
  4. + joins as soon as a string is involved. Convert input to numbers before adding. Watch for empty fields, full-width digits, commas, NaN and decimals; handle money in whole numbers.
  5. Put calculations in functions kept apart from the page, and check them with tests that include the boundaries.

Arai¥42,240, right there! …But that's only in the console, isn't it? The total box on the page is still empty.

FuruiNext time: the DOM and events. Type, and the total changes; press a button, and a line item appears. The page finally comes to life.

References

The page photos show the author's demo pages, captured on 17 October 2026 in Chromium (Playwright) in the cloud. The console screen is a home-made imitation that writes console.log output onto the page, not the real DevTools. The trap expressions and the tests were run in Chromium 141 and Node.js 22, and the results matched. The script-position results were measured in the same Chromium. The characters are fictional.

Old-School Front-End, part 7: JavaScript basics and the total function / 2026.10

← Back to all posts