- Published on
Interesting topics of the JS land
- Authors
- Name
- Nazarii Mural
- @NazariiMural
Why does this post exist?
This post was created to collect cool topics of the JS land, that sometimes may be crucial to remember, and sometimes are just funny to know about. Briefly, this post it's a collection of the 'JS moments' - that we as developers don't see very often, but understanding them may be very helpful.
Most of the topics gonna be something like: guess the output or why it doesn't work.
EventLoop execution order: promises
, queueMicrotask
.
Guess the output order.
Promise.resolve().then(() => console.log(1));
queueMicrotask(() => console.log(2));
setTimeout(() => {
console.log(3);
}, 0);
console.log(4);
new Promise(() => {
console.log(5);
});
(async () => console.log(6))();
Answer
4, 5, 6, 1, 2, 3
Explanation:
console.log(4);
- that is a synchronous function call.Body of the
new Promise()
always runs synchronously.Immediately invoked function expression IIFE:
(async () => console.log(1))(); new Promise(() => console.log(2)); console.log(3); // 1, 2, 3
Since the call stack is empty now, EventLoop can process queues: microtask and after macrotask. And first item in microtask queue is resolved promise:
then(() => console.log(1)
.Next
queueMicrotask(() => console.log(2));
- based on the name it's obvious from what queue this callback came to call stack.Since callstack is empty and nothing else in microtask queue, EventLoop will process macrotask queue:
setTimeout(() => console.log(3), 0);
Which of the following are not considered microtask in JavaScript's EventLoop?
a. Script
loading
b. setTimeout
callback
c. mousemove
event
d. requestAnimationFrame
callback
e. UI rendering
f. then
callback
g. new Promise
execution function
h. fetch
calls
Answer
All of them except then
callback. Only then
callback will be added to the microtask
queue.
EventLoop execution order: promises
, async/await
.
What value will be last one?
async function asyncFunction() {
console.log(1);
new Promise(() => console.log(2));
await new Promise((res) => {
setTimeout(() => console.log(3), 0);
res();
});
}
new Promise((res) => {
console.log(4);
(async () => {
console.log(5);
await asyncFunction()
console.log(6);
})();
res();
}).then(() => console.log(7));
console.log(8);
Answer
4, 5, 1, 2, 8, 6, 7, 3
So, What exactly happened?
new Promise
pushed to the call stack.- First thing in its body is:
console.log(4);
pushed to the callstack, executed, and popped out. - After that we have IIFE
async () =>
:- this body runs synchronously till the
await
moment.console.log(5);
-> to stack, executed, popped.
- this body runs synchronously till the
asyncFunction
- pushed to stack.console.log(1);
- to stack, executed, popped.new Promise(() => console.log(2));
- to stack, executed, popped.await new Promise((res)
- pushed to stack.setTimeout(() => console.log(3), 0)
- to stack, to the web timer API, and on the NextTick of EventLoop to the macrotask queue.
- Body of
asyncFunction
executed, pushed to its queues - we are still awaiting andasyncFunction
itself pushed to the microtask queue and EventLoop will execute, rest of the code. res();
pushed to the stack,await new Promise
popped out from the stack.then(() => console.log(7));
- to stack and its callbackconsole.log(7)
to the microtask queue.console.log(8);
- to stack, executed, popped.- Now callstack is empty: EventLoop will start to pop out from queues.
asyncFunction
from the microtask queue - implicitreturn undefined
.- Since
asyncFunction
executed completely, rest of(async () => {
can be pushed to microtask queue -console.log(6);
to the microtask queue. console.log(7)
- to stack, executed, popped.- Rest of the
(async () => {
to the stack ->console.log(6);
- to stack, executed, popped. () => console.log(3)
- to stack, executed, popped.
EventLoop execution order: async
, Promise.all
.
What value will be first one?
(async () => {
const asyncFunc = async () => "asyncFunc";
const promise = new Promise(() => {
console.log("promise");
}).then(() => console.log("then"));
console.log("asyncBody");
queueMicrotask(() => {
console.log("queueMicrotask");
});
const results = await Promise.all([asyncFunc(), promise]);
return results;
})();
console.log("script");
Answer
promise
, asyncBody
, script
, queueMicrotask
Why, there is no then
from the line 6? Because new Promise
defined on line 4, was never resolved - it's remained in a pending
state.
Scopes and Closures
There are three types of scopes in JavaScript: Global
, Block
, Function
const name = "John" ; // Global Scope
{
const name = "John"; // Block Scope
}
function foo() {
const name = "John"; // Function Scope
}
When one function is defined inside another - the inner function has access to the outer function's scope. JavaScript closure it's a combination of a function and references to its lexical environment. In JavaScript, closures are created every time a function is created, at function creation time.
What is the output?
function createCounter() {
let global = 0;
function incrementCounter() {
const incremented = ++global;
return incremented;
}
return { incrementCounter };
}
const counter = createCounter();
console.log(counter.incrementCounter());
console.log(counter.incrementCounter());
console.log(createCounter().incrementCounter());
Answer
1
, 2
, 1
Are these objects are equal?
function userFactory() {
let user = { name: "", createAt: Date.now() };
return (name) => {
"use strict";
user.name = name;
user.createAt = Date.now();
return user;
};
}
const createUser = userFactory();
console.log(createUser("John") === createUser("NotJohn"));
Answer
Yes, it's the same object.
userFactory
function it's a buggy implementation of the factory pattern, to fix the bug new object must be returned. But I think that is great example of the closure in JavaScript.
When we are comparing objects - we are comparing just reference in memory
.
What is a Hoisting?
Spoiler alert: Word Hoisting is a bit a magic word. JS Engine does not move anything to the top of file while parsing it. So, there is no such thing as a hoisting...?
There is a term called hoisting, but it doesn't mean that something changing its position, we will return to the hoisting definition a bit later in this post, but for now, let's consider the next example.
Which statements are correct?
- a. Hoisting is a process of moving functions and variables to the top of the file
- b. Variables declared with the
let
andconst
are hoisted - c. Variables declared with the
var
keyword are uninitialized - d. Hoisting occurred during the execution phase
- e.
import
declarations are hoisted
Answer
b
and e
When the JS engine starts to work with any .js
files it makes an environment called the Execution Context and it has two phases:
- Creation phase
- Execution phase
During the creation phase JS engine:
- Creates a global object:
window/global
- Sets up memory for storing
variables
andfunctions
. - Stores the variables with the
var
keyword with default valueundefined
and function declaration name with the function reference.
During the execution phase:
- JavaScript engine executes the code line by line. This includes evaluating and executing statements.
Hoisting examples
const framework = "Next.js";
let renderingPattern = "SSR";
var bestProgrammingLanguage = "HTML๐";
During the creation phase, all those variables except var bestProgrammingLanguage
gonna be Uninitialized
and only variables with var
keyword will have an initial value as undefined
, during the execution phase, all variables gonna be assigned to corresponding values.
Please take a look at the one more example below:
var counter = 1;
function resetCounter() {
console.log(counter); // undefined
var counter = 0;
console.log(counter); // 0
};
resetCounter();
One more example with error:
let counter = 1;
function resetCounter() {
console.log(counter);
const counter = 0;
console.log(counter);
};
resetCounter();
As you may guess above code gonna throws an error Uncaught ReferenceError: Cannot access 'counter' before initialization
.
All JS code runs line by line during the execution phase and when we are trying to log console.log(counter);
JS engine throws an error - that counter isn't initialized - this means that counter
variable is so-called hoisted - it already exists in our Function lexical environment, otherwise error would be totally different. Check image below:
So shortly - Hoisting it's a process of setting the default value to the corresponding declaration during the creation phase.
What is a this
keyword?
The value of this
keyword depends on where we are using it.
Global context - when this
is used in a global context the value of this
gonna be a global object.
Function context - when this
is used in function context the value of this
gonna be the object
on which the function is invoked.
Arrow function - when this
is used in arrow function the value of this
gonna be determined by the lexical environment in which the arrow function is defined.
Classes - when this
is used in classes the value of this
gonna be an instance of that class.
Strict mode - when this
is pointing to a global object in strict mode the value of this
gonna be undefined
.
this
examples
Would you be able to answer all of these questions correctly?
function printContext() {
console.log(this);
}
const thatObj = {
printContext,
printContext_1() {
return printContext();
},
printContext_2() {
return thatObj.printContext();
},
};
thatObj.printContext();
thatObj.printContext_1();
thatObj.printContext_2();
Answer
thatObj
, global
, thatObj
thatObj.printContext
- printContext function was assigned to the printContext props ofthatObj
, and we called body of this function in the context of thethatObj
.thatObj.printContext_1
- is just the return result ofprintContext
function, so nothing related tothatObj
.thatObj.printContext_2
- is the same asthis.printContext()
.
const thatObj_1 = {
foo() {
console.log(this);
},
bar: () => {
console.log(this);
},
};
const thatObj_2 = {
foo: thatObj_1.foo,
bar: () => thatObj_1.bar(),
baz() {
thatObj_1.foo();
},
bam() {
thatObj_1.foo.call(this);
},
bah() {
const nestedF = () => {
console.log(this);
};
nestedF();
},
};
thatObj_2.foo();
thatObj_2.bar();
thatObj_2.baz();
thatObj_2.bam();
thatObj_2.bah();
Answer
thatObj_2
, global
, thatObj_1
, thatObj_2
, thatObj_2
thatObj_2.bam
- we calledfoo
method withthis
ofthatObj_2
. In case of object methods, it doesn't matter where object method is defined - it only matters how it's invoked.thatObj_2.bah
- in this casethis
will be pointing to the parent scope, which isthatObj_2
.
const thatObj = {
printContext() {
console.log(this);
},
printContext_1() {
function nestedF() {
console.log(this);
};
nestedF.apply(this);
},
};
const { printContext, printContext_1 } = thatObj;
printContext();
printContext_1();
thatObj.printContext();
thatObj.printContext_1();
Answer
global
, global
, thatObj
, thatObj
printContext()
andprintContext_1()
- when those methods were destructured, the value ofthis
was pointed to the context in which these methods were called, in this case, global object.
Is it possible to iterate through an object using Array.forEach
method?
Yes! We can just call
it๐.
const arrayLike = {
length: 3,
0: 2,
1: 3,
2: 4,
3: 5, // ignored by forEach() since length is 3
};
Array.prototype.forEach.call(arrayLike, (x) => console.log(x)); // 2, 3, 4