{…}
­
How Java­Script Works by David Crockford

Chapitre 6
How booleans work

Truth is truth. You can’t have opinions about truth.

Peter Schickele

The boolean type was named after George Boole, an English mathematician who developed a system of algebraic logic. Claude Shannon adapted Booles’s system for the design of digital circuits. That is why we call computer circuits logic.

The boolean type contains only two values, true and false. Bool­e­an values are usually generated by the comparison operators ; manipulated by the logical operators ; and consumed by the ternary operator and the condition part of the if, do, for, and while statements.

The typeof operator returns "boolean" when its operand is true or false.

Relational Operators

{equal sign equal sign equal signequal
!==exclamation point equal sign equal signnot equal
<less thanless than
<=less than equal signless than or equal
>greater thangreater than
>=greater than equal signgreater than or equal

It is deeply unfortunate that the equality operator is ===equal sign equal sign equal sign and not =equal sign. It is even more unfortunate that the not equal operator is !==exclamation point equal sign equal sign and not not equal sign. All of these operators do pretty much what you would expect, except there is also plenty of nonsense. Here are some examples of nonsense :

undefined < null
undefined > null
undefined === null

Nan === NaN
Nan !== NaN

"11" < "2"
"2" < 5
5 < "11"
1
2
3
4
5
6
7
8
9
10
undefined < null                        // false
undefined > null                        // false
undefined === null                      // false

Nan === NaN                             // false
Nan !== NaN                             // true

"11" < "2"                              // true
"2" < 5                                 // true
5 < "11"                                // true
copy to clipboard

===equal sign equal sign equal sign and !==exclamation point equal sign equal sign generally do the right thing except when both operands are NaN. ===equal sign equal sign equal sign and !==exclamation point equal sign equal sign can be used to determine if a value is null or undefined or any value other than NaN. To test if x is NaN, always use Number.isNaN(x).

===equal sign equal sign equal sign should not be used to test for the completion of a loop unless the induction variable is in the safe integer range. Even then, it is safer to use >=greater than equal sign.

<less than and <=less than equal sign and >=greater than and >=greater than equal sign generally do the right thing when both operands are strings or when both operands are numbers. In most other cases, the result is nonsense. Avoid mixing types when doing comparisons. Java­Script does not prohibit mixing types in that way, so you need to bring your own discipline.

Java­Script also has some comparison operators that are even less reliable. I recommend never using ==equal sign equal sign (and !=exclamation point equal sign). They do type coersion before doing the comparison, and so are likely to produce false positives (and false negatives). Always use ===equal sign equal sign equal sign (and !==exclamation point equal sign equal sign) instead. Always.

Boolish

Java­Script has a lovely boolean type but does not make good use of it. These are the positions in a program where the booleans do the most good :

In a well designed language, only boolean values would be allowed in those positions. Java­Script instead allows any type in all of those positions.All values in the language are members of the boolish type. All values in the boolish type are either truthy or falsy.

The falsy values are

All other values are truthy, including empty objects, empty arrays, and strings that might look falsy, like "false" and "0".

The falsy values often act like false, but most of them are not, strictly speaking, false. The truthy values often act like true, but most of them are not true. This boolishness was a mistake, but it was not an accident. It was done intentionally in order to permit idioms of the C language in Java­Script.

C is an inadequately typed language. It uses a single value to represent 0, FALSE, NULL, end of string, and other things. They are all the same to C. So in the condition position of an if statement, C is looking to see if the expression is 0 or not. There is a style of C programming that exploits that by making the conditions terse.

Java­Script has a sound boolean type, but boolishness tosses much of the value away. A condition should be either true or false. Any other value should be an error, ideally a compile time error. That is not the case in Java­Script. Conditional expressions can be as terse and cryptic as in C. Values that accidentally fall into condition position are not flagged as errors. Instead, they can send the program off in unexpected directions. The Java language requires that conditions be booleans, and so eliminates a class of errors. I wish that Java­Script had done that too.

I recommend that you write programs as though Java­Script had been designed correctly. Use booleans in all conditions. If you write in the style of a better language, you will write better programs.

Logical Operators

The logical operators are also subject to boolishness.

!exclamation pointlogical notIf the operand is truthy then the result is false. If the operand is falsy then the result is true.
&&ampersand ampersandlogical andIf the first operand is falsy, then the result is the first operandand the second operand is not evaluated. If the first operand is truthy, then the result is the second operand.
||vertical bar vertical barlogical orIf the first operand is truthy, then the result is the first operand and the second operand is not evaluated. If the first operand is falsy, then the result is the second operand.

Not !

Logical expressions can get complicated. There are formal transformations that can simplify them. Unfortunately, boolishness and NaN can cause formal transformations to produce errors.

It is usually wise to simplify double negatives. In a logical system,

!!p === p
1
!!p === p
copy to clipboard

That is only true in Java­Script when p is a boolean. If p is of any other type, then !!p is equal to Boolean(p), not necessarily p.

Some of the comparison operators have opposites, so < is the opposite of >=,and > is the opposite of <=. So it should be possible to simplify !(a < b) as a >= b because

!(a === b) === (a !== b)
!(a <= b) === (a > b)
!(a > b) === (a <= b)
!(a >= b) === (a < b)
1
2
3
4
!(a === b) === (a !== b)
!(a <=  b) === (a >   b)
!(a >   b) === (a <=  b)
!(a >=  b) === (a <   b)
copy to clipboard

If either a or b is NaN, then the code transformation fails. This is because comparing any number to NaN produces a result of false, regardless of the comparison operator. So,

7 < NaN
NaN < 7
!(7 < NaN) === 7 >= NaN
1
2
3
7 < NaN                                 // false
NaN < 7                                 // false
!(7 < NaN) === 7 >= NaN                 // false
copy to clipboard

It might make sense if NaN were smaller than all other numbers or if comparing a number to NaN would raise an exception. Java­Script instead implements nonsense. The only meaningful operation on NaN is Number.isNaN(NaN). Avoid NaN in all other contexts.

The De Morgan Laws can be helpful in simplfying logical expressions. I use them frequently :

!(p && q) === !p || !q
!(p || q) === !p && !q
1
2
!(p && q) === !p || !q
!(p || q) === !p && !q
copy to clipboard

The laws are sound in Java­Script as long as p and q were not corrupted with nonsense. Boolish calues should be avoided. Use booleans instead.


Chapitre 7
How arrays work

Everything is numbered here.
The monster is zero.

The Controller of Planet X

The array is the most venerable data structure. An array is a contiguous section of memory, divided into equal size chunks, each associated with and quickly accessed with an integer. Java­Script failed to include arrays in its first release. That omission was barely noticed because Java­Script’s objects are so powerful. Ignoring performance issues, objects can do everything that arrays can do.

That is still the case. Any string can be used to index into an array. Java­Script’s arrays are really objects. In today’s Java­Script, arrays slightly differ from objects in four ways :

Java­Script is itself confused about arrays. When given an array, the typeof operator returns the string "object", which is obviously wrong. You should instead use the Array.isArray(value) function to determine if a value is an array.

const what_is_it = new Array(1000);
typeof what_is_it
Array.isArray(what_is_it)
1
2
3
const what_is_it = new Array(1000);
typeof what_is_it                       // "object"
Array.isArray(what_is_it)               // true
copy to clipboard

Since the invention of counting, people have started numbering with whatever word they used to indicate 1. In the mid 1960s, a small but influential group of programmers determined that we should instead be starting with 0. Today virtually all programmers begin numbering with 0. The rest of humanity, including most of the mathematicians, still start with 1. Mathematicians usually label the origin as 0, but most label the first member of an ordered set as 1. How they do this is still a mystery.

There is an efficiency argument for starting at 0, but it is so weak as to be worthless. Similary, there is a correctness argument that says that starting at 0 causes fewer off-by-wun errors, but that is also suspect. It seems that there should be a good reason for why programmers count differently than every wun else. Perhaps wun day we will discover why.

The place where this affects Java­Script is in the numbering of the elements of arrays, and to a lesser extent, in the numbering of the characters in a string. The idea that arrays should be processed wun element at a time goes back at least as far as FORTRAN. A more modern idea is to process arrays functionally. This simplifies code by eliminating explicit loop management and it creates the potential for distributing the work over multiple processors.

A well written program in a well designed language should not care if arrays start at 0 or 1. I would not accuse Java­Script of being a well designed language, but it is a significant improvement over FORTRAN, and in fact, by stepping away from the FORTRAN model, we do not usually need to care about how the elements are numbered.

Sometimes we do need to care. The word first becomes problematic because of its association with wun so instead I use zeroth. It makes my origin clear.

[0]zeroth0th
[1]wunth1th
[2]twoth2th
[3]threeth3th

Moving on from those, fourth, fifth, and sixth seem ambiguous, but by the time we have counted past the smallest integers, it should be clear what the origin was.

Initialization

There are two ways to make a new array.

let my_little_array = new Array(10).fill(0);
let same_thing = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

my_little_array === same_thing
1
2
3
4
let my_little_array = new Array(10).fill(0);
// my_little_array is [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let same_thing = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

my_little_array === same_thing          // false
copy to clipboard

Note that my_little_array and same_thing are exactly the same. They are also two separate, unique values. Unlike strings, but like objects, identical arrays are only equal if they are actually the same array.

Stacks and Queues

Arrays have methods that act on an array as a stack. The pop method removes and returns the last element of the array. The push method appends a new element to the array.

Stacks are often used in interpreters and calculators.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
4
5
6
7
8
9
10
11
13
14
15
16
17
18
19
20
21
22
23
24
function make_binary_op(func) {
  return function (my_little_array) {
    let wunth = my_little_array.pop();
    let zeroth = my_little_array.pop();
    my_little_array.push(func(zeroth, wunth));
    return my_little_array;
  };
}

let addop = make_binary_op(function (zeroth, wunth) {
  return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
  return zeroth * wunth;
});

let my_little_stack = [];
// my_little_stack is []
my_little_stack.push(3); 
// my_little_stack is [3]
my_little_stack.push(5); 
// my_little_stack is [3, 5]
my_little_stack.push(7); 
// my_little_stack is [3, 5, 7]
mulop(my_little_stack);  
// my_little_stack is [3, 35]
addop(my_little_stack);  
// my_little_stack is [38]
let answer = my_little_stack.pop();
// my_little_stack is [], answer is 38
copy to clipboard

The shift method is similar to the pop method, except that it removes and returns the zeroth element instead of the last. The weirdly named unshift method is like push except that it prepends the new element at the front of the array, not at the end. The shift and unshift methods can be much slower than pop and push, especially if the array is large. Using shift and push together makes a queue, where you append new items to the back and ultimately harvest them from the front.

Searching

Java­Script provides methods for searching thru an array. The indexOf method takes a value that it compares to each of the elements of the array starting at the beginning. If your value matches an element, it stops its search and returns the ordinal of the element.

If it fails to find a match, it returns -1. Note that this is a type error that is waiting to happen because -1 is a number, much like every other number. If you use the return value of indexOf without first explicitly checking for -1, then the computation it contributes to could go wrong without warning. Java­Script has many bottom values. Wun of those should have been used instead.

The lastIndexOf function is similar to indexOf except that it starts at the end of the array and searches backward. It also uses -1 as the failure code.

The includes function is similar to indexOf except that it returns true if the value is found, and false if it is not found.

Reducing

The reduce method reduces an array to a single value. It takes a function that takes two arguments. The reduce method calls that function with pairs of values, over and over again, until there is just a single value that is the result.

There are two ways to design a reduce method. Wun way is to have it call the function for every element of the array. An initial value must be provided to get things started.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
4
5
6
7
function add(reduction, element) { 
  return reduction + element;
}

let my_little_array = [3, 5, 7, 11];

let total = my_little_array.reduce(add, 0);
// total is 26
copy to clipboard

For each element, the reduction value is passed to add, along with the current array element. We explicitly pass in the 0 which is the initial reduction value. The add function sees

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
4
(0, 3)                                   // 3
(3, 5)                                   // 8
(8, 7)                                   // 15
(15, 11)                                 // 26
copy to clipboard

Each value that add returns becomes the reduction on the next call to add.

The initial reduction will not always be 0. If we pass a multiply function to reduce, then the initial reduction should be 1. If we pass Math.max to reduce, the initial reduction should be -Infinity. This creates a hazard : You need to be careful in selecting the initial reduction value.

The other way to design reduce is to not require an initial reduction value. Instead, the function is called wun less time. The first time it is called, it receives the zeroth and wunth elements. The zeroth element becomes the initial reduction value.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
total = my_little_array.reduce(add);     // 26
copy to clipboard

Now the add function sees

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
(0, 3)                                   // 8
(3, 5)                                   // 15
(8, 7)                                   // 26
copy to clipboard

The function is called wun fewer time, and there is no error possible in selecting the wrong initial reduction value.

The thing that Java­Script did that is kind of brilliant is that its reduce method works either way. If you pass in the initial reduction value, then the function is called for every element. If you do not pass in an initial reduction value, then your function is not called for the zeroth element. Instead, the first element is used as the initial reduction value. So both of the examples above of reduce add work.

The reduceRight function works the same way, except that it begins at the end of the array. I wish it had been named reduce_reverse.

I used reduce to compute the check digit of the ISBN number of this book.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
4
5
6
7
8
9
10
11
13
14
15
16
17
18
19
20
21
22
23
24
function isbn_13_check_digit(isbn_12) {
  const string_of_digits = isbn_12.replace(/-/g, "");
  if (string_of_digits.length === 12) {
    const check = string_of_digits.split("").reduce(
      function (reduction, digit, digit_nr) {
        return reduction + (
          digit_nr % 2 === 0
          ? Number(digit)
          : Number(digit) * 3
        );
      },
      0
    ) % 10;
    return (
      check > 0
      ?10-check
      : check
    );
  }
}
isbn_13_check_digit("978-1-94-981500")   // 9
copy to clipboard
Iterating

Wun of the most common operations on arrays is to do something to each element. Historically this was done with the for statement. Java­Script offers a more modern approach.

The forEach method takes an array and a function. It calls that function for each element of the array. The function can have three parameters : element, element_nr, and array. The element parameter is an element of the array. The element_nr parameter is the ordinal of the element, which can be handy if there is another array in the computation and you need to coordinate. The array parameter was a mistake and should not be there. It is an invitation to modify the array that is being processed, which is usually a bad thing.

Unfortunately, there is not a method that can process the array in the reverse order (like reduceRight can). There is a reverse method that could be called first, but reverse is a destructive method that cannot be used on frozen arrays.

The forEach method ignores the return value of the function it calls. Interesting methods can be made by paying attention to the return value.

The every method looks at the return value. If it is falsy, the every method stops processing and returns false. If it is truthy, the every method continues processing. If the every method reaches the end of the array, it returns true.

The some method is so similar to the every method that it isn’t clear why it should even be in the language. If the function’s return value is truthy, the some method stops processing and returns true. If it is falsy, the some method continues processing. If the some method reaches the end of the array, it returns false.

The find method is like the some method, except that instead of returning true or false, it returns the element that was being processed when the function returned something truthy.

The findIndex method is like the find method, except that instead of returning the element, it returns the ordinal of the element that was being processed when the function returned something truthy.

The filter method is also like the find method, except it always processes to the end, and it returns an array collecting all of the elements for which the function returned a truthy value. So find returns the first match. The filter method returns all of the matches.

The map method is like the forEach method, except that it collects all of the return values and returns them in a new array. The map method is an ideal way of doing transformations by creating a new array that is a refinement or amplification of the original.

These methods are a better way of processing arrays without resorting to for loops. But the set of methods is incomplete.

forEach and find are able to exit early. (The exiting forms of forEach are every and some.) map, reduce, and filter do not have the option of exiting early.

The reduce method has the option of working backwards with reduceRight, but forEach, map, filter, and find do not have the option of working backwards.

These omissions are used as justification for not abolishing the for statement.

Sorting

Java­Script has a sort method. Unfortunately, it has some problems.

It sorts in place, modifying the array it sorts. That means it is not lpossible to sort a frozen array, and it is not safe to sort a shared array.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
let my_little_array = ["unicorns", "rainbows", "butterflies", "monsters"];
my_little_array.sort();
// my_little_array is ["butterflies", "monsters", "rainbows", "unicorns"]
copy to clipboard

Its default comparison function wants to arrange values as strings, even if they are numbers. For example,

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
let my_little_array = [11, 2, 23, 13, 3, 5, 17, 7, 29, 19];
my_little_array.sort();
// my_little_array is [11, 13, 17, 19, 2, 23, 29, 5, 3, 7]
copy to clipboard

That isn’t just terribly inefficient, it is also terribly wrong. Fortunately, we can mitigate this by passing a comparison function to sort. The function is given two elements. It is expected to return a negative number if the first element should come first, a positive number if the second element should come first, or a zero if the comparison function can not tell.

This comparison function could sort an array of numbers correctly (as long as all of the elements are finite numbers) :

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
function compare(first, second) {
  return first - second;
}
copy to clipboard

If you want to compare numbers that are not finite (like NaN and Infinity) then your compare function will have to work harder.

Next on the list of problems with the sort function is the lack of stability. A sort is stable if elements that are equalish (your compare function return szero) retain their original relative order. Java­Script does not guarantee stability. This is not a concern when sorting an array of strings or an array of numbers. It can be a concern when sorting an array of objects or an array of arrays. A complex sort might want to sort by las tname, and when last names are the same, sort by first name. Wun way to do that would be to first sort by first name, and then sort again by last name. But that does not work because the sort is not stable, and the information added by the first name sort may be lost.

We can mitigate that by using a more complex comparison function. To make that easier, we make a factory that makes comparison functions.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
function refine(collection, path) {
copy to clipboard

Take an array or object and a path in the form of an array of strings, and return the value at the end of the path. If there is no value, return undefined.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
4
5
6
7
8
9
10
11
  return path.reduce(
    function (refinement, element) {
      try {
        return refinement[element];
      } catch (ignore) {}
    };
    collection
  );
}

function by(...keys) {
copy to clipboard

This factory creates a comparison function to help in sorting an array of objects or an array of arrays. The arguments are wun or more strings or integers that will identify the properties or elements to compare. If the first argument sees a tie, then try the second, and then the third…

Convert each key into an array of strings.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
  const paths = keys.map(function (element) {
    return element.toString().split(".");
  });
copy to clipboard

Compare each pair of values until finding a mismatch. If there is no mismatch then the two values are equal.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
4
5
6
7
8
9
10
  return function compare(first, second) {
    let first_value;
    let second_value;
    if (paths.every(function (path) {
      first_value = refine(first, path);
      second_value = refine(second, path);
      return first_value === second_value;
    })) {
      return 0;
    }
copy to clipboard

If the two values are of the same type, then we can compare the two values. If the types are not the same, then we need some policy to cope with the weirdness. Our simple policy here is to compare the names of the types, so boolean < number < string < undefined. (It might be better to fail instead of sorting disagreeable types.)

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
4
5
6
7
8
9
10
11
  return (
    (
      typeof first_value === typeof second_value
      ? first_value < second_value
      : typeof first_value < typeof second_value
    )
    ? -1
    : 1
  );
  };
}
copy to clipboard

Example :

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let people = [
  {first: "Frank", last: "Farkel"},
  {first: "Fanny", last: "Farkel"},
  {first: "Sparkle", last: "Farkel"},
  {first: "Charcoal", last: "Farkel"},
  {first: "Mark", last: "Farkel"},
  {first: "Simon", last: "Farkel"},
  {first: "Gar", last: "Farkel"},
  {first: "Ferd", last: "Berfel"}
]

people.sort(by("last", "first"));

  return (
    (
      typeof first_value === typeof second_value
      ? first_value < second_value
      : typeof first_value < typeof second_value
    )
    ? -1
    : 1
  );
  };
}

