Functions, Expressions & Operators
|
This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve. This section’s bibliography lists the reference material consulted while preparing these pages. |
Every JavaScript program is built out of two kinds of building blocks: expressions, which evaluate to a value, and statements (see Statements), which do things — branching, looping, jumping around a function’s control flow. This page covers the expression side of that split: the full operator reference JavaScript builds expressions out of, and function fundamentals — the three ways to define a function, and how each one is invoked.
Expressions & Operators
An expression is any phrase of JavaScript that evaluates to a value. The simplest expressions — literals like
1.23 or "hello", the keywords true/false/null/this, and bare variable references — stand alone.
Everything more complex is built by combining simpler expressions with an operator: x * y combines the
expressions x and y with the multiplication operator to produce a new value. This section documents
JavaScript’s operators, grouped by what they do.
Arithmetic Operators
The basic arithmetic operators are * (exponentiation), , /, % (remainder), `, and `-`. All but `
simply convert both operands to numbers and compute the obvious result — an operand that can’t convert to a
number becomes NaN, and NaN poisons the whole operation:
2 ** 10; // => 1024
7 % 2; // => 1: remainder after whole-number division
5 / 2; // => 2.5: JavaScript has no separate integer division --
// every division produces a floating-point result
"3" * "5"; // => 15: numeric strings are converted before multiplying
"x" * 2; // => NaN: "x" cannot convert to a number
is right-associative (2 2 3 is 2 8, not 4 3), and combining it with unary minus requires
explicit parentheses — -3 2 is a syntax error, precisely because (-3) 2 and -(3 2) disagree and
JavaScript refuses to guess which one you meant.
The + operator: addition vs. concatenation
+ is the one arithmetic operator with a split personality: it adds when both operands are numeric and
concatenates when either operand is (or converts to) a string, with string behavior taking priority:
1 + 2; // => 3: numeric addition
"1" + "2"; // => "12": string concatenation
"1" + 2; // => "12": 2 is converted to a string
1 + {}; // => "1[object Object]": object converts to a string
true + true; // => 2: booleans convert to numbers (1 + 1)
2 + null; // => 2: null converts to 0
2 + undefined; // => NaN: undefined converts to NaN
Because of this, + is not always associative when strings and numbers mix — 1 + 2 + " mice" evaluates
left to right as (1 + 2) + " mice" ("3 mice"), while 1 + (2 + " mice") forces the string concatenation
first, producing "12 mice".
Unary and increment/decrement operators
Unary ` and `-` convert their single operand to a number (optionally flipping its sign); `+ and --
increment or decrement an lvalue (a variable, array element, or object property) by 1. Both come in a
pre and post form, which differ in what the expression itself evaluates to:
let i = 1, j = ++i; // pre-increment: i becomes 2, and j is also 2
let n = 1, m = n++; // post-increment: n becomes 2, but m is 1 (the old value)
x` is not the same as `x = x + 1` -- `/-- always convert their operand to a number before adjusting
it, whereas plain + would concatenate if x were a numeric string.
| Automatic semicolon insertion (see Lexical Structure) means a line break is never allowed between a post-increment/decrement operator and the operand before it — put one there and JavaScript inserts a semicolon after the operand, silently turning it into a no-op statement. |
Comparison Operators
The relational operators <, ⇐, >, and >= test the relative order of two operands. If both operands are
strings after any needed object-to-primitive conversion, they’re compared alphabetically by UTF-16 code unit
(which is case-sensitive — every capital ASCII letter sorts before every lowercase one, so "Zoo" < "aardvark"
is true); otherwise both operands are converted to numbers and compared numerically:
11 < 3; // => false: numeric comparison
"11" < "3"; // => true: string comparison, "1" sorts before "3"
"11" < 3; // => false: numeric comparison, "11" converts to 11
"one" < 3; // => false: "one" converts to NaN, and every comparison
// against NaN returns false
⇐ and >= are not defined in terms of equality — ⇐ simply means "not greater than" and >= means "not
less than." The one place this matters is NaN: because NaN is never equal to (or less/greater than)
anything, all four comparison operators return false whenever either operand is NaN.
Equality Operators: == vs. ===
=== (strict equality) compares two values with no type conversion: values of different types are never
equal, and two objects/arrays are only equal if they’re the exact same reference, never merely
same-shaped. == (loose equality) first tries the same strict comparison, but when the operand types differ
it attempts a chain of type conversions — null == undefined is true, a string operand converts to a
number when compared against one, a boolean operand converts to 0/1, and so on — before comparing again.
"1" == true; // => true: true -> 1, then "1" -> 1, then 1 === 1
"1" === true; // => false: different types, no conversion attempted
NaN === NaN; // => false: NaN is never equal to anything, including itself
0 === -0; // => true: zero and negative zero are considered equal
Because ==’s conversion rules are a frequent source of surprising bugs, prefer `===/!== over
==/!= in new code. Reach for == only for the one genuinely useful idiom it enables, x == null, which
tests for null or undefined in a single comparison.
|
The table below works through representative pairs, including the ones most often gotten wrong:
| Operands | == |
=== |
Why |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Zero and negative zero are considered equal by both operators ( |
|
|
|
|
|
|
|
|
|
|
|
Two distinct object literals are two distinct references — neither operator considers them equal no matter how similar their contents are. |
==’s conversions are about type, not truthiness — don’t confuse this table with the separate
truthy/falsy rules a bare `if (value) test applies. See
Types, Values & Conversions: Boolean Values for the truthy/falsy
table; a value can convert to true under an if test while still comparing false with == against a
specific operand ([] == false above is true, yet [] itself is truthy).
|
Logical Operators
&&, ||, and ! perform boolean logic, but && and || operate on — and return — the operands
themselves rather than coercing to true/false, using JavaScript’s notion of truthy and falsy values
(the falsy values are false, null, undefined, 0, -0, NaN, and ""; everything else, including
every object, is truthy).
&& and || short-circuiting
&& evaluates its left operand first. If that value is falsy, && returns it immediately without evaluating
the right operand at all — the overall expression can’t become truthy no matter what the right side is. Only
when the left operand is truthy does && go on to evaluate and return the right operand:
const o = {x: 1};
const p = null;
o && o.x; // => 1: o is truthy, so the expression evaluates (and returns) o.x
p && p.x; // => null: p is falsy, so p.x is never evaluated -- avoiding a TypeError
|| works the mirror-image way: it returns its left operand immediately if that value is truthy (short-
circuiting past the right operand), and only evaluates the right operand when the left one is falsy. This
makes || the classic idiom for "pick the first truthy value in a list of alternatives":
let max = providedMax || preferences.maxWidth || 500;
Because the skipped operand is never evaluated, this short-circuiting is more than an optimization — it’s
routinely used to guard against errors (user && user.name avoids a TypeError when user is null) or to
conditionally run code for a side effect (isReady && start(); instead of if (isReady) start();). It also
means you should be careful writing a right-hand operand with side effects (an assignment, an increment, a
function call): whether it runs at all depends on the left operand’s value.
! (logical NOT)
! is a unary operator that always converts its operand to a boolean before inverting it, so unlike &&/||
it always returns exactly true or false. Applying it twice (!!x) is a common idiom for coercing any value
to its boolean equivalent. ! binds tightly (high precedence), so inverting a compound expression like
p && q needs explicit parentheses: !(p && q).
Nullish Coalescing (??)
?? (ES2020) looks like || but tests for a narrower condition: it returns its right operand only when the
left operand is specifically null or undefined — not merely falsy. This makes it the correct choice
whenever 0, "", or false are legitimate values that || would otherwise (and wrongly) skip past:
const options = {timeout: 0, title: "", verbose: false};
options.timeout || 1000; // => 1000: WRONG -- 0 is falsy, so || skips right past it
options.timeout ?? 1000; // => 0: RIGHT -- 0 is defined, so ?? keeps it
options.missing ?? 1000; // => 1000: missing is undefined, so ?? falls through
Like &&/||, ?? is short-circuiting. It also has no defined precedence relative to &&/|| — mixing
?? with either of them in the same expression without parentheses is a SyntaxError, forcing you to make
the intended order explicit: (a ?? b) || c or a ?? (b || c).
Optional Chaining (?.)
?. (ES2020) guards a property access or function call against a null/undefined value on its left,
returning undefined instead of throwing a TypeError. It comes in three forms:
a?.b // property access: undefined if a is null/undefined, else a.b
a?.[expr] // computed property access, same guard
f?.(x) // function-call invocation, same guard: undefined if f is null/undefined
Like the other short-circuiting operators, ?. stops evaluating the rest of the chain the instant it hits a
nullish value — it doesn’t just guard the one access it’s written next to:
const a = {b: null};
a.b?.c.d; // => undefined: a.b?.c short-circuits to undefined, and the chain stops
// there entirely -- the .d access after it is never attempted
(a.b?.c).d; // !TypeError: the parentheses force a.b?.c to be evaluated to
// undefined *first*, and .d is then attempted on that undefined
The same short-circuiting applies to ?.(): if the expression before it is nullish, none of the call’s
argument expressions are evaluated at all, which matters if any of them have side effects.
function square(x, log) {
log?.(x); // only calls log if a function was actually passed
return x * x;
}
?. only checks for null/undefined — it does not verify that a.b is actually a function before
?.() calls it, or that a is actually an object before ?. accesses a property on it. Chaining ?. past a
value of the wrong type still throws.
|
Bitwise Operators
The bitwise operators treat their operands as 32-bit integers and manipulate individual bits: & (AND), |
(OR), ^ (XOR), and unary ~ (NOT) perform boolean algebra bit by bit; <<, >>, and >>> shift bits left
or right (>> preserves the sign bit when shifting right, >>> always fills with zero). They’re rarely needed
in everyday application code — mostly reached for in bit-flag packing, low-level binary protocols, or
performance-sensitive numeric tricks:
0x1234 & 0x00FF; // => 0x0034: AND
0xFF00 ^ 0xF0F0; // => 0x0FF0: XOR
7 << 2; // => 28: shifting left 2 places multiplies by 4
-1 >> 4; // => -1: sign-preserving shift keeps the high bits set
-1 >>> 4; // => 0x0FFFFFFF: zero-fill shift clears them instead
typeof and instanceof
typeof is a unary operator that returns a string naming its operand’s type — useful for distinguishing
primitives from each other and from objects, but coarse-grained: every non-function object, Array and plain
object alike, reports "object", and (for historical reasons that can’t be fixed without breaking the web)
typeof null also reports "object":
typeof 42; // => "number"
typeof "hi"; // => "string"
typeof true; // => "boolean"
typeof undefined; // => "undefined"
typeof null; // => "object" (a long-standing historical quirk)
typeof {}; // => "object"
typeof []; // => "object" (arrays are objects too)
typeof function(){}; // => "function"
instanceof tests whether an object was constructed from a given class, walking the prototype chain (see
Classes) so subclass instances still test positive for a superclass:
const d = new Date();
d instanceof Date; // => true
d instanceof Object; // => true: everything is ultimately an Object
d instanceof Array; // => false
[] instanceof Array; // => true
[] instanceof Object; // => true: arrays are also objects
Use typeof to distinguish primitives from objects (and from each other); once you know you have an object,
reach for instanceof to tell which kind of object it is.
Assignment and the Conditional Operator
= assigns its right operand’s value to an lvalue on the left and evaluates to that same value; because it
has very low precedence and right-to-left associativity, a = b = c = 0 initializes all three variables to
0. Every arithmetic and bitwise operator has a combined op= form (=`, `-=`, `**=`, `&=`, and so on) that
is shorthand for `a = a op b` -- with the difference that the left side is only evaluated once, which matters
when it has side effects (`data[i] *= 2` is not the same as `data[i] = data[i+] * 2).
The conditional (ternary) operator ?: is JavaScript’s only three-operand operator, and the closest thing it
has to an inline if/else expression: it evaluates its first operand as a boolean, then evaluates and
returns only the second or third operand accordingly — never both:
const label = n === 1 ? "item" : "items";
const greeting = "hello " + (username ? username : "there");
Function Fundamentals
A function is a block of code, defined once, that can be invoked any number of times, optionally
parameterized by named arguments and optionally producing a return value. JavaScript gives you three distinct
syntaxes to define one — function declarations, function expressions, and arrow functions — which behave
identically once invoked but differ in hoisting, in whether they need a name, and (for arrow functions) in how
they treat this.
Function Declarations
A function declaration is the function keyword, a required name, a parenthesized parameter list, and a
braced body:
function distance(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
The name becomes a variable bound to the function object, and that binding is hoisted: the entire function is available throughout its enclosing script, function, or block before the declaration statement is reached, so it’s legal to call a function from code written above where it’s declared:
console.log(square(4)); // => 16: works even though square() is declared below
function square(x) {
return x * x;
}
If a function’s body runs to completion without hitting a return, the invocation evaluates to undefined — exactly as if it had ended with a bare return;.
Function Expressions
A function expression looks almost identical, but it appears as part of a larger expression (typically the right-hand side of an assignment) rather than as a standalone statement, and its name is optional:
const square = function (x) {
return x * x;
};
// A name is still useful for a function that needs to call itself:
const factorial = function fact(x) {
return x <= 1 ? 1 : x * fact(x - 1);
};
Unlike a declaration, a function expression is not hoisted — the function object doesn’t exist until the
expression that creates it is actually evaluated, and (because you can’t invoke what you can’t yet refer to)
it therefore cannot be called from code that runs before its definition. A name given to a function expression
(like fact above) is scoped only to that function’s own body, purely so the function can refer to itself
recursively — it is not visible in the surrounding scope the way a declaration’s name is.
Arrow Functions
Introduced in ES6, arrow functions trade the function keyword and a name for a compact ⇒ syntax, and come
with several shorthand rules that make them especially convenient as one-off callbacks:
const sum = (x, y) => { return x + y; }; // full form: braced body, explicit return
const sum2 = (x, y) => x + y; // concise form: body is the returned expression
const square = x => x * x; // exactly one parameter: parens are optional
const constant = () => 42; // zero parameters: empty parens are required
[1, null, 2, 3].filter(x => x !== null); // => [1, 2, 3]
[1, 2, 3, 4].map(x => x * x); // => [1, 4, 9, 16]
If a concise-body arrow function needs to return an object literal, wrap it in parentheses — x ⇒ ({value: x}) — otherwise the opening { is parsed as the start of a block body, not an object
literal, and the function silently returns undefined instead of the object you intended.
|
this binding: the critical difference
Function declarations, function expressions, and methods each establish their own this binding on every
invocation — determined by how they’re called (see How Invocation Determines this) rather than where they’re
defined. Arrow functions are different: they have no this of their own at all, and instead inherit this
lexically from whatever scope they were written in, exactly like an ordinary variable reference.
This is the main reason arrow functions exist, and it directly fixes a longstanding trap with nested regular functions:
const timer = {
seconds: 0,
start() {
// A regular nested function loses access to `this` -- inside it,
// `this` is the global object (or undefined in strict mode), NOT `timer`.
setInterval(function () {
this.seconds++; // BUG: `this` is not `timer` here
}, 1000);
},
};
const timerFixed = {
seconds: 0,
start() {
// An arrow function inherits `this` from start()'s own scope,
// where `this` correctly refers to timerFixed.
setInterval(() => {
this.seconds++; // correct: `this` is timerFixed
}, 1000);
},
};
Because they don’t bind their own this, arrow functions also have no prototype property and cannot be
used as constructors with new (see Classes for constructors proper). As a
rule of thumb: use a regular function/method when a function is meant to operate on an object via this
(especially a class method); reach for an arrow function for callbacks, array-method iteratees, and anything
else that should simply see the this of its surrounding code.
How Invocation Determines this
For non-arrow functions, this is set fresh on every call, based purely on invocation syntax, not on where
the function was defined:
-
Plain function call (
f()) —thisis the global object in non-strict mode, orundefinedin strict mode (see Lexical Structure). -
Method call (
obj.f()orobj["f"]()) —thisisobj, the object the property access was made through. -
Constructor call (
new F()) —thisis a newly created object that inherits fromF.prototype; see Classes. -
Indirect call via
Function.prototype.call()/apply()/bind()—thisis whatever object is passed explicitly.
const calculator = {
value: 10,
double() {
return this.value * 2; // `this` is calculator, because it was called as calculator.double()
},
};
calculator.double(); // => 20
const detached = calculator.double;
detached(); // TypeError or NaN-ish nonsense: called as a plain function, `this` is not calculator
Because this is recomputed on every non-arrow call, passing a method around as a bare value (as detached
does above) strips it of the object it was defined on — a common source of bugs when passing obj.method as
a callback. Wrapping it in an arrow function (() ⇒ obj.method()) or binding it (obj.method.bind(obj))
preserves the intended this.
Immediately Invoked Function Expressions (IIFEs)
Wrapping a function expression in parentheses and calling it right where it’s defined — an immediately invoked function expression, or IIFE — creates a private scope that runs exactly once and leaves nothing behind in the enclosing scope:
(function () {
const secret = computeSecret();
expose(secret);
})();
// Arrow-function IIFEs work the same way:
(() => {
const secret = computeSecret();
expose(secret);
})();
The outer parentheses around the function are required syntax, not decoration — without them, function () {
… }() is parsed as a function declaration followed by a stray, invalid (), since a statement can’t
begin with the function keyword and also be invoked in the same breath.
|
The IIFE pattern was, for years, the standard way JavaScript libraries kept their internal variables out of the global namespace. ES modules (see Modules) give every module file its own top-level scope automatically, which has superseded this trick for that purpose — see Function Parameters & Namespaces for the related pre-ES6 "namespacing with functions" pattern and more on why both are now legacy techniques. IIFEs remain useful today mainly for running one-off setup code that genuinely needs its own scope, independent of any module boundary. |