Preamble
This is a self-study effort from January 2025. Scripts I wrote for the applicable chapter exercises are at the bottom of this page's source code.
I've been eying this textbook for a few days now, but since I have this website, I figure this is a good place to host my notes in an environment where I can actually execute JavaScript. I didn't have a place to put it, though, so I changed my site structure to add supporting pages that lead to this one.
All this just to start studying? Yes. I enjoy the content of the work and the learning involved, plus an investment in myself pays dividends in the future. What kind of fool works without being told? One who's curious or creative or future focused or otherwise self-driven... that kind of fool! And now, I'll fool around with the content of this book.

Chapter 0 - Introduction
"A sense of what a good program looks like is developed with practice, not learned from a list of rules."
This chapter discusses very basic concepts on the what & why of programming & JavaScript, so the part that shines out to me is the above quote. The author isn't teaching from a place of telling you what should be done, but rather from preparing you to tell yourself what should be done.
One who's not an artist doesn't know the rules. A good artist knows the rules well and follows them well. A great artist knows the rules well... and breaks them. Not by simply ignoring them, but by deconstructing the very reasons those rules exist.
The author and I share some values. That's what I've learned from this book's introduction.
Chapter 1 - Values, Types, and Operators
That's not a bug, it's a feature.
This chapter had lots of basic operations, plus the nullish operator ("??"), which browsers didn't even widely support until 2020. I'm not sure a great number of people formally educated until just recently would even know about it if they're not doing occasional reviews of the base technology like I'm doing here. Even then, lots of documentation was made before this was well-supported. It's a case where the latest textbook edition is necessary if it's to supply this knowledge. Sure enough, this feature isn't in the same textbook's previous edition.
These are some concepts from this chapter that I haven't been using in my JavaScript:
-
2.998e8
// yields 299800000 -
x = a && b;
// "guard operator" where a is not boolean; only gives b if a is truthy, and gives back the falsey a otherwise -
x = a || b;
// "default operator" where a is not boolean; gives a if it's truthy, and gives the default value b otherwise -
x = a ?? b;
// "nullish coalescing operator" where a is not boolean; gives a if it's not null or undefined (so can give falsey a values like 0 or ""), and gives the default value b otherwise -
NaN != NaN
Here are some concepts I know that supplement this chapter:
-
+'5' + 1 === 6 //
plus sign as a unary operator explicitly coerces a string to a number, though it's not as complete as parseInt('5'), as +'5px' would instead yield NaN -
!!'5' === true; !!{} === true; !!0 === false;
// double exclamation point as a unary operator explicitly coerces any value to a boolean -
!'5' === false; !{} === false; !0 === true;
// single exclamation point as a unary operator explicitly converts any value to its opposite boolean -
299_800_000
// yields 299800000; underscores can be added in any numeric literals to make them more readable
Between these two lists, some of them are maybe not intended by the language, but they're all consistent enough to be used as outright features.
Chapter 2 - Program Structure
The devil's in the details.
I already knew everything that was in this chapter, or at
least, I knew everything it intended for me to know. Of
the (offhand-mentioned) list of reserved keywords, I
haven't been using most of the object-oriented ones in my
JavaScript. The keywords async
and
await
are conspicuously missing from the
chapter's list of keywords, and I don't think I've ever
used the keywords with
or yield
.
None of this is in the intended scope of the chapter, but
if I'm brushing up on my JavaScript, I think it's good to
look out for places where I can be thorough.
The with
keyword is deprecated, but here's
my first script using yield
. The below
elements generate a number between the lower & upper
bounds, then stops generating after 10 yields, unless you
change the bounds:
[press the button to
yield
a value from a generator
function]
Chapter 2 Exercises
-
Exercise 2.1 - Looping a Triangle
#
##
###
####
#####
######
####### -
Exercise 2.2 - FizzBuzz
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
31
32
Fizz
34
Buzz
Fizz
37
38
Fizz
Buzz
41
Fizz
43
44
FizzBuzz
46
47
Fizz
49
Buzz
Fizz
52
53
Fizz
Buzz
56
Fizz
58
59
FizzBuzz
61
62
Fizz
64
Buzz
Fizz
67
68
Fizz
Buzz
71
Fizz
73
74
FizzBuzz
76
77
Fizz
79
Buzz
Fizz
82
83
Fizz
Buzz
86
Fizz
88
89
FizzBuzz
91
92
Fizz
94
Buzz
Fizz
97
98
Fizz
Buzz -
Exercise 2.3 - Chessboard
_#_#_#_#
#_#_#_#_
_#_#_#_#
#_#_#_#_
_#_#_#_#
#_#_#_#_
_#_#_#_#
#_#_#_#_
Chapter 3 - Functions
Every instruction is made of smaller instructions, every little bit's got a million bits.
These chapters are getting longer now. There's a little quirk to be found in a chapter that instructs how to write sets of instructions, while part of that instruction is an example specifically about recursion.
The book doesn't specifically endorse one way or the
other when it comes to creating functions, but I'm bought
in on the arrow functions. I always thought that unless
two functions must reference one another, it's bad form to
use a function before it's defined in the code. So I
didn't use this quirk of function fun() {}
declaration notation and wound up forgetting about it.
Plus, this notation never lets you define fun
as a constant, and I always define functions as
const
whenever possible. const fun = ()
=> {}
is valid JavaScript, and const function
fun() {}
is not.
I should also note that I'd forgotten about default
function inputs that replace undefined ones, where a
function is defined (a, b = 'default input') =>
{}
so that if a function call is missing an
argument, it can fill b
in with the
assignment expression in the function arguments
definition.
Chapter 3 Exercises
-
Exercise 3.1 - Minimum
Their minimum: -
Exercise 3.2 - Recursion
Even or odd? -
Exercise 3.3 - Bean Counting
CountBs:
CountChar:
Chapter 4 - Data Structures: Objects and Arrays
This is the longest chapter in the book!
Sure enough, my ability with objects in JavaScript has
been pretty under-utilized. In fact, I probably learned
things in the wrong order the first time around, because I
never use the in
or delete
keywords, because I didn't understand them when I first
learned about them, because I wasn't using JavaScript
objects until later on! So here's another list of things I
haven't been using in my JavaScript:
-
for(const keyName in object) { ... }
// for-in loops iterate over the keys in an object -
const obj = {a, b}
// gives the keys "a" and "b" to an object and assigns them the corresponding values -
obj['2']; obj['two words']; {"two words": 2, "fun()": 'yay'}
// can assign object keys using otherwise invalid variable names, by using quotes -
delete obj.a
// removes a key from an object -
Object.assign(a, b, c, ...)
// merges objects, copying all properties from later objects into the first object -
arr[-2]; arr['three']
// arrays can have any key because they're just objects; I think it's bad form, but it's good to know -
arr.includes('somevalue', num)
// checks if an array has a given value at or above the index number num -
'somekey' in obj
// returns a boolean representing whether some keyname is in the object -
let array2 = array1.slice()
// slice an array without arguments to just return a new independent copy of the array -
str.padStart(8, "0")
// pads a string; if this string is not 8 characters long, leading 0s are added until it is -
arr.join(' - ')
// joins all elements of an array into a single string, with the argument being added between each entry; [1, 2, 3] -> 1 - 2 - 3 -
let array2 = ['a', ...array1, 'z']
// puts all elements from array1 into array2, between the 'a' and 'z' values -
([a, b, c, d]) => {...}
// assigns values from an input array to the values of the variables a, b, c, and d; more wieldy than just using the array -
JSON.stringify({}); JSON.parse(someJsonString);
// converts to and from JSON strings, largely for API calls -
arr?.[0]; obj?.fun?.()
// the winking elvis operator's notation for arrays & function calls -
n => ({prop: n})
// shorthand for an arrow function that returns an object; parentheses keep object from being interpreted as a function body
In spite of the increasing verbosity of the chapters, I
know some things would remain a complete mystery for an
actual beginner reading this book. In the "Destructuring"
section, it says you can use square brackets to "look
inside" of an array value to bind its contents, but it
doesn't give an example, making this a nearly
indecipherable remark if you don't see an example
(let [a, b, c, d] = someArrayWith4Elements
).
The chapter had a bit of math, too, which is cool. It sometimes takes lots of math & working out general formulas to make my scripts act the way I want them to, so seeing someone else using math as part of their process is refreshing. That's surprisingly uncommon for what programming fundamentally is.
Chapter 4 Exercises
-
Exercise 4.1 - The Sum of a Range
Range values:[1,2,3,4,5,6,7,8,9,10]
Sum of the range:55
-
Exercise 4.2 - Reversing an Array
Reversed array:[0,"+ -",true,3,"A"]
Array before in-place reversal:["A",3,true,"+ -",0]
Array after in-place reversal:[0,"+ -",true,3,"A"]
-
Exercise 4.3 - A List
List constructed from array:{"value":"A","rest":{"value":3,"rest":{"value":true,"rest":{"value":"+ -","rest":{"value":null,"rest":null}}}}}
Array reconstructed from list:["A",3,true,"+ -",null]
Nth element from the list:"A"
-
Exercise 4.4 - Deep Comparison
Are they deeply equal?false
Chapter 5 - Higher-order Functions
Yo, dawg, I heard you like functions.
Well, I looked ahead and saw that chaper 4 was the longest chapter in the entire book. This chapter was significantly shorter, though it also covers inbuilt language specifics that are wheels I've tended to reinvent rather than using their implementations that are available by default. Hopefully by having run by these here, I'll have better recall when it comes time to use them:
-
filteredArray = array.filter(item => { return boolean; })
// array function to filter out items that return falsey when passed to the given function -
mappedArray = array.map(item => { return doStuff(item); })
// array function to process each item into a value derived from that item, then return those derived values in an array -
value = array.reduce((item1, item2) => { return combinedItem; }, startItem?)
// array function that combines the first value with each subsequent value, using the combining function, iteratively until they're all combined, returning the combination -
array.some(arg => { return boolean; })
// array function returning true if ANY of its values return truthy when passed into the given function -
array.find( item => { return boolean; } )
// array function finding the first element that returns true when it's passed to the given function -
(a, {b}) => { ... }
// asserts that b is a property of an object and uses that property by its value when it's in the function
Chapter 5 Exercises
-
Exercise 5.1 - Flattening
Flattened array:["A",3,-1,true,"+ -",null]
-
Exercise 5.2 - Your Own Loop
This exercise is disabled on the live page. Below is my solution; it can be pasted in the code area in the "Your Own Loop" section here on the textbook site.
const loop = (value, test, update, body) => { let i = value; while(test(i)) { body(i); i = update(i); console.log(i); console.log(output1.innerText); } return i; }; loop(3, n => n > 0, n => n - 1, console.log); // → 3 // → 2 // → 1
-
Exercise 5.3 - Everything
Any exercise whose input would require arbitrary function definitions won't be allowed to run off of UI elements on the live site. My solution below can be checked by pasting it into the code section under the "Everything" header here on the textbook site.
const every = (arr, test) => { for(const item of arr) { if(!test(item)) { return false; } } return true; } console.log(every([1, 3, 5], n => n < 10)); // → true console.log(every([2, 4, 16], n => n < 10)); // → false console.log(every([], n => n < 10)); // → true
-
Exercise 5.4 - Dominant Writing Direction
This exercise has in-page dependencies on the Eloquent JavaScript site. My solution is below and can be pasted & run on the code section near the bottom of this page.
const dominantDirection = (text) => countBy(text, n => characterScript(n.codePointAt(0))) .filter(n => n.name != null) .reduce((a, b) => a.count > b.count ? a : b) .name .direction ; console.log(dominantDirection("Hello!")); // → ltr console.log(dominantDirection("Hey, مساء الخير")); // → rtl
Chapter 6 - The Secret Life of Objects
It's short for "classification."
There's something so arcane about the Symbol type. When a Symbol is instantiated, I imagine a unique glyph appearing and being used as the label for whatever property it corresponds to.
This chapter had more tinkering with objects, so here are some more concepts I haven't been using in my code:
-
const fun = () => { console.log(this) }; const a = {fun}, b = {fun}; a.fun(); b.fun();
// the "this" keyword in functions binds dynamically to any object that uses the function as a method -
const fun = (property) => { console.log(this[property]) }; fun.call(obj, fun);
// "this" can also be dynamically bound by using an object in the function's "call" method -
const obj = { fun(arg) { return () => { return this[arg] } } };
// shorthand java-like object method assignment, and "this" is usable in ARROW functions defined in the method (not "function fun()"" declarations) -
Object.getPrototypeOf(obj)
// returns the prototype of an object, which is like a parent object it's extending; obj always has access to all properties of its prototype, even if obj itself is empty -
let obj2 = Object.create(obj1);
// creates an empty obj2 that has access to all the properties of obj1; Object.getPrototypeOf(obj2) will return obj1 -
"function constructObj(arg) {const obj2 = Object.create(obj); obj2.instanceProperty = arg; return obj2;}" ~= "class Obj {constructor(arg) {this.instanceProperty = arg} classProperty: }"
// class instead of prototype as an object's template; its prototype only any methods defined in the class - // a class is fundamentally just a function with a .prototype property where it references methods added to the class, with the new keyword doing the work to make objects invoked with the constructor contain the methods in .prototype; was confusing me until I read this rundown
- // only non-arrow functions have the .prototype property, which is why only arrow functions work well with "this" bindings when they're defined within a class method's body
-
const SomeClass = class { ... }
// constant class name binding declaration -
let obj = new class { log() { console.log(this) } }; obj.log();
// object declaration as an instance of an anonymous class -
class { #secretField = 3; #secretMethod() { console.log('this can only be called from inside the class') } publicMethod() { this.#secretMethod(); } }
// a class can have private properties by starting their names with # - // private properties MUST be declared within the class; only public properties can be assigned to the class or object instance after the class is defined
-
SomeClass.prototype.property2 = 'new property';
// property2 becomes visible in all objects that are instances of SomeClass -
SomeClass.prototype.method2 = function() { console.log(this); };
// for adding new methods to the prototype, it's best to use the function keyword instead of an arrow function, so that any "this" instances don't bind to the global scope -
const pureObject = Object.create(null);
// creates an object with no prototype; only has what you define without inheriting any properties from Object.prototype -
Object.keys(obj);
// note that it does not return its prototype's values, only the object's own values -
let a = Symbol(2); let obj = { a: 2, [a]: 'symbol property', b: 4}; obj.a === 2; obj[a] === 'symbol property';
// can define a symbol as an object property name by giving it square brackets in the object definition declaration -
class { static fun() { new this() } }
// classes can have static methods that are class members instead of instance members, and new this() calls the constructor of the class where it's contained -
class IterableThing{ constructor(){} [Symbol.iterator]() { return new class ThingIterator{ constructor(){} next() { return {value, done} } } } }
// iterator interface; makes the class objects usable for a for-of loop and for the destructuring operator ("...thing") -
static staticProperty = 'some value'; static staticMethod() { return this.staticProperty }
// static methods can access static properties with the this keyword, as the object instance is the class constructor in this context
I already knew about maps, the "extends" & "static" & "super" keywords work like in Java. The syntax is similar, but you can tell that all things about JavaScript classes are syntactic construct made by composing objects & functions such that they can look & act like Java classes, with the syntax hiding all of the complexity required for that underneath. It's not a fundamental of the language like it is in Java; it's a bunch of other concepts all coming together to make this, which makes it harder to wrap your head around, despite the code itself being mostly familiar.
Chapter 6 Exercises
-
Exercise 6.1 - A Vector Type
Vector 1:
Vector 2:
Vector 1 + Vector 2:[11, 19]
Vector 1 - Vector 2:[5, 11]
Vector 1 length:17
Vector 2 length:5
-
Exercise 6.2 - Groups
Add this value to the group:
Delete this value from the group:
Check if the group has this value:
Does the group have the above value?Yes
-
Exercise 6.3 - Iterable Groups
Add this value to the group:
Delete this value from the group:
Check if the group has this value:
Does the group have the above value?Yes
Iterative printout of values in this Group:
10
20
Chapter 7 - Project: A Robot
Go, RoboJackets!
It's a program that aggregates what's been covered so far and makes a little crawler that navigates around a graph data structure. It's pretty walkthrough-ey and is about as much of a project as any one the DHTML effects I noodled out to add motion to some pages on this site. It adds a couple of new things, though:
-
const frozenObj = Object.freeze(obj)
// frozenObj will ignore any changes made to its properties -
const roll = Symbol('roll'); Array.prototype[roll] = function() { return this[Math.floor(Math.random() * this.length)]; };
// adds arr[roll]() method to arrays that returns a random element from that array - // all prototype changes to commonly used objects should be done through a Symbol address, to avoid naming collisions
-
let {property, item} = obj;
// can assign variable bindings from an object with properties of the same names
Chapter 7 Exercises
-
Exercise 7.1 - Measuring a Robot
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
runRobot = function(state, robot, memory) { for (let turn = 0;; turn++) { if (state.parcels.length == 0) { return turn; } let action = robot(state, memory); state = state.move(action.direction); memory = action.memory; } }; function compareRobots(robot1, memory1, robot2, memory2) { let [sum1, sum2] = [0, 0]; for(let i = 0; i < 100; i++) { const initialState = VillageState.random(); sum1 += runRobot(initialState, robot1, memory1); sum2 += runRobot(initialState, robot2, memory2); } const [avg1, avg2] = [sum1 / 100, sum2 / 100]; console.log(`Average 1: ${avg1}; Average 2: ${avg2}`); return [avg1, avg2]; }; compareRobots(routeRobot, [], goalOrientedRobot, []);
-
Exercise 7.2 - Robot Efficiency
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
I think I derived some of graph theory when making this. My explanations for this are in code comments on my JavaScript file for the chapter exercises.
const buildRouteGraph = (graph) => { const routeGraph = {}; // create all one-step routes for(const fromLocation in graph) { const routes = {}; routeGraph[fromLocation] = routes; for(const toLocation in graph) { if(toLocation !== fromLocation) { routes[toLocation] = graph[fromLocation].includes(toLocation) ? [toLocation] : []; } } } // for each location, branch from the known routes until all destinations have routes for(const fromLocation in routeGraph) { const routesFromLocation = routeGraph[fromLocation]; const knownLocations = [fromLocation]; // get all known locations for this location for(const knownRoutes in routesFromLocation) { const routeToDestination = routesFromLocation[knownRoutes]; const knownDestination = routeToDestination.length > 0 ? routeToDestination[routeToDestination.length - 1] : null; if(knownDestination !== null && !knownLocations.includes(knownDestination)) { knownLocations.push(knownDestination); } } // get the next step that comes after these locations, and from this, build routes to the places that weren't known for(const knownLocation of knownLocations) { const adjacentLocations = graph[knownLocation]; for(const adjacentLocation of adjacentLocations) { if(!knownLocations.includes(adjacentLocation)) { routesFromLocation[adjacentLocation] = routesFromLocation[knownLocation].slice(); routesFromLocation[adjacentLocation].push(adjacentLocation); knownLocations.push(adjacentLocation); } } } // !!! BECAUSE KNOWN LOCATIONS EXPANDS WITH EACH SUCCESSFUL ITERATION, THIS ALSO ITERATES OVER NEW KNOWN LOCATIONS, FINDING ALL ROUTES // (this exceeded expected behavior upon writing; I expected it to find routes up to 2 spaces away) } return routeGraph; }; const routeGraph = buildRouteGraph(roadGraph); const currentState = VillageState.random(); const myRobot = (villageState, memory) => { // if it's not currently en-route, pick a next route if(memory.length === 0) { const currentLocation = villageState.place; const parcels = villageState.parcels; // if a parcel is held, assess its destination; if not, assess its location let distance = Infinity; for(const parcel of parcels) { const potentialDestination = parcel.place == currentLocation ? parcel.address : parcel.place ; const potentialRoute = routeGraph[currentLocation][potentialDestination]; if(potentialRoute.length <= distance) { memory = potentialRoute; distance = memory.length; } } } return { direction: memory[0], memory: memory.slice(1) }; }; runRobotAnimation(currentState, myRobot, []); // robot measurement assessment; copied from my answer to the last exercise runRobot = function(state, robot, memory) { for (let turn = 0;; turn++) { if (state.parcels.length == 0) { return turn; } let action = robot(state, memory); state = state.move(action.direction); memory = action.memory; } }; function compareRobots(robot1, memory1, robot2, memory2) { let [sum1, sum2] = [0, 0]; for(let i = 0; i < 100; i++) { const initialState = VillageState.random(); sum1 += runRobot(initialState, robot1, memory1); sum2 += runRobot(initialState, robot2, memory2); } const [avg1, avg2] = [sum1 / 100, sum2 / 100]; console.log(`Average 1: ${avg1}; Average 2: ${avg2}`); return [avg1, avg2]; }; // myRobot average: ~12.5, goalOrientedRobot average: ~15 compareRobots(myRobot, [], goalOrientedRobot, []);
-
Exercise 7.3 - Persistent Group
Here's the link to the exercise, and below is my solution that can be pasted into the code box.
const PGroup = class { static #empty = new this([]); #group; constructor(values) { this.#group = values; } static get empty() { return this.#empty; } add(item) { const newGroup = this.#group.slice(); if(!this.has(item)) { newGroup.push(item); } return new PGroup(newGroup); } delete(item) { const newGroup = this.#group.slice(); const itemIndex = newGroup.indexOf(item); if(itemIndex !== -1) { newGroup.splice(itemIndex, 1); } return new PGroup(newGroup); } has(item) { return this.#group.includes(item); } }; let a = PGroup.empty.add("a"); let ab = a.add("b"); let b = ab.delete("a"); console.log(b.has("b")); // → true console.log(a.has("b")); // → false console.log(b.has("a")); // → false
Chapter 8 - Bugs and Errors
I start from the wall outlet, then I check the power supply unit, then I check the motherboard, ...
About the only debugging feature I've used in JavaScript was exception handling & console logging, so this is mostly new territory for me. Here are some features to keep a note of from this chapter:
-
"use strict";
// can be used as the first line in a script or function description to enable strict mode, throwing errors to help debug code-
function fun() { "use strict"; i = 10; console.log(i) };
// keeps undeclared function variables from silently being declared in the global scope -
"use strict"; function fun(property) { this.property = property };
// keeps function variables from binding "this" to the global scope whenever the functions aren't used as methods -
"use strict"; with
// keeps from using "with" statements -
"ust strict"; function fun(a, b, a) {};
// keeps a function from getting multiple parameters with the same name - // + much more, but these are the features highlighted in the book
-
-
debugger;
// sets a break point in the program; in a debug-enabled environment (like the browser's F12 developer tools), this pauses the program and allows you to inspect the values of variable bindings at that point -
try { ... } finally { ... }
// a finally block does not stop an exception from propagating; it runs, then the exception continues up the call stack until caught -
if(thing.happens) throw new Error("thing happened");
// this is an "assertion" construct, asserting that something doesn't happen, by throwing an exception if it DOES happen -
const thing = new class { ... }
// creates an instance of an anonymous class
Chapter 8 Exercises
-
Exercise 8.1 - Retry
Product: -
Exercise 8.2 - The Locked Box
Here's the link to the exercise, and below is my solution that can be pasted into the final code box.
class LockedBoxError extends Error {} const box = new class { locked = true; #content = []; unlock() { this.locked = false; } lock() { this.locked = true; } get content() { if (this.locked) throw new LockedBoxError("Locked!"); return this.#content; } }; function withBoxUnlocked(body) { try { box.unlock(); body(); } catch(e) { if(e instanceof LockedBoxError) { console.log(e); } throw e; } finally { box.lock(); } } withBoxUnlocked(() => { box.content.push("gold piece"); }); try { withBoxUnlocked(() => { throw new Error("Pirates on the horizon! Abort!"); }); } catch (e) { console.log("Error raised: " + e); } console.log(box.locked); // → true box.unlock(); console.log(box.content);
Chapter 9 - Regular Expressions
It's slow, it's cryptic, it can always be done another way, but it is powerful.
It's a one-line solution for otherwise many-line problems. Sure, it's general-use code and has the common pitfalls when compared against purpose-built code where it's applicable, but it wraps a LOT of functionality into a relatively tiny package.
Here's a long list of concepts for regular expressions in JavaScript, enough to take me from zero to practical:
-
new Regexp("abc"); /abc/;
// these represent the same expression; they mean "a" followed by "b" followed by "c" -
/A\+\/\/;
// backslashes must still be used to represent special character codes, plus a few other ones like /, +, and ?, and are not escaped to represent the backslash character (/\/ instead of /\\/ means single backslash "\") -
/abc/.test(string);
// tests whether the given string contains a match of the regular expression pattern -
/abc/.test(string); string.contains('abc');
// regex is a slower-running solution for simple tasks like this -
/[0123456789]/; /[0-9]/;
// both match to all strings that contain a digit -
/[a-z]/
// square brackets with a hyphen in the middle represent the whole range of characters between those characters, inclusive; for this, character values are their unicode values -
/[0a-z]/
// square brackets can be thought of as a "one of" block, where it's one of the characters in the brackets; this one represents one of either: 0 or a through z -
// some special shortcuts
-
/\d/
// any digit; same as /[0-9]/ -
/\w/
// any alphanumeric character -
/\s/
// any whitespace character, incl. newlines -
/\D/
// any NON-digit character -
/\W/
// any NON-alphanumeric character -
/\S/
// any NON-whitespace character -
/./
// any character except for newline - // the above were made with standard latin characters in mind; international characters may have unintended behavior for the expressions that don't evaluate against whitespace
-
/p{L}/u
// any letter (includes unicode international characters) -
/p{N}/u
// any number (includes unicode international characters); not great if actual math is to be performed on it in JS -
/p{P}/u
// any punctuation (includes unicode international characters) -
/P{L}/u
// any non-letter (letter includes unicode international characters) -
/p{Script=Hangul}/u
// any character from the given writing system
-
-
/\d\d:\d\d/
// can mean hh:mm time -
/[\d.]/
// any digit or period character; special regex operators put between brackets are instead read as standard characters -
/[^01]/
// caret means "not," and this specific regex tests whether the value contains any character that isn't 0 or 1 -
/\d+/
// plus means 1 or more of the preceding character definition; this matches any length of digits but not an empty input -
/\d*/
// asterisk means 0 or more of the preceding character definition; this matches any length of digits or no digits at all -
/\d?/
// question mark means optional, so 0 or 1 instance of the preceding character is allowed; this matches a no-digit or 1-digit input; /colou?r/ -
/\d{2, 4}-\d{3}/
// brackets mean the specified amount of characters must be present; this matches a pattern of 2 to 4 digits, then a hyphen, then 3 digits, no more & no less -
/\d{5,}/
// comma with no entry means open-ended range; this matches 5 or more digits (but {,5} does NOT mean 5 or fewer; must use {0,5} for the lower-than range) -
/boo+(hoo+)+/
// parentheses group up the inner expression for the sake of the next operator; this matches "boo+" (+ applies to o) followed by "hoo+" (+ applies to o) followed by any number of "hoo+" patterns (+ applies to all of hoo+) -
/db/i
// trailing i means case-insensitive; this matches "db" or "Db" or "dB" or "DB" -
new RegExp('an', 'i')
// this is how to add trailing flags to the regular RegExp constructor -
new RegExp('an', 'gi')
// this regular expressions can have multiple trailing flags just by adding their flag characters -
/\d+/.exec(string)
// like test, but returns null for non-matching strings and an array-like object with various properties for matching strings-
const obj = /\d+/.exec('abc123xyz456')
// obj = ['123', index: 3, input: 'abc123xyz456', groups: undefined]; obj[0] = '123'; obj[0] is always the first whole match in the string -
const obj = /\d+/.exec('abcxyz')
// obj = null -
const obj = /'([^']*)'/.exec("she said 'hello'")
// obj[0] = "'hello'"; obj[1] = 'hello'; the second item matches the inner subexpression, in this case ([^']*) -
/bad(ly)?/.exec('bad')
// obj[0] = 'bad'; obj[1] = undefined; the inner expression wasn't in the string -
/(\d)+/.exec('123')
// obj[0] = '123'; obj[1] = '3'; obj[n] for subexpressions is always the last match in the string for that subexpression -
/(?:na)+/.exec('banana')
// obj[0] = 'nana'; the "?:" operator means to not include the expression in the returned array-like object, so there is no obj[1] here
-
-
// Date construction...
-
new Date()
// gives current date & time with timezone; example: Fri Feb 02 2024 18:03:06 GMT+0100 (CET) -
new Date(2009, 11, 9)
// Wed Dec 09 2009 00:00:00 GMT+0100 (CET) -- !! MONTHS ARE INDEXED FROM 0, BUT DAYS ARE INDEXED FROM 1 -
new Date(2009, 11, 9, 12, 59, 59, 999)
// Wed Dec 09 2009 12:59:59 GMT+0100 (CET) -- AM/PM not included -
new Date(2009, 11, 9).getTime()
// 1260334800000 -- UNIX time -
new Date(1260334800000)
// Wed Dec 09 2009 00:00:00 GMT+0100 (CET) -- single-argument Date constructor always takes its argument to be UNIX time -
const date = new Date(); date.getFullYear(); date.getMonth(); date.getDate(); date.getHours(); date.getMinutes(), date.getSeconds()
// all give numbers, the same ones used in a constructor to reconstruct it -
[fullMatch, month, day, year] = /(\d{1,2})-(\d{1,2})-(\d{4})/.exec("1-30-2003"); new Date(year, month - 1, day);
// Thu Jan 30 2003 00:00:00 GMT+0100 (CET)
-
-
/(\d{1,2})-(\d{1,2})-(\d{4})/.exec("100-1-30000")
// ['00-1-3000', '00', '1', '3000', index: 1, input: '100-1-30000', groups: undefined] -- it matches... this wants to define a better boundary-
/^\d+$/
// pseudo-character "^" defines beginning of the string, "$" defines the end; this expression matches strings starting and ending with a digit sequence -
/^!/
// matches strings that start with ! -
/x^/
// matches nothing; this represents an x before the beginning of the string
-
-
/a(?=e)/.exec("braeburn")
// ['a'] -- fails if 'e' is missing, but doesn't include it in the matched string -
/a(?=e)e/.exec("braeburn")
// ['ae'] -- still passes, but includes e because (?=e) just didn't move the string position forward when it was matching, then it encountered the last e, where it DID move the position forward -
/a(?! )/.exec("a b")
// null -- negative look ahead, only matches if the parameter DOESN'T match -
/a(?! )/.exec("ab")
// ['a'] -- did not match the space after a pattern, so it returned the matched "a" substring -
/\d+ (pig|cow|chicken)s?/.test('15 pigs')
true -- digits, then space, then (pig or cow or chicken), then an optional "s" -
/\d+ (pig|cow|chicken)s?/.test('15 pugs')
false -- pipe character "|" is an "OR" operator for expressions & sub-expressions -
/^([01]+b|[\da-f]+h|\d+)$/
// first part means sequence of 0s & 1s followed by "b," second part means digits OR letters a through f followed by "h," and third part means sequence of digits; binary, hex, and decimal -
/^([01]+|[\da-f]+|\d+)$/.test(1234)
// will match the MIDDLE expression, as the regex tests from the start of the string, trying the different rules in sequence on each character one by one -
/([01]+)+b/.test(10010110...)
// due to the regex implementation, this can take a long time; read this for more details -
'papa'.replace(/p/, 'm') = 'pama'; 'papa'.replace(/p/g, 'm') = 'mama';
// "g" flag at the end means "global," to it applies the regex to all matches in the string -
'Liskov, Barbara\nMcCarthy, John'.replace(/(\p{L}+), (\p{L}+)/gu, "$2 $1")
// converts "(last name), (first name)" to "(first name) (last name)" with the "$#" strings representing the parenthetical subexpressions -
'123abc456xyz'.replace(/[a-z]+/g, "|$&|") = '123|abc|456|xyz|'
// the "$&" expression refers to an entire matched expression in the string -
'1 lemon, 2 cabbages, 3 eggs'.replace(/(\d+) (\p{L}+),?/gu, (match, amount, ingredient) => { return ingredient + ": " + amount + ";" }) = 'lemon: 1; cabbages: 2; eggs: 3;'
// injects args from the regex into the function, "match" being the whole expression; can simplify JSONification! -
'1 /* a */+/* b */ 1'.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
// the [^]* will match all characters (characters not in the empty set) until the end before backtracking to find the next part of the match; adding "?" makes it check each character for the next rule instead of reaching the end of this rule before backtracking; works with any repetition operator (?+, *?, ??, {1, 2}?) -
/[\\[.+*?(){|^$]/g
// matches any character literal that's represented between the first & last brackets, like \, [, ., +, and * -
' letters'.search(/\S/) = 3
; search function finds the first index in a string that matches a given regular expression -
let exp = /y/g; exp.lastIndex = 3; exp.exec('xyzzy').index == 4; exp.lastIndex == 5;
// regex object lastIndex property can be used to change where exec searches from in a string, and it's modified after a pattern is found using exec - // the above only works with /g or /y options; /y only matches if the match starts at lastIndex, and /g works if the match starts at OR AFTER the lastIndex value
- // lastIndex starts at 0, so /g searches the whole string from index 0 by default; resets to 0 when it doesn't find a further match & and its exec returns null
-
'Banana'.match(/an/g)
// with /g, this finds ALL substrings matching the regular expression and returns them after an array; this returns ['an', 'an'] -
'Banana'.matchAll(/an/g)
// with /g, this finds all matching substrings AND returns an iterator containing exec results for each match; [['an', index: 1, ...], ['an', index: 3, ...]] -
'Banana'.match(/an/) == /an/.exec('Banana')
// these two return identical results without the /g flag on the regex, incl. subexpression matches at any added array indices -
'123|abc|456|def'.split(/\|/)
// can use a regular expression to define what to split a string along the lines of; /\r?\n/ is particularly useful for separating the lines from a file -
/\[.]/.test("[🌹]"); /\[.]/u.test("[🌹]")
// without u, this returns false, but with u, this returns true, because the character being tested is 2 code points; u flag for Unicode is needed to treat these characters properly
And that's pretty much everything. A shorter list of just the basics is in the summary to this chapter. The book suggests the tool Debuggex for quick quality assurance of regular expressions, to ensure that a regex will work as intended.
Chapter 9 Exercises
-
Exercise 9.1 - Regexp Golf
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
verify(/ca[rt]/, ["my car", "bad cats"], ["camper", "high art"]); verify(/pr?op/, ["pop culture", "mad props"], ["plop", "prrrop"]); verify(/ferr(et|y|ari)/, ["ferret", "ferry", "ferrari"], ["ferrum", "transfer A"]); verify(/\wious(\s|$)/, ["how delicious", "spacious room"], ["ruinous", "consciousness"]); verify(/\s[.,:;]/, ["bad punctuation ."], ["escape the period"]); verify(/\w{6,}/, ["Siebentausenddreihundertzweiundzwanzig"], ["no", "three small words"]); verify(/(^|\s)[^\P{L}eE]+(\s|$)/u, ["red platypus", "wobbling nest"], ["earth bed", "bedrøvet abe", "BEET"]); function verify(regexp, yes, no) { // Ignore unfinished exercises if (regexp.source == "...") return; for (let str of yes) if (!regexp.test(str)) { console.log(`Failure to match '${str}'`); } for (let str of no) if (regexp.test(str)) { console.log(`Unexpected match for '${str}'`); } }
-
Exercise 9.2 - Quoting Style
-
Exercise 9.3 - Numbers Again
IS THE ABOVE A NUMBER?YES
Testing values from the textbook
Should be numbers:
- 1
- -1
- +15
- 1.55
- .5
- 5.
- 1.3e2
- 1E-4
- 1e+12
Should not be numbers:
- 1a
- +-1
- 1.2.3
- 1+1
- 1e4.5
- .5.
- 1f5
- .
Chapter 10 - Modules
The bread & butter needed for the frontend frameworks to jam.
It's just imports & exports, how they work, and how to leverage that for JavaScript software design. Here are the chapter's concepts about ECMAscript / ES modules, plus a little extra that's helpful for my learning context:
- // using either "export" or "import" in a script turns it into a module
-
export const myBinding = ...
// anything marked as "export" becomes visible to other modules; can be any classes or functions or objects or primitives, anything that can be named can be exported - // exports can only be made from the global context of the script; nothing can be exported directly from within a function or class or other block
- // the set of exports in a module are collectively called the "interface" of that module
-
import {myBinding, myOtherBinding} from "./my-bindings.js";
// "import" can be used within scripts to access any of the specified bindings from an exporting module; those bindings must be marked as "export" in the module being exported from - // the above means that anything not marked as "export" is not visible to any outside module
- // the set of imports in a module are collectively called the "dependencies" of that module
-
import {myBinding as myVar} from "./my-bindings.js";
// can rename an imported binding upon its import; this importing module will use "myVar" in place of "myBinding" to refer to that binding - // the above is especially important for managing the namespace, as it allows the use of bindings that might otherwise be incompatible because they share a name
-
export default ['a', 1, true]
// export default can only be defined once in a module, it can be nameless within the module, and it's usually used in modules that only have a single export -
import myArray from "./my-bindings.js"
// default exports can be imported without brackets and named by the importing module -
import * as bindingsObj from "./my-bindings.js"; bindings.myArray
// "import *" means the import name becomes an object that holds all the exports from the exporting module -
<script type="module">
// type="module" is required to use modules with inline scripts on an HTML page -
import {parse} from "ini";
// module addresses can be plain names (like "ini") when managed by NPM node package manager - // a distributable collection of code is termed a "package," and NPM is a standard distribution system for JavaScript packeges; see the NPM website for information about creating, distributing, and utilizing packages
As imports & exports were only implemented well after JavaScript was a standard in programming, a common proto-module system agreed upon by developers was CommonJS, which uses a "require" function and "exports" object to modularize JavaScript dependencies:
-
const ordinal = require("ordinal"); const {days, months} = require("date-names");
// "require" is implemented by a CommonJS module to return bindings that provide the module's functionality -
exports.formatDate = function(date) { return `${days[date.getDay()]}, ${months[date.getMonth()]} ${ordinal(date.getDate())}, ${date.getFullYear()}`; };
// all of the bindings that came from "requires" above can be used to define properties for the current script's "exports" object -
const {formatDate} = require("format-date.js"); formatDate(new Date(2017, 9, 13));
// yet another file can then find & use the formatDate binding, using the requires function to import it - // NPM provides management & interoperability between CommonJS and ES modules, but writing new CommonJS is NOT recommended; it's no longer standard and can be harder to understand, but it's better to have at least this much knowledge so as to not be blindsided when it's encountered
And here are some other pointers from the chapter:
-
let fun = Function('a, b', 'return a + b;');
// the "Function" function allows strings to be read as code; the first argument is the list of arguments for the function being defined, and the second argument is the function body that goes between the brackets -
let answer = eval('(a, b) => { return a + b }');
// a more powerful binding than Function, "eval" parses ANY string passed to it as JavaScript code - // transpilers or compilers -- converters that parse other coding styles & dialects (like TypeScript) into browser-readable JavaScript (or even to past versions of JavaScript, for backwards compatibility)
- // bundlers -- converters that merge all the code from a module's dependencies into a single large file, as fetching a single large file generally has MUCH better performance than fetching many smaller files from many different sources
- // minifiers -- converters that reduce the file size of a script by removing comments & renaming bindings into (often illegible but) short names like "a" and "_"
- // for the above 3 types of converter tools, this is post-processing done with one's code, and you still want to keep the raw code, as written, for development & maintenance purposes, while the converted version is used by the live run environment
Chapter 10 Exercises
-
Exercise 10.1 - A Modular Robot
This is a design brainstorming exercise rather than a coding exercise.
The top-level functionality of this program is provided by
runRobot
, so the modular structure of the program should haverunRobot
be the top-level interface to build the program around, as adefault export
from a"robot"
file.One would also need arguments to give to the
runRobot
function, which includes aVillageState
, arobot
function that returns an object containing its next move (in a "direction
" property) & an arbitrary value as itsmemory
, and that same arbitrary value would be passed in to the robot function as its third argument. Because theVillageState
doesn't have the most apparent format for a developer to create one themselves, it should be able to be generated by an in-package function. That function may take in arguments to define specifics of a village state, but for the scope of the project, it can simply return the output ofVillageState.random()
. Such a function and its requirements (including theVillageState
class itself) can be included in a"village"
file. That function returning the output ofVillageState.random()
would be thedefault export
, I would modifyVillageState
to include itsroadGraph
among its object properties, and theroadGraph
can be generated usingbuildGraph
and injected through a slightly extendedVillageState
constructor.Due to the above,
buildGraph
may be separated into a module and its output used as the default argument for the"village" default export
function, so that it may also be custom-defined by a developer wanting their own custom village map.The
robot
argument to the"robot" default export
function doesn't make itself apparent, so it may definerandomRobot
as a default input and have that along withgoalOrientedRobot
added to another"sample-robots"
module, including any of their dependencies.Routing operations like
findRoute
may be executed with something like"dijskstrajs"
from NPM and interacted with through a translated version of theroadGraph
that's now in theVillageState
. For this,"village"
may also be a dependency to the"sample-robots"
module.Sample usage of the
"robot"
module should be documented in that module, and with the prescribed dependency structure, using it just within the scope of its project should be straightforwardly simple. -
Exercise 10.2 - Roads Module
This exercise uses modular JS files and is demonstrated through my creation of the modules chapter-10-exercises-graph.js and chapter-10-exercises-road-graph.js, along with the <script title="Chapter 10 Exercise 2" type="module"> tag near the bottom of the code on this page.
The content of the graph will be displayed in the browser's F12 developer console.
-
Exercise 10.3 - Circular Dependencies
This is a brainstorming exercise rather than a coding exercise.
Circular dependencies in CommonJS can work through the base script, the one defining
require
, wrapping CommonJS modules into functions and using exception handling to determine if that function is unable to run due to an unfulfilled dependency.It may ultimately resolve circular dependencies by concatenating them and defining their contents through the
function
initializer so that the code that appears earlier in the concatenation can reference code that appears later, and vice-versa.To determine which dependency to concatenate, the exception message may be cross-referenced with the
require()
arguments in the module, determined through regex, then that dependency can be sought, determined if circular (again through determining itsrequire()
args), and if it isn't circular, resolve that dependency before resolving this one. If it IS circular, then go ahead & concatenate before resolving the concatenated dependencies together.Because this allows the cache to populate all modules before a CommonJS module calls
require()
, the in-module call torequire()
will be calling a pre-loaded known dependency in either dependency direction.That may not be how it DOES work, but that may be how it COULD work.
Chapter 11 - Asynchronous Programming
MEANWHILE,
I sort of understand Promises, await, and callback functions going into this, enough that I actually use them in my coding, but going by the depth of the last chapters, I'm up for some new surprises. And I know enough to know that Promises & async await deserve their own sections in my notes...
Callback functions:
-
setTimeout(() => { ... }, 1000)
// runs the inner function after 1 second -
myCallbackRunner(() => { ... }, "conditions")
// custom-made callback function; takes the function to run after the callback is fulfilled, along with the data needed to define whatever conditions will trigger the function - // a function that calls an asynchronous function becomes asynchronous itself...
Promises:
- // a Promise is a receipt for a potentially future value; a Promise is called "resolved" when its value becomes available
-
promise.then(something => { ... }); promise.then(sameThing => { ... });
// the same promise can resolve into multiple then functions, and it will send them all the value that it resolved to -
Promise.resolve(15).then( value => console.log(value + 1) )
// Promise.resolve will wrap the given value in a Promise that resolves to the given value; this Promise outputs 16 to the console -
Promise.resolve(() => { stuff that takes a long time })
// may use this to use the value of a Promise after its argument completes -
new Promise(resolve => { myCallbackRunner(arg => resolve(arg), "conditions") })
// Promise constructor takes a function that takes a function that returns the result value from a callback function-
let resolver = callback => setTimeout(arg => callback(arg), 30000);
// 30-second timer -
let prom = new Promise(resolver);
-
let callback = arg => { if(arg === undefined) console.log("setTimeout doesn't send args to its callback!"); };
-
prom.then(callback);
- // after the above, the Promise will resolve 30 seconds after its creation, outputting the message to the console at that point
-
prom.then(callback);
// after this, the same Promise will supply its result immediately to any callback functions chained to it; this prints the message immediately, because the Promise has already resolved by the time it's called - // the Promise gave its callback functions the return value that the callback runner (in this case, setTimeout) would normally give them when they're called as standard callback functions
-
-
promise.then(arg => arg + 1).then(argPlusOne => { ... })
// promise chaining; the initial promise injects its callback runner's argument into the first then function as the argument to the then function, after which it returns a Promise that can be further chained to inject the result of the previous then function - // the code in any "then" call executes immediately as its input becomes available
-
promise.then(arg => return new Promise(callback => ...)).then(arg2 => 'arg2 is not a Promise!')
// Promises returned by the functions given to "then" send their Promise's resolution value, not the Promise itself, to the next "then" call -
Promise.reject()
// returns an immediately rejected Promise -
promise.then(...).catch(reason => {...})
// when a Promise is rejected / throws an exception, it passes the exception / "reason" to the nearest "catch" call; "catch" returns another Promise after handling the Promise's rejection reason -
promise.then(arg => {...}, reason => {...})
// the "then" call can take a second argument, a reject handler, to handle the error in case a Promise had an unhandled rejection reason before it -
new Promise((callback, reject) => setTimeout((arg, err) => { if(err) reject(err); else callback(arg); }, 30000));
// Promise constructor also takes an error handler in the form of a second function argument to the function passed as an argument to the constructor, along with a second argument added to the callback -
new Promise(...).then(...).catch(...).then(...)
// if the initial Promise is rejected and its then handler doesn't include error handling, the reason passes to the catch handler, whose return value is passed to the next then handler -
new Promise((successHandler, errorHandler) => { console.log(successHandler); console.log(errorHandler); })
// immediately logs 2 functions that return undefined; the Promise constructor implicitly injects these functions into the argument function upon being called - // but how does it know to inject any error arguments into callback functions used as arguments to internal function calls... could be native code, as the function injections above are also determined by the browser
-
new Promise(arg => 5); new Promise(arg => arg(5));
// the former will never be fulfilled, but the latter is always fulfilled; the inner function's argument MUST be used for a Promise to fulfill, and static returns should be done with Promise.resolve -
Promise.reject('abc').catch(arg => console.log(arg));
// Promise.reject is the counterpart to Promise.resolve; creates a rejected promise with the specified return value, so this one just console.logs "abc" -
Promise.all(arr.map(item => new Promise(resolver => ...)))
// Promise.all converts an arrat of promises that returns a single promise that resolves into an array of results - // never operate directly on an external binding in Promise.all; only the initial value of that binding will be used in every array item calculation, even when other Promise stacks change that value in their own scopes
-
Promise.resolve("Promise resolved").then(console.log); console.log("End of the main script");
// End of the main script executes before the Promise callback executes; this is because Promise actions are added to a queue in an outer JavaScript event loop that runs events in the order they're added
Async & await
-
async function readFile(filename) { try {await new Promise(...); return 'success'; } catch(reason) { throw new Error('failure'); } };
// ANY FUNCTION MARKED "async" RETURNS A PROMISE; it just writes like a synchronous function -
await new Promise(...)
// await can ONLY be used in async functions; it awaits the resolution of any Promise value and either returns the resolution value or throws the reason from a rejection, continuing with the rest of the code after it's done waiting -
class MyThing { async someMethod(arg) { ... } }
// a method can also be async -
let somePromise = new Promise(...); ...; let someResolution = await somePromise;
// can declare a Promise to create the Promise & request its resolution, then await its resolution on another line
Miscellaneous
-
function* generator(arg) { ... while(...){ yield val; } }
// generator functions, covered above in chapter 2; this works asynchronously, as it suspends execution between yields, waiting for a read from its current yield before running to the next one -
for(let value of generator(1)) { ... }
// the yields from a generator function can be treated as an iterator; the generator RETURNS a custom iterator, whose next value is the next yield of the function -
MyClass.prototype[Symbol.iterator] = function*() { ... };
// a generator function can be attached as a method to a Symbol.iterator binding in a class, to quickly turn that class into an iterable - // async functions can be thought of as a type of generator that produces a Promise when called and resolves on return or rejects on throw; it suspends execution during an await, which either resolves to a value or throws an exception before resuming the async function
- // asynchronous code runs on its own call stack, not on the main thread; keeps exceptions thrown in them from ever reaching a catch statement outside of a Promise or async function
ASYNC FUNCTIONS RETURN PROMISES. That's a big thing to not forget, because an async function is a easier to interpret than the Promise constructor
Chapter 11 Exercises
-
Exercise 11.1 - Quiet Times
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box (visible on scrolling down).
async function activityTable(day) { let logFileList = (await textFile("camera_logs.txt")).split(/\n\r?/); let result = new Array(); for(const logFileName of logFileList) { let activityHours = (await textFile(logFileName)) .split(/\n\r?/) .map(timestamp => new Date(+timestamp)) .filter(date => date.getDay() == day) .map(date => date.getHours()) ; let readingsForEachHour = new Array(24).fill(0); for(const hourReading of activityHours) { readingsForEachHour[hourReading]++; } result.push(readingsForEachHour); } result = result.reduce( (totals, hourReadings) => { for(let i = 0; i < totals.length; i++) { totals[i] += hourReadings[i]; } return totals; } ); return result; } activityTable(1) .then(table => console.log(activityGraph(table))) ;
-
Exercise 11.2 - Real Promises
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
function activityTable(day) { return textFile("camera_logs.txt") .then(logsFileContent => logsFileContent.split(/\n\r?/)) .then(logFileList => Promise.all( function*() { for(const logFileName of logFileList) yield textFile(logFileName); }() // immediately invoked to return promise iterator )) .then(logFilesContent => { const hourLists = new Array(); for(const log of logFilesContent) { const hourOfEachActivation = log.split(/\n\r?/) .map(timestamp => new Date(+timestamp)) .filter(date => date.getDay() == day) .map(date => +date.getHours()) ; let totalsForEachHour = new Array(24).fill(0); for(const hourReading of hourOfEachActivation) { totalsForEachHour[hourReading]++; } hourLists.push(totalsForEachHour); } return hourLists.reduce( (totals, hourReadings) => { for(let i = 0; i < totals.length; i++) { totals[i] += hourReadings[i]; } return totals; }, new Array(24).fill(0) ); }) ; }; activityTable(6) .then(table => console.log(activityGraph(table))) ;
-
Exercise 11.3 - Building Promise.all
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
function Promise_all(promises) { return new Promise((resolve, reject) => { if(promises.length === 0) resolve([]); const incomplete = Symbol('incomplete'); const result = new Array(promises.length).fill(incomplete) ; // if the result contains this symbol, it's incomplete for(let i = 0; i < promises.length; i++) { promises[i].then( res => { result[i] = res; if(!result.includes(incomplete)) resolve(result); }, err => { reject(err); } ); } }); }; // Test code. Promise_all([]).then(array => { console.log("This should be []:", array); }); function soon(val) { return new Promise(resolve => { setTimeout(() => resolve(val), Math.random() * 500); }); }; Promise_all([soon(1), soon(2), soon(3)]).then(array => { console.log("This should be [1, 2, 3]:", array); }); Promise_all([soon(1), Promise.reject("X"), soon(3)]) .then(array => { console.log("We should not get here"); }) .catch(error => { if (error != "X") { console.log("Unexpected failure:", error); } else { console.log("This should be X:", error); } }) ;
Chapter 12 - Project: A Programming Language
Yo, dawg, I heard you like programming languages.
I'd call this one a more proper textbook project, since it
mostly just uses knowledge from earlier in the book. There
are some other JavaScript Error types this uses, like
SyntaxError
and TypeError
, but
other than that, it's almost all an application of
previous concepts, sprinkled in with some theory.
This chapter introduces syntax trees, parsers, interpreters, and compilation, all in terms of the work that's done in this chapter. The parser turns the raw code into a syntax tree, the evaluator executes the code defined by the syntax tree, and that all adds up to an interpreter for the "Egg" language built in this chapter. Lastly, compilation is taking it a step further and preprocessing the executable code into a further optimized version of the interpreted code, frontloading as much of that processing as possible and outputting a compiled version of the code that runs faster than the language just being interpreted. For example, code in the Egg language from this chapter might compile into JavaScript, and instead of running the Egg code directly, you would compile it into JavaScript code and then run that JavaScript code instead of the Egg interpreter.
Chapter 12 Exercises
-
Exercise 12.1 - Arrays
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
topScope.array = (...args) => args; topScope.length = arr => arr.length; topScope.element = (arr, index) => arr[index]; run(` do(define(sum, fun(array, do(define(i, 0), define(sum, 0), while(<(i, length(array)), do(define(sum, +(sum, element(array, i))), define(i, +(i, 1)))), sum))), print(sum(array(1, 2, 3)))) `); // → 6
-
Exercise 12.2 - Closure
This is a brainstorming exercise rather than a coding exercise.
The JavaScript-language function represented by the Egg-language function runs a recursive sub-evaluation that passes local-scope bindings to its arguments in place of the program's higher scope. This higher scope is the prototype of the local scope by way of any higher scopes (including that from other functions!) being passed in as the second argument to the "fun" special form definition before being used as the prototype of the function's local scope through the local scope effectively being defined by
Object.create(higherScope)
. This makes a function's scope visible to all of its embedded functions, as well as functions embedded in those sub-functions, recursively. -
Exercise 12.3 - Comments
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
function skipSpace(string) { let first = string.search(/\S/); if (first == -1) return ""; let result = string.slice(first); if(result[0] === '#') { result = result.slice(/^.*?(\n\r?|$)/.exec(result)[0].length); if(!result.length) return ""; result = skipSpace(result); } return result; }; console.log(parse("# hello\nx")); // → {type: "word", name: "x"} console.log(parse("a # one\n # two\n()")); // → {type: "apply", // operator: {type: "word", name: "a"}, // args: []}
-
Exercise 12.4 - Fixing Scope
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
specialForms.set = (args, scope) => { if (args.length != 2 || args[0].type != "word") { throw new SyntaxError("Incorrect use of set"); } const binding = args[0].name; let currentScope = scope; do { if(Object.hasOwn(currentScope, binding)) { break; } else { currentScope = Object.getPrototypeOf(currentScope); } if(currentScope === null) { throw new ReferenceError(`Binding "${binding}" wasn't defined before set was called on it.`); } } while(true); let value = evaluate(args[1], scope); currentScope[binding] = value; return value; }; run(` do(define(x, 4), define(setx, fun(val, set(x, val))), setx(50), print(x)) `); // → 50 run(`set(quux, true)`); // → Some kind of ReferenceError
Chapter 13 - JavaScript and the Browser
Back in familiar territory again...
This chapter is a bit of background info on the Internet & how we get from zero to JavaScript. If I was formally educated in computer science, I'd probably know all about TCP/IP and the distinction between the World Wide Web and the Internet. I knew about communication protocols from my time with industrial controllers & IoT, protocols like Modbus and DeviceNet and OPC/UA. They're just languages that machines speak, and as long as both machines speak that language, they can communicate meaningfully with each other. HTTP is a structured request-and-reply resource exchange protocol / language, and it happens over TCP, which is a listen-and-connect protocol / language that manages routing to listening ports. The World Wide Web is the set of these standards & languages, with the "Web" referring to the interconnections between many devices. A URL points to a protocol ("https://"), a server address / domain name to send a request to via that protocol ("nickhz.live/"), and a path to a specific resource within that server to request ("professional/self-development/eloquent-javascript-2024/index.html").
After that, it's HTML & embedding scripts. The character
entities like >
and
&
are mentioned,
of
which there are many more, but
a
small subset of character entities is generally
sufficient to meet the needs of an HTML document. The
general HTML syntax & embedding JavaScript is demonstrated
by this Web page. If this chapter had any exercises, this
page would be sufficient to fulfill them. Press F12!
Chapter 13 Exercises - There are none!
Chapter 14 - The Document Object Model
Yo, dawg, I heard you like docum--
It's still familiar territory for me, but this chapter also draws out alternative ways to do some things.
-
this === window
// the window is the top-level element that browser JavaScript runs in -
document === window.document
// the document is the .html document; it represents the document file, NOT the <html> tag -
document.documentElement
// this is the representation of the <html> tag -
document.body; document.head;
// these represent the <body> and <head> tags, respectively -
document.nodeType === Node.DOCUMENT_NODE; document.body.nodeType === Node.ELEMENT_NODE;
// the document and all of its sub-elements (incl. items like the <!DOCTYPE html> and the text strings between HTML tags) are types of nodes; all possible node type values can be seen by using Object.keys(Node) -
document.getElementsBy...; element.getElementsBy...
// element querying methods are applied to all nodes, not just the document or the body; using it as a node method restricts the search to within that node -
textNode.textContent === textNode.nodeValue
// for a text node, these are the same value -
Array.from(getElementsByTagName('p')); Array.from({0: 'one'; 1: 'two', length: 2});
// can construct an array from an object with indexed values and a length property; this means it can convert a live NodeList to a plain array that doesn't live change when the DOM is modified -
element.remove()
// entirely removes the element and all of its children from the DOM -
element.dataset['attribute'] === element.getAttribute('data-attribute')
// 2 ways to get custom "data" attributes from an element; the latter also allows selection of custom attributes that are not marked as "data-" -
element.getAttribute('class'); element.setAttribute('class', someClassName);
// with getAttribute, the className property uses its HTML name, "class" -
element.offsetHeight >= element.clientHeight
// "client" dimension properties exclude the element border -
element.getBoundingClientRect()
// returns an object holding dimensional parameters of the element WITH RESPECT TO THE CURRENT VIEWPORT -
pageYOffset; pageXOffest;
// these give the vertical & horizontal scroll positions of the current viewport -
element.querySelector('p > a')
// querySelector can be applied within a node
Chapter 14 Exercises
-
Exercise 14.1 - Build a Table
Due to this exercise being a Web page, it's being presented in an iframe, with the code being in the "srcdoc" attribute of that iframe.
-
Exercise 14.2 - Elements by Tag Name
Due to this exercise being a Web page, it's being presented in an iframe, with the code being in the "srcdoc" attribute of that iframe.
-
Exercise 14.3 - The Cat's Hat
Due to this exercise being a Web page, it's being presented in an iframe, with the code being in the "srcdoc" attribute of that iframe.
Chapter 15 - Handling Events
Listening is arguably the most important part of communication.
Event listeners, event handlers, event types, a bit of event bubbling (though the text doesn't use that word specifically), all stuff that I generally know but has some extra detail that I want written down. Not all of this is in the text, but reading the text did make me think of including all the things that are here:
-
event.type
// holds whether an Event object is a "click" or a "mousedown" or a "keyup" etc. -
event.stopPropagation()
// keeps an event from bubbling or capturing into the event handlers of parents or children -
aDivWithButtonsInside.addEventListener('click', () => { event.target === aButtonInsideTheDiv })
// registering an event handler on a parent will pass in events whose event.target identifies a child that the event propagates from -
event.preventDefault()
// used in an event handler, this prevents the event's default behavior, like changing the page on an <a> tag click or scrolling the page downward when the down arrow is pressed -
if(event.ctrlKey && event.altKey && event.key == " ") ...
// an event holds information about whether shiftKey, ctrlKey, altKey, or metaKey was being pressed when the event was created -
<div tabindex>
// the tabindex attribute allows an element to be focused by keyboard navigation, like links and inputs can be by default -
event.type === 'input'; activeElement.value
// input events are said to be best for handling changes to input & textarea fields, and referencing their content is to be done with the dynamic activeElement property of the document -
event.type === 'click'
// DETAIL: the click event fires after the mouseup event, on the most specific element that contained both the press and release of the mouse button -
event.type === 'dblclick'
// DETAIL: the dblcick event fires AFTER THE SECOND CLICKEVENT -
mouseEvent.clientX; mouseEvent.clientY;
// for mouse events, a "client" dimensional property is the px coordinate relative to the upper left of the viewport -
mouseEvent.pageX; mouseEvent.pageY;
// for mouse events, a "page" dimensional property is the px coordinate relative to the upper left of the entire document -
event.type === 'mousemove'; event.buttons
// mousemove events keep a binary identifier of buttons currently being pressed; LMB is +1, RMB is +2, and MMB is +4 -
event.type === 'touchstart'; event.type === 'touchmove'; event.type === 'touchend'
// TOUCH EVENTS ALSO FIRE MOUSE EVENTS AFTERWARD; can use preventDefault() to keep the mouse event from firing -
touchEvent.touches[0]
// a touch event has an array-like "touches" property, the elements of which hold the clientX, clientY, pageX, and pageY properties for each touch -
event.type === 'scroll'; event.preventDefault()
// scroll events happen AFTER the page has already scrolled, so page scrolling is not prevented by preventDefault() -
innerHeight; innerWidth;
// dynamic window properties that give the dimensions of the viewport -
pageYOffset; pageXOffset;
// dynamic window properties that give how far the viewport has scrolled; gives coordinate from the top left of the document to the top left of the viewport -
event.type === 'focus'; event.type === 'blur';
// fires when an element is focused or unfocused; THIS EVENT DOES NOT BUBBLE TO OTHER ELEMENTS IN ITS NODE HIERARCHY BY DEFAULT -
event.type === 'load'
// fires on the window & document.body when the page elements have completely loaded, and also fires on elements (like <script> and <img>) when they finish loading external files; THIS EVENT DOES NOT BUBBLE -
event.type === 'beforeunload'
// fires when a page is closed or navigated away from; when its preventDefault() is called and event.returnValue is set to a string, the browser requests confirmation on whether the user want to leave the page - // the event loop runs async, and an event fires, its listener only fires after the event loop is done being tied up with other work
-
// web workers operate outside the page's event loop, and the script it runs should have its own message listener
-
IN page script:
let worker = new Worker('script.js'); worker.addEventListener('message', event => { event.data; }); worker.postMessage(...args);
// the page should invoke the worker, tell it which script to run, listen for its message, and post messages to it to work off of -
IN script.js:
addEventListener('message', event => { postMessage(event.data) });
// web worker has its own global scope and works entirely through messages; it receives a copy of the postMessaged data, not the data itself, and data can only be in a JSONable format - // because a web worker has its own global scope, it can't know things like the mouse position or modify window-scoped things like DOM elements
-
IN page script:
- // for events that fire rapidly (like mousemove and keydown), limiting how often the event-handling code fires is called debouncing the event
-
clearTimeout(setTimeoutReturnValue); clearInteral(setIntervalReturnValue); cancelAnimationFrame(requestAnimationFrameReturnValue);
// functions that nullify timer triggers -
element.removeEventListener('click', listenerFunction)
// does what it says, removing the instruction of addEventListener() -
element.dispatchEvent(new Event('click'))
// can programmatically activate event handlers on an element by using that element's dispatchEvent() method with a synthetic Event as its argument; this method is fired by the browser by default, so this is 1 to 1 with a user-supplied event
Chapter 15 Exercises
-
Exercise 15.1 - Balloon
Press the up & down arrow keys in this frame! If you pop it, click to reset it.
-
Exercise 15.2 - Mouse Trail
Move the mouse around inside this frame!
-
Exercise 15.3 - Tabs
Click the buttons in the frame to switch between 3 sets of content.
I think it's less cool than the ones I make in pure CSS, but it does have a function function in the function that returns a function to use in a higher-order function. It's Functional™!
Chapter 16 - Project: A Platform Game
"The gravity strength, jumping speed, and other constants in the game were determined by simply trying out some numbers and seeing which ones felt right. You can try experimenting with them." And that, I did.
It all feels so disconnected until it all comes together. I think it would be better presented as one big long script that can be used as a reference during the reading, or even better: emphasize that the reader should write out this program in order to understand it best.
At least for me, seeing the program all in one place is more readable than seeing little bits of it interspersed throughout the chapter. It makes all the arguments sent to the functions less cryptic. I guess there's a meta-lesson there, about the risks of over-modularization, but learning the lesson in real time by having to reference other parts of the page for each line of code made the exercises probably more painful than they needed to be. Albeit, over time spent with custom library-like code like this, you get used to the names & abstractions & structure of it, just not if it's a library that's only relevant for one chapter in a textbook. You just don't spend enough time with it to really know it holistically before you have to work with it, and it's like working with an alien toolbox. But we soldier on.
-
element.scrollLeft; element.scrollTop;
// these are modifiable scroll readings WITHIN an element; if the element has content overflow and the scroll bars within the element are not at their default position, this can read and change the positions of their inner scrollbars
Chapter 16 Exercises
-
Exercise 16.1 - Game Over
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
<link rel="stylesheet" href="css/game.css"> <body> <script> async function runGame(plans, Display) { const initialLives = 3; let lives = initialLives; for (let level = 0; level < plans.length;) { console.log('Current lives: ' + lives); let status = await runLevel(new Level(plans[level]), Display); if (status == "won") level++; else if (status == "lost") { lives--; } if(lives < 0) { console.log('Game over!'); level = 0; lives = initialLives; } } console.log("You've won!"); }; runGame(GAME_LEVELS, DOMDisplay); </script> </body>
-
Exercise 16.2 - Pausing the Game
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
<link rel="stylesheet" href="css/game.css"> <body> <script> function runLevel(level, Display) { let display = new Display(document.body, level); let state = State.start(level); let ending = 1; return new Promise(resolve => { const arrowListeners = Object.create(null); let paused = false; let arrows = trackKeys(["ArrowLeft", "ArrowRight", "ArrowUp"]); const animCallback = time => { if(paused) { return false; } state = state.update(time, arrows); display.syncState(state); if (state.status == "playing") { return true; } else if (ending > 0) { ending -= time; return true; } else { display.clear(); resolve(state.status); window.removeEventListener('keyup', pauser); untrackKeys(); return false; } }; const pauser = event => { if(event.key === 'Escape') { paused = !paused; if(!paused) { runAnimation(animCallback); } } }; window.addEventListener('keyup', pauser); function trackKeys(keys) { let down = Object.create(null); function track(event) { if (keys.includes(event.key)) { down[event.key] = event.type == "keydown"; event.preventDefault(); } }; arrowListeners['keydown'] = track; arrowListeners['keyup'] = track; for(const eventType in arrowListeners) { window.addEventListener(eventType, arrowListeners[eventType]); } return down; }; function untrackKeys() { for(const eventType in arrowListeners) { window.removeEventListener(eventType, arrowListeners[eventType]); } }; runAnimation(animCallback); }); }; runGame(GAME_LEVELS, DOMDisplay); </script> </body>
-
Exercise 16.3 - A Monster
This exercise has in-page dependencies on the Eloquent JavaScript site. My solution is below and can be pasted & run on the code section near the bottom of this page.
<link rel="stylesheet" href="css/game.css"> <style>.monster { background: purple }</style> <body> <script> // Complete the constructor, update, and collide methods class Monster { constructor(pos, speed) { this.pos = pos; this.speed = speed; } get type() { return "monster"; } static create(pos) { return new Monster(pos.plus(new Vec(0, -1)), new Vec(-2, 0)); } update(time, state) { let newPos = this.pos.plus(this.speed.times(time)); let newSpeed = this.speed; if (state.level.touches(newPos, this.size, "wall")) { newSpeed = newSpeed.times(-1); } return new Monster(newPos, newSpeed); } collide(state) { const playerVulnerableY = state.player.pos.y + state.player.size.y * 0.6 ; if(playerVulnerableY < this.pos.y) { let filtered = state.actors.filter(a => a != this); return new State(state.level, filtered, state.status); } return new State(state.level, state.actors, "lost"); } } Monster.prototype.size = new Vec(1.2, 2); levelChars["M"] = Monster; runLevel(new Level(` .................................. .################################. .#..............................#. .#..............................#. .#..............................#. .#...........................o..#. .#..@...........................#. .##########..............########. ..........#..o..o..o..o..#........ ..........#...........M..#........ ..........################........ .................................. `), DOMDisplay); </script> </body>
Chapter 17 - Drawing on Canvas
This one might be the most exciting chapter for me! The browser yields many under-explored artforms. Up to this point, have I been drawing without a canvas?
I've had my eyes on SVG & canvas. The book doesn't go much into SVG, which is understandable since JavaScript isn't a requirement for its use. Canvas is the mainstay, but there's enough covered to think that one chapter in a textbook is more of just an introduction than anything else. It's a welcome one, since one of the troubles with learning Canvas is having an idea of where to start, but now that I've peeked at the Canvas 2d context API, I realize that this chapter actually covers a significant chunk of what it can do. Its power comes from getting creative with the toolset, which I thought would've been bigger for all that it's capable of. But it really goes to show how effective this chapter is at throwing you into canvas concepts.
-
<svg xmlns="http://www.w3.org/2000/svg">
// SVG uses a an HTML-embeddable XML namespace separate from the rest of the document; xmlns makes the element & its children into a different namespace - // elements like circle & rect that are SVG-only tags can be interacted with through the DOM like regular HTML elements
-
<canvas width="300" height="150"></canvas>
// canvas more directly interacts with pixels without keeping whole mathematical constructs like SVG; its default size is 300px wide by 150px tall, and it's transparent by default -
const ctx = canvasElement.getContext("2d")
// standard code for interacting with the canvas; it's entirely JS interfaces, with the specific interface depending on the getContext arg ("2d", "webgl", "webgpu", "webgl2", ...) - // the chapter is about the "2d" context, which is the one context preferred for 2d canvas rendering; the other main ones focus on 3d rendering
-
ctx.fillStyle = "red"; ctx.strokeStyle = "blue"; ctx.lineWidth = 5;
// 2d canvas sets up with relevant properties of the context being set before a command to draw things to the screen -
ctx.fillRect(x, y, width, height); ctx.strokeRect(x, y, width, height);
// stroke draws an outline, fill makes the color occupy the defined inner space -
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.stroke(); ctx.fill();
// beginpath starts a new path whose elements will all be stroked or filled together, moveTo lifts the pen and places it at the specified position, lineTo moves to the specified position without lifting the pen, and the stroke & fill will color in the lines or the closed space between them -
ctx.quadraticCurveTo(controlPointx, controlPointy, nextPointX, nextPointY)
// paths from current point to nextPoint by first moving directly toward controlPoint & curving such that its arrival at nextPoint points directly away from controlPoint -
ctx.bezierCurveTo(ctrlPt1x, ctrlPt1y, ctrlPt2x, ctrlPt2y, nextPointX, nextPointY)
// like the above, but there's one control point for the current point and another control point for the next point -
ctx.closePath()
// takes the path along a straight line to close the figure (often to the last moveTo element) -
ctx.arc(centerX, centerY, radius, startAngleRadians, endAngleRadians)
// draws an arc of a circle from the specified figure; angle at 0 means the rightmost end of the circle, and angle at pi/2 means the bottom of the circle - // to start a new path so that filling & stroking doesn't affect other elements, call beginPath() again
-
ctx.font = "italic 28px monospace"; ctx.fillText('abc', 10, 50); ctx.strokeText('123', 10, 100)
// can draw text; coordinates specify the bottom left of the text by default, but this can change with textAlign and textBaseline properties -
imgElement.addEventListener('load', () => ctx.drawImage(imgElement, x, y));
// can draw bitmaps of images onto the canvas -
ctx.clearRect(x, y, width, height)
// clears the canvas within the specified rectangle -
ctx.drawImage(img, xInImg, yInImg, widthInImg, heightInImg, xInCanvas, yInCanvas, widthInCanvas, heightInCanvas);
// 9-arg spritesheet-type image draw; specifies rectangle w/ respect to top left of the image to select a crop area, then specifies rectangle w/ respect to the top left of the canvas to select a draw location for the cropped image -
ctx.drawImage(otherCanvas, ...)
// can also draw from another canvas element, from an SVG, or even from a video frame -
ctx.scale(xMultiplicand, yMultiplicand); ctx.translate(xDisplacement, yDisplacement); ctx.rotate(clockwiseRadians);
// applies transformations to any draw definitions (like stroke or arc or even other transforms) that come after it; everything happens about the origin, which starts at the top left corner but can move with translate method calls - // TRANSFORMS STACK WITH ONE ANOTHER; rotate pi/2 & translate right will move the origin (and all subsequent draws) DOWNWARD, while translate right then rotate pi/2 will move the origin (& subsequent draws) RIGHTWARD
-
ctx.save(); ctx.translate(20, 20); ctx.fillRect(10, 10, 10, 10); ctx.restore(); ctx.fillRect(10, 10, 10, 10);
// save() bookmarks the current transform state, and restore() recovers it; this code moves the origin to (20, 20), draws a rectangle at (10, 10) with respect to it ((30, 30) with respect to the default position), then recovers the origin being at (0, 0) before drawing a rectangle at (10, 10) ((10, 10) with respect to the default position) -
ctx.resetTransform()
// fully resets all transformations; origin is back at the top-left, x is right & y is down, and everything is at the same scale -
ctx.scale(-1, 0.1)
// mirrors elements about the x-axis but squishes them close along the y-axis; intuitively does the same about the y-axis & can be used to mirror about the origin with both args negative - // graphics interface recommendations are to use HTML/DHTML for textful elements, SVG for scalable custom-shaped elements that provide much of the functionality that comes with HTML tags, and canvas to leverage per-pixel rendering tasks (esp. postprocessing or using large numbers of elements)
After this, I'm more confident in exploring the other powerful browser rendering techs instead of just defaulting to DHTML & CSS3 for artistic Web pages. And there are some visual effects I've seen that are suddenly a lot less mysterious now. Maybe I'll be making mysteries for those after me to ponder, too!
Chapter 17 Exercises
-
Exercise 17.1 - Shapes
-
Exercise 17.2 - The Pie Chart
-
Exercise 17.3 - A Bouncing Ball
-
Exercise 17.4 - Precomputed Mirroring
This is a design brainstorming exercise rather than a coding exercise.
The title of this exercise feels like a hint...
To avoid having to re-compute a mirrored element for canvas, you can just overlay a DOM element. Or better yet, overlay another canvas and preload the transform by applying it to that instead.
Chapter 18 - HTTP and Forms
Now, let's take a look under the hood.
I think that starting with the actual text of the protocol communications is a great way to start when teaching fetch. A lot of confusion can be avoided with a concrete grounding like this, demystifying what lies beneath what's being taught.
-
// this is what an HTTP GET request looks like
GET /18_http.html HTTP/1.1 Host: eloquentjavascript.net User-Agent: Your browser's name
- // this is what's meant by making a GET, POST, PUT, DELETE, PATCH, (...) request; this bit of text declares the method
-
GET /18_http.html HTTP/1.1
// HTTP request method, then the resource, then the protocol version -
// this is what an HTTP GET response looks like
HTTP/1.1 200 OK Content-Length: 87320 Content-Type: text/html Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT <!DOCTYPE html> ... the rest of the HTML document
- // a response body need not be an HTML document; it can be just about anything
-
HTTP/1.1 200 OK
// protocol version, response code, human-readable response message -
// HEADERS are the "name: value" pairs present in both requests above (like "
Host: eloquentjavascript.net
" & "Content-Length: 87320
" bytes) -
Content-Type: text/html
// this header tells the browser how to handle displaying the body content - // some request types (incl. GET & DELETE) don't require bodies, and some response types (incl. error responses) also don't have bodies; this is technically a soft rule but is typically what Web software is built to support
-
// HTML form element constructs translate into HTTP requests
<form method="GET" action="example/message.html"> <input name="a" value="abc"> <input name="b" value="123"> <button type="submit"> </form>
^ BECOMES:GET /example/message.html?a=abc&b=123 HTTP/1.1
"GET" from the form method, "/example/message.html" from the form action, "a" & "abc" from the first input, "b" & "123" from the second input, and "HTTP/1.1" version is always added by the browser -
<form action="example/message.html">
// when no method or "GET" is specified, the inputs are packed into search params / a "query string" (like?a=abc&b=123
above) -
encodeURIComponent("Yes?"); decodeURIComponent("Yes%3F");
// the "URL encoding" for characters can be dealt with using these native JavaScript encoder & decoder functions -
// HTTP methods with bodies handle form-to-HTTP translation differently
<form method="POST" action="example/message.html"> <input name="a" value="abc"> <input name="b" value="123"> <button type="submit"> </form>
^ BECOMES:POST /example/message.html HTTP/1.1 Content-length: 24 Content-type: application/x-www-form-urlencoded a=abc&b=123
-
fetch("example/data.txt").then(httpResponse => { ... })
// the fetch interface returns a promise that resolves into an object representing an HTTP response -
httpResponse.status === 200; httpResponse.headers.get("Content-Type") === "text/plain"
// the httpResponse object has various identifiers for the items in the plain-text response -
httpResponse.headers.get("cOnTeNt-TyPe") === "text/plain"
// httpResponse objects' header names are not case sensitive -
fetch("...").then(errorResponse => { ... }, rejection => { ... })
// server-side error responses will also RESOLVE a promise, and the inability to get a response will REJECT a promise -
fetch("...").then(response => response.text()).then(text => console.log(text))
// httpResponse.text() gets the response body as text, returned as a Promise because headers may resolve before the full text body does -
fetch("...").then(r => r.json()).then(json => console.log(json), err => console.log('not valid json'))
// similar to the above but for JSON; rejects the Promise if the response isn't valid JSON -
fetch("...", { method: "DELETE" }).then(httpResponse => { ... })
// fetch sends GET requests by default; can change this by adding a config object that sets a different "method" value -
fetch("...", { method: "POST", headers: {Range: "bytes=8-19", ...}, body: ...}).then(...)
// config object can also include header values & body data; body data can be in several different formats -
... headers: {Range: "bytes=8-19", ...} ...
// this becomes the header "Range: bytes=8-19", which means only the 8th through 19th characters of the response body are returned; the typically valid header values are listed right here - // some headers will be overridden or forbidden by the environment, such as "Date" or "Origin" headers
-
Access-Control-Allow-Origin: *
// if this header is present in a RESPONSE FROM THE SERVER, it tells the browser that it can request from any origin / domain; this is not default behavior, as CORS usually disallows this - // REMOTE PROCEDURE CALLS are function call requests made to another machine through HTTP & responded to with the return value of that function
- // the method used by my first backends (Java Servlets / Spring) doesn't technically use remote procedure calls, but instead uses the resources identified in the headers & bodies to configure the operation carried out by the server
-
https://
// HTTPS is a layer over HTTP, where before data exchange, the client verifies the server by first requesting a cryptographic certificate issued by a recognized certificate authority, before using cryptography to encode every request that leaves the client or server and only be able to decode it when it gets to its destination - // HTML form elements are made selectable through keyboard & tabbing elements; they gain focus & blur listening
-
document.activeElement
// the currently focused element in a document; can be useful for HTML forms -
<input type="text" autofocus />
// the "autofocus" attribute marks a field as focused when the page is loaded; useful for a form-centric page -
<input type="text" tabindex="1" /><a href="./">link</a><input type="text" tabindex="2" />
// "tabindex" allows the keyboard selection order to be manually set; tabbing after selecting the first input in this will skip the link and jump to the second input -
<br tabindex="0" />
// can add tabindex to make any element focusable, and tabIndex="0" can be used to avoid changing the focus order from that -
<input type="text" disabled />
// disables the element; usable on any form element -
formElement.elements === {0: inputElement, 1: otherInputElement, nameOfInputElement: inputElement, nameOfOtherInputElement: otherInputElement, ...}
// form elements contain an array-like map-like formElement.elements property, detailing the elements used as form controls / inputs, indexed by their "name" attributes -
<button type="submit">Click</ button>
// button type="submit" will submit a form's contents, and so will hitting the enter key while the form is focused -
formElement.addEventListener("submit", submitEvent => { ... })
// can use this to fire an action upon form submit; can preventDefault() on the submit event to keep the browser from navigating to the page specified in the form action, with UI instead updated through fetch & DOM manipulation while staying on the page -
textElement.selectionStart; textElement.selectionEnd;
// in a text element (like textarea or input type="password"), these indicate where the cursor / selection is -
document.querySelectorAll("[name=color]");
// selects ALL elements whose "name" attribute is as specified ("color" in this case) -
<select name="my-selection" multiple><option value="a">Alpha</option><option value="b">Beta</option></ select>
// select element with "multiple" allows multiple select options to be chosen -
selectElement.value = lastSelectedOption.value
// a select element's "value" property only holds one option's value even when "multiple" is enabled; to check all, one may get the options from the array-likeselectElement.options
and check theoptionElement.selected
attribute of every returned option -
<input type="file" multiple />
// file inputs support a "multiple" attribute -
inputTypeFileElement.addEventListener("change", event => { input.files[0].name; input.files[0].type })
// an input type="file" tag can access the selected file's properties through its "change" event listener -
let reader = new FileReader(); reader.addEventListener("load", () => { reader.result; }); reader.readAsText(file);
// the above CANNOT DIRECTLY ACCESS FILE CONTENT; this requires an asynchronous wrapper to fetch the data, such as a FileReader with a "load" event listener and the file content being assigned to the reader.result property -
reader.addEventListener("error", () => { reader.error; });
// if a file reader fails its operations, it fires an "error" event, and the content of the error can be accessed through the reader.error property -
localStorage.setItem(key, value); localStorage.getItem(key) === value; localStorage.removeItem(key);
// basic usage of localStorage, which stores data between reloads; different domains also get different localStorage scopes -
sessionStorage.setItem(...); ...
// sessionStorage works similarly to localStorage but resets when the session ends (usually just when the browser closes)
Chapter 18 Exercises
-
Exercise 18.1 - Content Negotiation
Hosting this exercise here would be hotlinking a resource from the Eloquent JavaScript site, so it is not demonstrated here. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
const mediaTypes = [ 'text/plain', 'text/html', 'application/json', 'application/rainbows+unicorns' ]; for(const type of mediaTypes) { fetch('/author', { method: 'GET', headers:{Accept: type} }) .then(response => response.text()) .then(text => { console.log(`For ${type}:\n\n${text}\n\n==========\n\n`); }) ; }
-
Exercise 18.2 - A JavaScript Workbench
Below is my solution; it can be pasted into the code area in the exercise section here on the textbook site.
<textarea id="code">return "hi";</textarea> <button id="button" onclick="executeWorkbench()">Run</button> <pre id="output"></pre> <script> const code = document.getElementById('code'); const button = document.getElementById('button'); const output = document.getElementById('output'); const executeWorkbench = () => { try { output.style.removeProperty('color'); output.innerText = Function('', code.value)(); } catch(e) { output.style.color = 'red'; output.innerText = e; } } </script>
-
Exercise 18.3 - Conway's Game of Life
Chapter 19 - Project: A Pixel Art Editor
Redux?
This is the last of the browser JavaScript chapters. The project structure here echoes a React application with the component-based structure & state management, and I don't think that's an accident. Looking back at previous editions, then sure enough, this structure only came about when React hit the author's lips. I can appreciate the thoughtfulness of directing the skill in a way that's marketable.
That said, I believe that knowledge of plain JavaScript, like from this book, will get a lot more mileage than knowledge of any particular UI library or framework that's built on top of JavaScript. Over the course of this book, I've learned more about all the frontend frameworks at once. It's fine enough to see how a stateful frontend application is structured without so many layers of abstraction, but I put this architecture firmly in the category of being a preference or tool rather than being the one right way to do things. It's a single methodology out of the many that can be composed with this knowledge.
-
newState = {...oldState, ...stateUpdates}
// this overrides same-named properties in the old state to create a merged state -
for(const {property1, property2} of someObject) { ... }
// object destructure as for-of loop iteration entry -
linkElement.href = canvasElement.toDataURL(); linkElement.download = 'image.png'; link.click();
// can get an image URL from a canvas element, choose a file name for it, and click on a link leading to that URL to save an image from a canvas -
canvasContext.getImageData(0, 0, canvasWidth, canvasHeight).data
// can get image data from a canvas with this; data is a 1D array of color & alpha channels per pixel ([R1, G1, B1, A1, R2, G2, B2, A2, R3, ...] where each number denotes a pixel, value 0-255), and this may be prepared with a canvas context that has a drawImage, to get all data from a raw image -
number.toString(16)
// changes the radix or base of a number; easy conversion to hex
Chapter 19 Exercises
-
Exercise 19.1 - Keyboard Bindings
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
<div></div> <script> // The original PixelEditor class. Extend the constructor. class PixelEditor { constructor(state, config) { let {tools, controls, dispatch} = config; this.state = state; const toolKeyMap = Object.create(null); for(const tool in tools) { toolKeyMap[tool[0].toLowerCase()] = tool; } this.canvas = new PictureCanvas(state.picture, pos => { let tool = tools[this.state.tool]; let onMove = tool(pos, this.state, dispatch); if (onMove) { return pos => onMove(pos, this.state, dispatch); } }); this.controls = controls.map( Control => new Control(state, config)); this.dom = elt("div", { tabIndex: 0, onkeydown: event => { let selectedTool = null; if(event.key === 'z' && event.ctrlKey || event.metaKey) { event.preventDefault(); dispatch({undo: true}); } else if(event.key in toolKeyMap) { event.preventDefault(); dispatch({tool: toolKeyMap[event.key]}); } } }, this.canvas.dom, elt("br"), ...this.controls.reduce( (a, c) => a.concat(" ", c.dom), [])); } syncState(state) { this.state = state; this.canvas.syncState(state.picture); for (let ctrl of this.controls) ctrl.syncState(state); } } document.querySelector("div") .appendChild(startPixelEditor({})); </script>
-
Exercise 19.2 - Efficient Drawing
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
<div></div> <script> const picDiff = (oldPic, newPic) => { const xLimit = Math.min(oldPic.width, newPic.width); const yLimit = Math.min(oldPic.height, newPic.height); const diff = []; for(let x = 0; x < xLimit; x++) { for(let y = 0; y < yLimit; y++) { if(oldPic.pixel(x, y) !== newPic.pixel(x, y)) { diff.push({x, y}); } } } return diff; }; // Change this method PictureCanvas.prototype.syncState = function(picture) { if (this.picture == picture) return; let diff = null; if(this.picture) { diff = picDiff(this.picture, picture); } this.picture = picture; drawPicture(this.picture, diff, this.dom, scale); }; function firstDraw(picture, cx, scale) { for (let y = 0; y < picture.height; y++) { for (let x = 0; x < picture.width; x++) { cx.fillStyle = picture.pixel(x, y); cx.fillRect(x * scale, y * scale, scale, scale); } } } // You may want to use or change this as well function drawPicture(picture, diff, canvas, scale) { let cx = canvas.getContext("2d"); if(!diff) { canvas.width = picture.width * scale; canvas.height = picture.height * scale; firstDraw(picture, cx, scale); } else { for(const coord of diff) { cx.fillStyle = picture.pixel(coord.x, coord.y); cx.fillRect(coord.x * scale, coord.y * scale, scale, scale); } } } document.querySelector("div") .appendChild(startPixelEditor({})); </script>
-
Exercise 19.3 - Circles
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
<div></div> <script> function circle(start, state, dispatch) { function drawCircle(pos) { let diff = {x: Math.abs(pos.x - start.x), y: Math.abs(pos.y - start.y)}; let rSquared = diff.x * diff.x + diff.y * diff.y; let[maxX, maxY] = [state.picture.width, state.picture.height]; let drawn = []; for(let x = 0; x < maxX; x++) { for(let y = 0; y < maxY; y++) { const xDiff = Math.abs(x - start.x); const yDiff = Math.abs(y - start.y); if(xDiff * xDiff + yDiff * yDiff <= rSquared) { drawn.push({x, y, color: state.color}); } } } dispatch({picture: state.picture.draw(drawn)}); } drawCircle(start); return drawCircle; } let dom = startPixelEditor({ tools: {...baseTools, circle} }); document.querySelector("div").appendChild(dom); </script>
-
Exercise 19.4 - Proper Lines
This exercise has in-page dependencies on the textbook site. Here's the link to the exercise, and below is my solution that can be pasted into the code box.
<div></div> <script> // The old draw tool. Rewrite this. function draw(start, state, dispatch) { function getSubLine(lastX, lastY, thisX, thisY, state) { // (pos) const [xDiff, yDiff] = [thisX - lastX, thisY - lastY] ; let [xFactor, yFactor] = [ xDiff >= 0 ? 1 : -1, yDiff >= 0 ? 1 : -1 ]; const abslope = Math.abs(yDiff) / Math.abs(xDiff); let drawn = []; if(abslope <= 1) { let limit = Math.abs(xDiff); yFactor = xFactor * yDiff / xDiff; for(let i = 0; i < limit; i++) { drawn.push({x: lastX + i * xFactor, y: lastY + Math.round(i * yFactor), color: state.color}) } } else { let limit = Math.abs(yDiff); xFactor = yFactor * xDiff / yDiff; for(let i = 0; i < limit; i++) { drawn.push({x: lastX + Math.round(i * xFactor), y: lastY + i * yFactor, color: state.color}) } } return drawn; } function drawPixel({x, y}, state) { let drawn = []; if(state.lastDrawn && Date.now() - state.lastDrawn < 200) { drawn = getSubLine( state.lastX, state.lastY, x, y, state ); } drawn.push({x, y, color: state.color}); dispatch({picture: state.picture.draw(drawn), lastX: x, lastY: y, lastDrawn: Date.now()}); } drawPixel(start, state); return drawPixel; } function line(start, state, dispatch) { function drawLine(pos) { const [xDiff, yDiff] = [pos.x - start.x, pos.y - start.y] ; let [xFactor, yFactor] = [ xDiff >= 0 ? 1 : -1, yDiff >= 0 ? 1 : -1 ]; const abslope = Math.abs(yDiff) / Math.abs(xDiff); let drawn = []; if(abslope <= 1) { let limit = Math.abs(xDiff); yFactor = xFactor * yDiff / xDiff; for(let i = 0; i < limit; i++) { drawn.push({x: start.x + i * xFactor, y: start.y + Math.round(i * yFactor), color: state.color}) } } else { let limit = Math.abs(yDiff); xFactor = yFactor * xDiff / yDiff; for(let i = 0; i < limit; i++) { drawn.push({x: start.x + Math.round(i * xFactor), y: start.y + i * yFactor, color: state.color}) } } dispatch({picture: state.picture.draw(drawn)}); } drawLine(start); return drawLine; } let dom = startPixelEditor({ tools: {draw, line, fill, rectangle, pick} }); document.querySelector("div").appendChild(dom); </script>
Chapter 20 - Node.js
Into the last terminal.
Having the client & server both speak the same language is absolutely stellar for understanding what's going on in the client-server interaction. For all the flak you can give to server-side JavaScript, Node.js and browser JavaScript are a powerful toolset when it comes to learning & teaching about fullstack applications.
-
node hello-world.js
// runs the given JavaScript file and will output to the terminal; scripts run in Node have no global "this" binding by default, though some global objects likeprocess
are available in node -
node
// without an argument, the Node command opens the Node console, which works similarly to the browser developer tools console; standalone lines of JavaScript can be entered & executed here -
process.exit(0)
// in the Node console, this ends the console session and returns the command line to the context that called Node -
process.exit(number)
// any code other than 0 is an error code -
process.argv
// holds the command line arguments given to the running script -
node showargv.js one --and two -f "7 8"
// outputs an array containing all the space-separated items in the command to Node, including the path to Node.exe and the .js file path -
process.argv = ['node', '/tmp/showargv.js', 'one', '--and', 'two', '-f', '7 8']
// this is the argv value from a "showargv.js" script, if the script is invoked with the above command in Node -
my-script.mjs
// the .mjs extension is for ES modules in Node; they mark support for imports and exports and don't need the "require" function binding for modular scripting designs -
import { thing } from "node:fs"
// when the item being imported is not at a file path, Node looks for built-in modules or a node_modules directory; "node:fs" is a built-in filesystem module, so it imports from that -
import { robot } from "robot"
// this looks for a JavaScript library in "node_modules/robot" that would commonly be installed into node_modules through Node Package Manager (NPM) -
// a Node library is just a module script that runs in Node; the below is an example
-
export default (a, b) => a + b
// this is the content of "folder/add.mjs" -
import adder from "./add.mjs"; console.log(adder(process.argv[2], process.argv[3]));
// this is the content of "folder/main.mjs" -
node main.js 3 5
// 35 is the console output from this whenever Node is run from the "folder/" context - // Node args are automatically cast to strings by default, and "add.mjs" is termed as a library
-
-
// Node Package Manager (NPM) is an online JavaScript module repository, and the "npm" command comes packaged with Node.js
-
npm install ini
// installs the "ini" package from the NPM online repository; many packages are lightweight, and this install runs in less than a second, with it functionally just adding a 7kB "node_modules/ini/lib/ini.js" file with some documentation from its creator on how to use it -
node
// packages installed through NPM can be accessed from JavaScript modules, but they can also be accessed through the node console after using the node command -
const {parse} = require('ini'); parse('x = 1\ny = 2') === { x: '1', y: '2' }
// the "ini" module is a CommonJS module, not an ES module, and node supports both; this code require()s & uses the parse() function from the module.exports object in ini.js -
npm config ls -l | grep config
// this can be used to see the default locations of the "npmrc" filesystem entry that can determine where the "node_modules" folder will be; different node projects typically package their own copies of "node_modules" folder to self-contain a project with its dependencies, but a base npmrc folder is used by the console
-
-
npm init
// creates a "package.json" file that each node project should have, in the directory where npm init was called -
package name: @hernandezn/my-package
// putting the @ indicates that the package is within a scope, be it a user scope or an organization scope -
... "dependencies": {"dijskstrajs": "^1.0.1", "random-item": "^1.0.0"}, ...
// if "npm install" runs with the package.json listing dependencies like this, the install will use these dependencies as the arguments; this list auto-updates when "npm install" runs WITH user-given arguments and installs a new dependency, so dependencies stay in sync -
... "version": "1.0.0", ...
// package.json lists the program's own version and versions from its dependencies, using semantic versioning (#.#.#) -
// general semantic versioning rules for each number in the version:
- // MAJOR version for changes that break compatibility, to where code built on a previous version of the package would break
- // MINOR version for when non-breaking functionality additions are made
- // PATCH version for fixing bugs & crash cases / optimizations / other things that don't change the interface or dependencies
- // 0.#.# versions are to be considered unstable, where anything may change at any time
- // all versions 1.0.0 and above must have their versions incremented for ANY changes made; as a corollary, the code in any one version may never be changed
- // when a major or minor version increments, versions below it reset to 0
- // #.#.#-some-new.identifier.0.1-2 denotes a pre-release version with a hyphen followed by an alphanumeric identifier with hyphens and dots allowed
- // pre-release versions indicate instability & may be backwards-incompatible, while the mainline patch version will be the backward-compatible replacement; subsequent pre-release versions for the same patch version must be in alphabetical sort order, with letters coming after numbers
-
... "some-lib": "^2.3.4", ...
// caret before the dependency version indicates that any compatible version may be used; in this case, that's anything at or above 2.3.4 but not EXcluding anything 3.0.0 or above -
npm publish
// publishes a directory that has a package.json to NPM, provided that the package name and version (both indicated in the package.json) aren't taken - // NPM documentation declares that all packages, scoped, unscoped, or private, require an NPM user account
-
import {readFile} from "node:fs"; readFile("file.txt", "utf8", (error, txt) => { if(error) throw error; console.log(txt) });
// from Node Filesystem module "node:fs"; readFile takes in a file name, a file character encoding, and a callback function that'll operate on the file's contents, with the callback operating AFTER the file data returns -
readFile("file.txt", (error, bytes) => { if(error) throw error; console.log(`Total bytes: ${bytes.length}\nFirst byte: ${bytes[0]}`) });
// with 2 arguments (no character encoding), the file content is read as an array-like object of 8-bit bytes, a "Buffer" type of object -
import {writeFile} from "node:fs"; writeFile("file.txt", "Some file content!", err => console.log(err));
// writeFile does what it says; all text is written with UTF-8 character encoding -
import {readdir, stat, rename, unlink} from "node:fs";
// several other node:fs methods, for reading directory content, viewing file metadata, renaming files, and deleting them -
import {readFile} from "node:fs/promises"; readFile("file.txt", "utf8").then(text => console.log(text))
// the "node:fs/promises" subpackage restructures node:fs functions to explicitly use Promises -
import {readFileSync} from "node:fs"; console.log(readFileSync("file.txt", "utf8"));
// node:fs functions have synchronous variants THAT CAUSE ALL PROGRAM OPERATIONS TO WAIT UNTIL THE node:fs OPERATION FINISHES -
import {createServer} from "node:http";
// this function is all it takes to create a simple HTTP serverlet server = createServer((request, response) => { response.writeHead(200, {"Content-Type": "text/html"}); response.write(`<h1>Hello!</h1><p>This is ${request.url}</p>`); response.write(`<footer>Method: ${request.method}</footer>`); response.end(); }); server.listen(8000); console.log("Listening at post 8000...");
- // writeHead() writes the HTTP response code & content type in the HTTP response header, response.write() takes the body of the HTTP response, and response.end() commits it to send back to the client
- // while the above is running on the local system, the HTML in the "write" argument will be served from http://localhost:8000 or any subpage of it, like http://localhost:8000/my/node-server
- // the write() function may be called multiple times to update the response with further data (which concatenates onto the last writeline by default)
- // the function passed as an argument to createServer() is called every time a client connects to the server, so all client-to-server interaction may happen through the content of this function
-
request.url
// erves the requested resource path, not the fully qualified url; at "http://localhost:8000",request.url
is "/", while at "http://localhost:8000/my/node-server",request.url
is "/my/node-server" -
request.method
// "GET" or "POST" or "DELETE" etc. -
console.log(request.constructor.name); console.log(response.constructor.name);
// the request & response types are "IncomingMessage" and "ServerResponse", respectively -
server.listen(8000)
// opens the 8000 port for server requests; in a simple live environment, this can just be 80, which would allow access over "http://localhost" without the port number, since 80 is the default port for HTTP requests
-
import { request } from "node:http";
// the function request() can be used to make HTTP requests from within Node, though fetch is still available in Node (and often simpler!) - // the ServerResponse response type has a "writable stream" object interface that's common in Node, which includes the "write" method (that takes a string or Buffer to add to the stream, plus a callback function to run after the method call) and an "end" method (that takes similar arguments that do the same thing)
-
import { createWriteStream } from "node:fs";
// creates a writable stream object to a file, with the same write & end methods - // the IncomingMessage request type has a "readable stream" object interface, which does much of its interaction by emitting events
-
request.on("data", dataChunk => response.write(dataChunk.toString())); request.on("end", () => response.end('stream ended'));
// readable streams' "on" method works like addEventListener; common events are "data" and "end" events that indicate when another data chunk has arrived or when the request has finished sending its data -
import { createReadStream } from "node:fs";
// similar to the above; this readable stream has the same "on" method that listens for "data" and "end" events -
on("data", dataChunk => ...)
// the dataChunk type is a binary Buffer that must be converted to the type it's meant to represent (like dataChunk.toString() does above) -
// all of the above can be applied to create basic operation as an asynchronous server and client, where the client can request a server resource with various HTTP methods, without reloading the page
-
// server-side code;
uppercase-server.mjs
import {createServer} from "node:http"; createServer((request, response) => { response.writeHead(200, {"Content-Type": "text/plain", "Access-Control-Allow-Origin": "null"}); request.on("data", chunk => response.write(chunk.toString().toUpperCase())); request.on("end", () => response.end()); }).listen(8000);
-
"Access-Control-Allow-Origin": "null"
// this allows null origins (a common default for localsystem) to request from the server resource without a CORS restriction; this is opposed to"Access-Control-Allow-Origin": "*"
which would also work because it allows the server resource to respond to requests from ALL client origins -
// client-side code;
uppercase-client.html
<input type="text" id="text-entry" placeholder="enter some text" oninput="makeRequest()" /> <p id="output"></p> <script> const input = document.getElementById("text-entry"); const output = document.getElementById("output"); const makeRequest = () => fetch("http://localhost:8000/", { method: "POST", body: input.value }).then(response => response.text()).then(text => output.innerText = text); // → UPPERCASE VALUE OF THE INPUT </script>
-
node uppercase-server.mjs
-
// in
uppercase-client.html
, typing into the input field activates themakeRequest()
function -
// the
makeRequest()
function fetches fromhttp://localhost:8000
with a POST request using a request body that's the value of the text from the input field -
// the request arrives in
uppercase-server.mjs
, which writes HTTP response code 200, a "text/plain" content type, and a "null" allowed requester origin into the header -
// in
uppercase-server.mjs
, the request is given a "data" listener that writes AND UPPERCASES each chunk from the request's body into the response as the request data arrives -
// in
uppercase-server.mjs
, the request's "end" listener closes the response writeable stream to finish sending data back to the client -
// back in
uppercase-client.html
, the response fromuppercase-server.mjs
streams into the fetch's Promise as the "response" object sent to the fetch Promise's "then" callback function -
// when the response from
uppercase-server.mjs
ends, the text from theresponse.text()
Promise is completed and sent to the next "then" callback -
// the text from the response is then set as the innerText of the "output" element in
uppercase-client.html
, completing the server-client interaction
-
// in
-
// server-side code;
-
process.cwd()
// returns the current working directory of the node program running the command -
import {resolve, sep} from "node:path";
// "resolve()" resolves file paths from URLs, and "sep" provides the file-path separator used in the host operating systemimport {resolve, sep} from "node:path"; const basePath = process.cwd(); function urlPath(url) {
// "http://my-site/its/my%20file.txt" -> "/its/my%20file.txt"
let pathName = new URL(url, 'http://d').pathname;// "/its/my%20file.txt" -> "/its/my file.txt"
let decodedPathName = decodeURIComponent(pathName);// "/its/my file.txt" -> "C:\Users\...\its\my file.txt"
let filePath = resolve(decodedPathName.slice(1)); if(filePath != basePath && !filePath.startsWith(basePath + sep)) {// if the file path doesn't follow the base path, throw this object (notice it doesn't have to be declared an Error type)
throw {status: 403, body: "Forbidden"}; } return filePath; }; -
(JavaScript) resolve(relativePath) == (Java) File.getCanonicalPath(relativePath)
// the resolve function finds the absolute location of the given path argument, resolving anything like "../" and filesystem shortcuts, after which it can be compared against the Node program's desired working directory; this ensures that the requested resource stays within the desired filesystem bounds of the program -
npm install mime-types@2.1.0
// this package contains a mapping between file types & MIME typesimport {createReadStream} from "node:fs"; import {stat, readdir} from "node:fs/promises"; import {lookup} from "mime-types"; methods.GET = async function(request) {
// the function described above
let path = urlPath(request.url); let stats; try {// checks if file exists and whether it's a directory
stats = await stat(path); } catch(error) {// error.code "ENOENT" means stat didn't find the file
if(error.code != "ENOENT") throw error; else return {status: 404, body: "File not found"}; } if(stats.isDirectory()) {// newline-separated files in the requested path
return {body: (await readdir(path)).join("\n")}; } else {// returns the file reader for the given path, along with the MIME type associated with the file extension
return { body: createReadStream(path), type: lookup(path) }; } }; - // the above makes the firectory contents in the node directory requestable; .txt files & directories will output plain text to the browser, .pdf files will be handled accordingly by the browser, and HTML files will be rendered to the browser
-
stats.size(); stats.mtime(); stats.isDirectory()
// the stats return from a file has several useful diagnostic operations for the given file -
if(stats.isDirectory()) await rmdir(path); else await unlink(path); return {status: 204};
// using this instead of the last conditional at the end of the above function gives a file & directory deleter; HTTP status code 204 means "no content" -
import {createWriteStream} from "node:fs";
// can be used for a PUT method to write to filesmethods.PUT = async function(request) { let path = urlPath(request.url); await pipeStream(request, createWriteStream(path)); return {status: 204}; }; function pipeStream(from, to) { return new Promise((resolve, reject) => {
// applies "error" listeners to the request & the file path's writestream
from.on("error", reject); to.on("error", reject);// applies "finish" listener for when the write stream is to close
to.on("finish", resolve);// runs the data stream from the request body to the file content
from.pipe(to); }); }; - // the above will write to the file path specified in the URL, with the content of that written file being the body of the request
-
// the command-line tool "curl" (in UNIX-like systems & UNIX emulators like mingw) can be used to test endpoints by calling URLs with request specifications
-
curl http://localhost:8000/file.txt
=GET http://localhost:8000/file.txt
// default curl sends a GET request to the specified URL -
curl -X PUT -d "some stuff to use as the body" http://localhost:8000/file.txt
=PUT http://localhost:8000/file.txt ... some stuff to use as the body
// the content after "-d" is the body (-d for data) of the request -
curl -X DELETE http://localhost:8000/file.txt
// sends a non-GET HTTP method without any data in the body
-
As this chapter doesn't aim to provide an exhaustive course on Node, here are important links provided by the text to use & learn of these tools in greater depth:
Chapter 20 Exercises
-
Exercise 20.1 - Search Tool
This exercise runs in a live Node.js environment.
import {stat, readFile, readdir} from 'node:fs'; import {sep} from 'node:path'; const regex = new RegExp(process.argv[2]); const fileNames = process.argv.slice(3); const encoding = 'utf8'; console.log(`\nFILES WHOSE CONTENT MATCHES WITH REGEX "${regex}":\n`); const doGrep = (files) => { for(const filePath of files) { stat(filePath, (err, stats) => { if(err) { console.log(err); return; } if(stats.isFile()) { readFile(filePath, encoding, (err, text) => { if(err) { console.warn(err); return; } else if(regex.test(text)) { console.log(filePath); } }); } else if(stats.isDirectory()) { readdir(filePath, (err, folderContents) => { if(err) { console.warn(err); return; } folderContents = folderContents.map((entry) => filePath + sep + entry); doGrep(folderContents); }); } }); } }; doGrep(fileNames);
-
Exercise 20.2 - Directory Creation
This exercise runs in a live Node.js environment, and to run it, it should be pasted into the file_server.mjs file that's on the textbook site. This code will allow the server script to execute the "MKDIR" HTTP method on the given URL path (i.e., the command
curl -X MKDIR http://localhost:8000/my-directory
while the script runs will allow the script to create a "my-directory" folder).import {mkdir} from "node:fs/promises"; methods.MKCOL = async function(request) { let path = urlPath(request.url); try { await stat(path); } catch(error) { if(error.code != "ENOENT") throw error; await mkdir(path); } return {status: 204}; };
-
Exercise 20.3 - A Public Space on the Web
This turned into probably a more extensive exercise than was intended by the textbook. It's a locally-running Node.js web app that provides a UI for full file-system control within a specified folder, including subfolder & file creation, file uploads, file & folder deletion, a visual structure that matches the directory structure, sandboxing against the application deleting or overwriting itself or any files that weren't sent through the application, automatically generated links to files that are added, and a UI that dynamically updates in response to changes made in the file system by the user.
As this mini-project is composed of a fair amount of source code, the code is not planned to be on this page. Instead, it's hosted on a subfolder of this website's Node.JS subdomain Github repo.
Chapter 21 - Project: Skill-Sharing Website
The final stretch!
This bit was essentially a meta-exercise in interpreting a front & backend project that's already pre-written. That's an important professional skill to have, sure, but I also appreciate it as victory lap for the reader to be able to say, "yes, I know JavaScript."
This chapter's entire project is somewhat less complicated than the one I wrote for chapter 20's final exercise, but that's at least partly because it offloads much of the complexity with its structure and its dependencies. I might have skill, but the author Marijn has skill AND experience.
They assert that creating UI elements using an element creation function that takes the tag name, tag attributes, and child elements has led to messy code, but I think it's pretty clean, so clean that it's easy to know how to make it cleaner. They've basically built the JSX that underlies React.
- // CommonJS modules imported through NPM can be implemented through the "import" clause instead of needing the "requires" function; this adds forward compatibility from CommonJS to ES modules
-
request.headers['if-none-match']
// ON SERVER-SIDE; Node stores request header keys as LOWERCASE values -
await new Promise(resolve => setTimeout(resolve, 500));
// putting this statement in an async function is a simple way to force it to wait
Chapter 21 Exercises
These exercises are modifications for the skill-sharing application code outlined in this chapter.
-
Exercise 21.1 - Disk Persistence
This solution can be defined in a few parts:
-
I created a
save-data.json
file in theskillsharing
directory and gave it only an empty object as content:{}
-
In
skillsharing_server.mjs
, I added this import statement to the top:import {readFile, writeFile} from 'node:fs/promises';
-
In
skillsharing_server.mjs
, I replaced this code:new SkillShareServer({}).start(8000);
with this code:(async () => { const talks = JSON.parse(await readFile('save-data.json')); console.log(talks); new SkillShareServer(talks).start(8000); })();
-
In
skillsharing_server.mjs
, I added this line to theSkillShareServer.prototype.updated
function:writeFile('save-data.json', JSON.stringify(this.talks)).catch(err => { console.log('Couldn\'t write to file!'); console.log(String(err)); });
With this, the application now reads from a save file upon starting, and it writes to that save file every time there's an update from a user. This preserving the application contents after crashes & restarts, fulfilling the feature requested by this exercise.
-
I created a
-
Exercise 21.2 - Comment Field Resets
All of the changes I made for this exercise are in the
renderTalk()
function defined inpublic/skillsharing_client.js
:let thisClientSubmitted = false; function renderTalk(talk, dispatch) { const commentAttributes = {type: "text", name: "comment", oninput: event => { localStorage.setItem('commentInProgressTalk', talk.title); localStorage.setItem('commentInProgressContent', event.target.value); }}; if(talk.title === localStorage.getItem('commentInProgressTalk') && thisClientSubmitted === false) { commentAttributes.value = localStorage.getItem('commentInProgressContent'); } thisClientSubmitted = false; const commentElement = elt("input", commentAttributes); const submitCommentButton = elt("button", {type: "submit", onclick: event => { thisClientSubmitted = true; }}, "Add comment"); return elt( "section", {className: "talk"}, elt("h2", null, talk.title, " ", elt("button", { type: "button", onclick() { dispatch({type: "deleteTalk", talk: talk.title}); } }, "Delete")), elt("div", null, "by ", elt("strong", null, talk.presenter)), elt("p", null, talk.summary), ...talk.comments.map(renderComment), elt("form", { onsubmit(event) { event.preventDefault(); let form = event.target; dispatch({type: "newComment", talk: talk.title, message: form.elements.comment.value}); form.reset(); } }, commentElement, " ", submitCommentButton)); }
With this, the content & location of a comment in progress is saved to local storage, and if that location is encountered when the page re-renders, it restores that comment from local storage to the client's comment box. Because it uses local storage, it's also refresh-safe.
Additionally, because local storage is shared when two clients are on one system, the UI tracks whether a comment was submitted from that particular client, rescinding the content of the comment box only whenever the comment was submitted by this client. This lets the comment box continue to empty itself upon submit.
Reflection
I would recommend this book to others who want to learn JavaScript. I still believe I was fairly capable with browser JS from the beginning, but now, it feels like I've completed a toolbox that I didn't even know was incomplete! It's handed me a lot of leads for moving forward with, too. I think the intended route here is React, but I'm also interested in websockets and SVG. It's made several concepts like canvas & asynchronous code more approachable, too.
Because JavaScript is supported by the ecosystems built on it, the more complete knowledge of the pure language can feel like having all the cheat codes. I can imagine how more things must work behind the scenes now, like how JSX could be implemented similarly to how the Egg programming language from chapter 12 was, or how React & Angular components are essentially just the stateful class components created in the final chapter & handled as ES modules, with the syntactic sugar of JSX added to them. I can see through things that were once arcane, and it's thanks to the understanding from this book.
The application from the final chapter was a logical conclusion, a webapp whose UI doesn't even render without the browser running a mess of JavaScript. The mainstream Web UI frameworks have reached similar conclusions before, too, and I personally don't agree with it. There are performance reasons & bad-practice reasons & probably environmental reasons that can support me on that, but my personal core for being against it is that JavaScript should be reserved for the good stuff. It's the ace in the hole for making the impossible possible in Web UI, and when you make it handle every menial little task on the browser, what's happened is that you spread its capability thin in the developer's mind and wind up underselling what JavaScript is truly capable of. It's powerful, and there's less & less thought about that power as people abstract it away into Web frameworks that regularly try to put it in your head that you need an entire JavaScript framework for an interactive application. I don't need a single line of JavaScript for basic functions like single-url pagination or motion graphics rendering to work. The frameworks make applications that are maintainable & easy to collaborate on. They're tools that help the bottom line for larger teams, not a statement on the one right way to do things. Converging on it as if it IS the one right way to do things is probably getting at least a little caught up in the current trends, but as it's also being taught as a professional skill to help people get further along in the actual software industry, I think that part of it is commendable.
The last project did feel more like a victory lap than anything, and it makes me glad that I overcomplicated the final exercise from chapter 20, rebuilding that from scratch & adding my own new features instead of sticking strictly to the book. It gave me the chance to compose everything from the whole text together into a program that's truly my own, where I took it as a problem statement to create a solution for rather than as a LeetCode-esque exercise to figure out an abstract algorithm. I now have an extensible application that works as my own custom & quickly searchable file manager, and it's thanks to the application requested by this book. With some polish, it can even become something you can sell to people, and that crosses a line from being a textbook exercise to being a peek into the vision of an engineer, the vision of the one who wrote this book.
I have my own visions, too: problems to solve and solutions to be made. And with this book now behind me, I'm definitively better prepared for that future.