You don't know JS
Chapter 2: Illustrating Lexical Scope
// outer/global scope: RED
var students = [
{ id: 14, name: "Kyle" },
{ id: 73, name: "Suzy" },
{ id: 112, name: "Frank" },
{ id: 6, name: "Sarah" }
];
function getStudentName(studentID) {
// function scope: BLUE
for (let student of students) {
// loop scope: GREEN
if (student.id == studentID) {
return student.name;
}
}
}
var nextStudent = getStudentName(73);
console.log(nextStudent); // Suzy
函数参数可以看做函数内部定义的一个本地变量
Inside the getStudentName function body, studentID is treated as a local variable.
箭头函数和this指针
在 JavaScript 中,普通函数的 this 会根据调用的方式动态改变。
而箭头函数(arrow function),作为一种匿名函数,它的 this 是在定义时就固定好的,绑定到创建它的作用域中的 this,这就是“词法绑定” this 的含义.
Chapter 4
Around the Global Scope
Globals Shadowing Globals
The difference between following two code snippets.
window.something=12
let something=13
window.something=12
var something=13
DOM Globals
One surprising behavior in the global scope you may encounter with browser-based JS applications: a DOM element with an id attribute automatically creates a global variable that references it.
Consider this markup:
<ul id="my-todo-list">
<li id="first">Write a book</li>
..
</ul>
And the JS for that page could include:
first;
// <li id="first">..</li>
window["my-todo-list"];
// <ul id="my-todo-list">..</ul>
What's in a (Window) Name?
var name = 42;
console.log(name, typeof name);
// "42" string
But the truly surprising behavior is that even though we assigned the number 42 to name (and thus window.name), when we then retrieve its value, it's a string "42"! In this case, the weirdness is because name is actually a pre-defined getter/setter on the window object, which insists on its value being a string value. Yikes!
Web Workers
Web Workers are a web platform extension on top of browser-JS behavior, which allows a JS file to run in a completely separate thread (operating system wise) from the thread that's running the main JS program.
Since these Web Worker programs run on a separate thread, they're restricted in their communications with the main application thread, to avoid/limit race conditions and other complications. Web Worker code does not have access to the DOM, for example. Some web APIs are, however, made available to the worker, such as navigator.
In a Web Worker, the global object reference is typically made using self:
var studentName = "Kyle";
let studentID = 42;
function hello() {
console.log(`Hello, ${ self.studentName }!`);
}
self.hello();
// Hello, Kyle!
self.studentID;
// undefined
Node
global.studentName = "Kyle";
function hello() {
console.log(`Hello, ${ studentName }!`);
}
hello();
// Hello, Kyle!
module.exports.hello = hello;
Others
const theGlobalScopeObject =
(new Function("return this"))();
const theGlobalScopeObject =
(typeof globalThis != "undefined") ? globalThis :
(typeof global != "undefined") ? global :
(typeof window != "undefined") ? window :
(typeof self != "undefined") ? self :
(new Function("return this"))();
Chapter 5: Lifecycle of Variables
Hoisting: Declaration vs. Expression
A function declaration is hoisted and initialized to its function value (again, called function hoisting). A var variable is also hoisted, and then auto-initialized to undefined. Any subsequent function expression assignments to that variable don't happen until that assignment is processed during runtime execution.
greeting();
// TypeError
var greeting = function greeting() {
console.log("Hello!");
};
greeting = "Hello!";
console.log(greeting);
// Hello!
var greeting = "Howdy!";
Re-declaration?
var studentName = "Frank";
console.log(studentName);
// Frank
var studentName;
console.log(studentName); // ???
hoisting is actually about registering a variable at the beginning of a scope
var studentName = "Frank";
console.log(studentName); // Frank
var studentName;
console.log(studentName); // Frank <--- still!
// let's add the initialization explicitly
var studentName = undefined;
console.log(studentName); // undefined <--- see!?
var greeting;
function greeting() {
console.log("Hello!");
}
// basically, a no-op
var greeting;
typeof greeting; // "function"
var greeting = "Hello!";
typeof greeting; // "string"
let studentName = "Frank";
var studentName = "Suzy"; // SyntaxError
Loop
var keepGoing = true;
while (keepGoing) {
let value = Math.random();
if (value > 0.5) {
keepGoing = false;
}
}
All the rules of scope (including "re-declaration" of let-created variables) are applied per scope instance. In other words, each time a scope is entered during execution, everything resets.
Each loop iteration is its own new scope instance, and within each scope instance, value is only being declared once.
for (let i = 0; i < 3; i++) {
let value = i * 10;
console.log(`${ i }: ${ value }`);
}
// 0: 0
// 1: 10
// 2: 20
equivalent form
{
// a fictional variable for illustration
let $$i = 0;
for ( /* nothing */; $$i < 3; $$i++) {
// here's our actual loop `i`!
let i = $$i;
let value = i * 10;
console.log(`${ i }: ${ value }`);
}
// 0: 0
// 1: 10
// 2: 20
}
TDZ
The TDZ is the time window where a variable exists but is still uninitialized, and therefore cannot be accessed in any way. Only the execution of the instructions left by Compiler at the point of the original declaration can do that initialization. After that moment, the TDZ is done, and the variable is free to be used for the rest of the scope.
studentName = "Suzy"; // let's try to initialize it!
// ReferenceError
console.log(studentName);
let studentName;
var studentName = "Kyle";
{
console.log(studentName);
// ???
// ..
let studentName = "Suzy";
console.log(studentName);
// Suzy
}
Chapter 6: Limiting Scope Exposure
Hiding in Plain (Function) Scope
var factorial = (function hideTheCache() {
var cache = {};
function factorial(x) {
if (x < 2) return 1;
if (!(x in cache)) {
cache[x] = x * factorial(x - 1);
}
return cache[x];
}
return factorial;
})();
factorial(6);
// 720
factorial(7);
// 5040
So, in other words, we're defining a function expression that's then immediately invoked. This common pattern has a (very creative!) name: Immediately Invoked Function Expression (IIFE).
var and let
var attaches to the nearest enclosing function scope, no matter where it appears. That's true even if var appears inside a block:
function diff(x,y) {
if (x > y) {
var tmp = x; // `tmp` is function-scoped
x = y;
y = tmp;
}
return y - x;
}
Chapter 7: Using Closures
closure is variable-oriented
var studentName = "Frank";
var greeting = function hello() {
// we are closing over `studentName`,
// not "Frank"
console.log(
`Hello, ${ studentName }!`
);
}
// later
studentName = "Suzy";
// later
greeting();
// Hello, Suzy!
var keeps = [];
for (var i = 0; i < 3; i++) {
keeps[i] = function keepI(){
// closure over `i`
return i;
};
}
keeps[0](); // 3 -- WHY!?
keeps[1](); // 3
keeps[2](); // 3
var keeps = [];
for (var i = 0; i < 3; i++) {
// new `j` created each iteration, which gets
// a copy of the value of `i` at this moment
let j = i;
// the `i` here isn't being closed over, so
// it's fine to immediately use its current
// value in each loop iteration
keeps[i] = function keepEachJ(){
// close over `j`, not `i`!
return j;
};
}
keeps[0](); // 0
keeps[1](); // 1
keeps[2](); // 2
var keeps = [];
for (let i = 0; i < 3; i++) {
// the `let i` gives us a new `i` for
// each iteration, automatically!
keeps[i] = function keepEachI(){
return i;
};
}
keeps[0](); // 0
keeps[1](); // 1
keeps[2](); // 2
Per Variable or Per Scope?
An Alternative Perspective
// outer/global scope: RED(1)
function adder(num1) {
// function scope: BLUE(2)
return function addTo(num2){
// function scope: GREEN(3)
return num1 + num2;
};
}
var add10To = adder(10);
var add42To = adder(42);
add10To(15); // 25
add42To(9); // 51
Why Closure?
var APIendpoints = {
studentIDs:
"https://some.api/register-students",
// ..
};
var data = {
studentIDs: [ 14, 73, 112, 6 ],
// ..
};
function makeRequest(evt) {
var btn = evt.target;
var recordKind = btn.dataset.kind;
ajax(
APIendpoints[recordKind],
data[recordKind]
);
}
// <button data-kind="studentIDs">
// Register Students
// </button>
btn.addEventListener("click",makeRequest);
it's unfortunate (inefficient, more confusing) that the event handler has to read a DOM attribute each time it's fired.
var APIendpoints = {
studentIDs:
"https://some.api/register-students",
// ..
};
var data = {
studentIDs: [ 14, 73, 112, 6 ],
// ..
};
function setupButtonHandler(btn) {
var recordKind = btn.dataset.kind;
btn.addEventListener(
"click",
function makeRequest(evt){
ajax(
APIendpoints[recordKind],
data[recordKind]
);
}
);
}
// <button data-kind="studentIDs">
// Register Students
// </button>
setupButtonHandler(btn);
function setupButtonHandler(btn) {
var recordKind = btn.dataset.kind;
var requestURL = APIendpoints[recordKind];
var requestData = data[recordKind];
btn.addEventListener(
"click",
function makeRequest(evt){
ajax(requestURL,requestData);
}
);
}
Two similar techniques from the Functional Programming (FP) paradigm that rely on closure are partial application and currying. Briefly, with these techniques, we alter the shape of functions that require multiple inputs so some inputs are provided up front, and other inputs are provided later; the initial inputs are remembered via closure. Once all inputs have been provided, the underlying action is performed.
function defineHandler(requestURL,requestData) {
return function makeRequest(evt){
ajax(requestURL,requestData);
};
}
function setupButtonHandler(btn) {
var recordKind = btn.dataset.kind;
var handler = defineHandler(
APIendpoints[recordKind],
data[recordKind]
);
btn.addEventListener("click",handler);
}
Summary
We explored two models for mentally tackling closure:
Observational: closure is a function instance remembering its outer variables even as that function is passed to and invoked in other scopes.
Implementational: closure is a function instance and its scope environment preserved in-place while any references to it are passed around and invoked from other scopes.
Benefits to our programs:
Closure can improve efficiency by allowing a function instance to remember previously determined information instead of having to compute it each time.
Closure can improve code readability, bounding scope-exposure by encapsulating variable(s) inside function instances, while still making sure the information in those variables is accessible for future use. The resultant narrower, more specialized function instances are cleaner to interact with, since the preserved information doesn't need to be passed in every invocation.
Chapter 8: The Module Pattern
Data Structures (Stateful Grouping)
// data structure, not module
var Student = {
records: [
{ id: 14, name: "Kyle", grade: 86 },
{ id: 73, name: "Suzy", grade: 87 },
{ id: 112, name: "Frank", grade: 75 },
{ id: 6, name: "Sarah", grade: 91 }
],
getName(studentID) {
var student = this.records.find(
student => student.id == studentID
);
return student.name;
}
};
Student.getName(73);
// Suzy
Use IIFE as module (singleton)
var Student = (function defineStudent(){
var records = [
{ id: 14, name: "Kyle", grade: 86 },
{ id: 73, name: "Suzy", grade: 87 },
{ id: 112, name: "Frank", grade: 75 },
{ id: 6, name: "Sarah", grade: 91 }
];
var publicAPI = {
getName
};
return publicAPI;
// ************************
function getName(studentID) {
var student = records.find(
student => student.id == studentID
);
return student.name;
}
})();
Student.getName(73); // Suzy
use function for multi-instances
// factory function, not singleton IIFE
function defineStudent() {
var records = [
{ id: 14, name: "Kyle", grade: 86 },
{ id: 73, name: "Suzy", grade: 87 },
{ id: 112, name: "Frank", grade: 75 },
{ id: 6, name: "Sarah", grade: 91 }
];
var publicAPI = {
getName
};
return publicAPI;
// ************************
function getName(studentID) {
var student = records.find(
student => student.id == studentID
);
return student.name;
}
}
var fullTime = defineStudent();
fullTime.getName(73); // Suzy
Node CommonJS Modules
module.exports.getName = getName;
// ************************
var records = [
{ id: 14, name: "Kyle", grade: 86 },
{ id: 73, name: "Suzy", grade: 87 },
{ id: 112, name: "Frank", grade: 75 },
{ id: 6, name: "Sarah", grade: 91 }
];
function getName(studentID) {
var student = records.find(
student => student.id == studentID
);
return student.name;
}
var Student = require("/path/to/student.js");
Student.getName(73);
// Suzy
Modern ES Modules (ESM)
export { getName };
// ************************
var records = [
{ id: 14, name: "Kyle", grade: 86 },
{ id: 73, name: "Suzy", grade: 87 },
{ id: 112, name: "Frank", grade: 75 },
{ id: 6, name: "Sarah", grade: 91 }
];
function getName(studentID) {
var student = records.find(
student => student.id == studentID
);
return student.name;
}
export function getName(studentID) {
// ..
}
import { getName } from "/path/to/students.js";
export default function getName(studentID) {
// ..
}
import getName from "/path/to/students.js";
import * as Student from "/path/to/students.js";
Student.getName(1);