Please support this book: buy it or donate

6. Syntax



6.1. An overview of JavaScript’s syntax

6.1.1. Basic syntax

Comments:

  1. // single-line comment
  2. /*
  3. Comment with
  4. multiple lines
  5. */

Primitive (atomic) values:

  1. // Booleans
  2. true
  3. false
  4. // Numbers (JavaScript only has a single type for numbers)
  5. -123
  6. 1.141
  7. // Strings (JavaScript has no type for characters)
  8. 'abc'
  9. "abc"

An assertion describes what the result of a computation is expected to look like and throws an exception if those expectations aren’t correct. For example, the following assertion states that the result of the computation 7 plus 1 must be 8:

  1. assert.equal(7 + 1, 8);

assert.equal() is a method call (the object is assert, the method is .equal()) with two arguments: the actual result and the expected result. It is part of a Node.js assertion API that is explained later in this book.

Logging to the console of a browser or Node.js:

  1. // Printing a value to standard out (another method call)
  2. console.log('Hello!');
  3. // Printing error information to standard error
  4. console.error('Something went wrong!');

Operators:

  1. // Operators for booleans
  2. assert.equal(true && false, false); // And
  3. assert.equal(true || false, true); // Or
  4. // Operators for numbers
  5. assert.equal(3 + 4, 7);
  6. assert.equal(5 - 1, 4);
  7. assert.equal(3 * 4, 12);
  8. assert.equal(9 / 3, 3);
  9. // Operators for strings
  10. assert.equal('a' + 'b', 'ab');
  11. assert.equal('I see ' + 3 + ' monkeys', 'I see 3 monkeys');
  12. // Comparison operators
  13. assert.equal(3 < 4, true);
  14. assert.equal(3 <= 4, true);
  15. assert.equal('abc' === 'abc', true);
  16. assert.equal('abc' !== 'def', true);

Declaring variables:

  1. let x; // declaring x (mutable)
  2. x = 3 * 5; // assign a value to x
  3. let y = 3 * 5; // declaring and assigning
  4. const z = 8; // declaring z (immutable)

Control flow statements:

  1. // Conditional statement
  2. if (x < 0) { // is x less than zero?
  3. x = -x;
  4. }

Ordinary function declarations:

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

Arrow function expressions (used especially as arguments of function calls and method calls):

  1. const add2 = (a, b) => a + b;
  2. // Calling function add2()
  3. assert.equal(add2(5, 2), 7);
  4. const add3 = (a, b) => { return a + b };

The previous code contains the following arrow functions (the terms expression and statement are explained later in this chapter):

  1. // An arrow function whose body is an expression
  2. (a, b) => a + b
  3. // An arrow function whose body is a code block
  4. (a, b) => { return a + b }

Objects:

  1. // Creating a plain object via an object literal
  2. const obj = {
  3. first: 'Jane', // property
  4. last: 'Doe', // property
  5. getFullName() { // property (method)
  6. return this.first + ' ' + this.last;
  7. },
  8. };
  9. // Getting a property value
  10. assert.equal(obj.first, 'Jane');
  11. // Setting a property value
  12. obj.first = 'Janey';
  13. // Calling the method
  14. assert.equal(obj.getFullName(), 'Janey Doe');

Arrays (Arrays are also objects):

  1. // Creating an Array via an Array literal
  2. const arr = ['a', 'b', 'c'];
  3. // Getting an Array element
  4. assert.equal(arr[1], 'b');
  5. // Setting an Array element
  6. arr[1] = 'β';

6.1.2. Modules

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

  1. file-tools.js
  2. main.js

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

  1. export function isTextFilePath(filePath) {
  2. return filePath.endsWith('.txt');
  3. }

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

  1. // Import whole module as namespace object `path`
  2. import * as path from 'path';
  3. // Import a single export of module file-tools.js
  4. 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:

  • Unicode letters: AZ, az (etc.)
  • $, _
  • Unicode digits: 09 (etc.)
    • Variable names can’t start with a digit
      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:

  1. const if = 123;
  2. // SyntaxError: Unexpected token if

But they are allowed as names of properties:

  1. > const obj = { if: 123 };
  2. > obj.if
  3. 123

6.1.4. Casing styles

Common casing styles for concatenating words are:

  • Camel case: threeConcatenatedWords
  • Underscore case (also called snake case): three_concatenated_words
  • Dash case (also called kebab case): three-concatenated-words

6.1.5. Capitalization of names

In general, JavaScript uses camel case, except for constants.

Lowercase:

  • Functions, variables: myFunction
  • Methods: obj.myMethod
  • CSS:

    • CSS entity: special-class
    • Corresponding JavaScript variable: specialClass
      Uppercase:
  • Classes: MyClass

  • Constants: MY_CONSTANT
    • Constants are also often written in camel case: myConstant

6.1.6. Where to put semicolons?

At the end of a statement:

  1. const x = 123;
  2. func();

But not if that statement ends with a curly brace:

  1. while (false) {
  2. // ···
  3. } // no semicolon
  4. function func() {
  5. // ···
  6. } // no semicolon

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

  1. // Function declaration followed by empty statement:
  2. function func() {
  3. // ···
  4. };

6.2. (Advanced)

All remaining sections of this chapter are advanced.

6.3. Identifiers

6.3.1. Valid identifiers (variable names etc.)

First character:

  • Unicode letter (including accented characters such as é and ü and characters from non-latin alphabets, such as α)
  • $
  • _
    Subsequent characters:

  • Legal first characters

  • Unicode digits (including Eastern Arabic numerals)
  • Some other Unicode marks and punctuations
    Examples:
  1. const ε = 0.0001;
  2. const строка = '';
  3. let _tmp = 0;
  4. const $foo2 = true;

6.3.2. Reserved words

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

All JavaScript keywords are reserved words:

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 tokens are also keywords, but currently not used in the language:

enum implements package protected interface private public

The following literals are reserved words:

true false null

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

Infinity NaN undefined async

You shouldn’t use the names of global variables (String, Math, etc.) for your own variables and parameters, either.

6.4. Statement vs. expression

In this section, we explore how JavaScript distinguishes two kinds of syntactic constructs: statements and expressions. Afterwards, we’ll see that that can cause problems, because the same syntax can mean different things, depending on where it is used.

6.4.1. Statements

A statement is a piece of code that can be executed and performs some kind of action. For example, if is a statement:

  1. let myStr;
  2. if (myBool) {
  3. myStr = 'Yes';
  4. } else {
  5. myStr = 'No';
  6. }

One more example of a statement: a function declaration.

  1. function twice(x) {
  2. return x + x;
  3. }

6.4.2. Expressions

An expression is a piece of code that can be evaluated to produce a value. For example, the code between the parentheses is an expression:

  1. 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.

Let’s look at more examples of expressions. We enter expressions and the REPL evaluates them for us:

  1. > 'ab' + 'cd'
  2. 'abcd'
  3. > Number('123')
  4. 123
  5. > true || false
  6. true

6.4.3. What is allowed where?

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

  • The body of a function must be a sequence of statements:
  1. function max(x, y) {
  2. if (x > y) {
  3. return x;
  4. } else {
  5. return y;
  6. }
  7. }
  • The arguments of a function call or a method call must be expressions:
  1. console.log('ab' + 'cd', Number('123'));

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 code demonstrates that any expression bar() can be either expression or statement – it depends on the context:

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

6.5. Ambiguous syntax

JavaScript has several programming constructs that are syntactically ambiguous: The same syntax is interpreted differently, depending on whether it is used in statement context or in expression context. This section explores the phenomenon and the pitfalls it causes.

6.5.1. Same syntax: function declaration and function expression

A function declaration is a statement:

  1. function id(x) {
  2. return x;
  3. }