//  [
//    {first: "Ferd", last: "Berfel"},
//    {first: "Charcoal", last: "Farkel"},
//    {first: "Fanny", last: "Farkel"},
//    {first: "Frank", last: "Farkel"},
//    {first: "Gar", last: "Farkel"},
//    {first: "Mark", last: "Farkel"},
//    {first: "Simon", last: "Farkel"},
//    {first: "Sparkle", last: "Farkel"}
// ]
copy to clipboard
Potpourri

The concat method takes two or more arrays and concatenates them together to make a new array.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
4
let part_zero = ["unicorns", "rainbows"];
let part_wun = ["butterflies", "monsters"];
let whole = part_zero.concat(part_wun);
// whole is ["unicorns", "rainbows", "butterflies", "monsters"]
copy to clipboard

The join method takes an array of strings and a separator string. It makes a big string that combines everything. Use an empty string as the separator if you don’t want separation. This is the inverse of the string split method.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
let string = whole.join(" & ");
// string is "unicorns & rainbows & butterflies & monsters"
copy to clipboard

The reverse method moves the elements around in an array so that they are in the opposite order. This is destructive, like the sort function.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
whole.reverse();
// whole is ["monsters", "butterflies", "rainbows", "unicorns"]
copy to clipboard

The slice method can make a copy of an array, or a copy of a part of an array. The zeroth parameter determines at what ordinal to start. The wunth parameter is the zeroth parameter plus the number of elements to copy. If the wunth parameter is omitted, all of the remaining elements are copied.

