05 — Function Internals: Hoisting, Arrow Lexical this & Arguments Object

Function Declarations vs Expressions: Hoisting Internals

javascript
// ── Function declaration: fully hoisted (declaration + body) ──
// Available throughout the entire scope, even before the declaration line.
console.log(typeof greet);  // "function" — already defined
greet();                     // "hello" — can call before declaration

function greet() { console.log("hello"); }  // declaration: hoisted entirely

// ── Function expression: NOT hoisted (only the variable, in TDZ or undefined) ──
console.log(typeof greet2);  // "undefined" (var) or ReferenceError (let/const TDZ)
// greet2();  // TypeError: greet2 is not a function (var) or ReferenceError (let/const)

var greet2 = function() { console.log("hello"); };  // expression: only the var is hoisted
// const greet2 = () => console.log("hello");  // let/const: TDZ until this line

// ── Why declarations are hoisted but expressions aren't ──
// Function declarations are processed in the "creation phase" of the execution context:
//   1. Variable Environment: var declarations → initialized to undefined
//   2. Function declarations: fully created and assigned (name → function object)
//   3. let/const: created but in TDZ (uninitialized)
// Function expressions: the function object is created at runtime when the line executes,
// and assigned to the variable (which was hoisted as undefined or TDZ).

// ── Named function expression (the name is only visible inside the function) ──
const factorial = function fib(n) {
    // `fib` is only visible inside this function (for recursion)
    // `factorial` is visible outside (the variable it's assigned to)
    return n <= 1 ? n : fib(n - 1) + fib(n - 2);  // recursion via the internal name
};
// console.log(typeof fib);  // ReferenceError: fib is not defined (only inside)
factorial(10);  // works — uses `factorial` from outside or `fib` from inside

// ── Anti-pattern: conditional function declaration (not reliable) ──
// ❌ Function declarations inside if-blocks are hoisted in non-strict mode (inconsistent)
if (true) {
    function f() { return "yes"; }
}
// f();  // Works in some engines, not others — undefined behavior
// ✅ Use function expressions for conditional assignment:
let f2;
if (true) {
    f2 = function() { return "yes"; };
}

Arrow Functions: Lexical this and No arguments

javascript
// ── Arrow function syntax variants ──
const fn1 = (x) => x * 2;           // single param: parens optional
const fn2 = x => x * 2;             // no parens needed for single param
const fn3 = (a, b) => a + b;        // multiple params: parens required
const fn4 = () => 42;               // no params: empty parens required
const fn5 = (a, b) => {             // block body: needs return
    const sum = a + b;
    return sum;
};
const fn6 = (a, b) => ({ x: a, y: b });  // returning object literal: wrap in parens

// ── Arrow functions have LEXICAL `this` (inherited from enclosing scope) ──
// Regular functions: `this` is determined by HOW the function is called
// Arrow functions: `this` is determined by WHERE the function is defined (lexical)

function Counter() {
    this.count = 0;

    // ── Regular function: `this` is lost when used as a callback ──
    setInterval(function() {
        this.count++;           // ✗ `this` is NOT the Counter instance here
        // In a regular function called by setInterval, `this` is the global object
        // (or undefined in strict mode) — NOT the Counter instance
    }, 1000);

    // ── Arrow function: `this` is lexically inherited from Counter ──
    setInterval(() => {
        this.count++;           // ✓ `this` IS the Counter instance (lexical)
    }, 1000);
}

// ── Arrow functions have NO `arguments` object ──
function regular() {
    console.log(arguments);  // [Arguments] { '0': 1, '1': 2 } — array-like
}
regular(1, 2);

const arrow = () => {
    // console.log(arguments);  // ReferenceError: arguments is not defined
    // Arrow functions don't have their own `arguments` — they use the enclosing scope's
};
// ✅ Use rest params instead (...args):
const arrow2 = (...args) => console.log(args);  // [1, 2] — a real Array
arrow2(1, 2);

// ── Arrow functions have NO `new.target`, NO `prototype`, can't be constructors ──
const Arrow = () => {};
// new Arrow();  // TypeError: Arrow is not a constructor
console.log(Arrow.prototype);  // undefined (arrows have no prototype)

Anti-Pattern: Arrow Functions as Object Methods

javascript
// ❌ NAIVE — arrow function as method loses `this` (lexical, not dynamic)
const obj = {
    name: "Alice",
    greet: () => console.log(`Hi, ${this.name}`),  // this is NOT obj (lexical → outer)
    // this.name is undefined or window.name (the enclosing scope's this)
};
obj.greet();  // "Hi, undefined" (arrow's `this` is the module/global scope, not obj)

// ✅ CORRECT — regular function or shorthand method syntax for methods
const obj2 = {
    name: "Alice",
    greet() { console.log(`Hi, ${this.name}`); },  // method shorthand: dynamic `this`
};
obj2.greet();  // "Hi, Alice" (this is obj2 when called as obj2.greet())