A function expression is an expression (right-hand side of =):

  1. const id = function me(x) {
  2. return x;
  3. };

6.5.2. Same syntax: object literal and block

In the following code, {} is an object literal: an expression that creates an empty object.

  1. const obj = {};

This is an empty code block (a statement):

  1. {
  2. }

6.5.3. Disambiguation

The ambiguities are only a problem in statement context: If the JavaScript parser encounters ambiguous syntax, it doesn’t know if it’s a plain statement or an expression statement. For example:

  • If a statement starts with function: Is it a function declaration or a function expression?
  • If a statement starts with {: Is it an object literal or a code block?
    To resolve the ambiguity, statements starting with function or { are never interpreted as expressions. If you want an expression statement to start with either one of these tokens, you must wrap it in parentheses:
  1. (function (x) { console.log(x) })('abc');
  2. // Output:
  3. // 'abc'

In this code:

  • We first create a function, via a function expression:
  1. function (x) { console.log(x) }
  • Then we invoke that function: ('abc')

    1 is only interpreted as an expression, because we wrap it in parentheses. If we didn’t, we would get a syntax error, because then JavaScript expects a function declaration and complains about the missing function name. Additionally, you can’t put a function call immediately after a function declaration.

Later in this book, we’ll see more examples of pitfalls caused by syntactic ambiguity:

6.6. Semicolons

6.6.1. Rule of thumb for semicolons

Each statement is terminated by a semicolon.

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

Except: statements ending with blocks.

  1. function foo() {
  2. // ···
  3. }
  4. if (y > 0) {
  5. // ···
  6. }

The following case is slightly tricky:

  1. const func = () => {}; // semicolon!

The whole const declaration (a statement) ends with a semicolon, but inside it, there is an arrow function expression. That is: It’s not the statement per se that ends with a curly brace; it’s the embedded arrow 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:

  1. while (condition)
  2. statement

The body can be a single statement:

  1. while (a > 0) a--;

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

  1. while (a > 0) {
  2. a--;
  3. }

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

  1. while (processNextItem() > 0);

Your second option is an empty block:

  1. 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:

  • A semicolon
  • A line terminator followed by an illegal token
    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:

  1. return
  2. {
  3. first: 'jane'
  4. };

This code is parsed as:

  1. return;
  2. {
  3. first: 'jane';
  4. }
  5. ;

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. The following are three examples. There are more.

Example 1: Unintended function call.

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

Parsed as:

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

Example 2: Unintended division.

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

Parsed as:

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

Example 3: Unintended property access.

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

Executed as:

  1. const propKey = ('ul','ol');
  2. assert.equal(propKey, 'ol'); // due to comma operator
  3. someFunction()[propKey].map(x => x + x);

6.8. Semicolons: best practices

I recommend that you always write semicolons:

  • I like the visual structure it gives code – you clearly see when a statement ends.
  • There are less rules to keep in mind.
  • The majority of JavaScript programmers use 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:

  • The automatic code formatter Prettier can be configured to not use semicolons. It then automatically fixes problems. For example, if it encounters a line that starts with a square bracket, it prefixes that line with a semicolon.

  • The static checker ESLint has a rule that you tell your preferred style (always semicolons or as few semicolons as possible) and that warns you about critical issues.

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 (which is almost always located in modules). 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:

  1. '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:

  1. function functionInStrictMode() {
  2. 'use strict';
  3. }

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.

  1. function sloppyFunc() {
  2. unknownVar1 = 123;
  3. }
  4. sloppyFunc();
  5. // Created global variable `unknownVar1`:
  6. assert.equal(unknownVar1, 123);

Strict mode does it better:

  1. function strictFunc() {
  2. 'use strict';
  3. unknownVar2 = 123;
  4. }
  5. assert.throws(
  6. () => strictFunc(),
  7. {
  8. name: 'ReferenceError',
  9. message: 'unknownVar2 is not defined',
  10. });

The assert.throws() demands that its first argument, a function, throws a ReferenceError when it is called.