function make_binary_op(func) {
 return function (my_little_array) {
  let wunth = my_little_array.pop();
  let zeroth = my_little_array.pop();
  my_little_array.push(func(zeroth, wunth));
  return my_little_array;
 };
}

let addop = make_binary_op(function (zeroth, wunth) {
 return zeroth + wunth;
});

let mulop = make_binary_op(function (zeroth, wunth) {
 return zeroth * wunth;
});

let my_little_stack = [];
my_little_stack.push(3);
my_little_stack.push(5);
my_little_stack.push(7);
mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();
1
2
3
4
5
6
let element_nr = whole.indexOf("butterflies");
let good_parts;
if (element_nr !== -1) {
  good_parts = whole.slice(element_nr);
}
// good_parts is ["butterflies", "rainbows", "unicorns"]
copy to clipboard
Pure and impure

Some of the array methods are pure, not changing their inputs. Some of them are not.Some of them should have been pure, bur are not. Some by their nature can not be pure, but are still valuable.

When coping with the pure/impure duality, it is important to know what is pure and what is not.

Pure methods :

concat
every
filter
find
findIndex
forEach
indexOf
join
lastIndexOf
map
reduce
reduceRight
slice
some

Impure methods :

fill
pop
push
shift
splice
unshift

Impure methods that should have been pure :

reverse
sort


Chapitre 8
How objects work

Have grat­i­tude for the things you’re dis­­card­­ing. By giv­ing grati­tude, you’re giv­ing clo­sure to the rela­tion­ship with that ob­ject, and by do­ing so, it be­comes a lot eas­i­er to let go.

Marie Kondo

Java­Script over­loads the word object. This is a lan­guage in which every­thing (ex­cept the two bot­tom val­ues, null and undefined) is an ob­ject. But usu­ally, es­pe­cially in this chap­ter, ob­ject means some­thing more specific.

Java­Script’s pri­mary data struc­ture is called object. An ob­ject is a con­tainer of prop­er­ties (or mem­bers). Each prop­erty has a name and a val­ue. The name is a string. The val­ue can be of any type. In oth­er lan­guag­es, this type of ob­ject might be called a hash ta­ble, map, re­cord, struct, as­so­cia­tive ar­ray, dic­tion­ary, or in some very rude lan­guag­es, a dict.

A new ob­ject can be cre­ated with an ob­ject lit­eral. An ob­ject lit­eral cre­ates a val­ue that can be stored in a vari­able or ob­ject or ar­ray, or passed to a func­tion, or re­turned from a func­tion.

An ob­ject lit­eral is de­lim­ited by {left brace and }right brace. Nes­tled in­side can be zero or more prop­er­ties, sep­a­rated with ,comma. A prop­erty can be :

So, for example,

let bar = "a long rod or rigid piece of wood or metal";
let my_little_object = {
  "0/0": 0,
  foo: bar,
  bar,
  my_little_method() {
    return "So small.";
  }
};
1
2
3
4
5
6
7
8
9
let bar = "a long rod or rigid piece of wood or metal";
let my_little_object = {
    "0/0": 0,
    foo: bar,
    bar,
    my_little_method() {
        return "So small.";
    }
};
copy to clipboard

We can ac­cess a prop­erty of an ob­ject by us­ing the dot no­ta­tion with a name.

my_little_object.foo === my_little_object.bar
1
my_little_object.foo === my_little_object.bar  // true
copy to clipboard

We can also ac­cess a prop­erty of an ob­ject us­ing the brack­et no­ta­tion. We can use the brack­et no­ta­tion to ac­cess a prop­erty whose name is not a val­id iden­ti­fier. We can also use the brack­et no­ta­tion to ac­cess com­puted prop­erty names. The ex­pres­sion in the brack­ets is eval­u­ated and con­vert­ed into a string if necessary.

