The book “Expressive JavaScript. Modern web programming. 3rd edition

image Hello, habrozhiteli! This book will allow you to dive deep into the topic, learn how to write beautiful and effective code. You will learn about syntax, arrow and asynchronous functions, iterator, pattern strings, and block scope.



Marein Haverbeke - practitioner. Gain experience and learn the language through a multitude of examples through exercises and training projects. First, you will become familiar with the structure of the JavaScript language, managing structures, functions and data structures, then study error handling and bug fixing, modularity and asynchronous programming, and then move on to browser programming.



Review this book



This book is divided into three large parts. The first 12 chapters discuss the JavaScript language. The next seven chapters are about browsers and how JavaScript is used to program them. Finally, two chapters are devoted to Node.js, another JavaScript programming environment.



Throughout the book, you will meet five chapters of projects that describe larger examples of programs so that you can feel the taste of real programming. In the order of their appearance, we will work on creating a delivery robot, a programming language, a gaming platform, a raster graphics editor, and a dynamic site.



The language part of the book begins with four chapters that will introduce you to the basic structure of the JavaScript language. You will learn about control structures (such as the while keyword, which you already saw in the introduction), functions (writing your own building blocks), and data structures. After that, you can write the simplest programs. Further, chapters 5 and 6 describe how to use functions and objects to write more abstract code and control its complexity.



After the chapter of the first project, the language part of the book will be continued - the following chapters are devoted to detecting and correcting errors, regular expressions (an important tool for working with text), modularity (another defense against complexity) and asynchronous programming (working with events that last for some time). The first part of the book is completed by the chapter of the second draft.



The second part, in chapters 13 through 19, describes the tools that a JavaScript-enabled browser has access to. You will learn how to display elements on the screen (chapters 14 and 17), respond to user input (chapter 15) and share them over the network (chapter 18). This part also contains two chapters of projects.



After that, Node.js is described in chapter 20, and a small site is created in chapter 21 using the specified tool.



Excerpt. Summation with reduce



Another common thing that is often done with arrays is calculating a single value based on them. A special case of this is the example we have already used with summation of a set of numbers. Another example is finding a font containing the largest number of characters.



A higher order operation that implements this pattern is called shorthand (sometimes also called convolution). This operation builds the value by repeatedly getting one element from the array and combining it with the current value. When summing the numbers, we start from zero and then add each subsequent element to the sum.



The parameters of the reduce function, in addition to the array, are a combining function and an initial value. This function is a bit more complicated than filter and map, so take a closer look at it:



function reduce(array, combine, start) { let current = start; for (let element of array) { current = combine(current, element); } return current; } console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0)); // → 10
      
      





The standard method for working with reduce arrays, which, of course, corresponds to this function, has additional convenience. If the array contains at least one element, you can omit the start argument. The method will select the first element of the array as the initial value and begin reduction from the second element.



 console.log([1, 2, 3, 4].reduce((a, b) => a + b)); // → 10
      
      





To use reduce (twice) to find the font with the most characters, we can write something like this:



 function characterCount(script) { return script.ranges.reduce((count, [from, to]) => { return count + (to — from); }, 0); } console.log(SCRIPTS.reduce((a, b) => { return characterCount(a) < characterCount(b) ? b : a; })); // → {name: "Han", ...}
      
      





The characterCount function reduces the ranges assigned to this font by calculating the sum of their sizes. Note the use of destructuring in the parameter list of the reduction function. Then the second call to reduce uses the previous result to find the largest font, repeatedly comparing the two fonts and returning the larger one.



The Han font has over 89,000 characters assigned to it in the Unicode standard, making it the largest writing system in our data set. Han is a font sometimes used for Chinese, Japanese, and Korean texts. Their languages ​​have many common characters, although they are written differently. The Unicode Consortium (located in the USA) decided to consider such characters as a single recording system in order to save character codes. This is called Han Unification and is still very annoying for some people.



Composability



Let's think: how could we rewrite the previous example (find the largest font) without higher-order functions? The following code is not much worse.



 let biggest = null; for (let script of SCRIPTS) { if (biggest == null || characterCount(biggest) < characterCount(script)) { biggest = script; } } console.log(biggest); // → {name: "Han", ...}
      
      





Several additional bindings appeared, and the program became four lines longer. But this code is still pretty clear.



Higher-order functions begin to be really useful when you need to compose operations. As an example, let's write a code that calculates the average year of creating fonts of living and dead languages ​​in a data set.



 function average(array) { return array.reduce((a, b) => a + b) / array.length; } console.log(Math.round(average( SCRIPTS.filter(s => s.living).map(s => s.year)))); // → 1188 console.log(Math.round(average( SCRIPTS.filter(s => !s.living).map(s => s.year)))); // → 188
      
      





Thus, scripts of dead languages ​​in Unicode are on average older than scripts of living languages.



These are not particularly significant or surprising statistics. But you hopefully agree that the code used to compute it is easy to read. This can be imagined as a conveyor: we start by analyzing all the fonts, filter out the living (or dead), take the years of their creation, calculate the average value and round off the result.



