- Original text by Kyle Simpson
- https://leanpub.com/ydkjsy-get-started
- https://github.com/getify/You-Dont-Know-JS
-
Official name of JS is ECMAScript (ES)
- Since 2016 annual releases as ES2019, ES2020
-
Contrary to some perceptions there are not multiple versions of JS in the wild: there is just one and it's the version maintained by TC39 and ECMA
-
Various JS environments (browser JS engines, Node.js, etc.) add APIs into the global scope of JS programs to give environment specific capabilities (i.e.
alert()
,console.log
,fs.write()
)
Typical paradigm-level code categories include procedural, object-oriented (OO/classes), and functional (FP):
- Procedural style organizes code in a top-down, linear progression through a pre-determined set of operations, usually collected together in related units called procedures.
- OO style organizes code by collecting logic and data together into units called classes.
- FP style organizes code into functions (pure computations as opposed to procedures), and the adaptations of those functions as values.
- Languages can be heavily slanted toward a paradigm
- C: procedural
- Java/C++: class-oriented/OO
- Haskel: FP
- JS is a multi-paradigm language
- interpreted script or compiled program (p.21)
- JS source code is parsed before it is executed (23)
- Spec requires early errors, statically determined (e.g. duplicate parameter names) to be reported before code starts executing
- Sensible also to consider it to be a compiled language
- Because it is compiled, we are informed of static errors before the code is executed.
- Explained to draw a distinction with "interpreted" and "scripted" languages, which have often been regarded as less mature than "compiled" languages
-
Aims to reduce the parse and compile time of JS = performance gains
-
Parsing/compilation of WASM-targeted program happens ahead of time
- JS engine is provided a binary-packed program, which is ready to be executed with very little processing
-
Another motivation is to bring non-JS programming languages (e.g. Go) to the web platform
-
All code is automatically defaulted to strict mode in ES6 modules (30)
JS is an implementation of the ECMAScript standard (version ES2019 as of this writing), which is guided by the TC39 committee and hosted by ECMA. It runs in browsers and other JS environments such as Node.js.
JS is a multi-paradigm language, meaning the syntax and capabilities allow a developer to mix and match (and bend and reshape!) concepts from various major paradigms, such as procedural, object-oriented (OO/classes), and functional (FP).
JS is a compiled language, meaning the tools (including the JS engine) process and verify a program (reporting any errors!) before it executes.
let
allows for more limited access to a variable thanvar
- block scoping vs regular/function scoping
- Common challenge that we should avoid
var
to favour the ES6 variables- Counterpoint:
var
usefully conveys that "this variable will be seen by a wider scope (of the whole function)" - That scoping may be appropriate, and the developer may want to use
var
to indicate that
- Counterpoint:
const
= cannot be reassigned- Not, cannot be changed
- Advice to avoid
const
with objects because their values can be changed/mutated without the object itself being reassigned
function robsNamedFunction() {
...
}
- function declaration because it appears as a statement by itself (rather than as an expression in another statement)
- Association between the identifier (
robsNamedFunction
) and the function value occurs at compile time
let robsNamedFunction = function() {
...
}
- function expression because the function is an expression that is assigned to a variable
- Association between the identifier and the function value occurs at run time
It's extremely important to note that in JS, functions are values that can be assigned and passed around. Not all languages treat functions as values, but it's essential for a language to support the functional programming pattern
- Curious about any implications of the compile vs. run phase of identifier association
- Maybe affects static analysis?
===
is often considered to check both the type and value- In fact, all comparisons check type and value, but
===
just doesn't allow for any type coercion
- In fact, all comparisons check type and value, but
NaN === NaN; //false
0 === -0; //false
- Two special values where the
===
operator is designed to "lie"- "Deep historical and technical reasons"
- Can use
Number.isNaN(...)
orObject.is(...)
Object.is(0, -0); // false
Object.is(-0, -0); // true
Object.is(NaN, 0/0); // true
Object.is(...)
as the====
check- With objects, a content-aware comparison is referred to as "structural equality"
- In JS
===
provides a check of identity equality for objects and NOT structural equality
- In JS
In JS, all object values are held by reference, are assigned and passed by reference-copy, and are compared by reference (identity) equality
-
To properly cover all edge cases in structural equality is very complex, which is why it doesn't exist in JS
- For instance, stringifying a function doesn't take into account things like closure
-
Coercive comparisons: The
==
operator can be considered coercive equality- Simple enough to prefer
===
, but you don't have a choice with<
or>=
, which will allow coercion first if types differ
- Simple enough to prefer
var x = "10"
var y = "5"
x < y // true!
- If both values are strings, the relational operators will perform an alphabetical comparison
- Could imagine this being a sneaky case where a numeric value is passed into a context as a string and a comparison yields an unexpected result
- Two patterns for organizing code (data and behaviour): classes and modules
A class in a program is a definition of a "type" of custom data structure that includes both data and behaviors that operate on that data.
- Classes define how the data structure works, but are not themselves concrete values
- A class must be instantiated with the
new
keyword - Does React do this "under the hood" for class components? i.e. When I say
<MyComponent />
, is React instantiating an instance of that class?
class Page {
constructor(text) {
this.text = text;
}
print() {
console.log(this.text);
}
}
class Notebook {
constructor() {
this.pages = [];
}
addPage(text) {
var page = new Page(text);
this.pages.push(page);
}
print() {
for (let page of this.pages) {
page.print();
}
}
}
var mathNotes = new Notebook();
mathNotes.addPage("Arithmetic: + - * / ...");
mathNotes.addPage("Trigonometry: sin cos tan ...");
mathNotes.print();
- Key point that the data (e.g.
text
string andpages
array) are organized alongside their behaviors (e.g.print
andaddPage
methods) - We can write a program without an organizing mechanism like a class, but it would be harder to reason about and maintain
- Reminds me of some of the massive 600+ line files I've had to work with where you need multiple tabs open at various lines of the file
class Publication {
constructor(title,author,pubDate) {
this.title = title;
this.author = author;
this.pubDate = pubDate;
}
print() {
console.log(`
Title: ${ this.title }
By: ${ this.author }
${ this.pubDate }
`);
}
}
class Book extends Publication {
constructor(bookDetails) {
super(
bookDetails.title,
bookDetails.author,
bookDetails.publishedOn
);
this.publisher = bookDetails.publisher;
this.ISBN = bookDetails.ISBN;
}
print() {
super.print();
console.log(`
Publisher: ${ this.publisher }
ISBN: ${ this.ISBN }
`);
}
}
class BlogPost extends Publication {
constructor(title,author,pubDate,URL) {
super(title,author,pubDate);
this.URL = URL;
}
print() {
super.print();
console.log(this.URL);
}
}
Book
andBlogPost
extend the generalPublication
class with more specific behaviorsuper()
delegates the initialisation work to the parent class'sconstructor
- Both child classes have a
print()
method that overrides the inherited method- polymorphism = both inherited and overridden methods can have same name
- Inheritance allows children classes to cooperate with parent classes by accessing / using its behavior and data, while being organized in their own separate logical units (as a class)
- ES6 added a module syntax form ot native JS syntax, but another module pattern has been important and common previously
an outer function (that runs at least once), which returns an "instance" of the module with one or more functions exposed that can operate on the module instance's internal (hidden) data
function Publication(title, author) {
var publicAPI = {
print() {
console.log(title, author)
}
};
return publicAPI;
}
function Book(bookDetails) {
var pub = Publication(bookDetails.title, bookDetails.author);
var publicAPI = {
print() {
pub.print()
console.log(bookDetails.publisher)
}
};
return publicAPI;
}
- Similar to classes, but with methods and data accessed as identifier variables in scope rather than via
this.
- All data and methods are public with a class, but a module factory function exposes public methods explicitly via the returned object, while other methods remain private inside the factory function
var robsBook = Book({
title: "Rob's cool book",
author: "Rob"
})
robsBook.print()
- Usage is quite similar to a class, just that there's no
new
keyword and the module factory function is called directly
- Introduced in ES6 to provide same general utility as classic modules
- Implementation is very different though
- Instead of a wrapping context that the factory functions provided: the file for the ESM is the wrapping context
- ESMs are always module based: one file = one module
- Use
export
keyword to add a variable or method to the ESM's public API definition- Defined in a module, but not exported = stays hidden
- We don't "instantiate" an ES module
- First
import
creates a single instance - All other
import
s just receive a reference to that same single instance - singleton
- First
- The
...
operator- Two symmetrical forms:
spread
andrest
- Two symmetrical forms:
- Spread is an iterator-consumer
- Official JS spec only allows spread into an array or a function call
- Array spread
- Function call spread
- But object spread is common enough (needs Babel)
- Official JS spec only allows spread into an array or a function call
var myNewArray = [...previouslyDefinedIterable]
myGreatFunction(...previouslyDefinedIterable)
for...of
is another ES6 iterator-consumer
for (let value of previouslyDefinedIterable){
...
}
ES6 defined the basic data structure / collection types in JS as iterables
-
strings, arrays, maps, sets (and others)
-
Most all these will have
keys()
,values()
, andentries()
methods -
A
Map
data structure has a default iteration method over its entries rather than its values- An entry = tuple (two-element array) with both a key and a value
// Given two DOM elements button1 + button2
var buttonNames = new Map();
buttonNames.set(button1, "Home")
buttonNames.set(button2, "Cart")
for (let [button, buttonName] of buttonNames) {
button.addEventListener('click', function onClick(){
console.log(`Clicked ${buttonName}`)
})
}
Closure is when a function remembers and continues to access variables from outside its scope, even when the function is executed in a different scope.
- Closure is an aspect of functions
- To observe a closure, you must execute a function in a different scope than where it was originally defined
function giveMeAGreeting(greeting) {
return function giveMeAName(name) {
console.log(`${greeting}, ${name}!`)
}
}
const hello = giveMeAGreeting("Hello")
const sup = giveMeAGreeting("Sup")
hello("Harold")
sup("Sandra")
- Nice higher-order function example where the
greeting
value is saved in the instance of the inner function thanks to closure- The inner function
giveMeAName
closes over thegreeting
variable from its outer scope hello
andsup
are functions that persist thegreeting
argument passed intogiveMeAGreeting
- The inner function
When the
giveMeAGreeting(...)
function finishes running, normally we would expect all of its variables to be garbage collected (removed from memory)
- But because the inner function instances are still alive, being assigned to
hello
andsup
, their closures preserve thegreeting
variables
function counter(step = 1) {
let count = 0
return function increment(){
count = count + step;
return count
}
}
const add100 = counter(100)
const add1 = counter(1)
add100();
add100();
add1();
const bigMoney = add100() //300
const broke = add1() //2
- Instances of the inner function close over both the
count
andstep
values from the outer scope - Point here is that the variables within a closure are not a snapshot and can be changed
a direct link and preservation of the variable itself
function getData(url) {
ajax(url, function onResponse(response){
console.log(`Response from ${url}: ${response}`)
})
}
Closure is most common when working with asynchronous code, such as with callbacks
getData
finishes right away, but theurl
parameter variable stays alive as long as needed
for (let [index, button] of buttons.entries()) {
button.addEventListener('click', function onClick(){
console.log(`Button ${index} clicked`)
})
}
- Example where the outer scope isn't a function
- But the click handler is a function, and it creates the closure that preserves the
index
variable- Only functions have closure
-
Common misconceptions
this
refers to the function itselfthis
points to the instance that a method belongs to
-
Functions have two key characteristics determining what they can access:
- scope: the set of rules that controls how references to variables are resolved
- When it is defined, a function is attached to its enclosing scope via closure
- execution context: can be thought of as a tangible object whose properties are made available to a function while it executes
- Exposed to the function via
this
- Exposed to the function via
- scope: the set of rules that controls how references to variables are resolved
-
Scope is static
- Contains fixed variables at moment and location a function is defined
-
Execution context is dynamic
- Dependant on how a function is called
- Not a fixed characteristic, but a dynamic one determined every time a function is called
-
The benefit of a this-aware function is the ability to flexibly re-use a function with different data from different objects
function classroom(teacher) {
return function study() {
console.log(`${teacher} says to study ${this.topic}`)
}
}
var assignment = classroom("Robert")
assignment() // Robert says to study undefined
- Here, the inner function,
study
, is dependent on its execution context because it is a this-aware function - Without a context specified, the default context is the global object
globalThis.topic
will equalundefined
let homework = {
topic: "JS",
assignment: assignment
}
homework.assignment() // Robert says to study JS
- A copy of the assignment function reference is set on the homework object
this
for the function call = homework object
let otherHomework = {
topic: "slam dunks"
}
assignment.call(otherHomework) // Robert says to study slam dunks
call()
takes an object to set thethis
reference for the function call
-
A characteristic of objects
- Provides rules for the resolution of property access
-
Helpful to think of prototypes as a form of linkage between objects
- prototype chain = series of objects linked together via prototypes
The purpose of this prototype linkage is so that accesses against object B for properties/methods that B does not have, are delegated to object A
-
Notion of delegation is useful
-
Object.create()
defines a an object prototypes linkagesObject.create(null)
creates a standalone object without any of the built in JS object properties or methods
-
Another way to create an object with prototype linkage that used to be very common prior to ES6 is through "Prototypal 'Classes'"
- One of the main reasons
this
is dynamic based on how a function is called is so that prototype-delegated function calls maintain the expectedthis
let homework = {
study() {
console.log(`Study ${this.topic}`);
}
};
let jsHomework = Object.create(homework);
jsHomework.topic = "JS";
jsHomework.study(); // "Study JS"
let dunkHomework = Object.create(homework);
dunkHomework.topic = "slam dunks";
dunkHomework.study(); // "Study slam dunks"
- Both
jsHomework
anddunkHomework
prototype link to the samehomework
object- Because neither has a
study
method, they delegate up the chain tohomework
, which does - Cool thing to point out though is that
this
withinstudy
still resolves to the object which executedstudy
- Example of dynamic
this
being determined based on execution context - Other languages may place
this
onhomework
, since that's wherestudy
is defined, but not JS
- Because neither has a
Unlike may other languages, JS's
this
being dynamic is a critical component of allowing prototype delegation, and indeedclass
, to work as expected!
- JS is lexically scoped
- Many claim it isn't due to 2 characteristics not present in other lexically-scoped languages
- hoisting: where all variables declared anywhere in the scope are treated as if they were declared at the beginning of the scope
- function-scope
var
:var
variables are function scoped even if they appear within a block
Closure is the natural result of lexical scope when the language has functions as first-class values, as JS does.
- A function maintains access to to its original scope variables regardless of the scope it's executed in (this is closure)
- JS is one of only a few languages that lets you create objects directly and explicitly, without first defining their structure in a class
For many years, people implemented the class design pattern on top of prototypes
-
This is prototypal inheritance, by and large avoided in today's JS
-
ES6
class
keyword "doubled-down" on the learning toward OO/class-style programming -
This focus can obscure the potential of the prototype system
- The ability of two objects to simply cooperate dynamically through sharing a
this
context
- The ability of two objects to simply cooperate dynamically through sharing a
-
Classes are one patten you can build on top of the prototype system
- But can also forget classes and just let objects cooperate using the prototype chain
- Called behaviour delegation
But class inheritance gets almost all the attention. And the rest goes to functional programming, as the sort of "anti-class" way of designing programs.
-
Bits of advice:
- To help a dev team embrace new practices, let before and after code samples do much of the talking
- Work on building consensus on why it's important to revisit and reconsider an approach
- Push for decisions based on an analysis of code rather than authoritative personas
- Primitive values are always assigned as value copies
- Object values are always assigned by reference
- reference: two or more variables point to the same value
- Two or more variables can have copies of a reference to a single shared object
Primitives are held by value. Objects are held by reference
- Interesting that other languages might allow you to choose between assigning/passing the value itself or a reference to the value
- But not JS!
const myFunction = function(arg){
...
}
- On the right side is an anonymous function expression
- In the cases where an assignment uses
=
, ES6 will perform name inference to givemyFunction.name = "myFunction"
- But if the function expression is passed as an argument to another function call, there will be no name inference
- Any errors will then have a stack trace identifying an
anonymous function
- In the cases where an assignment uses
const myFunction = function robsFunction(arg){
...
}
- On the right side is a named function expression
myFunction.name = "robsFunction"
Most developers tend to be unconcerned with using anonymous functions. They're shorter and unquestionably more common
- Argument on the other hand is that any function in our code has a purpose, and you should name it in order to describe that purpose
- This prevents the next developer from having to evaluate the code in their head to identify its purpose
const myFunction = () => {...}
onClick(() => {...})
- Arrow functions are syntactically anonymous
- They can only have an inferred name if assigned, as in the first example
- As a call argument, in the second example, they are only ever anonymous. This is a very common use case
- Interesting challenge to it being used as "a new default"
- It is new(ish)
- It is shorter
- But it has the drawback of being easily anonymous, which doesn't serve debugging well
This kind of function actually has a specific purpose (i.e., handling the
this
keyword lexically )
- Prototypal "Classes"
- Good to have the name for this as I never know what to call this pattern