my_little_object["0/0"] === 0
1
my_little_object["0/0"] === 0            // true
copy to clipboard

If we ask for a name with the dot or the brack­et that can­not be found in the ob­ject, then the bot­tom val­ue un­de­fined is pro­vided in­stead. Ask­ing for a non­ex­is­tent or miss­ing prop­erty is not con­sid­ered an er­ror. It is a nor­mal op­er­a­tion that pro­duces undefined.

my_little_object.rainbow
my_little_object[0]
1
2
my_little_object.rainbow                 // undefined
my_little_object[0]                      // undefined
copy to clipboard

New prop­er­ties may be add­ed to an ob­ject by as­sign­ment. The val­ues of ex­ist­ing prop­er­ties may also be re­placed by assignment.

my_little_object.age = 39;
my_little_object.foo = "slightly frilly or fancy";
1
2
my_little_object.age = 39;
my_little_object.foo = "slightly frilly or fancy";
copy to clipboard

I rec­om­mend not stor­ing un­de­fined in ob­jects. Ja­vaS­cript al­lows it, and cor­rectly gives back the un­de­fined val­ue, but it is the same un­de­fined that means that the prop­erty is miss­ing. This is a source of con­fu­sion that can eas­ily be avoid­ed. I wish that stor­ing un­de­fined would cause the prop­erty to be re­moved, but it does not. A prop­erty can be re­moved by the de­lete operator.

delete my_little_object["0/0"];
1
delete my_little_object["0/0"];
copy to clipboard

Sophisticated data struc­tures can be built out of ob­jects be­cause ref­er­ences to ob­jects can be stored in ob­jects. All sorts of graphs and cy­clic struc­tures can be con­struct­ed. There is no lim­it on the depth of nest­ing, but don’t go crazy.

When the typeof op­er­­era­tor is giv­en an ob­ject, it gives the string "object".

typeof my_little_object === "object"
1
typeof my_little_object === "object"     // true
copy to clipboard
Case

The match­ing of keys is case sen­si­tive. So my​_​little​_​object​.​cat is not the same prop­erty as my​_​little​_​object​.​Cat or my​_​little​_​object​.​CAT. The === op­er­a­tor deter­mines the string match­ing of prop­erty names.

Copy

The Object.­as­sign func­tion can copy the prop­er ties from an ob­ject to an­oth­er. You can make a copy of an ob­ject by as­sign­ing to an emp­ty object.

let my_copy = Object.assign({}, my_little_object);
my_copy.bar
my_copy.age
my_copy.age += 1;
my_copy.age
delete my_copy.age;
my_copy.age
1
2
3
4
5
6
7
let my_copy = Object.assign({}, my_little_object);
my_copy.bar  // "a long rod or rigid piece of wood or metal"
my_copy.age                              // 39
my_copy.age += 1;   
my_copy.age                              // 40
delete my_copy.age;
my_copy.age                              // undefined
copy to clipboard

An ob­ject can be as­signed ma­te­rial from many ob­jects. In this way, a com­plex ob­ject can be con­struct­ed by the as­sem­bling of ma­te­rial from sim­pler objects.

inheritance

In Ja­vaS­cript, an ob­ject can be made that in­her­its from an­other ob­ject. This is a very dif­fer­ent style of in­her­i­tance than is prac­ticed in lan­guag­es that have tight­ly cou­pled pro­gram­matic struc­tures like class­es. In Ja­vaS­cript, it is the data that is cou­pled, which can sig­nif­i­cantly re­duce the brit­tle­ness that can seize an ap­pli­ca­tion architecture.

Object.­create(prototype) takes an ex­ist­ing ob­ject and re­turns a new ob­ject that in­her­its from the ex­ist­ing ob­ject. The ex­ist­ing ob­ject is the new ob­ject’s pro­to­type. Any ob­ject can be the pro­to­type of a new ob­ject. An ob­ject that in­her­its from a pro­to­type can also be the pro­to­type of new­er ob­jects. There is no lim­it on the length of a pro­to­type chain, but it is wise to keep them short.

If there is an at­tempt to ac­cess a miss­ing prop­erty, be­fore re­turn­ing un­de­fined, the sys­tem first goes to the pro­to­type, and then its pro­to­type, and so on. If a same named prop­erty is found in the pro­to­type chain, then that is giv­en as though it had been found in the ob­ject of interest.

When as­sign­ing to an ob­ject, only the top-most ob­ject is changed. No chang­es are made to ob­jects on the pro­to­type chain.

let my_clone = Object.create(my_little_object);
my_clone.bar
my_clone.age
my_clone.age += 1;
my_clone.age
delete my_clone.age;
my_clone.age
1
2
3
4
5
6
7
let my_clone = Object.create(my_little_object);
my_clone.bar  // "a long rod or rigid piece of wood or metal"
my_clone.age                             // 39
my_clone.age += 1;
my_clone.age                             // 40
delete my_clone.age;
my_clone.age                             // 39
copy to clipboard

The most pop­u­lar use of pro­to­types is as a place to store func­tions. Ja­vaS­cript it­self does this. When an ob­ject is made with an ob­ject lit­eral, that ob­ject in­her­its from Object​.​prototype. Sim­i­larly, ar­rays in­herit meth­ods from Array​.​prototype. Num­bers in­herit meth­ods from Number​.​prototype. Strings in­herit meth­ods from String​.​prototype. Even func­tions in­herit meth­ods from Function​.​prototype. The ar­ray and string meth­ods are pret­ty use­ful, but the meth­ods in Object​.​prototype are most­ly useless.

Be­cause we have in­her­i­tance, we now have two types of prop­er­ties : own prop­er­ties that live in the top-most ob­ject, and in­her­ited prop­er­ties that live in the pro­to­type chain. Most of the time they work just the same. Some­times you need to know if the prop­erty is tru­ly the ob­ject’s own. Most ob­jects in­herit a has​Own​Property​(string) func­tion, but un­for­tu­nately, it has a reli­abil­ity haz­ard. It takes a string and re­turns true if the ob­ject con­tains a prop­erty with that name and if the prop­erty is not in­her­ited. Un­for­tu­nately, if the ob­ject has a prop­erty named has​Own​Property, it will be called in­stead of the Object​.​proto​type​.​has​Own​Property meth­od. That could cause a fail­ure or oth­er con­fu­sion. It would have been bet­ter had has​Own​Property been an op­er­a­tor so that the state of an ob­ject could not cause the call to has​Own​Property to fail. It would have been even bet­ter still if there were no in­her­ited prop­er­ties, which would make this trou­ble some meth­od unnecessary.

