Combined Javascript Questions
Over the last few days, I've been going through the fantastic Javascript Questions repo to see what parts of javascript I can discover and understand better. I got 86/156 correct with a 44.9% error rate, but at least I passed!
Most of my mistakes were due to the general lack of knowledge about basic mechanics of functions and javascript classes, but there were a few I felt were interesting because they were pretty new and strange to me.
I'm still wrapping my head over generators as I don't use them much, except in redux-saga. Other things I thought were pretty useful to understand was the javascript event loop.
In this post, I share a question as a code snippet that I put together to illustrate and try to explain some of the things that were strange to me.
My question
What is the output of running this?
async function* strangeFunction(...args) {
for (arg of args) {
yield Promise.resolve(arg)
}
}
const myPromise = async () => {
const returnData = []
const strangeData = strangeFunction`${'1'}2${'3'}4`
for await (const data of strangeData) {
console.log(data)
returnData.push(data)
}
return { returnData }
}
function funcOne() {
myPromise()
setTimeout(() => console.log('Timeout!', 1))
console.log('funcOne Last line!')
}
async function funcTwo() {
let res = await myPromise()
res[Symbol.iterator] = function* () {
yield* Object.values(this)
}
console.log([...res])
setTimeout(() => console.log('Timeout!', 2))
console.log('funcTwo Last line!')
}
funcOne()
funcTwo()
Take a second to see if there are any strange syntax you are not familiar with, then go on to guess what the output would be...
Ready? And the answer is......
The answer
funcOne Last line!
[ '', '2', '4' ]
[ '', '2', '4' ]
1
1
3
3
[ [ [ '', '2', '4' ], '1', '3' ] ]
funcTwo Last line!
Timeout! 1
Timeout! 2
The explanation
The first thing to notice is that funcOne()
and funcTwo()
are the functions
that gets things started. Since the functions are called in sequence, you might
think that their outputs will be in order. However, things are not
straightforward when you have promises and async/await. While lines on a script
generally do occur synchronously on the main thread, whenever something
asynchronous is set to happen, it will be put aside on another queue to get it
done.
Event loop
To understand why 'funcOne Last line!' was printed first, we need to know that
when funcOne()
runs, myPromise()
is called but isn't executed since it is an
asynchronous function. Instead, it is queued up, waiting for execution.
Similarly, the callback in setTimeout
is queued. The difference is under what
category they are queuing in. myPromise
is queued as a microtask, but the
setTimeout
callback is queued as a task, that is, after both funcOne
and
funcTwo
have completed their execution. myPromise
, on the other hand, does
not wait so long, and completes execution as the function ends. This explains
why both setTimeout
callbacks appear only at the last lines.
To learn more, check out this post with nice animations to help understand the event loop
function funcOne() {
myPromise()
setTimeout(() => console.log('Timeout!', 1))
console.log('funcOne Last line!')
}
Strange stuffs (to me)
Before we carry on, there are some strange language features to explain. This line
const strangeData = strangeFunction`${'1'}2${'3'}4`
String literals
made me think that some typo had gone into the repo. Turns out it is an actual syntax! How it works is better explained in code:
const someFunction = (...args) => {
console.log(args)
}
someFunction`${'1'}2${'3'}4`
// Returns
// [ [ '', '2', '4' ], '1', '3' ]
This syntax splits the string literal into a few arguments, and is meant to be a way to allow custom formatting of the string literal. The first argument represents all the non-variables, and the following arguments are the variable values that were inside the string literal. If you replaced the commas in the first argument with the variables, you would get the expected string.
Asynchronous generators
async function* strangeFunction(...args) {
for (arg of args) {
yield Promise.resolve(arg)
}
}
const myPromise = async () => {
const returnData = []
const strangeData = strangeFunction`${'1'}2${'3'}4` < #1
for await (const data of strangeData) { < #2
console.log(data)
returnData.push(data)
}
return { returnData }
}
At #1, strangeData
is assigned an asynchronous generator. At #2, await is used
with a for..of
loop, which resolves to promises that strangeData
generates.
Remember I said I wasn't too good with generators? What about asynchronous
generators? Craziness of course! Generators give you a value each time you call
a generator, it returns the next value to yield
. Similarly, each time you call
an asynchronous generator, a Promise
is yield
.
Each time a promise completes, the next promise is generated. All this happens
at await myPromise()
, when waiting for the second asynchronous generator to
complete. Since there are two generators at work, and scheduled one after the
other, we see the the values as they generate the same things in each cycle.
[ '', '2', '4' ]
[ '', '2', '4' ]
1
1
3
3
Iterators
Objects are not iterable by default. That is, you cannot direct run a for
loop
on them. Which also means they are incompatible with the [...someObject]
syntax, which works with arrays. It turns out that there is a property that can
make objects iterable, and is used to make the array spread work in this part of
the code.
res[Symbol.iterator] = function* () {
yield* Object.values(this)
}
console.log([...res])
Summary
While we may never code with some of these features, I think understanding the Event Loop well will help make debugging timing related issues and prevent them from happening in the first place.
I am still not so familiar with these concepts, and perhaps my explanations are wrong. If they are I will be sure to refine them as I figure them out!
Cheers,
Jerome