CSE 134B Web Client Languages
  1. Interaction
  2. JavaScript
  3. Statements, variables and flow

Interaction · JavaScript 03

Statements, variables and flow

JavaScript's basic syntax looks like C or Java, and for a few minutes you can read it on that assumption. Then a function returns nothing because of a line break, a variable outlives the block that declared it, and a loop hands you the wrong thing. This page covers the rules underneath that familiar surface: how the language reads your text, how it scopes a variable, what a name can tell a reader, and how it makes decisions and repeats itself.

You already program, so this page does not explain what a loop is. It concentrates on what differs from the languages you know, and a good rule for learning any new language applies: pay most attention to whatever looks odd, because the odd parts are where the language is telling you something about itself. The playgrounds run real JavaScript. Each one states what it prints, so read the code, predict the output, then change something and run it again.

1. How JavaScript reads your text

Case counts

JavaScript is case sensitive, so total and Total are two variables. Built-in names follow a camelCase style, as in document.lastModified and getElementById, and must be typed exactly. This catches people who move between languages on a single line, because HTML tag and attribute names are not case sensitive: onclick, onClick and ONCLICK are the same attribute in an HTML document, while only onclick is the property in script. HTML values are another matter: id="Foo" does not match #foo.

let total = 1; let Total = 2; console.log(total, Total); // logs: 1 2

Whitespace is for people

Between tokens, spaces, tabs and most line breaks mean nothing to the language. Each of these lines does the same thing, and only the last fails, because type of is not the operator typeof: whitespace can separate tokens, not split one.

s = typeof x; s=typeof x; s = typeof x; s = type of x; // SyntaxError

Because whitespace is for people, production tools remove it. Minification strips spaces and comments and shortens local names to reduce what the network has to carry, so write for the next reader and let a build step write for the wire. It is also why the script you read in DevTools on a real site can look like a single impossible line.

Statements and semicolons

A script is a list of statements, and a semicolon ends one. A line break usually can as well. When the parser reaches a line break at a point where the next token could not continue the statement, it inserts a semicolon for you. The rule is called automatic semicolon insertion, and it is why both of these run.

let a = 1; let b = 2 console.log(a + b) // logs: 3

Between two statements on one line, nothing is inserted, so the semicolon is required, which is why removing line breaks from code by hand goes wrong. (A semicolon is also inserted before a closing } and at the end of the script.)

let x = 1; x = x + 1 console.log(x); // throws: SyntaxError