my_little_object.hasOwnProperty("bar")
my_copy.hasOwnProperty("bar")
my_clone.hasOwnProperty("bar")
my_clone.hasOwnProperty = 7;
my_clone.hasOwnProperty("bar")
1
2
3
4
5
my_little_object.hasOwnProperty("bar")   // true
my_copy.hasOwnProperty("bar")            // true
my_clone.hasOwnProperty("bar")           // false
my_clone.hasOwnProperty = 7;
my_clone.hasOwnProperty("bar")           // EXCEPTION!
copy to clipboard

If you have a prop­erty named has​Own​Property, then you can not use the in­her­ited has​Own​Property meth­od. You will be call­ing the own prop­erty instead.

Object.​prototype.​toString shares the same fail­ure risk. But even when it works, it is a disappointment.

my_clone.toString
1
my_clone.toString  // "[object Object]"
copy to clipboard

You don’t need to be told that your ob­ject is an ob­ject. You prob­ably al­ready knew that.­ You want to be shown what it con­tains. If y­ou want to con­vert an ob­ject into a string, JSON.​strin­gify does a much bet­ter job.

The ad­van­tage of Object​.​create​(prototype) over Object​.​assign​(​Object​.​​create​({}), prot­op­type) is that less mem­ory is used. In most cas­es, the sav­ings are not sig­nif­i­cant. Pro­to­types can add weird­ness with­out add­ing much benefit.

There is also a prob­lem with unin­tended inheri­tance. You might want to use an ob­ject as you would use a hash ta­ble, but the ob­ject in­her­its names like "to​String", "con​struct​or", and oth­er names that might be im­ple­men­ta­tion de­pen­dent. These could po­ten­tially be con­fused with your properties.

For­tu­nately, Object.​create​(null) makes an ob­ject that in­her­its noth­ing. There is no con­fu­sion over in­her­ited prop­er­ties or unin­tende­d in­heri­tance. There is noth­ing in the ob­ject ex­cept what you ex­plic­itly put into it. I am now mak­ing fre­quent use of Object.create(null).

Keys

The Object​.​­keys​(object) func­tion can take all of the names of the own (but not in­her­ited)­ prop­er­ties in an ob­ject and re­turn them as an ar­ray of strings. This al­lows you to use the ar­ray meth­ods to pro­cess the prop­er­ties of an object.

The strings in the ar­ray will be in the or­der in which they were in­sert­ed. If you need them in a dif­fer­ent or­der, you can use the Ar­ray sort method.

Freeze

Object​.​­freeze​(object) can take an ob­ject and freeze it, mak­ing it im­mu­table. Im­mu­ta­bil­ity can lead to more reli­able sys­tems. Wun­ce an ob­ject is con­struct­ed to your lik­ing, it can be­froz­en, which guar­an­tees that it can­not be dam­aged or tam­pered with. This is not a deep freeze.­Only the top lev­el ob­ject is frozen.

Im­mu­table ob­jects might some­day have valu­able per­for­mance char­ac­ter­is­tics. If it is known that an ob­ject can nev­er change, then a pow­er­ful set of opti­mi­za­tions be­come avail­able to the lan­guage implementation.

Im­mu­table ob­jects have ex­cel­lent se­cu­rity prop­er­ties. This is im­por­tant be­cause cur­rent in­dus­try prac­tices en­cour­age the in­stal­la­tion of un­trust­worthy code into our sys­tems. Im­mu­ta­bil­ity might some­day help that to be­come a safe prac­tice. With im­mu­ta­bil­ity, we can give ob­jects good in­ter­faces with which to de­fend themselves.

Object​.​­freeze​(object) and the const state­ment do two very dif­fer­ent things. Ob­ject​.​­freeze op­er­ates on val­ues. const op­er­ates on vari­ables. If you put a mu­table ob­ject in a const vari­able, you can still mu­tate the ob­ject, but you can­not re­place the ob­ject with a dif­fer­­­ent ­value. If you put an im­mu­table ob­ject in an­or­di­nary vari­able, you can not change the ob­ject, but you can as­sign a dif­fer­ent val­ue to the variable.

Object.freeze(my_copy);
const my_little_constant = my_little_object;

my_little_constant.foo = 7;
my_little_constant = 7;
my_copy.foo = 7;
my_copy = 7;
1
2
3
4
5
6
7
Object.freeze(my_copy);
const my_little_constant = my_little_object;

my_little_constant.foo = 7;              // allowed
my_little_constant = 7;                  // SYNTAX ERROR!
my_copy.foo = 7;                         // EXCEPTION!
my_copy = 7;                             // allowed
copy to clipboard
Prototypes and Freezing Do Not Mix

Wun use of pro­to­types is the crea­tion of light cop­ies of ob­jects. We might have an ob­ject full of data. We want an­oth­er ob­ject that is the same but with wun of the prop­er­ties changed. We can do that with Ob­ject​.​create, as we have seen. This will save some time on the crea­tion of the new ob­ject, but the re­triev­al of the prop­er­ties might cost a lit­tle more be­cause of look­ing thru the pro­to­type chain.

Un­for­tu­nate­ly, this does not work if the pro­to­type is fro­zen. If a prop­er­ty in a pro­to­type is im­muta­ble, then the in­stanc­es can not have their own ver­sions of that prop­er­ty. In some func­tion­al pro­gram­ming styles we want all ob­jects to be fro­zen for the reli­a­bil­ity ben­e­fits that come from im­mu­ta­bil­ity. So in­stead of mak­ing a copy to re­al­ize a change,it would have been nice if we could make an in­stance that in­her­its from the fro­zen pro­to­type, up­date the in­stance, and then freeze the in­stance. But that just does not work. Up­dat­ing the in­stance rais­es an ex­cep­tion. It also slows down the in­ser­tion of new prop­er­ties in gen­er­al. When­ev­er a new prop­er­ty is in­sert­ed, the whole pro­to­type chain must be searched for that key to de­ter­mine that there is not an im­muta­ble prop­er­ty in an an­ces­tor. This search can be avoid­ed by us­ing Object​.​­cre­ate​(null) to make your objects.

WeakMap

