Lesson 13 / 21
Function Declaration and Expression
The hoisting difference between the two definition forms, named function expressions, using functions as values, return rules, and the automatic semicolon insertion trap.
Contents
The previous lesson had checks and accumulators embedded in the loop body. Doing the same work a second time would mean copying code. The Programming Fundamentals course’s Function Definition and Invocation lesson established the reason for this separation.
JavaScript has more than one way to define a function, and the difference between them is not just syntax. This lesson compares the two basic forms; the third form, arrow functions, is covered in this topic’s fifth lesson.
Two Definition Forms
A function declaration is a statement that begins with the function keyword and
carries a name. A function expression is a form that produces a value and is
usually assigned to a variable.
The difference shown in the Naming and Scope Rules lesson repeats here: a declaration is hoisted together with its body, an expression is not.
console.log(averageDeclaration([21.5, 19.75])); function averageDeclaration(array) { let total = 0; for (const value of array) total += value; return total / array.length; } const averageExpression = function (array) { let total = 0; for (const value of array) total += value; return total / array.length; }; console.log(averageExpression([21.5, 19.75]));
20.625 20.625
The declaration could be called from the line before its definition. Had the expression been called before its assignment line, it would have thrown a temporal dead zone error.
The second difference between the two forms is that the expression is a value: it can be passed directly as an argument, made a property of an object, or returned from a function. A declaration cannot do this; it first has to be bound to a name.
This course’s rule: helper functions in the outermost scope are written in declaration form; functions passed to another function or carried in a variable are written in expression form. The reason is that a declaration is independent of call order and frees up the file’s reading order.
Named Function Expressions
A function expression may carry a name. This name is seen only inside the function’s own body:
const sum = function adder(array, index = 0) { if (index >= array.length) return 0; return array[index] + adder(array, index + 1); }; console.log(sum([21.5, 19.75, 23])); console.log(sum.name); try { console.log(adder([1])); } catch (error) { console.log(error.name + ": " + error.message); }
64.25 adder ReferenceError: adder is not defined
The inner name made the recursive call possible and showed up in the name property,
but it was not accessible from outside. This lets a recursive function expression call
itself without depending on the outer variable; even if the outer variable later gets
bound to another value, the recursion keeps working.
Functions carry two informative properties:
function aboveThreshold(array, threshold) { return array.filter((d) => d > threshold); } const anonymous = function (a, b) { return a + b; }; console.log(aboveThreshold.name, aboveThreshold.length); console.log(anonymous.name, anonymous.length); console.log(typeof aboveThreshold, aboveThreshold instanceof Object);
aboveThreshold 2 anonymous 2 function true
name is the name that appears in error messages and stack traces; it can be inferred
from the variable it is assigned to. length is the expected parameter count —
parameters with defaults and rest parameters are not counted, which is the next
lesson’s subject.
The last line is a type observation: functions are objects. typeof gives a distinct
result, but they carry properties and behave as objects.
Functions Are Values
A function being a value is what makes passing it as an argument and returning it from a function possible. This is the higher-order function concept defined in the Programming Fundamentals course’s Introduction to Functional Programming lesson.
const temperatures = [21.5, 19.75, 23, 18.25]; function thresholdFilter(threshold) { return function (value) { return value > threshold; }; } const above20 = thresholdFilter(20); console.log(temperatures.filter(above20)); console.log(temperatures.filter(thresholdFilter(22)));
[ 21.5, 23 ] [ 23 ]
The returned function keeps carrying the threshold binding from the call it was
created in. Two separate calls produced two separate bindings; the first saw 20, the
second 22. This behavior is called a closure and is one of the core subjects of
the next course. Here it is only being used.
A function expression can be called immediately where it is defined. This pattern is used to do work without leaking a name to the outer scope:
const report = (function () { const hidden = [21.5, 19.75]; return { count: hidden.length }; })(); console.log(report); console.log(typeof hidden);
{ count: 2 }
undefined
The name hidden is not visible outside. Module mode removes most of this need — as
seen in the Ways to Run Code lesson, a module’s outermost scope is already private —
but the pattern is still used to open a narrow scope.
Declarations Inside a Block
In strict mode, function declarations are block-scoped:
if (true) { function inBlock() { return "block"; } console.log(inBlock()); } try { console.log(inBlock()); } catch (error) { console.log(error.name + ": " + error.message); }
block ReferenceError: inBlock is not defined
This behavior is different in non-strict scripts, and compatibility rules come into play. The way to avoid ambiguity is to write conditional definitions as expressions rather than declarations: a variable declaration’s scope rule is the same in both modes.
Return Rules
A function with no return value written returns undefined. This is the value the
caller sees, independent of whether the function did any work.
function logRecord(record) { console.log(record.station, record.temperature); } const result = logRecord({ station: "A1", temperature: 21.5 }); console.log("return:", result); function earlyExit(value) { if (value == null) return; return value * 2; } console.log(earlyExit(null), earlyExit(10));
A1 21.5 return: undefined undefined 20
The command–query separation from the Programming Fundamentals course applies here too:
a function either does work or returns a value. logRecord does work; using its return
value is meaningless.
The Automatic Semicolon Insertion Trap
JavaScript inserts a semicolon at line end in certain situations. The rule has a sharp
consequence for the return statement: no line break may sit between return and
the expression to be returned.
function brokenReturn() { return 21.5; } function correctReturn() { return 21.5; } console.log(brokenReturn()); console.log(correctReturn());
undefined 21.5
In the first function, the return statement was terminated on its own line; the
21.5; line became a separate statement that never runs. No error was thrown, no
warning appeared — only the wrong value came back.
The same rule applies to throw, break, continue, and the increment operators. The
way to guard against it is to start the expression to be returned on the same line as
return. If a long expression is being returned, its opening parenthesis is placed on
the same line.
Applying It to the Measurement Script
Once the previous lessons’ checks and computations are split into functions, the script’s body shrinks to a few lines showing the flow:
function isValid(record) { return record.temperature != null && !Number.isNaN(record.temperature); } function average(records) { let total = 0; let count = 0; for (const record of records) { if (!isValid(record)) continue; total += record.temperature; count += 1; } return count === 0 ? null : total / count; } const records = [ { station: "A1", temperature: 21.5 }, { station: "A2", temperature: NaN }, { station: "B1", temperature: 23 }, { station: "B2", temperature: null }, ]; console.log("valid:", records.filter(isValid).length); console.log("average:", average(records)); console.log("empty list:", average([]));
valid: 2 average: 22.25 empty list: null
isValid was both passed as an argument to filter and called inside a loop — the
direct benefit of a function being a value. The average function returns null for
an empty list; had it divided anyway, it would have produced NaN and the error would
have moved forward silently.
Summary
- A function declaration is hoisted together with its body; a function expression comes into being on its assignment line and is in the dead zone before that.
- A named function expression’s name is seen only inside its own body and makes recursion independent of the outer variable.
- Functions are values: they are passed as arguments, returned, and carry properties as objects.
- In strict mode, function declarations inside a block are block-scoped; conditional definitions are written as expressions.
- A function with no return value written gives
undefined; if a line break sits betweenreturnand the expression, the statement ends there andundefinedis returned silently.
Next Step
In this lesson, functions were called with a fixed number of parameters. In real usage, an argument may be missing, extra, or its count may not be known in advance. The next lesson covers parameter rules: when default values kick in, how rest parameters collect, and why the old arguments object is not used in newer code.
To keep your progress and take notes, Log in
My notes
Log in to take notes.