The rule also fails to fire when the next line could continue the statement. A line that starts with [ or ( is read as part of the line above it, and the failure appears somewhere that looks unrelated.

let names = ['ada', 'grace'] [1, 2].forEach(function (n) { console.log(n); }); // throws: TypeError

The parser reads that as a single statement: index into the array with [1, 2], then call forEach on the result. There is no element to call it on, so it throws. Some teams write without semicolons and guard these cases with a linter. These pages write the semicolons, because then the code means what it looks like it means without anyone needing to remember the exceptions.

The most famous trap goes the other way. A few constructs forbid a line break at a certain point. After return, the parser ends the statement right there, so the value on the next line is never returned. (After throw, the line break is a SyntaxError.) The function below has a perfectly good value on the next line, and it returns undefined.

Blocks and comments

Curly braces group statements into a block, which acts as one larger statement wherever the language expects one: a function body, a branch of an if, the body of a loop. For most of JavaScript's life a block grouped statements and did nothing else. Since 2015 it can also create a scope, as the next section shows.

function add(x, y) { let result = x + y; return result; } let x = 12; let y = 0; if (x > 10) { x = 0; y = 10; } console.log(add(x, y)); // logs: 10

Comments come in two forms, // to the end of the line and /* */ around any span. Unlike the source of a compiled program, your comments can reach every visitor: the script you publish is the script they can read, so a comment naming an internal server or a temporary password is a published secret. Removing comments at build time helps performance and security, but it is a backstop. Do not write what you would not publish.

2. Three ways to declare a variable

JavaScript has three declaration keywords because it has a history. var was the only one until the 2015 edition of the language added let and const, and since old code has to keep working, all three remain.

var and let

The important difference is scope. A var belongs to the nearest function, or to the script's top level if there is none, whatever block it was written in. A let belongs to the nearest block. The first playground declares adj inside a block and uses it outside.

Function scope against block scopeTwo side-by-side functions, each containing a block. On the left, var adj is declared in the block and is visible throughout the function, including after the block. On the right, let adj is declared in the block and exists only inside it; using it after the block throws a ReferenceError.var: function scopefunction{ block }var adj = 'logical';console.log(adj);works: adj fills the functionlet and const: block scopefunction{ block }let adj = 'logical';console.log(adj);ReferenceError: adj ended with the block
A var escapes any block but stops at a function. A let or const stops at the block.

Three more differences follow from var's age. It can be declared twice in the same scope, silently, where a second let is a SyntaxError before anything runs. It is also usable before its declaration, because the declaration is hoisted to the top of the function while the assignment stays where it is, so reading it early gives undefined. Reading a let early throws: it exists from the start of the block but cannot be touched until its declaration runs, a stretch known as the temporal dead zone.

console.log(early); // logs: undefined var early = 1; console.log(late); // throws: ReferenceError let late = 2;

The last difference matters most in a browser. At the top level of a classic script, a var becomes a property of the global object, which in a page is window, shared with every other classic script on that page. A top-level let is also global and visible to those scripts, but it does not become a property of window.

var shared = 1; let own = 2; console.log(globalThis.shared, globalThis.own); // logs: 1 undefined

You will read a great deal of var in older code, so you need to know how it behaves. There is no reason to write it in new code.

let and const

A const has the same block scope as let, must be given a value when declared, and cannot be assigned again. How much to use it is where working developers disagree, so here are both positions.

This course's position: a const is a true constant, a value that is never to be reset, such as a tax rate or a retry limit. Anything else is a variable, so it is a let, even if one particular run happens never to reassign it. Used this way, const tells the reader the value is meant to stay fixed, a promise the language enforces only for the binding, and let describes what most names in a program are. These pages usually name constants in UPPER_SNAKE_CASE, so that they stand out wherever they are used.

const MAX_TRIES = 3; let tries = 0; tries = tries + 1; console.log(tries, 'of', MAX_TRIES); // logs: 1 of 3

The common convention: use const by default and let only for a binding that gets reassigned. Its supporters like that a reader who sees const knows that name is not reassigned further down, and that the language enforces it. Linters can require it, and you will meet it in many codebases, including the older demos on this site. It is a reasonable choice held by careful people. When you join a team, follow the team.

// convention: const const items = []; items.push('pear'); console.log(items.length); // logs: 1

That sample shows why the word causes trouble, and it is the reason for this course's position. items changed, and const did not object. What const freezes is the binding between the name and the value. It says nothing about the contents of an object or array, so the name that promises constancy can hold something that changes on every line. Mathias Bynens's note on const sets out the distinction, and from it reaches the other conclusion: use const by default. If you do want the contents fixed, Object.freeze() does that, one level deep. Later assignments to a frozen object are ignored in ordinary scripts and throw in strict mode and in modules. Methods that change it, such as push, throw everywhere.

3. What a name tells you

An identifier starts with a letter, _ or $, followed by any of those or a digit, and it may not be a reserved word. That is all the language requires, so everything else about a name is a choice, and the choices have a culture. $ is legal, so libraries used it as a short function name: Prototype for getElementById, and jQuery, which popularized it, for its whole API. A leading _ often signals "private" by agreement alone. Prefixes like UCSD_temp and wrapper objects like UCSD.temp stand in for the namespaces that browser scripts did not have for most of their history.

Every name also lives in a tension. Short names like x, tmp and el are fast to type and hard to read. Long names like shoppingCartTotal are the reverse. On the web the longer name used to cost bytes as well, but minifiers settled most of that: they shorten local names, while globals and property names usually stay. Write long, and let the build write short where it can.

The rest of this section is this course's view on naming. Two ideas in it matter most for writing JavaScript for the web.

Seeing the type in the name

JavaScript variables have no declared type, and one can hold a string now and a number later. So at the point where you use a variable, nothing in the code tells you what it holds. Look at this and decide whether it is right.

let total = 0; let input = '5'; total = total + input; console.log(total); // logs: 05

Input from a form field is always a string, so + joined the two instead of adding them. Types and coercion, the next page, explains exactly why. The point here is that the mistake is invisible at the line where it happens. Hungarian notation is an old answer to that problem. It puts a hint of the type in the name, so that you can observe the code and know what you are looking at.

let numTotal = 0; let strInput = '5'; numTotal = numTotal + strInput; // a number plus a string: read it again console.log(numTotal); // logs: 05 let boolLikeJS = true; let objConfig = {};

The technique is usually traced to Charles Simonyi, who spread it at Microsoft. Joel Spolsky's essay Making Wrong Code Look Wrong separates the original idea, where a prefix records what kind of thing a variable is, from the data-type prefixes shown here, which he considers far less useful. A weakly typed language is the case for using them anyway. The technique has real costs. Nothing enforces the prefix, so a name can lie after a change. It adds noise to every line. And tools now cover part of the same ground: an editor can show a variable's inferred type when you hover over it, and @ts-check, covered in Programming in the large, warns when a variable given an initial value later gets a different type. Many developers have dropped the prefixes for those reasons. But an editor tooltip helps only the person hovering, and a type hint in the name works in a code review, a slide or a printed page. In a weakly typed language, that remains a real benefit, so weigh it rather than dismissing it.

Where three languages meet

The second idea comes from the job itself. A web developer writes HTML, CSS and JavaScript at the same time, each language has its own naming culture, and the same name often has to cross between them. So -, _ and camelCase are not just style. Each one works differently in each language.

HTML attributes and CSS properties use hyphens, in the style called kebab-case: data-user-name, background-color. In JavaScript a hyphen is subtraction, so user-name means user minus name, and let user-name = 1; does not parse. The platform therefore maps one style onto the other when it exposes markup and CSS to script.

One name crossing from markup and CSS into JavaScriptFive names and how JavaScript reaches each. The attribute data-user-name becomes dataset.userName. The CSS property background-color becomes style.backgroundColor. The attribute class becomes className. A control with the id or name user-name is reached with brackets, elements['user-name']. A control named user_name can be reached with a dot, elements.user_name. A note reads: in JavaScript, user-name means user minus name.HTML and CSSJavaScriptdata-user-namedataset.userNamebackground-colorstyle.backgroundColorclassclassNamename="user-name"elements['user-name']name="user_name"elements.user_nameIn JavaScript, user-name means user minus name.
A hyphenated name needs a mapping or brackets to reach script. An underscore crosses as it is.

Put together on real markup, the crossings look like this.

<form id="signup"> <label for="user-name">Your name</label> <input id="user-name" name="user_name" data-user-role="student"> </form> let form = document.querySelector('#signup'); form.elements['user-name']; // the id has a hyphen, so brackets form.elements.user_name; // the name has an underscore, so a dot works let field = form.elements.user_name; field.dataset.userRole; // data-user-role, mapped to camelCase field.style.backgroundColor = 'canvas'; // background-color, mapped to camelCase field.className = 'wide'; // class, renamed

A few details decide whether a crossing works. Attribute names in HTML are lowercased as the document is parsed, so data-userRole becomes data-userrole and reads back as dataset.userrole, not dataset.userRole. Keep the hyphen in the markup. Some properties were renamed rather than mapped because the attribute name clashed with a keyword in JavaScript and other languages: class became className and for became htmlFor. Custom properties keep their hyphens everywhere, so script reaches --user-color with getPropertyValue('--user-color'). Attributes and mappings in the DOM series goes through these mappings in depth.

A form control's name crosses further still, because it becomes a key in the data sent to the server, and the form's request carries it there unchanged. So the name may also have to suit a server-side language and a database column, where user_name is usually at home and user-name usually is not. That makes the underscore a style that passes through HTML, CSS selectors, JavaScript and most server code without a mapping or brackets. It is worth considering for names you know will travel.

The style can also carry information of its own. JavaScript's built-ins are camelCase, so a function you named myLocalFunc looks just like a platform API when you meet it on a busy line. Is getItems yours or the browser's? A different shape answers that at a glance: my_local_func as snake_case, or a prefix like ucsdGetItems. It goes against the common camelCase habit, and it is a deliberate choice you can make.

None of these conventions matters as much as holding to one. A codebase that follows a style you dislike is easier to work in than one that mixes three styles you like. Choose with those crossings in mind, write the choice down, and stick to it. So that you meet one style at a time, the samples on the rest of these pages use camelCase in JavaScript and hyphens in markup and CSS.

4. Making decisions

if and else behave as they do elsewhere, with one difference covered fully on the next page: the condition does not have to be a boolean, because JavaScript converts whatever you give it. Without braces, only the single statement that follows belongs to the branch, so always write the braces. A stray semicolon is a worse trap. It is an empty statement, and it becomes the entire branch, so the block below it runs every time.

let x = 3; if (x > 10); { console.log('x is bigger than 10'); // logs: x is bigger than 10 }

switch compares one value against a list of cases, and two things surprise people. It compares strictly, as === does, so the string '3' does not match case 3. And it falls through: after a case matches, execution continues into the following cases until it reaches a break. Fall-through is occasionally useful for sharing a branch, and much more often it is a missing break.

let x = 2; switch (x) { case 1: console.log('x is 1'); break; case 2: console.log('x is 2'); // logs: x is 2 case 3: console.log('x is 3'); // logs: x is 3 break; default: console.log('x is not 1, 2 or 3'); }

5. Loops

JavaScript has the three loops you would expect. while checks before each pass, and do/while checks after, so its body always runs at least once. for gathers the setup, check and update into one line. Declare a for loop's counter with let, which scopes it to the loop, which also gives each pass a fresh copy, a detail that matters when closures arrive in Functions and closures.

break leaves the innermost loop or switch. continue ends the current pass and moves on: to the update step in a for loop, and straight to the condition in a while. That difference causes a classic bug. In a while loop that updates its counter at the bottom of the body, a continue skips the update, and the loop never ends.

let x = 0; while (x < 10) { x = x + 1; if (x === 3) { continue; } console.log('x=' + x); // logs: x=1 // logs: x=2 // logs: x=4 // logs: x=5 if (x === 5) { break; } } console.log('Loop done'); // logs: Loop done

An endless loop is not just a program that never finishes. A page's own script runs on the same thread that handles rendering and input, so the page stops responding until the browser offers to kill it. The JavaScript engine and the event loop explains why. That is why the playground below runs your edits only when you press Run, where the others rerun as you type: you can pass through an endless loop on the way to the code you meant. It is also the one playground on this page that does not save your edits. Its frame is sandboxed, so a hang here may not freeze the page around it the way your own page's script would, but it does stop every playground on the page until you reload. It builds a little HTML as a string, the way a lot of real script does.

6. Walking a collection

Two loops look alike and do different things, and the difference is one short word.

for/in walks the keys

for/in visits the enumerable property names of an object, including ones it inherited. It suits an object used as a set of named values. It is a poor fit for arrays: it hands you each index as a string, and it would also visit any extra property someone added. To separate an object's own properties from inherited ones, use Object.hasOwn(object, key), the newer and safer form of the hasOwnProperty() method you will see in older code.

for/of walks the values

for/of visits the values of anything iterable: arrays, strings, Map and Set, and DOM collections. A plain object is not iterable, so for/of on one throws a TypeError. To remember which goes with which: over [ ], use for/of. Over { }, use for/in, or for/of with Object.keys() or Object.entries(), which return arrays.

Here is an exercise for the console of two different browsers: run for (let key in navigator) console.log(key); in Chrome and in Firefox, and compare the lists. Then check a few of those names with Object.hasOwn(navigator, key). The browser's objects differ from one browser to the next, and every name you check is likely to be inherited, because these properties live on Navigator.prototype.

forEach

Arrays also have a forEach method, which calls a function once per item and passes it the item, its index and the whole collection.

let fruit = ['apple', 'pear']; fruit.forEach(function (item, index, all) { console.log(item, 'is', index, 'of', all.length); // logs: apple is 0 of 2 // logs: pear is 1 of 2 });

It is compact, and that is both its strength and its risk. You get the index without tracking it, but you cannot break out, and return ends only that one call. Pick it because it fits the job, not as a matter of style. A DOM collection may not have it, either: the NodeList from querySelectorAll() has forEach, while the HTMLCollection from getElementsByClassName() does not. for/of works on both, and that is a good reason to reach for it first.

7. Summary

ConceptKey points
Reading textCase sensitive, unlike HTML tag and attribute names. Whitespace separates tokens and is otherwise for people; minifiers remove it, and comments, for delivery.
SemicolonsA line break can end a statement, but not when the next line can continue it, and return ends at a line break. Write the semicolons.
varFunction scope, redeclarable, reads as undefined before its declaration, and a property of window at the top level of a classic script. Read it; do not write it.
letBlock scope, reassignable, and unusable before its declaration. This course's choice for variables.
constBlock scope, no reassignment, contents still changeable. This course's choice for true constants, usually in UPPER_SNAKE_CASE. Default for many codebases, so expect to read both.
Type in a nameNothing at the point of use shows a variable's type. Hungarian notation lets you see it, at a cost in noise and with nothing enforcing it.
Names crossing languages- is subtraction in script, so data-user-name maps to dataset.userName. user_name crosses as it is. A distinct local style can separate your names from built-ins. Consistency beats any one choice.
if and switchAlways brace. A stray ; becomes the branch. switch compares strictly and falls through without break.
Loopswhile, do/while, for, break, continue. An endless loop freezes the tab, because script shares a thread with the page.
Walking collectionsfor/in gives keys, inherited ones too. for/of gives values and rejects plain objects. forEach gives item, index and collection, and cannot break.