Wun of the de­sign er­rors in JavaS­cript is that the names of prop­er­ties in ob­jects must be strings. There are situa­tions when you want to use an ob­ject or an ar­ray as a key. Un­for­tu­nately, Ja­vaS­cript ob­jects do the wrong thing in that case, con­vert­ing the key ob­ject into a key string us­ing the to­String meth­od, which we al­ready saw is a disappointment.

In­stead of giv­ing us an ob­ject that works cor­rectly for all keys, JavaS­cript gives us a sec­ond type of ob­ject called a Weak­Map that al­lows ob­jects as keys, but not strings, and with a com­plete­ly dif­fer­ent interface.

ObjectWeakMap
object = Object.create(null);
weakmap = new WeakMap();
object[key]
weakmap.get(key)
object[key] = value;
weakmap.set(key, value);
delete object[key];
weakmap.delete(key);

It does not make sense that these two things which do the same thing are so syn­tac­ti­cally dif­fer­ent.­It also does not make sense that these two things are not wun thing. In­stead of a thing that al­lows only strings as keys and an­other thing that al­lows only ob­jects as keys, there should have been a sin­gle thing that al­lows strings and ob­jects as keys.

In spite of that, Weak­Map is bril­liant. Here are two ex­am­ples of its use.

We want to put a se­cret prop­erty on an ob­ject. In or­der to ac­cess the se­cret prop­erty, you need ac­cess to the ob­ject and to the se­cret key. You can not ac­cess the prop­erty un­less you have both. We can do this with a Weak­Map. We treat the Weak­Map as the se­cret key.

const secret_key = new WeakMap();
secret_key.set(object, secret);

secret = secret_key.get(object);
1
2
3
4
const secret_key = new WeakMap();
secret_key.set(object, secret);

secret = secret_key.get(object);
copy to clipboard

You must have ac­cess to both the ob­ject and the se­cret key in or­der to re­cover the se­cret. A nice thing about this tech­nique is that we can ef­fec­tively add se­cret prop­er­ties to fro­zen objects.

We want to be able to give an ob­ject to some code that might do some­thing use­ful for us, like cat­a­log it or store it for lat­er re­triev­al, but we do not want to also give that code the ca­pa­bil­ity to al­ter the ob­ject or call any of its meth­ods.­In the real world, we want to al­low a va­let to park the car, but not emp­ty the glove box and trunk or sell the car. The hon­or sys­tem can work for peo­ple in the real world, but not for code in a com­puter network.

So we make a de­vice called a seal­er. We give an ob­ject to a seal­er and it re­turns a box that can not be opened. That box can be giv­en to a va­let. To re­cover the orig­i­nal ob­ject, give the box to the match­ing un­seal­er. We can eas­ily make these func­tions us­ing WeakMap.

