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
. Boolean 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
The typeof
operator returns "boolean"
when its operand is true
or false
.
{ equal sign equal sign equal sign | equal |
!== exclamation point equal sign equal sign | not equal |
< less than | less than |
<= less than equal sign | less than or equal |
> greater than | greater than |
>= greater than equal sign | greater 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
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 |
===
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. JavaScript does not prohibit mixing types in that way, so you need to bring your own discipline.
JavaScript 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.
JavaScript 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 :
the condition position of an
if
statementthe condition position of a
while
statementthe condition position of a
for
statementthe condition position of a
do
statementthe operand of the
!
exclamation point operatorboth operands of the
&&
ampersand ampersand operatorboth operands of the
||
vertical bar vertical baroperatorthe first operand of the
?
questionmark:
colon ternary operatorthe return value of the function argument to the
Array
methodsfilter
,find
,findIndex
,indexOf
In a well designed language, only boolean values would be allowed in those positions. JavaScript 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
false
null
undefined
""
(the empty string)0
NaN
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 JavaScript.
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.
JavaScript 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 JavaScript. 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 JavaScript had done that too.
I recommend that you write programs as though JavaScript had been designed correctly. Use booleans in all conditions. If you write in the style of a better language, you will write better programs.
The logical operators are also subject to boolishness.
! exclamation point | logical not | If the operand is truthy then the result is false . If the operand is falsy then the result is true . |
&& ampersand ampersand | logical and | If 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 |
|| vertical bar vertical bar | logical or | If 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 |
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,
1 | !!p === p |
That is only true in JavaScript 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)
1 2 3 4 | !(a === b) === (a !== b) !(a <= b) === (a > b) !(a > b) === (a <= b) !(a >= b) === (a < b) |
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,
NaN < 7
!(7 < NaN) === 7 >= NaN
1 2 3 | 7 < NaN // false NaN < 7 // false !(7 < NaN) === 7 >= NaN // false |
It might make sense if NaN
were smaller than all other numbers or if comparing a number to NaN
would raise an exception. JavaScript 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
1 2 | !(p && q) === !p || !q !(p || q) === !p && !q |
The laws are sound in JavaScript as long as p
and q
were not corrupted with nonsense. Boolish calues should be avoided. Use booleans
Everything is numbered here.
The Controller of Planet X
The monster is zero.
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. JavaScript failed to include arrays in its first release. That omission was barely noticed because JavaScript’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. JavaScript’s arrays are really objects. In today’s JavaScript, arrays slightly differ from objects in four ways :
Arrays have a magical length property. An array’s length is not necessarily the number of elements in the array. It is instead the highest integer ordinal plus
1
. This supports the pretense that JavaScript’s arrays are truly arrays, allowing a JavaScript array to be processed using the same archaicfor
statement that you might find in a half century old C program.Arrays inherit from
Array.prototype
which contains a much better collection of methods thanObject.prototype
.Arrays are produced using array literals, not objec tliterals. Array literals are syntactically muchs impler :
[
left bracket and]
right bracket surrounding zero or more expressions separated with,
comma.JSON
treats arrays and objects very differently even though JavaScript mostly treats them as the same.
JavaScript 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.
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 |
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 JavaScript 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
A well written program in a well designed language should not care if arrays start at 0
or 1
. I would not accuse JavaScript of being a well designed language, but it is a significant improvement over
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] | zeroth | 0th |
[1] | wunth | 1th |
[2] | twoth | 2th |
[3] | threeth | 3th |
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.
There are two ways to make a new array.
array literal
new Array(integer)
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 |
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.
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.
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 |
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
JavaScript 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
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. JavaScript has many bottom values. Wun of those should have been used
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.
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.
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 |
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
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 |
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.
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 |
Now the add
function sees
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 |
The function is called wun fewer time, and there is no error possible in selecting the wrong initial reduction value.
The thing that JavaScript 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.
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 |
Wun of the most common operations on arrays is to do something to each element. Historically this was done with the for statement. JavaScript 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.
JavaScript 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.
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"] |
Its default comparison function wants to arrange values as strings, even if they are numbers. For example,
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] |
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) :
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; } |
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. JavaScript 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.
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) { |
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.
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) { |
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.
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("."); }); |
Compare each pair of values until finding a mismatch. If there is no mismatch then the two values are equal.
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; } |
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.)
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 ); }; } |
Example :
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"}
// ] |
The concat
method takes two or more arrays and concatenates them together to make a new array.
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"] |
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.
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" |
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.
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"] |
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.
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"] |
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
Have gratitude for the things you’re discarding. By giving gratitude, you’re giving closure to the relationship with that object, and by doing so, it becomes a lot easier to let go.
Marie Kondo
JavaScript overloads the word object. This is a language in which everything (except the two bottom values, null
and undefined
) is an object. But usually, especially in this chapter, object means something more specific.
JavaScript’s primary data structure is called object. An object is a container of properties (or members). Each property has a name and a value. The name is a string. The value can be of any type. In other languages, this type of object might be called a hash table, map, record, struct, associative array, dictionary, or in some very rude languages, a dict.
A new object can be created with an object literal. An object literal creates a value that can be stored in a variable or object or array, or passed to a function, or returned from a function.
An object literal is delimited by {
left brace and }
right brace. Nestled inside can be zero or more properties, separated with ,
comma. A property can be :
- A string followed by
:
colon followed by an expression. The property’s name is the string. The property’s value is the value of the expression. - A name followed by
:
colon followed by an expression. The property’s name is the name converted into a string. The property’s value is the value of the expression. - A name. The property’s name is the name converted into a string. The property’s value is the value of the variable or parameter having the same name.
- A name followed by a parameter list wrapped in
(
left paren and)
right paren followed by a function body wrapped in{
left brace and}
right brace. This is a contraction for a name followed by a colon followed by a function expression. This allows for omitting: function
which, given the loss of clarity, hardly seems worth it.
So, for example,
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."; } }; |
We can access a property of an object by using the dot notation with a name.
1 | my_little_object.foo === my_little_object.bar // true |
We can also access a property of an object using the bracket notation. We can use the bracket notation to access a property whose name is not a valid identifier. We can also use the bracket notation to access computed property names. The expression in the brackets is evaluated and converted into a string if necessary.
1 | my_little_object["0/0"] === 0 // true |
If we ask for a name with the dot or the bracket that cannot be found in the object, then the bottom value undefined is provided instead. Asking for a nonexistent or missing property is not considered an error. It is a normal operation that produces undefined.
my_little_object[0]
1 2 | my_little_object.rainbow // undefined my_little_object[0] // undefined |
New properties may be added to an object by assignment. The values of existing properties may also be replaced by assignment.
my_little_object.foo = "slightly frilly or fancy";
1 2 | my_little_object.age = 39; my_little_object.foo = "slightly frilly or fancy"; |
I recommend not storing undefined in objects. JavaScript allows it, and correctly gives back the undefined value, but it is the same undefined that means that the property is missing. This is a source of confusion that can easily be avoided. I wish that storing undefined would cause the property to be removed, but it does not. A property can be removed by the delete operator.
1 | delete my_little_object["0/0"]; |
Sophisticated data structures can be built out of objects because references to objects can be stored in objects. All sorts of graphs and cyclic structures can be constructed. There is no limit on the depth of nesting, but don’t go crazy.
When the typeof
opererator is given an object, it gives the string "object"
.
1 | typeof my_little_object === "object" // true |
The matching of keys is case sensitive. So my_little_object.cat
is not the same property as my_little_object.Cat
or my_little_object.CAT
. The ===
operator determines the string matching of property names.
The Object.assign
function can copy the proper ties from an object to another. You can make a copy of an object by assigning to an empty 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 |
An object can be assigned material from many objects. In this way, a complex object can be constructed by the assembling of material from simpler objects.
In JavaScript, an object can be made that inherits from another object. This is a very different style of inheritance than is practiced in languages that have tightly coupled programmatic structures like classes. In JavaScript, it is the data that is coupled, which can significantly reduce the brittleness that can seize an application architecture.
Object.create(prototype)
takes an existing object and returns a new object that inherits from the existing object. The existing object is the new object’s prototype. Any object can be the prototype of a new object. An object that inherits from a prototype can also be the prototype of newer objects. There is no limit on the length of a prototype chain, but it is wise to keep them short.
If there is an attempt to access a missing property, before returning undefined
, the system first goes to the prototype, and then its prototype, and so on. If a same named property is found in the prototype chain, then that is given as though it had been found in the object of interest.
When assigning to an object, only the top-most object is changed. No changes are made to objects on the prototype chain.
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 |
The most popular use of prototypes is as a place to store functions. JavaScript itself does this. When an object is made with an object literal, that object inherits from Object.prototype
. Similarly, arrays inherit methods from Array.prototype
. Numbers inherit methods from Number.prototype
. Strings inherit methods from String.prototype
. Even functions inherit methods from Function.prototype
. The array and string methods are pretty useful, but the methods in Object.prototype
are mostly useless.
Because we have inheritance, we now have two types of properties : own properties that live in the top-most object, and inherited properties that live in the prototype chain. Most of the time they work just the same. Sometimes you need to know if the property is truly the object’s own. Most objects inherit a hasOwnProperty(string)
function, but unfortunately, it has a reliability hazard. It takes a string and returns true
if the object contains a property with that name and if the property is not inherited. Unfortunately, if the object has a property named hasOwnProperty
, it will be called instead of the Object.prototype.hasOwnProperty
method. That could cause a failure or other confusion. It would have been better had hasOwnProperty
been an operator so that the state of an object could not cause the call to hasOwnProperty
to fail. It would have been even better still if there were no inherited properties, which would make this trouble some method unnecessary.
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! |
If you have a property named hasOwnProperty
, then you can not use the inherited hasOwnProperty
method. You will be calling the own property instead.
Object.prototype.toString
shares the same failure risk. But even when it works, it is a disappointment.
1 | my_clone.toString // "[object Object]" |
You don’t need to be told that your object is an object. You probably already knew that. You want to be shown what it contains. If you want to convert an object into a string, JSON.stringify
does a much better job.
The advantage of Object.create(prototype)
over Object.assign(Object.create({})
, protoptype) is that less memory is used. In most cases, the savings are not significant. Prototypes can add weirdness without adding much benefit.
There is also a problem with unintended inheritance. You might want to use an object as you would use a hash table, but the object inherits names like "toString"
, "constructor"
, and other names that might be implementation dependent. These could potentially be confused with your properties.
Fortunately, Object.create(null)
makes an object that inherits nothing. There is no confusion over inherited properties or unintended inheritance. There is nothing in the object except what you explicitly put into it. I am now making frequent use of Object.create(null)
.
The Object.keys(object)
function can take all of the names of the own (but not inherited) properties in an object and return them as an array of strings. This allows you to use the array methods to process the properties of an object.
The strings in the array will be in the order in which they were inserted. If you need them in a different order, you can use the Array sort method.
Object.freeze(object)
can take an object and freeze it, making it immutable. Immutability can lead to more reliable systems. Wunce an object is constructed to your liking, it can befrozen, which guarantees that it cannot be damaged or tampered with. This is not a deep freeze.Only the top level object is frozen.
Immutable objects might someday have valuable performance characteristics. If it is known that an object can never change, then a powerful set of optimizations become available to the language implementation.
Immutable objects have excellent security properties. This is important because current industry practices encourage the installation of untrustworthy code into our systems. Immutability might someday help that to become a safe practice. With immutability, we can give objects good interfaces with which to defend themselves.
Object.freeze(object)
and the const
statement do two very different things. Object.freeze
operates on values. const
operates on variables. If you put a mutable object in a const
variable, you can still mutate the object, but you cannot replace the object with a different value. If you put an immutable object in anordinary variable, you can not change the object, but you can assign a different value to the variable.
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 |
Wun use of prototypes is the creation of light copies of objects. We might have an object full of data. We want another object that is the same but with wun of the properties changed. We can do that with Object.create
, as we have seen. This will save some time on the creation of the new object, but the retrieval of the properties might cost a little more because of looking thru the prototype chain.
Unfortunately, this does not work if the prototype is frozen. If a property in a prototype is immutable, then the instances can not have their own versions of that property. In some functional programming styles we want all objects to be frozen for the reliability benefits that come from immutability. So instead of making a copy to realize a change,it would have been nice if we could make an instance that inherits from the frozen prototype, update the instance, and then freeze the instance. But that just does not work. Updating the instance raises an exception. It also slows down the insertion of new properties in general. Whenever a new property is inserted, the whole prototype chain must be searched for that key to determine that there is not an immutable property in an ancestor. This search can be avoided by using Object.create(null)
to make your objects.
Wun of the design errors in JavaScript is that the names of properties in objects must be strings. There are situations when you want to use an object or an array as a key. Unfortunately, JavaScript objects do the wrong thing in that case, converting the key object into a key string using the toString
method, which we already saw is a disappointment.
Instead of giving us an object that works correctly for all keys, JavaScript gives us a second type of object called a WeakMap that allows objects as keys, but not strings, and with a completely different interface.
Object | WeakMap |
---|---|
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 syntactically different.It also does not make sense that these two things are not wun thing. Instead of a thing that allows only strings as keys and another thing that allows only objects as keys, there should have been a single thing that allows strings and objects as keys.
In spite of that, WeakMap
is brilliant. Here are two examples of its use.
We want to put a secret property on an object. In order to access the secret property, you need access to the object and to the secret key. You can not access the property unless you have both. We can do this with a WeakMap
. We treat the WeakMap
as the secret key.
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); |
You must have access to both the object and the secret key in order to recover the secret. A nice thing about this technique is that we can effectively add secret properties to frozen objects.
We want to be able to give an object to some code that might do something useful for us, like catalog it or store it for later retrieval, but we do not want to also give that code the capability to alter the object or call any of its methods.In the real world, we want to allow a valet to park the car, but not empty the glove box and trunk or sell the car. The honor system can work for people in the real world, but not for code in a computer network.
So we make a device called a sealer. We give an object to a sealer and it returns a box that can not be opened. That box can be given to a valet. To recover the original object, give the box to the matching unsealer. We can easily make these functions using WeakMap
.
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); } }; } |
The weakMap
does not allow inspection of its contents. You can not get a value from it unless you also hold the key. WeakMap interacts well with JavaScript’s garbage collector. If there are no copies of a key still in existence, then that key’s property in the weakmap is automatically deleted. This can help avoid memory leaks.
JavaScript also has a similar thing called Map, but Map does not have the security and memory leak protection that WeakMap has. And whilst WeakMap is a terrible name, Map is even more confusing. It has no connection to the Array map
method, and it could be confused with a cartography function. That is why I do not recommend Map. But I like WeakMap and the Array map
function.
JavaScript has a thing called Symbol that can do wun of the things that WeakMap can do. I do not recommend Symbol because it is unnecessary. I am looking to simplify by eliminating the excessive features that I don’t need.
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 JavaScript 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.
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.
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" |
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.
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 |
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.
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 |
Strings containing similar contents are considered to be equal by the ===
operator. Similar arrays are only equal when they are the same array.
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 |
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.
JavaScript 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. JavaScript 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. JavaScript 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.
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 |
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.
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" |
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, JavaScript was designed during Unicode’s 16 bit phase.
Unicode takes JavaScript’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. JavaScript 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.
JavaScript 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 0xDBFF | high surrogate code units |
0xDC00 thru 0xDFFF | low 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 JavaScript stores U+1F4A9 as U+D83D U+DCA9. From JavaScript’s point of view, U+1F4A9 is two 16 bit characters. It will display as a single character if the operating system is competent. JavaScript 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,