// ── Arrow functions ARE correct for callbacks INSIDE methods ──
const obj3 = {
    name: "Alice",
    friends: ["Bob", "Charlie"],
    greetFriends() {
        // `this` here is obj3 (regular method call)
        this.friends.forEach(friend => {
            // Arrow inside method: `this` is lexically inherited from greetFriends
            console.log(`${this.name} greets ${friend}`);  // ✓ this.name = "Alice"
        });
    },
};
obj3.greetFriends();  // "Alice greets Bob" / "Alice greets Charlie"

// ── Rules for arrow vs regular function ──
// Use ARROW when:
//   - You need lexical `this` (callbacks, closures, array methods inside a class/method)
//   - You don't need `arguments`, `this`, or `new`
//   - Concise one-liners (map, filter, reduce callbacks)
// Use REGULAR when:
//   - Object methods (need dynamic `this`)
//   - Constructors (need `new`)
//   - You need `arguments` (or use rest params instead)
//   - Event handlers that use `this` (e.g., this.value, this.dataset)

arguments Object vs Rest Parameters

javascript
// ── arguments: array-LIKE (not a real Array) — available in regular functions ──
function sum() {
    console.log(arguments.length);     // 3
    console.log(arguments[0]);         // 1
    // arguments.map(x => x * 2);  // ✗ TypeError: arguments.map is not a function
    // arguments is NOT an Array — no .map, .filter, .reduce, etc.

    // Convert to real array (pre-ES6):
    const args = Array.prototype.slice.call(arguments);
    // or: Array.from(arguments);
    return args.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3);  // 6

// ── Rest parameters (...args): a REAL Array — always prefer this ──
function sumRest(...args) {
    console.log(args.length);           // 3
    console.log(args.map(x => x * 2));  // [2, 4, 6] — real Array, all methods available
    return args.reduce((a, b) => a + b, 0);
}
sumRest(1, 2, 3);  // 6

// ── Rest params must be LAST; can coexist with named params ──
function log(first, second, ...rest) {
    console.log(first, second, rest);
}
log(1, 2, 3, 4, 5);  // 1 2 [3, 4, 5]

// ── arguments is dynamic (tracks the caller's arguments); rest params are static ──
function dynamic() {
    console.log(arguments[0]);  // 1
    arguments[0] = 99;           // mutating arguments mutates the caller's argument!
    // (Only in non-strict mode. In strict mode, arguments is a copy.)
}
let x = 1;
dynamic(x);
console.log(x);  // 99 (non-strict) or 1 (strict — arguments is a copy)

Default Parameters and the TDZ

javascript
// ── Default parameters: evaluated at CALL TIME (not definition time) ──
function greet(name = "World", greeting = "Hello") {
    console.log(`${greeting}, ${name}!`);
}
greet();          // "Hello, World!" (both defaults)
greet("Alice");   // "Hello, Alice!" (greeting uses default)
greet("Alice", "Hi");  // "Hi, Alice!"
greet(undefined, "Hi");  // "Hi, World!" (undefined triggers default, not null)

// ⚠️ null does NOT trigger default (only undefined does)
greet(null, "Hi");  // "Hi, null!" (null is kept, not replaced with "World")

// ── Default params can reference earlier params (left-to-right) ──
function createRange(start, end = start + 10) {
    return [start, end];
}
createRange(5);  // [5, 15] — end defaults to start + 10

// ── ⚠️ Later params can't be referenced by earlier defaults (TDZ) ──
function bad(a = b, b = 10) {  // b is in TDZ when a's default is evaluated
    return a + b;
}
// bad();  // ReferenceError: Cannot access 'b' before initialization (TDZ)
bad(undefined, 20);  // works: a=undefined→b's default? No, a=undefined → a's default=b→TDZ!
// Actually: bad(undefined, 20) → a's default is `b`, but b=20 is passed, so a=b=20? No:
// When a=undefined triggers a's default (b), b is not yet initialized (TDZ) → ReferenceError
bad(1);  // a=1, b=10 (b's default, no TDZ issue since a has a value)

// ── Default param with function (evaluated at call time, not definition) ──
function withDate(data, timestamp = Date.now()) {
    return { data, timestamp };
}
// Each call gets a fresh Date.now() (default evaluated at call time, not definition)

// ── Default param with destructuring ──
function process({ port = 3000, host = "localhost" } = {}) {
    // The = {} default is for the WHOLE parameter (if no arg is passed)
    console.log(`${host}:${port}`);
}
process();          // "localhost:3000" (entire param defaults to {})
process({});        // "localhost:3000" (empty object → inner defaults)
process({ port: 8080 });  // "localhost:8080"

IIFE: Immediately Invoked Function Expression

javascript
// ── IIFE: creates a private scope, runs immediately (pre-ES6 module pattern) ──
// Pre-ES6, there were no block-scoped variables (only var, which is function-scoped).
// IIFEs were used to create private scopes and avoid polluting the global namespace.

// ── Standard IIFE ──
(function() {
    var privateVar = "secret";  // not accessible outside the IIFE
    console.log("runs immediately");
})();

// ── Arrow IIFE ──
(() => {
    const privateVar = "secret";
    console.log("runs immediately");
})();

// ── IIFE with parameters ──
(function(name, greeting) {
    console.log(`${greeting}, ${name}!`);
})("Alice", "Hello");

// ── Module pattern: IIFE returning an object (revealing module pattern) ──
const Counter = (function() {
    let count = 0;  // private (closure over the IIFE's scope)

    return {
        increment: () => ++count,
        decrement: () => --count,
        getCount: () => count,  // public API exposing private state
        // count is only accessible through these methods (encapsulation)
    };
})();
Counter.increment();
Counter.getCount();  // 1
// Counter.count;  // undefined — not accessible (private)

// ── Modern: ES modules replace IIFEs for scope isolation ──
// Each ES module has its own scope — no need for IIFEs for privacy.
// IIFEs are still useful for:
//   - One-off scope isolation in non-module scripts
//   - Avoiding global pollution in bookmarklets/inline scripts
//   - Creating async scopes: (async () => { await ... })();

💡 Tips & Tricks

javascript
// ── Named function expression for better stack traces ──
const handler = function handleClick(event) {
    // "handleClick" appears in stack traces (instead of "anonymous")
    // Also accessible inside for recursion
    console.log("clicked");
};
// Unnamed arrow functions show as "(anonymous)" in stack traces — harder to debug.

// ── Currying with arrow functions ──
const add = a => b => a + b;  // add(1)(2) = 3 (curried)
const add5 = add(5);          // partial application: add5(10) = 15

// ── Callback to promise (promisify a callback-style function) ──
function promisify(fn) {
    return (...args) => new Promise((resolve, reject) => {
        fn(...args, (err, result) => err ? reject(err) : resolve(result));
    });
}
// const readFile = promisify(fs.readFile);
// const content = await readFile("file.txt", "utf8");

// ── `call` and `apply` for explicit `this` binding ──
function greet(greeting) { return `${greeting}, ${this.name}`; }
const user = { name: "Alice" };
greet.call(user, "Hi");     // "Hi, Alice" — call: args one by one
greet.apply(user, ["Hi"]);  // "Hi, Alice" — apply: args as array
const bound = greet.bind(user);  // bind: creates new function with fixed `this`
bound("Hello");  // "Hello, Alice"

// ── `bind` for partial application ──
const add = (a, b) => a + b;
const add10 = add.bind(null, 10);  // first arg fixed to 10
add10(5);  // 15

⚠️ Edge Cases & Gotchas

javascript
// ── Arrow function returning object literal needs parentheses ──
const makeObj = () => { x: 1 };  // ⚠️ returns undefined! {x:1} is a block, not object
const makeObj2 = () => ({ x: 1 });  // ✓ parentheses force object literal return
console.log(makeObj());   // undefined
console.log(makeObj2());  // { x: 1 }

// ── Function declarations are hoisted; expressions are not ──
hoisted();  // works (declaration hoisted)
function hoisted() {}

// notHoisted();  // TypeError: notHoisted is not a function
var notHoisted = function() {};

// ── `this` in a regular function depends on the call site ──
function showThis() { console.log(this); }
showThis();          // global object (or undefined in strict mode)
const obj = { showThis };
obj.showThis();     // obj (method call — `this` is the receiver)
showThis.call({});  // {} (explicit binding via .call)

// ── Arrow function `this` is locked at definition, not call time ──
const obj = {
    name: "Alice",
    arrow: () => console.log(this),  // `this` is the module/global scope
    regular() { console.log(this); },  // `this` is obj when called as obj.regular()
};
obj.arrow();   // global object (or undefined in strict mode) — NOT obj
obj.regular();  // obj — dynamic `this`

// ── Default param TDZ: later params can't be referenced by earlier defaults ──
function f(a = b, b = 2) { return [a, b]; }
// f();  // ReferenceError: Cannot access 'b' before initialization
f(1);  // [1, 2] (a has a value, b uses default — no TDZ)
f(undefined, 3);  // ReferenceError (a=undefined → tries b's default → TDZ)

// ── `arguments` is NOT available in arrow functions ──
const arrow = () => arguments;  // ReferenceError (or uses enclosing function's arguments)
// Use ...args (rest params) in arrow functions instead.

🧠 Quick Quiz

What does this output?

javascript
const obj = {
    name: "Alice",
    greet: () => `Hello, ${this.name}`,
    greet2() { return `Hello, ${this.name}`; },
};
console.log(obj.greet());
console.log(obj.greet2());
Answer
"Hello, undefined"    // greet: arrow function — `this` is lexical (module scope), not obj
"Hello, Alice"        // greet2: regular method — `this` is obj (method call)

Arrow functions don't have their own this — they inherit it from the enclosing scope (the module/global scope, where this is undefined in strict mode or the global object in sloppy mode). this.name is therefore undefined.name → undefined.

Regular methods (shorthand syntax) have dynamic this — when called as obj.greet2(), this is obj, so this.name is "Alice".

The lesson: never use arrow functions for object methods that need this. Use shorthand method syntax (method() { ... }) or regular function expressions.