Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 5

Functions: Declarations, Expressions, and Arrows

This chapter assumes you can already read JavaScript fluently: you know how objects and property lookup work, and you have at least seen `this` and closures. If so, the interesting question is not "what is a function?" but "which of the three syntactic forms should I use, and what does each one actu
Level: AdvancedLength: In depth3 min read

Read at another depth

Functions: Declarations, Expressions, and Arrows

This chapter assumes you can already read JavaScript fluently: you know how objects and property lookup work, and you have at least seen this and closures. If so, the interesting question is not "what is a function?" but "which of the three syntactic forms should I use, and what does each one actually do at runtime?"

The three forms and what differs

JavaScript gives you three ways to create a function. They all produce a callable object, but they differ on four axes: hoisting, this binding, the internal [[Constructor]] slot, and identity under a name property.

Code example in javascript
// 1. Function declaration
function declared() { return 'declared'; }

// 2. Function expression (named and anonymous)
const expr = function named() { return 'expr'; };

// 3. Arrow function
const arrow = () => 'arrow';

console.log(declared.name, expr.name, arrow.name); // declared named arrow

A function declaration is fully hoisted: the binding exists and the body is available before the declaration line executes. A const-assigned expression is not — the binding hoists (in temporal-dead-zone fashion for let/const) but the value does not. This is the practical reason declarations dominate at module top level: it makes the file order-independent.

Code example in javascript
console.log(early()); // works: declaration fully initialized
function early() { return 'ok'; }

The four axes compared

PropertyDeclarationExpressionArrow
HoistingFully hoistedBinding onlyBinding only
Own `this`Yes (call-site)Yes (call-site)No (lexical)
Constructible with `new`YesYesNo
`arguments` objectYesYesNo

Arrows are not "shorter functions." They are a different kind of callable. Internally, an arrow has no [[Environment]]-independent this slot: its this is resolved as a normal variable lookup into its enclosing scope, which is exactly why .call() and .bind() cannot rebind it — there is nothing to bind.

Code example in javascript
const obj = {
  name: 'obj',
  regular() { return this.name; },
  arrow: () => this, // module/global scope's this
};

console.log(obj.regular()); // 'obj'
console.log(obj.arrow());   // not obj — this comes from the defining scope

Arrows: when lexical `this` is the point

The arrow function exists to close over the surrounding this. The classic pattern is registering a callback on an object or class instance:

Code example in javascript
class Timer {
  constructor() { this.ticks = 0; }
  start() {
    // Arrow inherits `this` from start(), which was called on the instance
    setInterval(() => { this.ticks += 1; }, 1000);
  }
}

The gotcha is symmetric: an arrow's this is fixed at definition. If you want per-call binding — event handlers, prototype methods, polymorphic functions — an arrow is wrong, not merely shorter. A method defined as an arrow class field (e.g. handler = () => {...}) permanently pins this to the instance, which is sometimes exactly what you want for callbacks but breaks subclass overrides that expect to be called with this of the subclass.

The rest of the arrow contract

Arrows also lack their own arguments binding, super, and new.target. They cannot be used as generator functions (=>* is a syntax error). Using new on one throws a TypeError immediately, not at call time via some fallback — the engine knows at parse time that [[Construct]] is absent.

One subtlety: named function expressions give you a self-reference that does not pollute the enclosing scope, and it survives even if the outer binding is reassigned:

Code example in javascript
const f = function self(n) {
  return n <= 1 ? 1 : n * self(n - 1);
};
const g = f;
f = null;
console.log(g(5)); // 120 — `self` still refers to the function

An arrow's implicit return is fine for expressions but has a well-known trap: returning an object literal requires parentheses, because { is parsed as a block body:

Code example in javascript
const bad   = (key) => { key: 42 };        // block with a labeled statement; returns undefined
const good  = (key) => ({ [key]: 42 });    // object literal
console.log(good('a')); // { a: 42 }

IIFEs and hoisting decisions

The immediately-invoked function expression — (function () { ... })() — was the pre-module mechanism for private scope. Today it survives mostly as the top-level wrapper for async work in scripts that cannot use top-level await, and inside async arrow variants: (async () => { ... })(). Note that an async IIFE's rejection becomes an unhandled promise rejection; attach .catch() if failure is possible.

When choosing between declaration and expression at the top level, the trade-off is: declarations hoist (order-independent, slightly easier reading flow) but can be reassigned implicitly and their hoisting can mask bugs where you call something before its dependencies ran. Expressions under const fail loudly and early. Style guides diverge here; the decision criterion is whether you prefer fail-fast ordering or maximum flexibility.

The dispatcher model

All forms route through the same call machinery; the differences are which slots the engine populates:

Flow chart showing a function call dispatching to three branches: declaration and expression bind this, arguments, and construct behavior from the call site; arrow inherits this from the enclosing scope and binds neither arguments nor a constructor slot.
100%
How the three function forms differ in what the engine binds at call time
Show diagram source
flowchart TD
    A[Function call] --> B{Form?}
    B -->|Declaration / Expression| C[Bind this from call site]
    B -->|Arrow| D[Inherit this from enclosing scope]
    C --> E[Bind arguments, new.target]
    D --> F[No arguments binding<br/>No construct slot]
    E --> G[Execute body]
    F --> G
    B -->|new on arrow| H[TypeError]

Exercise

Why does (0, obj.method)() call the method with this === undefined even though obj.method is a regular function?

Show answer

The comma operator makes the expression evaluate to the function value itself, detached from obj; the subsequent call has no receiver, so this defaults to undefined (in strict mode) — the same mechanism behind the classic const m = obj.method; m() bug. Arrows sidestep it by never using call-site this.

Exercise

You need a factory that returns a constructor-like callable usable with new. Which form must you use, and why can't an arrow work?

Show answer

A function declaration or expression. Arrows lack the internal [[Construct]] method (no new.target, no this binding machinery for construction), so new arrowFn() throws a TypeError at the call site.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Functions: Declarations, Expressions, and Arrows

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that