You Don't Know JS [Note]
https://github.com/getify/You-Dont-Know-JS
Chapter 1: What Is JavaScript?
When Brendan Eich first conceived of the language, he code-named it Mocha. Internally at Netscape, the brand LiveScript was used. But when it came time to publicly name the language, “JavaScript” won the vote.
Why? Because this language was originally designed to appeal to an audience of mostly Java programmers, and because the word “script” was popular at the time to refer to lightweight programs.
The official name of the language specified by TC39 and formalized by the ECMA standards body is ECMAScript.
TC39: the organization managing JS specifications
I mentioned TC39, the technical steering committee that manages JS. Their primary task is managing the official specification for the language (JS’s syntax and behavior). They meet regularly to vote on any agreed changes, which they then submit to ECMA, the standards organization.
The TC39 committee is comprised of between 50 and about 100 different people from a broad section of web-invested companies, such as browser makers (Mozilla, Google, Apple) and device makers (Samsung, etc). All members of the committee are volunteers, though many of them are employees of these companies and so may receive compensation in part for their duties on the committee.
JavaScript is a multi-paradigm language.
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.
JavaScript is most definitely a multi-paradigm language.
Not all JS-like codes are Web JS
Various JS environments (like browser JS engines, Node.js, etc.) add APIs into the global scope of your JS programs that give you environment-specific capabilities, like being able to pop an alert-style box in the user’s browser (alert(...)
).
JS is backwards-compatible, but not forwards-compatible
The idea is that JS developers can write code with confidence that their code won’t stop working unpredictably because a browser update is released. This makes the decision to choose JS for a program a more wise and safe investment, for years into the future.
HTML and CSS, by contrast, are forwards-compatible but not backwards-compatible.
For new and incompatible syntax, the solution is transpiling. Transpiling is a contrived and community-invented term to describe using a tool to convert the source code of a program from one form to another (but still as textual source code). Typically, forwards-compatibility problems related to syntax are solved by using a transpiler (the most common one being Babel (https://babeljs.io)) to convert from that newer JS syntax version to an equivalent older syntax.
If the forwards-compatibility issue is not related to new syntax, but rather to a missing API method that was only recently added, the most common solution is to provide a definition for that missing API method that stands in and acts as if the older environment had already had it natively defined. This pattern is called a polyfill (aka “shim”). Transpilers like Babel typically detect which polyfills your code needs and provide them automatically for you. But occasionally you may need to include/define them explicitly, which works similar to the snippet we just looked at.
The interpretation of JS
JS is a compiled language, meaning the tools (including the JS engine) process and verify a program (reporting any errors!) before it executes.
-
After a program leaves a developer’s editor, it gets transpiled by Babel, then packed by Webpack (and perhaps half a dozen other build processes), then it gets delivered in that very different form to a JS engine.
-
The JS engine parses the code to an AST.
-
Then the engine converts that AST to a kind-of byte code, a binary intermediate representation (IR), which is then refined/converted even further by the optimizing JIT compiler.
-
Finally, the JS VM executes the program.
JS strict mode
Back in 2009 with the release of ES5, JS added strict mode as an opt-in mechanism for encouraging better JS programs.
Strict mode is switched on per file with a special pragma (nothing allowed before it except comments/whitespace):
// only whitespace and comments are allowed
// before the use-strict pragma
"use strict";
// the rest of the file runs in strict mode
Strict mode can alternatively be turned on per-function scope, with exactly the same rules about its surroundings:
function someOperations() {
// whitespace and comments are fine here
"use strict";
// all this code will run in strict mode
}
Interestingly, if a file has strict mode turned on, the function-level strict mode pragmas are disallowed. So you have to pick one or the other.
The only valid reason to use a per-function approach to strict mode is when you are converting an existing non-strict mode program file and need to make the changes little by little over time. Otherwise, it’s vastly better to simply turn strict mode on for the entire file/program.
Chapter 2: Surveying JS
Each File is a Program
The reason this matters is primarily around error handling. Since JS treats files as programs, one file may fail (during parse/compile or execution) and that will not necessarily prevent the next file from being processed.
Values in JS: primitive and object
Primitive:
-
string
-
boolean
-
number
-
null
-
undefined
Objects:
-
array: a special type of object that’s comprised of an ordered and numerically indexed list of data
-
function
-
object: more general - an unordered, keyed collection of any various values
typeof 42; // "number"
typeof "abc"; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof null; // "object" -- oops, bug!
typeof { "a": 1 }; // "object"
typeof [1,2,3]; // "object"
typeof function hello(){}; // "function"
Declaring and Using Variables
Keyword | Initial assignment | Block scoping | Re-assignment |
---|---|---|---|
var | Optional | No | Yes |
let | Optional | Yes | Yes |
const | Must | Yes | No |
Best to use const
only with primitive values, as objects can still be re-assigned even declared as const
, which can cause confusion.
Other syntactic forms that declare identifiers (variables) in various scopes. For example:
function hello(myName) {
console.log(`Hello, ${ myName }.`);
}
hello("Kyle");
// Hello, Kyle.
hello
and myName
generally behave as var
-declared.
Another syntax that declares a variable is a catch
clause:
try {
someError();
}
catch (err) {
console.log(err);
}
The err
is a block-scoped variable that exists only inside the catch
clause, as if it had been declared with let
.
Comparisons
The ===
operator is designed to lie in two cases of special values: NaN
and -0
. Consider:
NaN === NaN; // false
0 === -0; // true
Since the lying about such comparisons can be bothersome, it’s best to avoid using ===
for them. For NaN
comparisons, use the Number.isNaN(..)
utility, which does not lie. For -0
comparison, use the Object.is(..)
utility, which also does not lie. Object.is(..)
can also be used for non-lying NaN
checks, if you prefer. Humorously, you could think of Object.is(..)
as the “quadruple-equals” ====
, the really-really-strict comparison!
[ 1, 2, 3 ] === [ 1, 2, 3 ]; // false
{ a: 42 } === { a: 42 } // false
(x => x * 2) === (x => x * 2) // false
JS does not define ===
as structural equality for object values. Instead, ===
uses identity equality for object values.
JS does not provide a mechanism for structural equality comparison of object values, only reference identity comparison. To do structural equality comparison, you’ll need to implement the checks yourself.
Coercive Comparisons
The ==
operator performs an equality comparison similarly to how the ===
performs it. In fact, both operators consider the type of the values being compared. And if the comparison is between the same value type, both ==
and ===
do exactly the same thing, no difference whatsoever.
Just being aware of this nature of ==
—that it prefers primitive numeric comparisons—helps you avoid most of the troublesome corner cases, such as staying away from a gotchas like "" == 0
or 0 == false
.
These relational operators typically use numeric comparisons, except in the case where both values being compared are already strings; in this case, they use alphabetical (dictionary-like) comparison of the strings:
var x = "10";
var y = "9";
x < y; // true, watch out!
How We Organize in JS: classes and modules (data and behavior)
Class:
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);
}
}
ar YDKJS = new Book({
title: "You Don't Know JS",
author: "Kyle Simpson",
publishedOn: "June 2014",
publisher: "O'Reilly",
ISBN: "123456-789"
});
YDKJS.print();
// Title: You Don't Know JS
// By: Kyle Simpson
// June 2014
// Publisher: O'Reilly
// ISBN: 123456-789
var forAgainstLet = new BlogPost(
"For and against let",
"Kyle Simpson",
"October 27, 2014",
"https://davidwalsh.name/for-and-against-let"
);
forAgainstLet.print();
// Title: For and against let
// By: Kyle Simpson
// October 27, 2014
// https://davidwalsh.name/for-and-against-let
Classic module:
function Publication(title,author,pubDate) {
var publicAPI = {
print() {
console.log(`
Title: ${ title }
By: ${ author }
${ pubDate }
`);
}
};
return publicAPI;
}
function Book(bookDetails) {
var pub = Publication(
bookDetails.title,
bookDetails.author,
bookDetails.publishedOn
);
var publicAPI = {
print() {
pub.print();
console.log(`
Publisher: ${ bookDetails.publisher }
ISBN: ${ bookDetails.ISBN }
`);
}
};
return publicAPI;
}
function BlogPost(title,author,pubDate,URL) {
var pub = Publication(title,author,pubDate);
var publicAPI = {
print() {
pub.print();
console.log(URL);
}
};
return publicAPI;
}
var YDKJS = Book({
title: "You Don't Know JS",
author: "Kyle Simpson",
publishedOn: "June 2014",
publisher: "O'Reilly",
ISBN: "123456-789"
});
YDKJS.print();
// Title: You Don't Know JS
// By: Kyle Simpson
// June 2014
// Publisher: O'Reilly
// ISBN: 123456-789
var forAgainstLet = BlogPost(
"For and against let",
"Kyle Simpson",
"October 27, 2014",
"https://davidwalsh.name/for-and-against-let"
);
forAgainstLet.print();
// Title: For and against let
// By: Kyle Simpson
// October 27, 2014
// https://davidwalsh.name/for-and-against-let
Comparing these forms to the class
forms, there are more similarities than differences.
The class
form stores methods and data on an object instance, which must be accessed with the this.
prefix. With modules, the methods and data are accessed as identifier variables in scope, without any this.
prefix.
With class
, the “API” of an instance is implicit in the class definition—also, all data and methods are public. With the module factory function, you explicitly create and return an object with any publicly exposed methods, and any data or other unreferenced methods remain private inside the factory function.
Consider also the usage (aka, “instantiation”) of these module factory functions: The only observable difference here is the lack of using new
, calling the module factories as normal functions.
ES modules (ESM), introduced to the JS language in ES6, are meant to serve much the same spirit and purpose as the existing classic modules just described, especially taking into account important variations and use cases from AMD, UMD, and CommonJS.
The implementation approach does, however, differ significantly.
First, there’s no wrapping function to define a module. The wrapping context is a file. ESMs are always file-based; one file, one module.
Second, you don’t interact with a module’s “API” explicitly, but rather use the export
keyword to add a variable or method to its public API definition. If something is defined in a module but not export
ed, then it stays hidden (just as with classic modules).
Third, and maybe most noticeably different from previously discussed patterns, you don’t “instantiate” an ES module, you just import
it to use its single instance. ESMs are, in effect, “singletons,” in that there’s only one instance ever created, at first import
in your program, and all other import
s just receive a reference to that same single instance. If your module needs to support multiple instantiations, you have to provide a classic module-style factory function on your ESM definition for that purpose.
In our running example, we do assume multiple-instantiation, so these following snippets will mix both ESM and classic modules.
Consider the file publication.js
:
function printDetails(title,author,pubDate) {
console.log(`
Title: ${ title }
By: ${ author }
${ pubDate }
`);
}
export function create(title,author,pubDate) {
var publicAPI = {
print() {
printDetails(title,author,pubDate);
}
};
return publicAPI;
}
To import and use this module, from another ES module like blogpost.js
:
import { create as createPub } from "publication.js";
function printDetails(pub,URL) {
pub.print();
console.log(URL);
}
export function create(title,author,pubDate,URL) {
var pub = createPub(title,author,pubDate);
var publicAPI = {
print() {
printDetails(pub,URL);
}
};
return publicAPI;
}
And finally, to use this module, we import into another ES module like main.js
:
import { create as newBlogPost } from "blogpost.js";
var forAgainstLet = newBlogPost(
"For and against let",
"Kyle Simpson",
"October 27, 2014",
"https://davidwalsh.name/for-and-against-let"
);
forAgainstLet.print();
// Title: For and against let
// By: Kyle Simpson
// October 27, 2014
// https://davidwalsh.name/for-and-against-let
As shown, ES modules can use classic modules internally if they need to support multiple-instantiation. Alternatively, we could have exposed a class
from our module instead of a create(..)
factory function, with generally the same outcome. However, since you’re already using ESM at that point, I’d recommend sticking with classic modules instead of class
.
If your module only needs a single instance, you can skip the extra layers of complexity: export
its public methods directly.