Function declarations vs function expressions
Function declarations
A function declaration is a statement that defines a function with a name. It is typically used to declare a function that can be called multiple times throughout the enclosing scope.
function foo() {
console.log("FOOOOO");
}Function expressions
A function expression is an expression that defines a function and assigns it to a variable. It is often used when a function is needed only once or in a specific context.
var foo = function () {
console.log("FOOOOO");
};Note: The examples uses var due to legacy reasons. Function expressions
can be defined using let and const and the key difference is in the hoisting
behavior of those keywords.
Key differences
-
Hoisting: The key difference is that function declarations have its body hoisted but the bodies of function expressions are not (they have the same hoisting behavior as
var-declared variables). For more explanation on hoisting, refer to the quiz question on hoisting. If you try to invoke a function expression before it is defined, you will get anUncaught TypeError: XXX is not a functionerror.Example
Function declarations:
foo(); // 'FOOOOO' function foo() { console.log("FOOOOO"); }Function expressions:
foo(); // Uncaught TypeError: foo is not a function var foo = function () { console.log("FOOOOO"); }; -
Name scope: Function expressions can be named by defining it after the
functionand before the parenthesis. However when using named function expressions, the function name is only accessible within the function itself. Trying to access it outside will result inundefinedand calling it will result in an error.Example
const myFunc = function namedFunc() { console.log(namedFunc); // Works }; console.log(namedFunc); // undefined
When to use each
- Function declarations:
- When you want to create a function on the global scope and make it available throughout the enclosing scope.
- If a function is reusable and needs to be called multiple times.
- Function expressions:
- If a function is only needed once or in a specific context.
- Use to limit the function availability to subsequent code and keep the enclosing scope clean.
In general, it’s preferable to use function declarations to avoid the mental overhead of determining if a function can be called. The practical usages of function expressions is quite rare.
Arrow functions
Arrow functions provide a concise syntax for writing functions in JavaScript.
They are particularly useful for maintaining the this context within methods
and callbacks. For example, in an event handler or array method like map,
arrow functions can simplify the code and avoid issues with this binding.
As a method inside a constructor
The main advantage of using an arrow function as a method inside a constructor
is that the value of this gets set at the time of the function creation and
can’t change after that. When the constructor is used to create a new object,
this will always refer to that object.
For example, let’s say we have a Person constructor that takes a first name as
an argument has two methods to console.log() that name, one as a regular
function and one as an arrow function:
const Person = function (name) {
this.name = name;
this.sayName1 = function () {
console.log(this.name);
};
this.sayName2 = () => {
console.log(this.name);
};
};
const john = new Person("John");
const dave = new Person("Dave");
john.sayName1(); // John
john.sayName2(); // John
// The regular function can have its `this` value changed, but the arrow function cannot
john.sayName1.call(dave); // Dave (because `this` is now the dave object)
john.sayName2.call(dave); // John
john.sayName1.apply(dave); // Dave (because `this` is now the dave object)
john.sayName2.apply(dave); // John
john.sayName1.bind(dave)(); // Dave (because `this` is now the dave object)
john.sayName2.bind(dave)(); // John
const sayNameFromWindow1 = john.sayName1;
sayNameFromWindow1(); // undefined (because `this` is now the window object)
const sayNameFromWindow2 = john.sayName2;
sayNameFromWindow2(); // JohnThe main takeaway here is that this can be changed for a normal function, but
this always stays the same for an arrow function. So even if you are passing
around your arrow function to different parts of your application, you wouldn’t
have to worry about the value of this changing.
Simplifying syntax
Arrow functions provide a more concise way to write functions. This is especially useful for short functions or callbacks.
// Traditional function
const add = function (a, b) {
return a + b;
};
// Arrow function
const add = (a, b) => a + b;Lexical `this` binding
Arrow functions do not have their own this context. Instead, they inherit
this from the surrounding scope. This is particularly useful in methods and
callbacks where the this context can be tricky.
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
const timer = new Timer();In the example above, using a traditional function inside setInterval would
require additional steps to maintain the correct this context.
Using arrow functions in array methods
Arrow functions are often used in array methods like map, filter, and
reduce for cleaner and more readable code.
const numbers = [1, 2, 3, 4, 5];
// Traditional function
const doubled = numbers.map(function (n) {
return n * 2;
});
// Arrow function
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]Event handlers
Arrow functions can be used in event handlers to maintain the this context of
the class or object.
class Button {
constructor() {
this.count = 0;
this.button = document.createElement("button");
this.button.innerText = "Click me";
this.button.addEventListener("click", () => {
this.count++;
console.log(this.count);
});
document.body.appendChild(this.button);
}
}
const button = new Button();Anonymous functions
Anonymous functions provide a more concise way to define functions, especially for simple operations or callbacks. Besides that, they can also be used in the following scenarios:
-
Immediate execution: Anonymous functions are commonly used in Immediately Invoked Function Expressions (IIFEs) to encapsulate code within a local scope. This prevents variables declared within the function from leaking to the global scope and polluting the global namespace.
Example
// This is an IIFE (function () { var x = 10; console.log(x); // 10 })(); // x is not accessible here console.log(typeof x); // undefinedIn the above example, the IIFE creates a local scope for the variable
x. As a result,xis not accessible outside the IIFE, thus preventing it from leaking into the global scope. -
Callbacks: Anonymous functions can be used as callbacks that are used once and do not need to be used anywhere else. The code will seem more self-contained and readable when handlers are defined right inside the code calling them, rather than having to search elsewhere to find the function body.
Example
setTimeout(() => { console.log("Hello world!"); }, 1000); -
Higher-order functions: It is used as arguments to functional programming constructs like Higher-order functions or Lodash (similar to callbacks). Higher-order functions take other functions as arguments or return them as results. Anonymous functions are often used with higher-order functions like
map(),filter(), andreduce().Example
const arr = [1, 2, 3]; const double = arr.map((el) => { return el * 2; }); console.log(double); // [2, 4, 6] -
Event Handling: In React, anonymous functions are widely used for defining callback functions inline for handling events and passing callbacks as props.
Example
function App() { return <button onClick={() => console.log("Clicked!")}>Click Me</button>; }
Higher Order Functions
A higher-order function is any function that takes one or more functions as arguments, which it uses to operate on some data, and/or returns a function as a result.
Higher-order functions are meant to abstract some operation that is performed
repeatedly. The classic example of this is Array.prototype.map(), which takes
an array and a function as arguments. Array.prototype.map() then uses this
function to transform each item in the array, returning a new array with the
transformed data. Other popular examples in JavaScript are
Array.prototype.forEach(), Array.prototype.filter(), and
Array.prototype.reduce(). A higher-order function doesn’t just need to be
manipulating arrays as there are many use cases for returning a function from
another function. Function.prototype.bind() is an example that returns another
function.
Example
function higherOrder(fn) {
fn();
}
higherOrder(function () {
console.log("Hello world");
});function higherOrder2() {
return function () {
return "Do something";
};
}
var x = higherOrder2();
x(); // Returns "Do something"Currying
Currying is a functional programming technique where a function with multiple arguments is decomposed into a sequence of functions, each taking a single argument. This allows for the partial application of functions, enabling more flexible and reusable code.
- Transformation: A function that takes multiple arguments is transformed into a series of nested functions, each taking one argument.
- Partial application: You can call the curried function with fewer arguments than it expects, and it will return a new function that takes the remaining arguments.
Benefits of currying:
- Reusability: Curried functions can be reused with different sets of arguments.
- Partial application: You can create new functions by fixing some arguments of the original function.
- Function composition: Currying makes it easier to compose functions, leading to more readable and maintainable code.
// Non-curried function
function add(a, b, c) {
return a + b + c;
}
// Curried version of the same function
function curriedAdd(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
// Using the curried function
const addOne = curriedAdd(1);
const addOneAndTwo = addOne(2);
const result = addOneAndTwo(3); // result is 6You can also use arrow functions to make the syntax more concise:
const curriedAdd = (a) => (b) => (c) => a + b + c;
const addOne = curriedAdd(1);
const addOneAndTwo = addOne(2);
const result = addOneAndTwo(3); // result is 6this keyword
this in JavaScript refers to the current execution context of a function or
script.
-
If the
newkeyword is used when calling the function, meaning the function was used as a function constructor, thethisinside the function is the newly-created object instance.Example
When a function is used as a constructor (called with the `new` keyword), `this` refers to the newly-created instance. In the following example, `this` refers to the `Person` object being created, and the `name` property is set on that object.function Person(name) { this.name = name; } const person = new Person("John"); console.log(person.name); // "John" -
If
thisis used in aclassconstructor, thethisinside theconstructoris the newly-created object instance.Example
In ES2015 classes, `this` behaves as it does in object methods. It refers to the instance of the class.class Person { constructor(name) { this.name = name; } showThis() { console.log(this); } } const person = new Person("John"); person.showThis(); // Person {name: 'John'} const showThisStandalone = person.showThis; showThisStandalone(); // `undefined` because all parts of a class' body are strict mode. -
If
apply(),call(), orbind()is used to call/create a function,thisinside the function is the object that is passed in as the argument.Example
Using the
call()andapply()methods allow you to explicitly set the value ofthiswhen calling the function.function showThis() { console.log(this); } const obj = { name: "John" }; showThis.call(obj); // { name: 'John' } showThis.apply(obj); // { name: 'John' }The
bind()method creates a new function withthisbound to the specified value.function showThis() { console.log(this); } const obj = { name: "John" }; const boundFunc = showThis.bind(obj); boundFunc(); // { name: 'John' } -
If a function is called as a method (e.g.
obj.method()) —thisis the object that the function is a property of.Example
const obj = { name: "John", showThis: function () { console.log(this); }, }; obj.showThis(); // { name: 'John', showThis: ƒ } -
If a function is invoked as a free function invocation, meaning it was invoked without any of the conditions present above,
thisis the global object. In the browser, the global object is thewindowobject. If in strict mode ('use strict';),thiswill beundefinedinstead of the global object.Example
In the global scope, `this` refers to the global object, which is the `window` object in a web browser or the `global` object in a Node.js environment.console.log(this); // In a browser, this will log the window object (for non-strict mode). -
If multiple of the above rules apply, the rule that is higher wins and will set the
thisvalue. -
If the function is an ES2015 arrow function, it ignores all the rules above and receives the
thisvalue of its surrounding scope at the time it is created.Example
In this example, `this` refers to the global object (window or global), because the arrow function is not bound to the `person` object.const person = { name: "John", sayHello: () => { console.log(`Hello, my name is ${this.name}!`); }, }; person.sayHello(); // "Hello, my name is undefined!"In the following example, the
thisin the arrow function will be thethisvalue of its enclosing context, so it depends on howshowThis()is called.const obj = { name: "John", showThis: function () { const arrowFunc = () => { console.log(this); }; arrowFunc(); }, }; obj.showThis(); // { name: 'John', showThis: ƒ } const showThisStandalone = obj.showThis; showThisStandalone(); // In non-strict mode: Window (global object). In strict mode: undefined.Therefore, the
thisvalue in arrow functions cannot be set bybind(),apply()orcall()methods, nor does it point to the current object in object methods.const obj = { name: "Alice", regularFunction: function () { console.log("Regular function:", this.name); }, arrowFunction: () => { console.log("Arrow function:", this.name); }, }; const anotherObj = { name: "Bob", }; // Using call/apply/bind with a regular function obj.regularFunction.call(anotherObj); // Regular function: Bob obj.regularFunction.apply(anotherObj); // Regular function: Bob const boundRegularFunction = obj.regularFunction.bind(anotherObj); boundRegularFunction(); // Regular function: Bob // Using call/apply/bind with an arrow function, `this` refers to the global scope and cannot be modified. obj.arrowFunction.call(anotherObj); // Arrow function: window/undefined (depending if strict mode) obj.arrowFunction.apply(anotherObj); // Arrow function: window/undefined (depending if strict mode) const boundArrowFunction = obj.arrowFunction.bind(anotherObj); boundArrowFunction(); // Arrow function: window/undefined (depending if strict mode)
call() and apply()
Both .call and .apply are used to invoke functions and the first parameter
will be used as the value of this within the function. However, .call takes
in comma-separated arguments as the next arguments while .apply takes in an
array of arguments as the next argument.
An easy way to remember this is C for call and comma-separated and A for
apply and an array of arguments.
function add(a, b) {
return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3Context management
.call and .apply can set the this context explicitly when invoking methods
on different objects.
const person = {
name: "John",
greet() {
console.log(`Hello, my name is ${this.name}`);
},
};
const anotherPerson = { name: "Alice" };
person.greet.call(anotherPerson); // Hello, my name is Alice
person.greet.apply(anotherPerson); // Hello, my name is AliceFunction borrowing
Both .call and .apply allow borrowing methods from one object and using them
in the context of another. This is useful when passing functions as arguments
(callbacks) and the original this context is lost. .call and .apply allow
the function to be invoked with the intended this value.
function greet() {
console.log(`Hello, my name is ${this.name}`);
}
const person1 = { name: "John" };
const person2 = { name: "Alice" };
greet.call(person1); // Hello, my name is John
greet.call(person2); // Hello, my name is AliceAlternative syntax to call methods on objects
.apply can be used with object methods by passing the object as the first
argument followed by the usual parameters.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2); // Same as arr1.push(4, 5, 6)
console.log(arr1); // [1, 2, 3, 4, 5, 6]Deconstructing the above:
- The first object,
arr1will be used as thethisvalue. .push()is called onarr1usingarr2as arguments as an array because it’s using.apply().Array.prototype.push.apply(arr1, arr2)is equivalent toarr1.push(...arr2).
bind()
Function.prototype.bind allows you to create a new function with a specific
this context and, optionally, preset arguments. bind() is most useful for
preserving the value of this in methods of classes that you want to pass into
other functions.
bind was frequently used on legacy React class component methods which were
not defined using arrow functions.
const john = {
age: 42,
getAge: function () {
return this.age;
},
};
console.log(john.getAge()); // 42
const unboundGetAge = john.getAge;
console.log(unboundGetAge()); // undefined
const boundGetAge = john.getAge.bind(john);
console.log(boundGetAge()); // 42
const mary = { age: 21 };
const boundGetAgeMary = john.getAge.bind(mary);
console.log(boundGetAgeMary()); // 21In the example above, when the getAge method is called without a calling
object (as unboundGetAge), the value is undefined because the value of
this within getAge() becomes the global object. boundGetAge() has its
this bound to john, hence it is able to obtain the age of john.
We can even use getAge on another object which is not john!
boundGetAgeMary returns the age of mary.
Preserving context and fixing the this value in callbacks
When you pass a function as a callback, the this value inside the function can
be unpredictable because it is determined by the execution context. Using
bind() helps ensure that the correct this value is maintained.
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}`);
},
};
const john = new Person('John Doe');
// Without bind(), `this` inside the callback will be the global object
setTimeout(john.greet, 1000); // Output: "Hello, my name is undefined"
// Using bind() to fix the `this` value
setTimeout(john.greet.bind(john), 2000); // Output: "Hello, my name is John Doe"You can also use
arrow functions
to define class methods for this purpose instead of using bind. Arrow
functions have the this value bound to its lexical context.
class Person {
constructor(name) {
this.name = name;
}
greet = () => {
console.log(`Hello, my name is ${this.name}`);
};
}
const john = new Person("John Doe");
setTimeout(john.greet, 1000); // Output: "Hello, my name is John Doe"Partial application of functions (currying)
bind can be used to create a new function with some arguments pre-set. This is
known as partial application or currying.
function multiply(a, b) {
return a * b;
}
// Using bind() to create a new function with some arguments pre-set
const multiplyBy5 = multiply.bind(null, 5);
console.log(multiplyBy5(3)); // Output: 15Method borrowing
bind allows you to borrow methods from one object and apply them to another
object, even if they were not originally designed to work with that object. This
can be handy when you need to reuse functionality across different objects
const person = {
name: "John",
greet: function () {
console.log(`Hello, ${this.name}!`);
},
};
const greetPerson = person.greet.bind({ name: "Alice" });
greetPerson(); // Output: Hello, Alice!Scoping
Global, function, and block scope
In JavaScript, scope determines the accessibility of variables and functions at
different parts of the code. There are three main types of scope: global scope,
function scope, and block scope. Global scope means the variable is accessible
everywhere in the code. Function scope means the variable is accessible only
within the function it is declared. Block scope, introduced with ES6, means the
variable is accessible only within the block (e.g., within curly braces {}) it
is declared.
- Global scope: Variables declared in the global scope are accessible from
anywhere in the code. In a browser environment, these variables become
properties of the
windowobject.
Example
var globalVar = "I'm global";
function checkGlobal() {
console.log(globalVar); // Accessible here
}
checkGlobal(); // Output: "I'm global"
console.log(globalVar); // Output: "I'm global"- Function scope: Variables declared within a function are only accessible
within that function. This is true for variables declared using
var,let, orconst.
Example
function myFunction() {
var functionVar = "I'm in a function";
console.log(functionVar); // Accessible here
}
myFunction(); // Output: "I'm in a function"
// console.log(functionVar); // Uncaught ReferenceError: functionVar is not defined- Block scope: Variables declared with
letorconstwithin a block (e.g., within{}) are only accessible within that block. This is not true forvar, which is function-scoped.
Example
if (true) {
let blockVar = "I'm in a block";
console.log(blockVar); // Accessible here
}
// console.log(blockVar); // Uncaught ReferenceError: blockVar is not defined
if (true) {
var blockVarVar = "I'm in a block but declared with var";
console.log(blockVarVar); // Accessible here
}
console.log(blockVarVar); // Output: "I'm in a block but declared with var"Lexical scope
JavaScript uses lexical scoping, meaning that the scope of a variable is determined by its location within the source code. Nested functions have access to variables declared in their outer scope.
Example
function outerFunction() {
var outerVar = "I am outside";
function innerFunction() {
console.log(outerVar); // Accessible here
}
innerFunction();
}
outerFunction();Lexical scoping is closely related to closures. A closure is created when a function retains access to its lexical scope, even when the function is executed outside that scope.
In this example:
outerFunctionreturnsinnerFunction.myInnerFunctionis assigned the returnedinnerFunction.- When
myInnerFunctionis called, it still has access toouterVariablebecause of the closure created by lexical scoping.
Avoid modifying global scope
JavaScript that is executed in the browser has access to the global scope (the
window object). In general it’s a good software engineering practice to not
pollute the global namespace unless you are working on a feature that truly
needs to be global – it is needed by the entire page. Several reasons to avoid
touching the global scope:
- Naming conflicts: Sharing the global scope across scripts can cause conflicts and bugs when new global variables or changes are introduced.
- Cluttered global namespace: Keeping the global namespace minimal avoids making the codebase hard to manage and maintain.
- Scope leaks: Unintentional references to global variables in closures or event handlers can cause memory leaks and performance issues.
- Modularity and encapsulation: Good design promotes keeping variables and functions within their specific scopes, enhancing organization, reusability, and maintainability.
- Security concerns: Global variables are accessible by all scripts, including potentially malicious ones, posing security risks, especially if sensitive data is stored there.
- Compatibility and portability: Heavy reliance on global variables reduces code portability and integration ease with other libraries or frameworks.
Follow these best practices to avoid global scope pollution:
- Use local variables: Declare variables within functions or blocks using
var,let, orconstto limit their scope. - Pass variables as function parameters: Maintain encapsulation by passing variables as parameters instead of accessing them globally.
- Use immediately invoked function expressions (IIFE): Create new scopes with IIFEs to prevent adding variables to the global scope.
- Use modules: Encapsulate code with module systems to maintain separate scopes and manageability.
Closures
-
Functions have access to variables that were in their scope at the time of their creation. This is what we call the function’s lexical scope. A closure is a function that retains access to these variables even after the outer function has finished executing.
Example
function outerFunction() { const outerVar = "I am outside of innerFunction"; function innerFunction() { console.log(outerVar); // `innerFunction` can still access `outerVar`. } return innerFunction; } const inner = outerFunction(); // `inner` now holds a reference to `innerFunction`. inner(); // "I am outside of innerFunction" // Even though `outerFunction` has completed execution, `inner` still has access to variables defined inside `outerFunction`. -
Closure allows a function to remember the environment in which it was created, even if that environment is no longer present. This is like the function has a memory of its original environment.
-
Closures are often used to maintain state in a secure way because the variables captured by the closure are not accessible outside the function.
-
Closures are used extensively in JavaScript, such as in callbacks, event handlers, and asynchronous functions.
-
With ES6, closures can be created using arrow functions, which provide a more concise syntax and lexically bind the
thisvalue.Example
const createCounter = () => { let count = 0; return () => { count += 1; return count; }; }; const counter = createCounter(); console.log(counter()); // Outputs: 1 console.log(counter()); // Outputs: 2
Benefits
Using closures provide the following benefits:
- Data encapsulation: Closures provide a way to create private variables and functions that can’t be accessed from outside the closure. This is useful for hiding implementation details and maintaining state in an encapsulated way.
- Functional programming: Closures are fundamental in functional programming paradigms, where they are used to create functions that can be passed around and invoked later, retaining access to the scope in which they were created, e.g. partial applications or currying.
- Event handlers and callbacks: In JavaScript, closures are often used in event handlers and callbacks to maintain state or access variables that were in scope when the handler or callback was defined.
- Module patterns: Closures enable the module pattern in JavaScript, allowing the creation of modules with private and public parts.
Pitfalls
- Memory leaks: Closures can cause memory leaks if they capture variables that are no longer needed. This happens because closures keep references to the variables in their scope, preventing the garbage collector from freeing up memory.
Example
function createClosure() {
let largeArray = new Array(1000000).fill("some data");
return function () {
console.log(largeArray[0]);
};
}
let closure = createClosure();
// The largeArray is still in memory because the closure keeps a reference to it- Debugging complexity: Closures can make debugging more difficult due to the complexity of the scope chain. When a bug occurs, it can be challenging to trace the source of the problem through multiple layers of nested functions and scopes.
Example
function outerFunction() {
let outerVar = "I am outside!";
function innerFunction() {
console.log(outerVar); // What if outerVar is not what you expect?
}
return innerFunction;
}
let myFunction = outerFunction();
myFunction();- Performance issues: Overusing closures or using them inappropriately can lead to performance issues. Since closures keep references to variables in their scope, they can prevent garbage collection, leading to increased memory usage and potential slowdowns.
Example
function createManyClosures() {
let counter = 0;
for (let i = 0; i < 1000000; i++) {
(function () {
counter++;
})();
}
console.log(counter); // This can be inefficient
}
createManyClosures();- Unintended variable sharing: Closures can lead to unintended variable sharing, especially in loops. This happens when all closures share the same reference to a variable, leading to unexpected behavior.
Example
function createFunctions() {
let functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function () {
console.log(i); // All functions will log the same value of i
});
}
return functions;
}
let funcs = createFunctions();
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3To avoid this, use let instead of var to create a new binding for each
iteration:
function createFunctions() {
let functions = [];
for (let i = 0; i < 3; i++) {
functions.push(function () {
console.log(i); // Each function will log its own value of i
});
}
return functions;
}
let funcs = createFunctions();
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2Hoisting
Hoisting is a term used to explain the behavior of variable declarations in your
code. Variables declared or initialized with the var keyword will have their
declaration “moved” up to the top of their containing scope during compilation,
which we refer to as hoisting.
Only the declaration is hoisted, the initialization/assignment (if there is one), will stay where it is. Note that the declaration is not actually moved – the JavaScript engine parses the declarations during compilation and becomes aware of variables and their scopes, but it is easier to understand this behavior by visualizing the declarations as being “hoisted” to the top of their scope.
Hoisting behavior
- Variable declarations (
var): Declarations are hoisted, but not initializations. The value of the variable isundefinedif accessed before initialization. - Variable declarations (
letandconst): Declarations are hoisted, but not initialized. Accessing them results inReferenceErroruntil the actual declaration is encountered. - Function expressions (
var): Declarations are hoisted, but not initializations. The value of the variable isundefinedif accessed before initialization. - Function declarations (
function): Both declaration and definition are fully hoisted. - Class declarations (
class): Declarations are hoisted, but not initialized. Accessing them results inReferenceErroruntil the actual declaration is encountered. - Import declarations (
import): Declarations are hoisted, and side effects of importing the module are executed before the rest of the code.
Under the hood
In reality, JavaScript creates all variables in the current scope before it even
tries to executes the code. Variables created using var keyword will have the
value of undefined where variables created using let and const keywords
will be marked as <value unavailable>. Thus, accessing them will cause a
ReferenceError preventing you to access them before initialization.
In ECMAScript specifications let and const declarations are
explained as below:
The variables are created when their containing Environment Record is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated.
However, this statement is
a litle bit different for the var keyword:
Var variables are created when their containing Environment Record is instantiated and are initialized to
undefinedwhen created.
Modern practices
In practice, modern code bases avoid using var and use let and const
exclusively. It is recommended to declare and initialize your variables and
import statements at the top of the containing scope/module to eliminate the
mental overhead of tracking when a variable can be used.
ESLint is a static code analyzer that can find violations of such cases with the following rules:
no-use-before-define: This rule will warn when it encounters a reference to an identifier that has not yet been declared.no-undef: This rule will warn when it encounters a reference to an identifier that has not yet been declared.