JavaScript for impatient programmers (beta)
Please support this book: buy it or donate
(Ad, please don’t block.)

6. Syntax

6.1. An overview of JavaScript’s syntax

6.1.1. Basic syntax

Comments:

// single-line comment

/*
Comment with
multiple lines
*/

Primitive (atomic) values:

// Booleans
true
false

// Numbers (JavaScript only has one type for numbers)
-123
1.141

// Strings (JavaScript has no type for characters)
'abc'
"abc"

Checking and logging to the console:

// “Asserting” (checking) the expected result of an expression
// (a method call with 2 parameters).
// Assertions are a Node.js API that is explained in the next chapter.
assert.equal(7 + 1, 8);

// Printing a value to standard out (another method call)
console.log('Hello!');

// Printing an error message to standard error
console.error('Something went wrong!');

Declaring variables:

let x; // declare x (mutable)
x = 3 * 5; // assign a value to x

let y = 3 * 5; // declare and assign

const z = 8; // declare z (immutable)

Control flow statements:

// Conditional statement
if (x < 0) { // is x less than zero?
  x = -x;
}

Ordinary function declarations:

// add1() has the parameters a and b
function add1(a, b) {
  return a + b;
}
// Calling function add1()
assert.equal(add1(5, 2), 7);

Arrow function expressions (used especially for arguments of function or method calls):

// The body of add2 is an expression:
const add2 = (a, b) => a + b;
// Calling function add2()
assert.equal(add2(5, 2), 7);

// The body of add3 is a code block:
const add3 = (a, b) => { return a + b };

Objects:

// Create plain object via object literal
const obj = {
  first: 'Jane', // property
  last: 'Doe', // property
  getFullName() { // property (method)
    return this.first + ' ' + this.last;
  },
};

// Get a property value
assert.equal(obj.first, 'Jane');
// Set a property value
obj.first = 'Janey';

// Call the method
assert.equal(obj.getFullName(), 'Janey Doe');

Arrays (Arrays are also objects):

// Creating an Array via an Array literal
const arr = ['a', 'b', 'c'];

// Get Array element
assert.equal(arr[1], 'b');
// Set Array element
arr[1] = 'β';

6.1.2. Modules

Each module is a single file. Consider, for example, the following two files with modules in them:

file-tools.js
main.js

The module in file-tools.js exports its function isTextFilePath():

export function isTextFilePath(filePath) {
  return str.endsWith('.txt');
}

The module in main.js imports the whole module path and the function isTextFilePath():

// Import whole module as namespace object `path`
import * as path from 'path';
// Import a single export of module file-tools.js
import {isTextFilePath} from './file-tools.js';

The grammatical category of variable names and property names is called identifier.

Identifiers are allowed to have the following characters:

Some words have special meaning in JavaScript and are called reserved. Examples include: if, true, const.

Reserved words can’t be used as variable names:

const if = 123;
  // SyntaxError: Unexpected token if

But they are allowed as names of properties:

> const obj = { if: 123 };
> obj.if
123

6.1.4. Capitalization of names

Lowercase:

Uppercase:

6.1.5. Where to put semicolons?

At the end of a statement:

const x = 123;
func();

But not if that statement ends with a curly brace:

while (false) {
  // ···
} // no semicolon

function func() {
  // ···
} // no semicolon

However, adding a semicolon after such a statement is not a syntax error – it is interpreted as an empty statement:

// Function declaration followed by empty statement:
function func() {
  // ···
};

  Quiz: basic

See quiz app.

6.2. (Advanced)

All remaining sections of this chapter are advanced.

6.3. Identifiers

6.3.1. Valid identifiers (variable names etc.)

First character:

Subsequent characters:

Examples:

const ε = 0.0001;
const строка = '';
let _tmp = 0;
const $foo2 = true;

6.3.2. Reserved words

Reserved words can’t be variable names, but they can be property names. They are:

await break case catch class const continue debugger default delete do else export extends finally for function if import in instanceof let new return static super switch this throw try typeof var void while with yield

The following words are also reserved, but not used in the language, yet:

enum implements package protected interface private public

These words are not technically reserved, but you should avoid them, too, because they effectively are keywords:

Infinity NaN undefined, async

Is is also a good idea to avoid the names of global variables (String, Math, etc.).

6.4. Statement vs. expression

Statement and expression are categories for syntactic constructs. That is, they split JavaScript’s syntax into two kinds of constructs. (In this book – for the sake of simplicity – we pretend that there are only statements and expressions in JavaScript.) How do they differ?

First, statements “do something”. For example, if is a statement:

let myStr;
if (myBool) {
  myStr = 'Yes';
} else {
  myStr = 'No';
}

Second, expressions are evaluated. They produce values. For example, the code between the parentheses is an expression:

let myStr = (myBool ? 'Yes' : 'No');

The operator _ ? _ : _ used between the parentheses is called the ternary operator. It is the expression version of the if statement.

6.4.1. What is allowed where?

The current location within JavaScript source code determines which kind of syntactic constructs you are allowed to use:

However, expressions can be used as statements. Then they are called expression statements. The opposite is not true: when the context requires an expression, you can’t use statements.

The following is an example of a function foo() whose body contains three expression statements:

function foo() {
  3 + 5;
  'hello world';
  bar();
}

The first two expression statements don’t do anything (as their results are ignored). The last expression statement may or may not do something – depending on whether it has side effects.

The following code demonstrates that any expression bar() can be either expression or statement – it depends on the context:

console.log(bar()); // bar() is expression
bar(); // bar() is (expression) statement

6.5. Syntactically ambiguous constructs

JavaScript has several programming constructs that are syntactically ambiguous: They are different depending on whether they are used in statement context or in expression context. This section explores the phenomenon and its consequences.

6.5.1. Function declaration vs. function expression

A function declaration is a statement:

function id(x) {
  return x;
}

An anonymous function expression looks similar, but is an expression and works differently:

const id = function (x) {
  return x;
};

If you give the function expression a name, it now has the same syntax as a function declaration:

const id = function me(x) {
  return x;
};

Disambiguation will have to answer the following question: What happens if I use a named function expression as a statement?

6.5.2. Object literal vs. block

The following is an object literal defining an object:

{
  foo: bar(3, 5)
}

The created object has a single property (field), whose name is foo. Its value is the result of the function call bar(3, 5) (an expression).

But it is also a code block that contains a single line:

6.5.3. Disambiguation

The ambiguities are only a problem is statement context: If the JavaScript parser encounters something ambiguous, it doesn’t know if it’s a plain statement or an expression statement. Therefore, expression statements must not start with:

If an expression starts with either of these tokens, you must put it in parentheses (which creates an expression context) if you want to use it as a statement. Let’s continue with examples.

6.5.4. Example: Immediately-Invoked Function Expression (IIFE)

Prior to ES6, an IIFE (pronounced “iffy”) was a popular way of simulating block scoping.

In the following code, the IIFE in line A is incorrectly interpreted as a statement. That causes a syntax error, because function declarations can’t be invoked immediately. We are using the function eval() to delay the execution of the code. Otherwise, JavaScript would reject the whole piece of code, before running it.

let result;
assert.throws(
  () => eval("function () { result = 'success' }();"), // (A)
  {
    name: 'SyntaxError',
    message: 'Unexpected token (',
  });

The solution is to put the function expression in parentheses, so that JavaScript interprets it as an expression.

let result;
(function f() { result = 'success' })();
assert.equal(result, 'success');

6.5.5. Example: immediate method call

The following code is similar to an IIFE: We create an object via an object literal and immediately call one of its methods.

let result;
assert.throws(
  () => eval("{ m() { result = 'yes' } }.m();"),
  {
    name: 'SyntaxError',
    message: 'Unexpected token {',
  });

The problem is that JavaScript thinks the initial open brace starts a code block (a statement) and not an object literal. Once again, we fix this via parentheses:

let result;
({ m() { result = 'yes' } }.m());
assert.equal(result, 'yes');

6.5.6. Example: destructuring via an object pattern

In the following example, we use object-destructuring to access property .prop of an object:

let p;
assert.throws(
  () => eval('{prop: p} = { prop: 123 };'),
  {
    name: 'SyntaxError',
    message: 'Unexpected token =',
  }
);

The problem is that JavaScript thinks the first open brace starts a code block. We fix it via parens:

let p;
({prop: p} = { prop: 123 });
assert.equal(p, 123);

6.5.7. Example: an arrow function that returns an object literal

You can use an arrow function with an expression body to return an object created via an object literal:

const func = () => ({ prop: 123 });
assert.deepEqual(func(), { prop: 123 });

If you don’t use parentheses, JavaScript thinks the arrow function has a block body:

const func = () => { prop: 123 };
assert.deepEqual(func(), undefined);

6.6. Semicolons

6.6.1. Rule of thumb for semicolons

Each statement is terminated by a semicolon.

const x = 3;
someFunction('abc');
i++;

Except: statements ending with blocks.

function foo() {
  // ···
}
if (y > 0) {
  // ···
}

The following case is slightly tricky:

const func = function () {}; // semicolon!

The whole const declaration (a statement) ends with a semicolon, but inside it, there is a function expression. That is: It’s not the statement per se that ends with a curly brace; it’s the embedded function expression. That’s why there is a semicolon at the end.

6.6.2. Semicolons: control statements

The body of a control statement is itself a statement. For example, this is the syntax of the while loop:

while (condition)
  statement

The body can be a single statement:

while (a > 0) a--;

But blocks are also statements and therefore legal bodies of control statements:

while (a > 0) {
  a--;
}

If you want a loop to have an empty body, your first option is an empty statement (which is just a semicolon):

while (processNextItem() > 0);

Your second option is an empty block:

while (processNextItem() > 0) {}

6.7. Automatic semicolon insertion (ASI)

While I recommend to always write semicolons, most of them are optional in JavaScript. The mechanism that makes this possible is called automatic semicolon insertion (ASI). In a way, it corrects syntax errors.

ASI works as follows. Parsing of a statement continues until there is either:

In other words, ASI can be seen as inserting semicolons at line breaks. The next subsections cover the pitfalls of ASI.

6.7.1. ASI triggered unexpectedly

The good news about ASI is that – if you don’t rely on it and always write semicolons – there is only one pitfall that you need to be aware of. It is that JavaScript forbids line breaks after some tokens. If you do insert a line break, a semicolon will be inserted, too.

The token where this is most practically relevant is return. Consider, for example, the following code:

return
{
  first: 'jane'
};

This code is parsed as:

return;
{
  first: 'jane';
}
;

That is, an empty return statement, followed by a code block, followed by an empty statement.

Why does JavaScript do this? It protects against accidentally returning a value in a line after a return.

6.7.2. ASI unexpectedly not triggered

In some cases, ASI is not triggered when you think it should be. That makes life more complicated for people who don’t like semicolons, because they need to be aware of those cases.

Example 1: Unintended function call.

a = b + c
(d + e).print()

Parsed as:

a = b + c(d + e).print();

Example 2: Unintended division.

a = b
/hi/g.exec(c).map(d)

Parsed as:

a = b / hi / g.exec(c).map(d);

Example 3: Unintended property access.

someFunction()
['ul', 'ol'].map(x => x + x)

Executed as:

const propKey = ('ul','ol');
assert.equal(propKey, 'ol'); // due to comma operator

someFunction()[propKey].map(x => x + x);

Example 4: Unintended function call.

const stringify = function (x) {
  return String(x)
}
`abc`.split('')

Executed as:

const func = function (x) {
  return String(x)
};
const _tmp = func`abc`;
assert.equal(_tmp, 'abc');

const stringify = _tmp.split('');
assert.deepEqual(stringify, ['a', 'b', 'c']);

A function put in front of a template literal (such as `abc`) leads to that function being called (the template literal determines what parameters it gets). More on that in the chapter on tagged templates.

6.8. Semicolons: best practices

I recommend that you always write semicolons:

However, there are also many people who don’t like the added visual clutter of semicolons. If you are one of them: code without them is legal. I recommend that you use tools to help you avoid mistakes. The following are two examples:

6.9. Strict mode

Starting with ECMAScript 5, you can optionally execute JavaScript in a so-called strict mode. In that mode, the language is slightly cleaner: a few quirks don’t exist and more exceptions are thrown.

The default (non-strict) mode is also called sloppy mode.

Note that strict mode is switched on by default inside modules and classes, so you don’t really need to know about it when you write modern JavaScript. In this book, I assume that strict mode is always switched on.

6.9.1. Switching on strict mode

In legacy script files and CommonJS modules, you switch on strict mode for a complete file, by putting the following code in the first line:

'use strict';

The neat thing about this “directive” is that ECMAScript versions before 5 simply ignore it: it’s an expression statement that does nothing.

You can also switch on strict mode for just a single function:

function functionInStrictMode() {
  'use strict';
}

6.9.2. Example: strict mode in action

Let’s look at an example where sloppy mode does something bad that strict mode doesn’t: Changing an unknown variable (that hasn’t been created via let or similar) creates a global variable.

function sloppyFunc() {
  unknownVar1 = 123;
}
sloppyFunc();
// Created global variable `unknownVar1`:
assert.equal(unknownVar1, 123);

Strict mode does it better:

function strictFunc() {
  'use strict';
  unknownVar2 = 123;
}
assert.throws(
  () => strictFunc(),
  {
    name: 'ReferenceError',
    message: 'unknownVar2 is not defined',
  });

  Quiz: advanced

See quiz app.