This calculation could also be represented as one large cycle.



 let total = 0, count = 0; for (let script of SCRIPTS) { if (script.living) { total += script.year; count += 1; } } console.log(Math.round(total / count)); // → 1188
      
      





But in this code it is more difficult to understand what and how is calculated. And since the intermediate results are not presented as consistent values, much more work would have to be done to separate something like average into a separate function.



In terms of what the computer actually does, the two approaches are also fundamentally different. The first creates new arrays when filter and map are run, while the second only calculates some numbers, doing less work. Usually you can afford a more easily readable option, but if you have to process very large arrays and do it many times, then a less abstract style can give you additional speed gain.



Strings and character codes



One use of datasets is to determine which font a given piece of text is typed in. Let's look at a program that does this.



Recall that for each font there is an array of character code ranges. Therefore, knowing the character code, we could use the following function to find the corresponding font (if any):



 function characterScript(code) { for (let script of SCRIPTS) { if (script.ranges.some(([from, to]) => { return code >= from && code < to; })) { return script; } } return null; } console.log(characterScript(121)); // → {name: "Latin", ...}
      
      





The some method is another higher order function. It takes a test function and reports if it returns true for any element of the array.



But how do we get character codes as a string?



In Chapter 1, I mentioned that in JavaScript, strings are represented as sequences of 16-bit numbers. These are the so-called code units. Initially, it was assumed that in Unicode the character code will be placed in such a block (which gives a little more than 65,000 characters). When it became clear that this was not enough, many began to object to the need to use more memory to store one character. To solve this problem, the UTF-16 format used in JavaScript strings was invented. In it, the most common characters occupy one 16-bit code unit, and the rest - two code units.



Today it is generally accepted that UTF-16 was a bad idea. It seems to be created to produce errors. You can easily write a program for which the code units and characters are one and the same. And if your native language does not use characters that occupy two code units, this program will work fine. But, as soon as someone tries to use such a program for a less common alphabet, for example, for Chinese characters, it will immediately break. Fortunately, after the emergence of emoticons, two code units began to be used everywhere for character encoding, and the burden of solving such problems was distributed more fairly.



Unfortunately, obvious operations with JavaScript strings, such as getting their length through the length property and accessing their contents using square brackets, deal only with code units.



image



The JavaScript charCodeAt method does not return the full character code, but a code unit. The codePointAt method that appears later returns the full Unicode character. So we could use this to get characters from a string. But the argument passed to codePointAt is still an index in a sequence of code units. Thus, in order to iterate over all the characters in a string, we still need to solve the question of whether one or two code units occupy a character.



In the previous chapter, I mentioned that the for / of loop can also be used for strings. Like codePointAt, this type of loop appeared at a time when programmers clearly realized the problems of UTF-16. When you apply this loop to a string, it gives real characters, not code units.



image



If you have a character (which is a string of one or two code units), then in order to get its code, you can use codePointAt (0).



Text recognising



We have a characterScript function and a way to correctly enumerate characters in a loop. The next step is to count the number of characters belonging to each font. Here we need a counting abstraction:



 function countBy(items, groupName) { let counts = []; for (let item of items) { let name = groupName(item); let known = counts.findIndex(c => c.name == name); if (known == -1) { counts.push({name, count: 1}); } else { counts[known].count++; } } return counts; } console.log(countBy([1, 2, 3, 4, 5], n => n > 2)); // → [{name: false, count: 2}, {name: true, count: 3}]
      
      





The countBy function accepts a collection (everything that can be sorted in a for / of loop) and a function that calculates the group name for a given element. The countBy function returns an array of objects, each of which contains the name of the group and the number of elements found for it.



This function uses another method of working with arrays - findIndex. This method is somewhat similar to indexOf, but instead of searching for a specific value, it finds the first value for which the given function returns true. If the item is not found, findIndex, like indexOf, returns -1.



Using countBy, we can write a function that tells which fonts were used in this piece of text.



image



The function first counts characters by font name, using characterScript to give them a name, and returns the string “none” for characters that do not belong to any font. Calling filter removes the “none” entry from the resulting array, since we are not interested in these characters.



To be able to calculate percentages, we first need to get the total number of characters belonging to the font that we can calculate using the reduce method. If no such characters are found, then the function returns a specific string. Otherwise, it converts the counting results into readable strings using map, and then combines them using join.



Summary



The ability to pass functional values ​​to other functions is a very useful aspect of JavaScript. This allows you to create functions that simulate calculations with spaces. Subsequently, when calling such functions in the code, these “gaps” are filled with functional values.



For arrays, there are a number of useful higher order methods. The forEach method can be used to loop through the elements of an array. The filter method returns a new array containing only elements that satisfy the condition of the predicative function. Array conversion by executing a function for each element is done using map. To combine all elements of an array into a single value, you can use reduce. The some method checks to see if any element matches a given predicative function. Finally, the findIndex method finds the position of the first element that matches the predicate.



»More details on the book can be found on the publisher’s website

» Contents

» Excerpt



25% off coupon for hawkers - JavaScript

Upon payment of the paper version of the book, an electronic book is sent by e-mail.



All Articles