training/front-end/01-modern-javascript.md
Note: run code quickly with https://codesandbox.io/s/
This course assumes you already have experience with JavaScript. If you don't, start with this:
Number, String, Boolean, Object, Undefined, BigInt1 / "a" evaluate to?
NaNJavaScript is a programming language evolving very rapidly that can run in different environments:
Key historical elements:
From 2016 to today, a new version of ECMAScript is released each year. The language has reached maturity and supports many modern constructs.
Key features of JavaScript:
if, while, switch, while, do whileString, Number, etc.) but uses implicit cast ("1" - "1" === 0).x can be associated with a String, then with a Number).eval. eval('1 === 1') evaluates to trueUse caniuse to check what browser support the feature you're using. For instance, for [1, 2].includes(1) requires Array.prototype.includes:
You can then use "polyfills" (or "shims") to support older browser. There are some polyfills for each specific feature, and some other that includes lots of polyfills (e.g. zloirock/core-js).
Polyfills are different from transpiling. Babel is a transpiler, you can see how it work online here:
For instance, it will transpile (notice the replacement of an arrow function):
[1, 2, 3].map((n) => n + 1)
Into:
"use strict";
[1, 2, 3].map(function (n) {
return n + 1;
});
var a;
// an uninitialized variable is undefined
console.assert(typeof a === "undefined");
// There are no required arguments in JavaScript
function hello(name) {
return name;
}
// No raise, will log "undefined"
console.log(hello());
// Here's how to compare to undefined
console.assert(typeof undefined === "undefined");
const anObject = { a: 1 };
// Accessing an absent object key also returns undefined
console.assert(typeof anObject.nonExistent === "undefined");
// Do not leave console.log in your code!
// There are linters such as eslint that will check for their absence
console.log("hello");
// In this document, we use assert to show the actual value
console.assert(true === true);
Rules for string conversion:
String are left as is.Number are converted to their string representation.Array are converted to string, then joined with commas ,.[object Object] where Object is the constructor of the object.Can you guess how those will be converted?
[] + []
{} + {}
"1" + 1
"1" + "1"
"1" - 1
[1, 2] + 2
[1] + 1
function Dog {}
const dog = new Dog()
dog + 1 + "a"
dog - 1
Other gotchas:
// NaN is fun!
console.assert(typeof NaN === "number");
Object.is(NaN, NaN) // true
NaN === NaN // false
A good talk on the topic: Wat
===) instead of double (==)// Double equals will coerce values to make them comparable!
console.assert("1" == 1);
// Better
console.assert(!("1" === 1));
console.assert("1" !== 1);
Applied on arrays and objects, == and === will check for object identity, which is almost never what you want.
console.assert({ a: 1 } != { a: 1 });
console.assert({ a: 1 } !== { a: 1 });
const obj = { a: 1 };
const obj2 = obj;
// This is true because obj and obj2 refer to the same object ("identity")
console.assert(obj == obj2);
console.assert(obj === obj2);
Use a library such as lodash to properly compare objects and array
import _ from "lodash";
console.assert(_.isEqual({ a: 1 }, { a: 1 }));
console.assert(_.isEqual([1, 2], [1, 2]));
Object and Array methods// Use Object.assign (ES 2015) to copy objects
const target = { a: 1, b: 1};
const source = { b: 2};
const merged = Object.assign(target, source);
console.assert(_.isEqual(merged, {a: 1, b:2});
// Spread operator
const merge2 = {...target, ...source};
// Array.includes (ES7)
const theArray = [1, 2]
console.assert(theArray.includes(1))
JavaScript has a very powerful prototypal inheritance system that is very interesting to study.
The truth is, it is much less used nowadays, and you don't really need to know it to develop with React. It also requires a bit of personal study to fully understand it. So we will leave it aside for now.
The book JavaScript: The Good Parts by Douglas Crockford (2008) is a great introduction to it. Here's a quote from the author:
You make prototype objects, and then … make new instances. Objects are mutable in JavaScript, so we can augment the new instances, giving them new fields and methods. These can then act as prototypes for even newer objects. We don't need classes to make lots of similar objects… Objects inherit from objects. What could be more object oriented than that?
Some good articles:
const toaster = { size: 2, color: "red", brand: "NoName" };
// Get ("destructure") one object key
const { size } = toaster;
console.assert(size === 2);
// Note: this also works with functions
function destructuredFunction({ color }) {
return color;
}
console.assert(destructuredFunction({ color: "red" }) === "red");
// Get the rest with ...rest
const { color, brand, ...rest } = toaster;
console.assert(_.isEqual(rest, { size: 2 }));
// Set default
const { size2 = 3 } = toaster;
console.assert(size2 === 3);
// Rename variables
const { size: size3 } = toaster;
console.assert(size3 === 2);
Enhanced object literals:
const name = "Louis";
const person = { name };
console.assert(_.isEqual(person, { name: "Louis" }));
// Dynamic properties
const person2 = { ["first" + "Name"]: "Olympe" };
console.assert(_.isEqual(person2, { firstName: "Olympe" }));
// Btw, you can include quotes although nobody does this
console.assert(_.isEqual(person2, { firstName: "Olympe" }));
// Short form function
// Before:
const es5Object = {
say: function () {
console.log("hello");
},
};
es5Object.say();
// After: (short form function)
const es6Object = {
say() {
console.log("hello");
},
};
es6Object.say();
Advanced (with prototype):
// Prototype and super()
const firstObject = {
a: "a",
hello() {
return "hello";
},
};
const secondObject = {
__proto__: firstObject,
hello() {
return super.hello() + " from second object";
},
};
console.assert(secondObject.hello() === "hello from second object");
const theArray = [1, 2, 3];
const [first, second] = theArray;
const [first1, second2, ...rest] = theArray;
console.assert(first === 1);
console.assert(second === 2);
console.assert(_.isEqualWith(rest, [3]));
let and constconst constantVar = "a";
// Raises "constantVar" is read-only
constantVar = "b";
let theVar = "a";
theVar = "a";
// Note: const != immutable
const constantObject = { a: 1 };
constantObject.a = 2;
constantObject.b = 3;
// Raises: "constantObject" is read-only
constantObject = { a: 1 };
// const and let are block scoped. A block is enclosed in {} (if, loops, functions, etc.)
{
const a = "a";
console.log({ a });
}
// Raises: ReferenceError: a is not defined
console.log({ a });
Note: try to use const as much as you can.
const without providing its initial value.Never use var:
var variables are initialized with undefined, while let and const vars are not initialized and will raise an error if used before definition.var is globally or function-scoped, depending on whether it is used inside a function.let and const are block-scopedlet and const cannot be reused for the same variable nameFuture of JavaScript: tc39/proposal-record-tuple: ECMAScript proposal for the Record and Tuple value types
See Hoisting on MDN
console.log(typeof variable); // undefined
// console.log(variable); // Raises: ReferenceError: variable is not defined
function hoist() {
a = 20;
var b = 100;
}
hoist();
// 20, accessible as a global variable outside of hoist
console.log(a);
// Raises: ReferenceError: b is not defined
// console.log(b);
The first advantage of arrow function is that they're shorter to write:
// You can define a function this way:
const myFunction = function () {
console.log("hello world");
};
// With an arrow function, you save a few characters:
const myArrowFunction = () => {
console.log("hello world");
};
// Some things, like params parentheses, and function code brackets, are optional
const myFunctionToBeShortened = function (a) {
return a;
};
// Shorter arrow function
const myFunctionToBeShortenedArrowV1 = (a) => {
return a;
};
// Shortest arrow function
// Remove single param parenthesis, remove function code bracket, remove return
const myFunctionToBeShortenedArrowV2 = (a) => a;
console.assert(myFunctionToBeShortenedArrowV2(1) === 1);
this works in arrow functionsTODO
I usually keep the parameters parenthesis. If you add a parameter and weren't including them, you'll have to add them back:
const a1 = (arg) => {};
const a2 = (arg1, arg2) => {};
// vs.
const a3 = (arg) => {};
class Toaster {
constructor(color) {
this.color = color;
}
dring() {
return "dring";
}
}
// Don't forget new!
// Raises: TypeError: Cannot call a class as a function
// const toaster = Toaster('red');
const toaster = new Toaster("red");
console.log(toaster.dring());
// Inheritance
class BunToaster extends Toaster {
dring() {
return super.dring() + " dring";
}
}
const bunToaster = new BunToaster("red");
console.assert(bunToaster.dring() === "dring dring");
Those are my opinions about other class features:
static methods, use plain functions instead.get and set).const longString = `multi
line
string`;
const name = "Louis";
// Template interpolation
const hello = `Hello ${name}`;
// You can have expressions
const hello1 = `Hello ${name + "!"}`;
const hello2 = `Hello ${name === "Louis" ? name : "Noname"}`;
They are used in some libraries, like Apollo and Styled Components.
// First arg is an array of string values, the rest is the expressions
// ["hello ", ""], 3
function templateTag(literals, ...expressions) {
console.assert(_.isEqual(literals, ["hello ", ""]));
console.assert(_.isEqual(expressions, [3]));
return _.join(_.flatten(_.zip(literals, expressions)), "");
}
const result = templateTag`hello ${1 + 2}`;
console.assert(result === "hello 3");
Here's an example with Styled Components:
const Button = styled.a`
/* This renders the buttons above... Edit me! */
display: inline-block;
border-radius: 3px;
padding: 0.5rem 0;
margin: 0.5rem 1rem;
width: 11rem;
background: transparent;
color: white;
border: 2px solid white;
/* The GitHub button is a primary button
* edit this to target it specifically! */
${props => props.primary && css`
background: white;
color: black;
`}
`
You can see how template tags and arrow functions lead to more concise code!
for... ofNote: prefer using some functional constructs such as map, reduce, etc.
for (const i of [1, 2, 3]) {
console.log({ i });
}
// 1, 2, 3
for (const key in { a: "aaa", b: "bbb" }) {
console.log({ key });
}
// 'a', 'b'
This is only going to be an introduction to the magnificent world of promise.
FetchNote: we use TypeScript in this example, to clarify what's return. You can ignore the type annotations for now.
let isDone: boolean = true
const thePromise = new Promise((resolve, reject) => {
if (isDone) {
resolve("the work is done");
} else {
reject("this is still pending");
}
}
console.assert(thePromise === 'the work is done')
TODO
CommonJS syntax:
const lodash = require("lodash");
ES Module syntax:
import lodash from "lodash";
// Import all and provide under name
import * as toaster from "./toaster";
// Named import (same as object destructuring!)
import { defaultColor, defaultSize } from "./toaster";
// Renaming imports
import { defaultBrand as toasterDefaultBrand } from "./toaster";
// Default import
import createToaster from "./toaster";
// Import both defaults and other
// import createToaster, {defaultColor} from './toaster'
In toaster.js:
// Shorthand definition + export
export const defaultSize = 4;
// Alternative export syntax
const defaultBrand = "Moulinex";
export { defaultBrand };
// Default export
const createToaster = ({ size, color }) => ({ size, color });
export default createToaster;
// Note that you have a shorthand default export, but it's not recommended to
// use it as the export won't have a name.
// export default () => ({})
let nestedProp = obj.first && obj.first.second;
// with optional chaining:
let nestedProp = obj.first?.second;
const a = 'a'
const r = a === 'a' ? 'isA' : 'isNotA'
console.assert(r === 'isA')
"1" + "1"? 3 + 2 + "5"?let, var, or const?const a = [1]; const b = [1];: what does a == b evaluates to?const {a} = {a: 1}: what does a evaluate to?Advanced:
// Write transform1 using a one-line arrow function with object structuring
console.assert(_.isEqual(transform1({name: "Foo"}), {FooA:1}))
console.assert(_.isEqual(transform1({name: "Bar"}), {Bar:1}))
The three things you need:
let and constconst { a } = {a: 1}const noop = () => { }Other assessments:
Future changes: