Марейн Хавербеке — практик. Получайте опыт и изучайте язык на множестве примеров, выполняя упражнения и учебные проекты. Сначала вы познакомитесь со структурой языка JavaScript, управляющими структурами, функциями и структурами данных, затем изучите обработку ошибок и исправление багов, модульность и асинхронное программирование, после чего перейдете к программированию браузеров.
Обзор этой книги
Эта книга делится на три большие части. В первых 12 главах обсуждается язык JavaScript. Следующие семь глав посвящены браузерам и тому, как JavaScript используется для их программирования. Наконец, две главы посвящены Node.js, еще одной среде для программирования на JavaScript.
На протяжении всей книги вам встретится пять глав проектов, в которых описаны более крупные примеры программ, чтобы вы могли почувствовать вкус настоящего программирования. В порядке их появления мы будем работать над созданием робота доставки, языка программирования, игровой платформы, растрового графического редактора и динамического сайта.
Языковая часть книги начинается с четырех глав, которые познакомят вас с основной структурой языка JavaScript. Вы узнаете, что такое управляющие структуры (такие как ключевое слово while, уже встречавшееся вам во введении), функции (написание собственных строительных блоков) и структуры данных. После этого вы сможете писать простейшие программы. Далее, в главах 5 и 6, описаны способы использования функций и объектов, позволяющие писать более абстрактный код и контролировать его сложность.
После главы первого проекта будет продолжена языковая часть книги — следующие главы посвящены обнаружению и исправлению ошибок, регулярным выражениям (важный инструмент для работы с текстом), модульности (еще одна защита от сложности) и асинхронному программированию (работа с событиями, которые длятся какое-то время). Первую часть книги завершает глава второго проекта.
Во второй части, в главах с 13-й по 19-ю, описаны инструменты, к которым имеет доступ браузер с поддержкой JavaScript. Вы научитесь отображать элементы на экране (главы 14 и 17), реагировать на ввод данных пользователем (глава 15) и обмениваться ими по сети (глава 18). В данной части также содержатся две главы проектов.
После этого в главе 20 описывается Node.js, а в главе 21 создается небольшой сайт с использованием указанного инструмента.
Отрывок. Суммирование с помощью reduce
Еще одна распространенная вещь, которую часто делают с массивами, — вычисление одного значения на их основе. Частный случай этого — уже использованный нами пример с суммированием набора чисел. Другой пример — поиск шрифта, содержащего наибольшее количество символов.
Операция высшего порядка, которая реализует этот шаблон, называется сокращением (иногда ее также называют сверткой). Данная операция строит значение путем многократного получения одного элемента из массива и комбинирования его с текущим значением. При суммировании чисел мы начинаем с нуля и затем прибавляем к сумме каждый следующий элемент.
Параметрами функции reduce, кроме массива, являются комбинирующая функция и начальное значение. Эта функция немного сложнее, чем filter и map, поэтому присмотритесь к ней внимательно:
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
Стандартный метод для работы с массивами reduce, который, конечно же, соответствует этой функции, имеет дополнительное удобство. Если массив содержит хотя бы один элемент, можно не указывать аргумент start. Метод выберет в качестве начального значения первый элемент массива и начнет сокращение со второго элемента.
console.log([1, 2, 3, 4].reduce((a, b) => a + b)); // → 10
Чтобы использовать reduce (дважды) для поиска шрифта с наибольшим количеством символов, мы можем написать нечто вроде этого:
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", ...}
Функция characterCount сокращает диапазоны, назначенные данному шрифту, вычисляя сумму их размеров. Обратите внимание на использование деструктуризации в списке параметров функции сокращения. Затем второй вызов reduce задействует предыдущий результат, чтобы найти самый большой шрифт, многократно сравнивая два шрифта и возвращая больший из них.
Шрифт Han насчитывает более 89 000 символов, назначенных ему в стандарте Unicode, что делает его самой большой системой письма в нашем наборе данных. Han — это шрифт, иногда применяемый для китайских, японских и корейских текстов. В их языках много общих символов, хотя они и пишутся по-разному. Консорциум Unicode (расположенный в США) принял решение рассматривать подобные символы как единую систему записи в целях экономии кодов символов. Это называется объединением Хань и до сих пор очень раздражает некоторых людей.
Компонуемость
Подумаем: как можно было бы переписать предыдущий пример (найти самый большой шрифт) без функций высшего порядка? Следующий код не намного хуже.
let biggest = null; for (let script of SCRIPTS) { if (biggest == null || characterCount(biggest) < characterCount(script)) { biggest = script; } } console.log(biggest); // → {name: "Han", ...}
Появилось несколько дополнительных привязок, и программа стала на четыре строки длиннее. Но этот код все еще весьма понятен.
Функции высшего порядка начинают быть по-настоящему полезны, когда нужно скомпоновать операции. В качестве примера напишем код, который вычисляет средний год создания шрифтов живых и мертвых языков в наборе данных.
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
Таким образом, скрипты мертвых языков в Unicode в среднем старше, чем скрипты живых языков.
Это не особенно значимая или удивительная статистика. Но вы, надеюсь, согласитесь, что код, используемый для ее вычисления, нетрудно читать. Это можно представить себе как конвейер: мы начинаем с анализа всех шрифтов, отфильтровываем живые (или мертвые), берем годы их создания, вычисляем среднее значение и округляем результат.
Данное вычисление также можно было бы представить в виде одного большого цикла.
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
Но в этом коде сложнее понять, что и как вычисляется. А поскольку промежуточные результаты не представлены в виде согласованных значений, пришлось бы проделать гораздо больше работы, чтобы выделить что-нибудь вроде average в отдельную функцию.
С точки зрения того, что на самом деле делает компьютер, эти два подхода также принципиально различаются. Первый создает новые массивы при запуске filter и map, тогда как второй вычисляет только некоторые числа, выполняя меньше работы. Обычно вы можете позволить себе более легко читаемый вариант, но если приходится обрабатывать очень большие массивы и делать это многократно, то менее абстрактный стиль может дать вам дополнительный выигрыш в скорости.
Строки и коды символов
Одно из применений наборов данных — определить, каким шрифтом набран заданный фрагмент текста. Давайте рассмотрим программу, которая это делает.
Вспомним, что для каждого шрифта существует массив диапазонов кодов символов. Поэтому, зная код символа, мы могли бы использовать следующую функцию, чтобы найти соответствующий шрифт (если он есть):
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", ...}
Метод some — это еще одна функция высшего порядка. Он принимает тестовую функцию и сообщает, возвращает ли она true для любого элемента массива.
Но как мы получим коды символов в виде строки?
В главе 1 я упоминал, что в JavaScript строки представляются как последовательности 16-битных чисел. Это так называемые кодовые единицы. Первоначально предполагалось, что в Unicode код символа будет помещаться в таком блоке (что дает чуть больше 65 000 символов). Когда стало ясно, что этого недостаточно, многие стали возражать против необходимости использовать больше памяти для хранения одного символа. Чтобы решить эту проблему, был изобретен формат UTF-16, используемый в строках JavaScript. В нем наиболее распространенные символы занимают одну 16-битную кодовую единицу, а остальные — две кодовые единицы.
Сегодня принято считать, что UTF-16 был плохой идеей. Кажется, он создан, чтобы плодить ошибки. Можно легко написать программу, для которой кодовые единицы и символы — одно и то же. И если для вашего родного языка не используются символы, занимающие две кодовые единицы, эта программа будет нормально работать. Но, как только кто-нибудь попытается использовать такую программу для менее распространенного алфавита, например для китайских иероглифов, она сразу сломается. К счастью, после появления смайликов для кодирования символов стали повсеместно использоваться две кодовые единицы и бремя решения таких проблем распределилось более справедливо.
К сожалению, очевидные операции со строками JavaScript, такие как получение их длины через свойство length и доступ к их содержимому с помощью квадратных скобок, имеют дело только с кодовыми единицами.
Метод JavaScript charCodeAt возвращает не полный код символа, а кодовую единицу. Появившийся позже метод codePointAt возвращает полный символ Unicode. Таким образом, мы могли бы использовать это, чтобы получить символы из строки. Но аргумент, переданный в codePointAt, все еще является индексом в последовательности кодовых единиц. Таким образом, чтобы перебрать все символы в строке, нам все равно нужно решить вопрос о том, занимает символ одну или две кодовые единицы.
В предыдущей главе я упоминал, что цикл for/of можно использовать в том числе и для строк. Подобно codePointAt, этот тип цикла появился в то время, когда программисты четко осознали проблемы UTF-16. Когда вы применяете этот цикл для строки, он дает реальные символы, а не кодовые единицы.
Если у вас есть символ (который представляет собой строку из одной или двух кодовых единиц), то для того, чтобы получить его код, можно использовать codePointAt(0).
Распознавание текста
У нас есть функция characterScript и способ корректного перебора символов в цикле. Следующий шаг — подсчитать количество символов, принадлежащих каждому шрифту. Здесь нам пригодится такая счетная абстракция:
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}]
Функция countBy принимает на вход коллекцию (все, что можно перебрать в цикле for/of) и функцию, вычисляющую имя группы для данного элемента. Функция countBy возвращает массив объектов, каждый из которых содержит имя группы и количество найденных для нее элементов.
В этой функции использован еще один метод работы с массивами — findIndex. Данный метод чем-то похож на indexOf, но вместо поиска конкретного значения он находит первое значение, для которого заданная функция возвращает true. В случае если элемент не найден, findIndex, как и indexOf, возвращает –1.
Используя countBy, мы можем написать функцию, сообщающую, какие шрифты были задействованы в данном фрагменте текста.
Функция сначала подсчитывает символы по имени шрифта, используя characterScript, чтобы назначить им имя, и возвращает строку «none» для символов, которые не относятся ни к какому шрифту. Вызов filter удаляет запись «none» из полученного массива, поскольку эти символы нас не интересуют.
Чтобы иметь возможность вычислять процентные соотношения, нам сначала нужно получить общее количество символов, принадлежащих шрифту, которое мы можем вычислить с помощью метода reduce. Если такие символы не найдены, то функция возвращает конкретную строку. В противном случае она преобразует результаты подсчета в удобно читаемые строки с помощью map, а затем объединяет их с помощью join.
Резюме
Возможность передавать функциональные значения другим функциям является очень полезным аспектом JavaScript. Это позволяет создавать функции, которые моделируют вычисления с «пробелами». Впоследствии при вызове таких функций в коде данные «пробелы» заполняются функциональными значениями.
Для массивов существует ряд полезных методов высшего порядка. Метод forEach можно использовать для циклического перебора элементов массива. Метод filter возвращает новый массив, содержащий только элементы, удовлетворяющие условию предикативной функции. Преобразование массива посредством выполнения функции для каждого элемента выполняется с помощью map. Чтобы объединить все элементы массива в одно значение, можно использовать reduce. Метод some проверяет, соответствует ли какой-либо элемент заданной предикативной функции. Наконец, метод findIndex находит позицию первого элемента, который соответствует предикату.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 25% по купону — JavaScript
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
ссылка на оригинал статьи https://habr.com/ru/company/piter/blog/463465/
Добавить комментарий