function sealer_factory() {
  const weakmap = new WeakMap();
  return {
    sealer(object) {
      const box = Object.freeze(Object.create(null));
      weakmap.set(box, object);
      return box;
    },
    unsealer(box) {
      return weakmap.get(box);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
function sealer_factory() {
    const weakmap = new WeakMap();
    return {
        sealer(object) {
            const box = Object.freeze(Object.create(null));
            weakmap.set(box, object);
            return box;
        },
        unsealer(box) {
            return weakmap.get(box);
        }
    };
}
copy to clipboard

The weak­Map does not al­low in­spec­tion of its con­tents. You can not get a val­ue from it un­less you also hold the key. Weak­Map in­ter­acts well with Ja­vaS­cript’s gar­bage col­lec­tor. If there are no cop­ies of a key still in exis­tence, then that key’s prop­erty in the weak­map is au­to­mat­i­cally de­leted. This can help avoid mem­ory leaks.

Ja­vaS­cript also has a sim­i­lar thing called Map, but Map does not have the se­cu­rity and mem­ory leak pro­tec­tion that Weak­Map has. And whilst Weak­Map is a ter­rible name, Map is even more con­fus­ing. It has no con­nec­tion to the Ar­ray map meth­od, and it could be con­fused with a car­tog­ra­phy func­tion. That is why I do not rec­om­mend Map. But I like Weak­Map and the Ar­ray map function.

Ja­vaS­cript has a thing called Sym­bol that can do wun of the things that Weak­Map can do. I do not rec­om­mend Sym­bol be­cause it is un­nec­es­sary. I am look­ing to sim­plify by elim­i­nat­ing the ex­ces­sive fea­tures that I don’t need.


Chapitre 9
How strings work

Not fair ! Not fair ! It isn’t fair, my precious, is it, to ask us what it’s got in its nasty little pocketses ?

Gollum

Computers are good at manipulating bit patterns. Humans are not. Strings bridge the gap between computers and humans. The mapping of characters onto integers was wun of the important advances in the development of digital computers. It was the first important step forward in the development of user interfaces.

We do not know why we call this type string. Why do we not call it text instead ? No wun knows. A Java­Script string does not resemble a piece of string. We can talk about a string of characters, but we can as easily talk about a string of statements, or a string of bits, or even a string of failures. In the real world, we do not concatenate strings. We tie strings.

Foundation

A string is an immutable array of 16 bit unsigned integers ranging from 0 thru 65535. A string may be made with the String.fromCharCode function, which takes any number of numbers. The elements of a string can be accessed with charCodeAt. Elements may not be changed because strings are always frozen. Strings, like arrays, have a length property.

function sealer_factory() {
  const weakmap = new WeakMap();
  return {
    sealer(object) {
      const box = Object.freeze(Object.create(null));
      weakmap.set(box, object);
      return box;
    },
    unsealer(box) {
      return weakmap.get(box);
    }
  };
}
1
2
3
4
5
const my_little_array = [99, 111, 114, 110];
const my_little_string = String.fromCharCode(...my_little_array);
my_little_string.charCodeAt(0) === 99    // true
my_little_string.length                  // 4
typeof my_little_string                  // "string"
copy to clipboard

The bracket notation can be used to obtainin dividual values from a string, but it does not deliver a number as an array would. Instead it returns a new string with a length of 1 whose content is the number.

function sealer_factory() {
  const weakmap = new WeakMap();
  return {
    sealer(object) {
      const box = Object.freeze(Object.create(null));
      weakmap.set(box, object);
      return box;
    },
    unsealer(box) {
      return weakmap.get(box);
    }
  };
}
1
2
my_little_string[0] === my_little_array[0]      // false
my_little_string[0] === String.fromCharCode(99) // true
copy to clipboard

String.prototype contains methods that act on strings. The concat and slice methods work very much like the array methods. But the indexOf method works very differently. The argument to indexOf is a string, not a number. It attempts to match all of the elements in the argument in sequence with elements in the first string.

function sealer_factory() {
  const weakmap = new WeakMap();
  return {
    sealer(object) {
      const box = Object.freeze(Object.create(null));
      weakmap.set(box, object);
      return box;
    },
    unsealer(box) {
      return weakmap.get(box);
    }
  };
}
1
2
my_little_string.indexOf(String.fromCharCode(111, 114))  // 1
my_little_string.indexOf(String.fromCharCode(111, 110))  // -1
copy to clipboard

Strings containing similar contents are considered to be equal by the === operator. Similar arrays are only equal when they are the same array.

function sealer_factory() {
  const weakmap = new WeakMap();
  return {
    sealer(object) {
      const box = Object.freeze(Object.create(null));
      weakmap.set(box, object);
      return box;
    },
    unsealer(box) {
      return weakmap.get(box);
    }
  };
}
1
2
3
my_little_array === my_little_array      // true
my_little_array === [99, 111, 114, 110]  //false
my_little_string === String.fromCharCode(99, 111, 114, 110) // true
copy to clipboard

The equality of strings is a very powerful feature. It eliminates the need for a symbol type because similar strings are considered to be the same object. This is not true in lesser languages like Java.

Unicode

Java­Script allows all 65536 of the 16 bit patterns as elements of strings. However, the usual practice is to consider each element to be a character, and the Unicode standard determines the encoding of characters. Java­Script has a lot of syntactic and methodic support for Unicode. The Unicode standard declares that some codes are not characters and should not be used. Java­Script does not care. It allows all 16 bit codes. If you intended to have your systems interact with systems that are written in lesser languages, then you should not abuse Unicode.

String literals are written by surrounding zero or more Unicode characters with "double quote. ('single quote can also be used, but is not recommended because it is unnecessary.) Each character is represented as a 16 bit element.

The +plus sign operator is used to do concatenation. We have already seen that + is also used to do addition. If you intend to do concatenation, then you need to assure that at least wun of the operands is a string. Wun way to do that is to make wun of the operands a string literal. Another way is to pass a value to the String function.

function sealer_factory() {
  const weakmap = new WeakMap();
  return {
    sealer(object) {
      const box = Object.freeze(Object.create(null));
      weakmap.set(box, object);
      return box;
    },
    unsealer(box) {
      return weakmap.get(box);
    }
  };
}
1
2
3
4
5
my_little_string === "corn"              // true
"uni" + my_little_string                 // "Unicorn"
3+4                                      // 7
String(3) + 4                            // 34
3 + String(4)                            // 34
copy to clipboard

A string literal must fit entirely on wun line. The \backslash is used as an escapement character that allows for including "double quote, \backslash, and line break in string literals.

Brackets can be used to retrieve characters from a string. The representation of a characteris a string with a length of 1. There is no character type. A charactercanbe represented as a number or a string.

Many languages do have a character type, and it is usually abbreviated as char, but there does not seem to be a standard pronounciation. I have heard it spoken as car, care, chair, char, and share.

All strings are frozen when they are created. Wunce made, a string can not be changed. Smaller pieces can be copied from strings. Each piece is a new string. New strings can be made by concatenating strings together.

function sealer_factory() {
  const weakmap = new WeakMap();
  return {
    sealer(object) {
      const box = Object.freeze(Object.create(null));
      weakmap.set(box, object);
      return box;
    },
    unsealer(box) {
      return weakmap.get(box);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const mess = "monster";
const the_four = "uni"
                 + my_little_string
                 + " rainbow butterfly "
                 + mess;
// the_four is "unicorn rainbow butterfly monster"

const first = the_four.slice(0, 7);      // first is "unicorn"
const last = the_four.slice(-7);         // last is "monster"
const parts = the_four.split(" ");       // part is [
                                         //   "unicorn"
                                         //   "rainbow"
                                         //   "butterfly"
                                         //   "monster"
                                         // ]
parts[2][0] === "b"                      // true
"*".repeat(5)                            // "*****"
parts[1].padStart(10, "/")               // "///rainbow"
copy to clipboard
More Unicode

Unicode’s original goal was to represent all of the world’s living languages in 16 bits. Its charter later changed to represent all of the world’s languages in 21 bits. Unfortunately, Java­Script was designed during Unicode’s 16 bit phase.

Unicode takes Java­Script’s character and breaks it into two new terms : code unit and code point. A code unit is wun of those 16 bit characters. A code point is the number that corresponds to a character. A code point is formed from 1 or more code units.

Unicode defines 1,114,112 code points, divided into 17 planes of 65,536 code points. The original plane is now called the Basic Multilingual Plane (BMP). The remaining 16 planes are the supplementary planes. Java­Script can easily use the characters in theBMP because that is where a code point can be identified with a single code unit. The supplemental characters are more difficult.

Java­Script accesses the supplemental characters by use of surrogate pairs. A surrogate pair is formed by two special code units. There are 1024 high surrogate code units, and 1024 low surrogate code units. The high surrogate code units have lower codes than the low surrogate code units.

0xD800 thru 0xDBFFhigh surrogate code units
0xDC00 thru 0xDFFFlow surrogate code units

When properly paired, each surrogate code unit contributes 10 bits to form a 20 bit offset, to which 65,536 is added to form the code point. (The addition was to eliminate confusion caused by having two different sequences of code units that could produce the same code point. I think this solution caused more confusion than it avoided. A simpler solution would have been to make it illegal to use a surrogate pair to represent a code point in the BMP. This would have yielded a 20 bit character set instead of a 21 bit character set.)

Consider the code point U+1F4A9 (or 128169 in decimal). We subtract 65,536 which yields 62,633 or 0xF4A9. The high 10 bits are 0x03D. The low 10 bits are 0x0A9. We add 0xD800 to the high bits, and 0xDC00 to the low bits, and we get a surrogate pair. So Java­Script stores U+1F4A9 as U+D83D U+DCA9. From Java­Script’s point of view, U+1F4A9 is two 16 bit characters. It will display as a single character if the operating system is competent. Java­Script is mostly unaware of the supplemental planes, butit is not overtly hostile to them.

There are two ways to write the code point U+1F4A9 with escapement in a string literal.

Douglas Crockford, How Java­Script Works, 2018.