Functions and Scoping
Note: This post is based on my old programming study notes when I taught myself.
Normal Functions
Syntax:
function functionName(parameters) {
return RESULT;
}
- A list of parameters are enclosed by parentheses and separated by commas.
- Statements that define the function are enclosed by curly brackets
{ }
- The statement
return
specifies the value returned by the function.
Types of parameters
- Value parameter
- Object parameter
- Array parameter
Passing Functions as Arguments
Before understanding of Passing functions as arguments, it is required to get familiar with Function expressions first.
Function Expressions
Function Expression means that function
keyword can be used to define a function inside an expression.
Sometimes function can be anonymous; it does not have to have name and in this case, we can use function expression.
const mathSquare = function (number) {
return number * number;
};
const x = mathSquare(3); // x gets 9
In above code, a name is provided with a function expression. Providing a name allows the function to refer to itself.
Now, below code is example of passing a function as an argument to another function.
//Normal function map
function map(f, a) {
const result = new Array(a.length);
for (let i = 0; i < a.length; i++) {
result[i] = f(a[i]);
}
return result;
}
//Using Function expression
const f = function (x) {
return x * x * x;
};
//Array 'numbers'
const numbers = [0, 1, 2, 5, 10];
const cube = map(f, numbers);
console.log(cube); //[0, 1, 8, 125, 1000]
In above code, map
function that receives a function as first argument and an array as second argument. A function f
is defined by function expression.
Arrow Functions(a.k.a fat arrow(=>))
Syntax of arrow functions
Refer: MDN Arrow functions
Case 1:
(param) => expression;
Case 2:
(param1, paramN) => expression;
Case 3:
(param) => {
statements;
};
Case 4:
(param1, paramN) => {
statements;
};
Features of Arrow function expressions:
- Always anonymous, unnamed.
- Shorter syntax compared to function expressions
- Arrow functions don’t have its own
this
,argument
,super
, ornew.target
and cannot be used asmethods
. - Arrow functions cannot be used as constructors. Calling them with
new
throws aTypeError
. They also don’t have access to thenew.target
keyword. - Arrow functions cannot use
yield
within their body and cannot be created as generator functions. - Rest parameters, default parameters, and destructuring within params are supported, and always require parentheses.
- Like Case 1 and Case 2, parentheses around the parameter can only be omitted if the function has a single parameter.
- Parentheses around parameters are required when it has
- multiple parameters
- no parameters
- default destructed
- or rest parameters
- The braces
{ }
can be omitted if the function directly returns an expression. - If the body has additional lines of processing, the braces
{ }
andreturn
are required. - If arrow function needs to call itself, use a named function expression instead, or assign the arrow function to a variable so it has a name.
Example:
const func = (x) => x * x;
// concise body syntax, implied "return"
const func2 = (x, y) => {
return x + y;
};
// with block body, explicit "return" needed
- Returning object literals using the concise body syntax
(params) => { object: literal }
does not work, because JavaScript only sees the arrow function as having a concise body if the token following the arrow is not a left brace, so the code inside braces () is parsed as a sequence of statements, not a key in an object literal.- To fix this, wrap the object literal in parentheses:
const func=()=>({foo:1})
- To fix this, wrap the object literal in parentheses:
Stack Trace and Call Stack
A call stack is typically “the current stack of operations” - i.e. while it’s running. A stack trace is typically a copy of the call stack which is logged at some sort of failure, e.g. an exception.
In other words, while you’re debugging you will look at the current call stack - but when you look at logs, you’ll get a stack trace.
How Call Stack Works
Based on Advanced Javascript Error and Stack trace:
- Function Calling: Every time a function is called, it gets pushed onto the top of the stack.
- Function Return: When a function finishes executing, it gets removed from the top of the stack.
- LIFO Structure: The stack follows Last In, First Out (LIFO) principle - the last item added is the first to be removed.
function first() {
console.log("First function start");
second();
console.log("First function end");
}
function second() {
console.log("Second function start");
third();
console.log("Second function end");
}
function third() {
console.log("Third function");
}
first();
// Call stack: first() -> second() -> third()
// Output order: First start -> Second start -> Third -> Second end -> First end
Interview Question: Explain how JavaScript’s call stack works and what happens during a stack overflow.
Answer: The call stack keeps track of function calls in LIFO order. Stack overflow occurs when too many functions are called without returning, typically in infinite recursion scenarios.
Hoisting
Definition: Hoisting is JavaScript’s behavior of moving var
declarations and function declarations
to the top of their scope during compilation phase.
Key Points:
- JavaScript is synchronous, so hoisting completes before code execution begins
- JavaScript interpreter hoists the entire function declaration to the top of the current scope
- Function hoisting only works with function declarations — not with function expressions
Hoisting Examples
// Variable hoisting with var
console.log(x); // undefined (not ReferenceError)
var x = 5;
// Equivalent to:
var x;
console.log(x); // undefined
x = 5;
// Function declaration hoisting
sayHello(); // "Hello!" - works due to hoisting
function sayHello() {
console.log("Hello!");
}
// Function expression - NOT hoisted
sayGoodbye(); // TypeError: sayGoodbye is not a function
var sayGoodbye = function () {
console.log("Goodbye!");
};
Interview Question: What’s the difference between var
, let
, and const
hoisting?
Answer:
var
: Hoisted and initialized withundefined
let
/const
: Hoisted but not initialized (Temporal Dead Zone)- Function declarations: Fully hoisted (can be called before declaration)
Refer to MDN Hoisting doc
Scoping
Definition: The scope is the current context of execution in which values and expressions are “visible” or can be referenced. If a variable or expression is not in the current scope, it will not be available for use.
Key Principle: Scopes can be layered in a hierarchy, so that child scopes have access to parent scopes, but not vice versa.
Types of Scope
1. Global Scope
Variables declared outside any function or block have global scope.
var globalVar = "I'm global";
let globalLet = "I'm also global";
function test() {
console.log(globalVar); // Accessible
console.log(globalLet); // Accessible
}
2. Function Scope
Variables declared inside a function are only accessible within that function.
function myFunction() {
var functionScoped = "Only accessible inside function";
function innerFunction() {
console.log(functionScoped); // Accessible (closure)
}
}
// console.log(functionScoped); // ReferenceError
3. Block Scope (let
and const
)
Variables declared with let
and const
have block scope.
if (true) {
var varVariable = "I'm function scoped";
let letVariable = "I'm block scoped";
const constVariable = "I'm also block scoped";
}
console.log(varVariable); // "I'm function scoped"
// console.log(letVariable); // ReferenceError
// console.log(constVariable); // ReferenceError
Lexical Scoping and Closures
JavaScript uses lexical scoping - functions have access to variables in their outer scope at the time they were defined.
function outerFunction(x) {
// Outer scope
function innerFunction(y) {
// Inner scope - has access to 'x'
return x + y;
}
return innerFunction;
}
const addFive = outerFunction(5);
console.log(addFive(3)); // 8 (closure maintains access to x=5)
Interview Question: Explain the difference between function scope and block scope. Give examples.
Answer:
- Function scope: Variables declared with
var
are function-scoped, accessible throughout the entire function - Block scope: Variables declared with
let
/const
are block-scoped, only accessible within the nearest enclosing block{}
Interview Question: What is a closure and how does it work?
Answer: A closure is when an inner function has access to variables from its outer (enclosing) scope even after the outer function has finished executing. The inner function “closes over” the outer function’s variables.
Refer to MDN Scope doc