Did you use some JavaScript to make your web app dynamic? That’s the common usage for this language, but there is far more waiting for you.
After reading the popular book series You Don’t Know JS by Kyle Simpson, I realised I didn’t know JS before. The JavaScript community considers this series as one of the references for the language. It’s thick but complete. This series is an invaluable (and free) ally to help you sharpen your skills.
In this article, I gathered the most important insights out of it for you. From the simple stuff to the tough (this keyword and promises). I didn’t quote the book but preferred to build my own examples. Consider this as an introduction to the book series.
If you learned JavaScript at school like me, I bet you learned Java first. Be careful, learning JavaScript isn’t about mimicing Java. It doesn’t work like that — you must learn it as new language.
LESSON #1 — Logical operators
In many languages, expressions which implement logical operators such as AND and OR return a boolean value. Instead, JavaScript returns one of the two operands as explained in this ECMAScript specification note.
With both operators, it returns the first operand which stops the evaluation. Give it a try by setting foo
or bar
to the false
boolean value. Also, if you don’t include any parenthesis, the AND operator has priority over OR.
It first evaluates foo && foo.bar
as if it’s between parenthesis. You can say AND has precedence over OR.
Given that the OR operator returns the first operand which fulfills it, you can use it to set a default value for empty or not defined variables. It was the preferred way to define default function parameters before ES6.
Another use case for those logical operators is to avoid if-else
blocks and ternary expressions:
Here are equivalencies for ternary expressions:
a || b
is equivalent toa ? a : b
a && b
is equivalent toa ? b : a
LESSON #2 — Type conversion
Besides functions such as valueOf
, JavaScript provides for type conversion. It exists as aother way to convert variables types.
- Cast occurs at compilation time and uses the explicit cast operator
- Coercion occurs at runtime and often with an implicit syntax
Implicit coercion is the harder type of conversion to see, so developers often avoid using them. Yet, it’s good to know some common implicit coercions. Here are examples for String
and Boolean
.
Another useful but rarely used operator is ~
, an equivalent to the -(x+1)
operation. It’s helpful to detect the common sentinel value -1
.
LESSON #3 — Falsy values
Conditions are one of the basic structures in programming and we use them a lot. By the way, the legend says artificial intelligence programs are full of if
. It’s important to know how it behaves in any programming language.
Values given to a condition are either considered falsy or truthy. The ECMAScript specification comes with a curated list of falsy values:
'’
empty stringundefined
null
false
boolean value0
number value-0
number valueNaN
not a number value
Experiment yourself with the following snippet:
Any other value not in the list is truthy. For instance, be careful about {}
(empty literal object), []
(empty array) and 'false'
(false string) which all are true
.
Combined with logical operators, you can call a function only if a value is truthy without using a if
.
LESSON #4 — Scope and IIFE
The first time you wrote some JavaScript, someone probably told you to use the following notation because “it works better”.
It does the same as declaring a regular function and then calling it immediately.
This notation is an IIFE, it stands for Immediately Invoked Function Expression. And it doesn’t work better but it prevents variable collisions.
foo
variable from a script tag is magically attached to the window. Pretty interesting when you know libraries and frameworks define their own variables using the same technique.
Actually the scope of variables defined with the var
keyword isn’t bound to all blocks. Those blocks are code parts delimited with curly braces as in if
and for
expressions, for instance.
Only function
and try-catch
blocks can restrict var
's scope. Even if-else
blocks and for
loops can’t do it.
Using IIFE provides a way to hide variables from the outside and restrict their scope. Thus, no one can alter the business logic by changing the window’s variable values.
ES6 comes with the let
and const
keyword. Variables using these keywords are bound to blocks defined with curly braces.
LESSON #5 — Object and maps
Objects help gather variables with the same topic under a unique variable. You end with an object containing many properties. There are two syntaxes to access an object property: dot and array syntax.
The array syntax seems to be the best solution to create maps but it’s not. In this setup, keys must be strings. If not it’s coerced into a string. For instance, any object is coerced as [object Object]
key.
// From here, examples are a bit lengthy.
// I’ll use emebeded code so you can copy/paste and try yourself!
let map = {};
let x = { id: 1 },
y = { id: 2 };
map[x] = 'foo';
map[y] = 'bar';
console.log(map[x], map[y]); // 'bar', 'bar'
From here, examples are a bit lengthy. I’ll use gists so you can copy/paste and try yourself!
In reality, this map got only one value under the [object Object]
key. First, its value is 'foo'
and then it becomes 'bar'
.
To avoid this issue, use the Map object introduced in ES6. Yet be careful, the lookup operation to get a value from a key is using a strict equality.
var map = new Map();
map.set(x, 'foo');
map.set(y, 'bar');
console.log(map.get(x), map.get(y)); // 'foo', 'bar'
// undefined, undefined
console.log(map.get({ id: 1 }, map.get({ id: 2 });
This detail only matters for complex variables such as objects. Because two objects with the same content won’t match with strict equality. You must use the exact variable you put as a key to retrieve your value from the map.
LESSON #6 — What’s this?
The this
keyword is used in languages built with classes. Usually, this
(and its sibling self
) refer to the current instance of the class being used. Its meaning doesn’t change a lot in OOP. But, JavaScript didn’t have classes prior to ES6 (although it still had the this
keyword).
The value of this
in JavaScript is different according to the context. To determine its value, you must first inspect the call-site of the function where you’re using it.
function foo () {
console.log( this.a );
}
// #1: Default binding
var a = 'bar';
// [call-site: global]
foo(); // 'bar' or undefined (strict mode)
It seems strange when you compare this behaviour with the OOP standards. This first rule isn’t that important because most JavaScript codes uses strict mode. Also, thank’s to ES6, developers will tend to use let
and const
instead of the legacy var
.
This is the first rule which is applied by default to bind a value to this
. There are 4 rules in total. Here are the remaining 3 rules:
// It’s not easy to understand, copy this code and do some tests!
// #2: Implicit binding
const o2 = { a: 'o2', foo };
const o1 = { a: 'o1', o2 };
o1.o2.foo(); // [call-site: o2] 'o2'
// #3: Explicit binding
const o = { a: 'bar' };
foo.call(o); // [call-site: o] 'bar'
const hardFoo = foo.bind(o); // [call-site: o]
hardFoo(); // [call-site: o] 'bar'
// #4: New binding
function foo() {
this.a = 'bar';
}
let result = new foo(); // [call-site: new]
console.log(result.a); // 'bar'
The last new binding rule is the first rule JavaScript tries to use. If this rule doesn’t apply, it’ll fall back to the other rules: explicit binding, implicit binding and eventually default binding.
The most important thing to remember:
this changes with the function call-site, rules for binding get priorities
Besides those rules, there are still some edge-cases. It becomes a bit tricky when some rules are skipped depending on the call-site or this
value.
// 1- Call-site issue
const o = { a: 'bar', foo };
callback(o.foo); // undefined
function callback(func){
func(); // [call-site: callback]
}
// 2- Default binding isn't lexical binding
var a = 'foo';
function bar(func){
var a = 'bar'; // Doesn't override global 'a' value for this
func();
}
bar(foo); // 'foo'
// 3- this is null or undefined
var a = 'foo';
foo.call(null); // 'foo' because given 'this' is null
That’s it about this
binding. I agree it’s not easy to understand at first glance but after a while it’ll sink in. You must put the effort in to learn how it works and practice a lot.
To be honest, it’s a sum up from the entire third book of the series. Don’t hesitate to begin with this book and read some chapters. Kyle Simpson gives far more examples and very detailed explanations.
LESSON #7— Promises pattern
Before ES6, the common way to handle asynchronous programming was using callbacks. You call a function which can’t provide a result immediately, so you provide a function it’ll call once it finishes.
Promises are related to callbacks, but they’re going to replace callbacks. The concept of promises isn’t easy to grasp, so take your time to understand the example and try them!
From callbacks to promises
First, let’s talk about callbacks. Did you realize that using them introduces an inversion of control (IoC) into the program execution? The function you’re calling gets the control over your script execution.
// Please call 'eatPizza' once you've finished your work
orderPizza(eatPizza);
function orderPizza(callback) {
// You don't know what's going on here!
callback(); // <- Hope it's this
}
function eatPizza() {
console.log('Miam');
}
You’ll eat your pizza, once it’s delivered and the order completed. The process behind orderPizza
isn’t visible to us, but it’s the same for library’s functions. It may call eatPizza
multiple times, none at all or even wait for a long time.
With promises, you can reverse the callbacks’ IoC. The function won’t ask for a callback but instead, give you a promise. Then, you can subscribe so you’ll get notice after the promise resolves (either with fulfillment or rejection).
let promise = orderPizza(); // <- No callback
// Subscribes to the promise
promise.then(eatPizza); // Fulfilled promise
promise.catch(stillHungry); // Rejected promise
function orderPizza() {
return Promise.resolve(); // <- returns the promise
}
Callback-based functions often ask for two callbacks (success and failure) or pass a parameter to the only callback and let you look for errors.
With promises, those two callbacks change into then
and catch
. It matches success and failure but promise terms are different. A fulfilled promise is a success (with then
) and a rejected promise is a failure (with catch
).
Depending on the API, or the library you use for promises, the catch
may not be available. Instead, then
takes two functions as arguments, and it’s the same pattern as for callback-based functions.
In the example, orderPizza
returns a fulfilled promise. Usually, this kind of asynchronous function returns a pending promise (documentation). But, in most cases, you won’t need the promise constructor because Promise.resolve
and Promise.reject
are enough.
A promise is nothing more than an object with a state property. The function you’re calling changes this state from pending to fulfilled or rejected once it completes its work.
// Function executed even if there are no then or catch
let promise = Promise.resolve('Pizza');
// Add callbacks later, called depending on the promise status
promise.then(youEatOneSlice);
promise.then(yourFriendEatOneSlice);
promise.then(result => console.log(result)); // 'Pizza'
// Promise is an object (with at least a then function: it's a thenable object)
console.log(promise); // { state: 'fulfilled', value: 'Pizza' }
You can join a value to a promise. It’s forwarded to the subscribed callbacks as a parameter (then
and catch
). In this example, there are two subscriptions on the fulfillment callback. Once the promise fulfills, the two subscribed functions trigger in any order.
To sum up: there are still callbacks with promises.
But promises act like a trusted third party. They’re immutable after completion and so can’t resolve multiple times. Also, in the next part, you’ll see that it’s possible to react when a promise is still pending for a long time.
Note you can turn a callback-based function into a promise-based function with a few lines of code (see this gist). For sure there are libraries. Sometimes it’s also included in the language API (TypeScript has a promisify function).
Leverage the Promise API
Both callback and promises have to deal with the issue of dependent asynchronous tasks. It occurs when the result of a first async function is necessary to call a second async function. Also, the third async function needs the result from the second function, and so on…
It’s important to look at how to handle this situation properly. That’s what leads to a horrible codebase. Look a the following code, you should be familiar with it:
You’ve just meet a callback hell. To eat a pizza, the chef must cook it, then pack it and the delivery guy deliver it to you. Finally, you can eat the delivered pizza.
Each step is asynchronous and needs the previous step’s result. That’s the point which leads you to write callback hell code. Promises can avoid it because they can either return other promises or values (wrapped in a promise).
This snippet looks complex and simple at the same time. The code is small but it seems like we put in some magic things. Let’s split each step and get rid of ES6 syntax to make it clear:
// Detailled promise chain with plain ES5, try the pratice part!
const cookPromise = cookPizza();
const packPromise = cookPromise.then(function(pizza) {
return pack(pizza); // Returns a promise stored in packPromise
});
const deliverPromise = packPromise.then(function (packedPizza) { // value from pack(pizza)
return deliver(packedPizza);
});
deliverPromise.then(function (deliveredPizza) {
return eat(deliveredPizza);
});
/* For you to practice */
// - An example for cookPizza, pack, deliver and eat implementation
// Each function append something to the previous step string
function pack(pizza) {
return Promise.resolve(pizza + ' pack');
}
// - Retrieve the result of eat and display the final string
// Should be something like: 'pizza pack deliver eat'
eatPromise.eat((result) => console.log(result));
Now, you have the short syntax and the most verbose. To better understand this piece of code, you should:
- Implement
cookPizza
,pack
,deliver
andeat
functions - Check that each function changed the string using the
eatPromise
- Refactor the code step by step to get to the short syntax
There’s also the regular usage from promises. The Promises API also provides helpers to handle common concurrency interaction conditions such as gate, race and latch.
In this example, only the then
is used but catch
is also available. For Promise.all
it’ll trigger instead of then
if at least one promise is rejected.
As explained before, you can use promises to “check and act when a promise is still pending for a long time”. It’s the common use case for Promise.race
. If you want to get a complete example with a timeout, check out this part of the book.
Going further with ES7
In some code, you might find deferred objects to handle promises. For instance, AngularJS provides it through the $q service.
Using them seems more natural and understandable but they’re not. You better take your time to learn promises.
You may need to return a promise and change its state later. Before you choose this solution, make sure there are no others ways. Anyway, the Promise API doesn’t return deferred objects.
Don’t use a deferred object. If you think you need to, go over promises again
But you can use the Promise constructor to mimic this behaviour. Check this gist of mine to know more but remember — it’s bad!
Last but not least, ES7 introduced a new way to handle promises by leverage generators syntax. It allows you to make asynchronous functions look like regular synchronous functions.
// ES6 syntax
function load() {
return Promise.all([foo(), bar()])
.then(console.log);
}
load();
// ES7 syntax
async function load() {
let a = await foo();
// Gets here once 'foo' is resolved and then call 'bar'
let b = await bar();
console.log(a, b);
}
load();
Flag the load
which calls the asynchronous functions foo
and bar
with the async
keyword. And put await
before the asynchronous calls. You’ll be able to use the load
as before, with a classic load()
.
This syntax is appealing, isn’t it? No more callback and promise hell with infinite indentation. But wait, you should consider how generators work to avoid performances issues.
In the above example, bar
is only executed once foo
promise resolves. Their execution isn’t parallelised. You’ll get the exact same result by writing something like foo.then(bar)
.
Here is how to fix it:
async function load() {
let fooPromise = foo();
let barPromise = bar();
// foo and bar are executed before Promise.all
let results = await Promise.all([fooPromise, barPromise]);
console.log(results);
}
load();
Make use of the Promise.all
. Actually, await
means you want to execute your function step by step. First, from the beginning to the first await
. Once the promise from the first await
resolves, it’ll resume the function up to the next await
keyword. Or to the end of the function if there aren’t more.
In this example, foo
and bar
execute during the first step. The load
function takes a break on Promise.all
. At this point foo
and bar
already began their work.
This was a quick introduction to promises with some notes about the traps you don’t want to fall into. This sums up of the fifth book of the series which describes in depth asynchronous patterns and promises.
You can also look at this article by Ronald Chen. He gathers a lot of promise anti-patterns. This article will help you to escape the so-called promise hell.
Wrapping up
These were the most important lessons I learned by reading You Don’t Know JS. This book series has way more lessons and details to teach you about how JavaScript works.
Just a heads up: for me, it was sometimes hard to follow when the author quotes the ECMAScript spec and lengthy examples. The books are long for sure, but also very complete. By the way, I almost give up but finally, I keep reading to the end and I can tell you — it was worth it.
This isn’t some kind of advertising for Kyle. I just like this series and consider it a reference. Also, it’s free to read and contribute to the series through the GitHub repository.
If you found this article useful, please click on the ? button a few times to make others find the article and show your support! ?
Don’t forget to follow me to get notified of my upcoming articles ?
Check out my Other Articles
➥ JavaScript
- React for beginners series
- How to Improve Your JavaScript Skills by Writing Your Own Web Development Framework
- Common Mistakes to Avoid While Working with Vue.js