This is a very first look at JavaScript’s syntax. Don’t worry if some things don’t make sense, yet. They will all be explained in more detail later in this book.
This overview is not exhaustive, either. It focuses on the essentials.
// single-line comment
/*
Comment with
multiple lines
*/
Booleans:
true
false
Numbers:
1.141
-123
The basic number type is used for both floating point numbers (doubles) and integers.
Bigints:
17n
-49n
The basic number type can only properly represent integers within a range of 53 bits plus sign. Bigints can grow arbitrarily large in size.
Strings:
'abc'
"abc"
`String with interpolated values: ${256} and ${true}`
JavaScript has no extra type for characters. It uses strings to represent them.
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:
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.
There is also assert.deepEqual()
that compares objects deeply.
Logging to the console of a browser or Node.js:
// Printing a value to standard out (another method call)
console.log('Hello!');
// Printing error information to standard error
console.error('Something went wrong!');
// Operators for booleans
assert.equal(true && false, false); // And
assert.equal(true || false, true); // Or
// Operators for numbers
assert.equal(3 + 4, 7);
assert.equal(5 - 1, 4);
assert.equal(3 * 4, 12);
assert.equal(10 / 4, 2.5);
// Operators for bigints
assert.equal(3n + 4n, 7n);
assert.equal(5n - 1n, 4n);
assert.equal(3n * 4n, 12n);
assert.equal(10n / 4n, 2n);
// Operators for strings
assert.equal('a' + 'b', 'ab');
assert.equal('I see ' + 3 + ' monkeys', 'I see 3 monkeys');
// Comparison operators
assert.equal(3 < 4, true);
assert.equal(3 <= 4, true);
assert.equal('abc' === 'abc', true);
assert.equal('abc' !== 'def', true);
JavaScript also has a ==
comparison operator. I recommend to avoid it – why is explained in “Recommendation: always use strict equality” (§15.4.3).
const
creates immutable variable bindings: Each variable must be initialized immediately and we can’t assign a different value later. However, the value itself may be mutable and we may be able to change its contents. In other words: const
does not make values immutable.
// Declaring and initializing x (immutable binding):
const x = 8;
// Would cause a TypeError:
// x = 9;
let
creates mutable variable bindings:
// Declaring y (mutable binding):
let y;
// We can assign a different value to y:
y = 3 * 5;
// Declaring and initializing z:
let z = 3 * 5;
// 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 are used especially as arguments of function calls and method calls:
const add2 = (a, b) => { return a + b };
// Calling function add2()
assert.equal(add2(5, 2), 7);
// Equivalent to add2:
const add3 = (a, b) => a + b;
The previous code contains the following two arrow functions (the terms expression and statement are explained later in this chapter):
// An arrow function whose body is a code block
(a, b) => { return a + b }
// An arrow function whose body is an expression
(a, b) => a + b
// Creating a plain object via an object literal
const obj = {
first: 'Jane', // property
last: 'Doe', // property
getFullName() { // property (method)
return this.first + ' ' + this.last;
},
};
// Getting a property value
assert.equal(obj.first, 'Jane');
// Setting a property value
obj.first = 'Janey';
// Calling the method
assert.equal(obj.getFullName(), 'Janey Doe');
// Creating an Array via an Array literal
const arr = ['a', 'b', 'c'];
assert.equal(arr.length, 3);
// Getting an Array element
assert.equal(arr[1], 'b');
// Setting an Array element
arr[1] = 'β';
// Adding an element to an Array:
arr.push('d');
assert.deepEqual(
arr, ['a', 'β', 'c', 'd']);
Conditional statement:
if (x < 0) {
x = -x;
}
for-of
loop:
const arr = ['a', 'b'];
for (const element of arr) {
console.log(element);
}
Output:
a
b
Each module is a single file. Consider, for example, the following two files with modules in them:
file-tools.mjs
main.mjs
The module in file-tools.mjs
exports its function isTextFilePath()
:
export function isTextFilePath(filePath) {
return filePath.endsWith('.txt');
}
The module in main.mjs
imports the whole module path
and the function isTextFilePath()
:
// Import whole module as namespace object `path`
import * as path from 'node:path';
// Import a single export of module file-tools.mjs
import {isTextFilePath} from './file-tools.mjs';
class Person {
constructor(name) {
this.name = name;
}
describe() {
return `Person named ${this.name}`;
}
static logNames(persons) {
for (const person of persons) {
console.log(person.name);
}
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
describe() {
return super.describe() +
` (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
assert.equal(
jane.describe(),
'Person named Jane (CTO)');
function throwsException() {
throw new Error('Problem!');
}
function catchesException() {
try {
throwsException();
} catch (err) {
assert.ok(err instanceof Error);
assert.equal(err.message, 'Problem!');
}
}
Note:
try-finally
and try-catch-finally
are also supported.
Error
and its subclasses.
The grammatical category of variable names and property names is called identifier.
Identifiers are allowed to have the following characters:
A
–Z
, a
–z
(etc.)
$
, _
0
–9
(etc.)
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
Common casing styles for concatenating words are:
threeConcatenatedWords
three_concatenated_words
three-concatenated-words
In general, JavaScript uses camel case, except for constants.
Lowercase:
Functions, variables: myFunction
Methods: obj.myMethod
CSS:
my-utility-class
(dash case)
myUtilityClass
Module file names are usually dash-cased:
import * as theSpecialLibrary from './the-special-library.mjs';
Uppercase:
MyClass
All-caps:
MY_CONSTANT
(underscore case)
The following naming conventions are popular in JavaScript.
If the name of a parameter starts with an underscore (or is an underscore) it means that this parameter is not used – for example:
arr.map((_x, i) => i)
If the name of a property of an object starts with an underscore then that property is considered private:
class ValueWrapper {
constructor(value) {
this._value = value;
}
}
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() {
// ···
};
All remaining sections of this chapter are advanced.
In a Unix shell script, we can add a first line that starts with #!
to tell Unix which executable should be used to run the script. These two characters have several names, including hashbang, sharp-exclamation, sha-bang (“sha” as in “sharp”) and shebang. Otherwise, hashbang lines are treated as comments by most shell scripting languages and JavaScript does so, too. This is a common hashbang line for Node.js:
#!/usr/bin/env node
If we want to pass arguments to node
, we have to use the env
option -S
(to be safe, some Unixes don’t need it):
#!/usr/bin/env -S node --enable-source-maps --no-warnings=ExperimentalWarning
First character:
é
and ü
and characters from non-latin alphabets, such as α
)
$
_
Subsequent characters:
Examples:
const ε = 0.0001;
const строка = '';
let _tmp = 0;
const $foo2 = true;
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.
In this section, we explore how JavaScript distinguishes two kinds of syntactic constructs: statements and expressions. Afterward, we’ll see that that can cause problems because the same syntax can mean different things, depending on where it is used.
We pretend there are only statements and expressions
For the sake of simplicity, we pretend that there are only statements and expressions in JavaScript.
A statement is a piece of code that can be executed and performs some kind of action. For example, if
is a statement:
let myStr;
if (myBool) {
myStr = 'Yes';
} else {
myStr = 'No';
}
One more example of a statement: a function declaration.
function twice(x) {
return x + x;
}
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:
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:
> 'ab' + 'cd'
'abcd'
> Number('123')
123
> true || false
true
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:
function max(x, y) {
if (x > y) {
return x;
} else {
return y;
}
}
The arguments of a function call or a method call must be expressions:
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 a statement.
The following code demonstrates that any expression bar()
can be either expression or statement – it depends on the context:
function f() {
console.log(bar()); // bar() is expression
bar(); // bar(); is (expression) statement
}
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.
A function declaration is a statement:
function id(x) {
return x;
}
A function expression is an expression (right-hand side of =
):
const id = function me(x) {
return x;
};
In the following code, {}
is an object literal: an expression that creates an empty object.
const obj = {};
This is an empty code block (a statement):
{
}
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:
function
: Is it a function declaration or a function expression?
{
: 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:
(function (x) { console.log(x) })('abc');
Output:
abc
In this code:
We first create a function via a function expression:
function (x) { console.log(x) }
Then we invoke that function: ('abc')
The code fragment shown in (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:
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 = () => {}; // 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.
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) {}
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.
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:
return;
{
'jane';
with label first:
}
;
Why does JavaScript do this? It protects against accidentally returning a value in a line after a return
.
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.
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'); // comma operator
assert.equal(propKey, 'ol');
someFunction()[propKey].map(x => x + x);
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:
Starting with ECMAScript 5, JavaScript has two modes in which JavaScript can be executed:
You’ll rarely encounter sloppy mode in modern JavaScript code, which is almost always located in modules. In this book, I assume that strict mode is always switched on.
In 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';
}
Let’s look at three things that strict mode does better than sloppy mode. Just in this one section, all code fragments are executed in sloppy mode.
In non-strict mode, changing an undeclared variable creates a global variable.
function sloppyFunc() {
undeclaredVar1 = 123;
}
sloppyFunc();
// Created global variable `undeclaredVar1`:
assert.equal(undeclaredVar1, 123);
Strict mode does it better and throws a ReferenceError
. That makes it easier to detect typos.
function strictFunc() {
'use strict';
undeclaredVar2 = 123;
}
assert.throws(
() => strictFunc(),
{
name: 'ReferenceError',
message: 'undeclaredVar2 is not defined',
});
The assert.throws()
states that its first argument, a function, throws a ReferenceError
when it is called.
In strict mode, a variable created via a function declaration only exists within the innermost enclosing block:
function strictFunc() {
'use strict';
{
function foo() { return 123 }
}
return foo(); // ReferenceError
}
assert.throws(
() => strictFunc(),
{
name: 'ReferenceError',
message: 'foo is not defined',
});
In sloppy mode, function declarations are function-scoped:
function sloppyFunc() {
{
function foo() { return 123 }
}
return foo(); // works
}
assert.equal(sloppyFunc(), 123);
In strict mode, you get an exception if you try to change immutable data:
function strictFunc() {
'use strict';
true.prop = 1; // TypeError
}
assert.throws(
() => strictFunc(),
{
name: 'TypeError',
message: "Cannot create property 'prop' on boolean 'true'",
});
In sloppy mode, the assignment fails silently:
function sloppyFunc() {
true.prop = 1; // fails silently
return true.prop;
}
assert.equal(sloppyFunc(), undefined);
Further reading: sloppy mode
For more information on how sloppy mode differs from strict mode, see MDN.