Function Declarations, Expressions, and Arrows
There are three ways to create a function in JavaScript, and each behaves differently with hoisting, this, and syntax.
Declarations are fully hoisted. Expressions are hoisted as undefined. Arrow functions are shorter and do not create their own this. Use const for function expressions.
EXERCISE
1Function Declarations: The Classic Way
A function declaration starts with the function keyword, has a name, and is fully hoisted. It is the oldest and most straightforward way to create a function.
A function declaration begins with the function keyword at the start of a statement and gives the function a name. It is the most predictable of the three forms, because it is stored complete during the memory phase and is therefore callable from anywhere in its scope.
function calculateTip(bill, percentage) {
const tip = bill * (percentage / 100);
return tip;
}
console.log(calculateTip(50, 15)); // 7.5
Four parts are worth naming, because the rest of this unit assumes them. calculateTip is the name, and it is what the memory phase records. bill and percentage are parameters, which are ordinary local variables that exist only inside the function. The braces hold the body. And return decides what the call evaluates to.
Position in the file does not matter:
console.log(add(3, 7)); // 10
function add(a, b) {
return a + b;
}
This is the hoisting behaviour from unit 2, and it is the practical advantage of the declaration form. You can order a file by importance rather than by dependency, putting the function that matters at the top and its helpers below.
A function with no return still produces a value:
function greet(name) {
console.log("Hello, " + name + "!");
}
const result = greet("Amir");
console.log(result); // undefined
greet did its work and printed. It just had nothing to hand back, so the call evaluates to undefined. This is the same undefined from unit 2: a real value meaning nothing was supplied.
It is a common source of confusion when chaining calls together, because the failure appears one step later:
function addItem(list, item) {
list.push(item);
}
const updated = addItem([], "book");
console.log(updated.length); // TypeError: Cannot read properties of undefined
addItem modified the array correctly. It simply never returned it, so updated is undefined and the next line fails on something that looks unrelated. Any function you intend to use for its result needs an explicit return.
EXERCISE
2Function Expressions and Arrow Functions
A function expression stores a function inside a variable. Arrow functions are a shorthand for writing function expressions with cleaner syntax.
Assigning a function to a variable creates a function expression. The function is no longer a declaration being recorded by name; it is a value being stored, and the variable follows the variable rules you already know.
var multiply = function (a, b) {
return a * b;
};
console.log(multiply(4, 5)); // 20
The function here is anonymous: there is no name after function. You call it through the variable.
A function expression may also carry a name, and that name is private to it:
var divide = function divideNumbers(a, b) {
return a / b;
};
console.log(divide(10, 2)); // 5
console.log(divideNumbers(10, 2)); // ReferenceError: divideNumbers is not defined
divideNumbers is visible only inside the function body, which makes it useful for recursion and for readable stack traces, and useless as a way to call the function from outside.
Arrow functions are a shorter syntax for the same idea:
const square = (x) => {
return x * x;
};
const squareShort = (x) => x * x;
const squareShorter = x => x * x;
console.log(square(5), squareShort(5), squareShorter(5)); // 25 25 25
Three levels of shorthand, all equivalent. Dropping the braces makes the body an expression whose value is returned automatically, and a single parameter does not need its parentheses.
That implicit return has one trap worth knowing now, because it produces a confusing result rather than an error:
const makeUser = (name) => { name: name };
console.log(makeUser("Ada")); // undefined
The braces were read as a function body, not an object literal, so the function returns nothing. Wrapping the object in parentheses tells the parser which you meant:
const makeUser = (name) => ({ name: name });
console.log(makeUser("Ada")); // { name: "Ada" }
Pro Tip: Neither form is hoisted as a function.
var sayHello = () => {}leavessayHelloholdingundefineduntil that line runs, and aconstversion sits in its Temporal Dead Zone. Calling either one early fails, which is the one real cost of choosing an expression over a declaration.
EXERCISE
3Choosing the Right Function Type
Each type has its ideal use case. Knowing when to use which type is what separates a beginner from someone who writes professional JavaScript.
All three forms produce a callable function, so the choice between them is about hoisting, about this, and about what you want to prevent. Here is what each one is actually for.
A declaration for a named, reusable function:
function formatCurrency(amount) {
return "$" + amount.toFixed(2);
}
console.log(formatCurrency(9.5)); // "$9.50"
Callable from anywhere in the file, which lets you order a module by what matters rather than by what depends on what.
An arrow function for a short callback:
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
The whole operation fits on one line and reads close to the sentence describing it. The equivalent function (n) { return n * 2; } carries three extra pieces of syntax that say nothing about the intent.
const for anything stored in a variable:
const calculate = (a, b) => a + b;
calculate = 42; // TypeError: Assignment to constant variable
Declared with var or let, that reassignment would succeed silently, and every later call would fail with calculate is not a function somewhere far from the line responsible.
A regular function when the value of this matters:
const counter = {
count: 0,
increment() {
this.count++;
},
};
counter.increment();
console.log(counter.count); // 1
Written as an arrow function, increment would have no this of its own and would look outward to the enclosing scope, where this is not counter. Methods on object literals are one of the few places where the shorter form is the wrong one.
| Situation | Choose |
|---|---|
| top-level named function | declaration |
| short callback | arrow |
| function stored in a variable | const plus arrow |
| needs to be callable before its line | declaration |
method that uses this | method shorthand or function |
Pro Tip: Most codebases use all three, and the mix is not indecision. Declarations for the module's main functions, arrows for the small inline work, and
consteverywhere something should not be reassigned.