КулЛиб - Классная библиотека! Скачать книги бесплатно
Всего книг - 712478 томов
Объем библиотеки - 1400 Гб.
Всего авторов - 274473
Пользователей - 125061

Новое на форуме

Новое в блогах

Впечатления

Влад и мир про Владимиров: Ирландец 2 (Альтернативная история)

Написано хорошо. Но сама тема не моя. Становление мафиози! Не люблю ворьё. Вор на воре сидит и вором погоняет и о ворах книжки сочиняет! Любой вор всегда себя считает жертвой обстоятельств, мол не сам, а жизнь такая! А жизнь кругом такая, потому, что сам ты такой! С арифметикой у автора тоже всё печально, как и у ГГ. Простая задачка. Есть игроки, сдающие определённую сумму для участия в игре и получающие определённое количество фишек. Если в

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).
DXBCKT про Дамиров: Курсант: Назад в СССР (Детективная фантастика)

Месяца 3-4 назад прочел (а вернее прослушал в аудиоверсии) данную книгу - а руки (прокомментировать ее) все никак не доходили)) Ну а вот на выходных, появилось время - за сим, я наконец-таки сподобился это сделать))

С одной стороны - казалось бы вполне «знакомая и местами изьезженная» тема (чуть не сказал - пластинка)) С другой же, именно нюансы порой позволяют отличить очередной «шаблон», от действительно интересной вещи...

В начале

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Стариков: Геополитика: Как это делается (Политика и дипломатия)

Вообще-то если честно, то я даже не собирался брать эту книгу... Однако - отсутствие иного выбора и низкая цена (после 3 или 4-го захода в книжный) все таки "сделали свое черное дело" и книга была куплена))

Не собирался же ее брать изначально поскольку (давным давно до этого) после прочтения одной "явно неудавшейся" книги автора, навсегда зарекся это делать... Но потом до меня все-таки дошло что (это все же) не "очередная злободневная" (читай

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Москаленко: Малой. Книга 3 (Боевая фантастика)

Третья часть делает еще более явный уклон в экзотерику и несмотря на все стсндартные шаблоны Eve-вселенной (базы знаний, нейросети и прочие девайсы) все сводится к очередной "ступени самосознания" и общения "в Астралях")) А уж почти каждодневные "глюки-подключения-беседы" с "проснувшейся планетой" (в виде галлюцинации - в образе симпатичной девчонки) так и вообще...))

В общем герою (лишь формально вникающему в разные железки и нейросети)

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
Влад и мир про Черепанов: Собиратель 4 (Боевая фантастика)

В принципе хорошая РПГ. Читается хорошо.Есть много нелогичности в механике условий, заданных самим же автором. Ну например: Зачем наделять мечи с поглощением душ и забыть об этом. Как у игрока вообще можно отнять душу, если после перерождении он снова с душой в своём теле игрока. Я так и не понял как ГГ не набирал опыта занимаясь ремеслом, особенно когда служба якобы только за репутацию закончилась и групповое перераспределение опыта

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).

Как устроен JavaScript [Дуглас Крокфорд] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

Tlgm: @it_boooks

Tlgm: @it_boooks

Tlgm: @it_boooks

Tlgm: @it_boooks

Дуглас Крокфорд
Как устроен JavaScript
Серия «Для профессионалов»
Перевел с английского Н. Вильчинский
Заведующая редакцией
Ведущий редактор
Литературный редактор
Художественный редактор
Корректоры
Верстка

Ю. Сергиенко
Н. Гринчик
Н. Рощина
С. Заматевская
Е. Павлович, Е. Павлович
Г. Блинов

ББК 32.988.02-018
УДК 004.738.5

Крокфорд Дуглас
К83 Как устроен JavaScript. — СПб.: Питер, 2019. — 304 с. — (Серия «Для профессио­
налов»).
ISBN 978-5-4461-1260-9
Большинство языков программирования выросли из древней парадигмы, порожденной еще во времена
Фортрана. Гуру JavaScript Дуглас Крокфорд выкорчевывает эти засохшие корни, позволяя нам задуматься
над будущим программирования, перейдя на новый уровень понимания требований к Следующему Языку
(The Next Language).
Автор начинает с основ: имен, чисел, логических значений, символов и другой базовой информации. Вы
узнаете не только о проблемах и трудностях работы с типами в JavaScript, но и о том, как их можно обойти.
Затем вы приступите к знакомству со структурами данных и функции, чтобы разобраться с механизмами,
лежащими в их основе, и научитесь использовать функции высшего порядка и объектно-ориентированный
стиль программирования без классов.

16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)
ISBN 978-1949815009 англ.
ISBN 978-5-4461-1260-9

© 2018 Virgule-Solidus LLC
© Перевод на русский язык ООО Издательство «Питер», 2019
© Издание на русском языке, оформление ООО Издательство «Питер», 2019
© Серия «Для профессионалов», 2019

Права на издание получены по соглашению с Douglas Crockford. Все права защищены. Никакая часть данной
книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев
авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может
гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные
ошибки, связанные с использованием книги. Издательство не несет ответственности за доступность материалов,
ссылки на которые вы можете найти в этой книге. На момент подготовки книги к изданию все ссылки на интернетресурсы были действующими.
Изготовлено в России. Изготовитель: ООО «Прогресс книга». Место нахождения и фактический адрес:
194044, Россия, г. Санкт-Петербург, Б. Сампсониевский пр., д. 29А, пом. 52. Тел.: +78127037373.
Дата изготовления: 05.2019. Наименование: книжная продукция. Срок годности: не ограничен.
Налоговая льгота — общероссийский классификатор продукции ОК 034-2014, 58.11.12 — Книги печатные
профессиональные, технические и научные.
Импортер в Беларусь: ООО «ПИТЕР М», 220020, РБ, г. Минск, ул. Тимирязева, д. 121/3, к. 214, тел./факс: 208 80 01.
Подписано в печать 26.04.19. Формат 70×100/16. Бумага офсетная. Усл. п. л. 24,510. Тираж 1500. Заказ 0000.

Tlgm: @it_boooks

Список глав
Капли дождя на розах и усы у котят.
Не Мария Августа фон Трапп
(Maria Augusta von Trapp). Не «Звуки музыки»
[

]

{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":
{"number":

0, "chapter": "Сначала прочитайте меня!"},
1, "chapter": "Как работают имена"},
2, "chapter": "Как работают числа"},
3, "chapter": "Как работают большие целые числа"},
4, "chapter": "Как работают большие числа с плавающей точкой"},
5, "chapter": "Как работают большие рациональные числа"},
6, "chapter": "Как работают булевы значения"},
7, "chapter": "Как работают массивы"},
8, "chapter": "Как работают объекты"},
9, "chapter": "Как работают строки"},
10, "chapter": "Как работают ничтожно малые значения"},
11, "chapter": "Как работают инструкции"},
12, "chapter": "Как работают функции"},
13, "chapter": "Как работают генераторы"},
14, "chapter": "Как работают исключения"},
15, "chapter": "Как работают программы"},
16, "chapter": "Как работает this"},
17, "chapter": "Как работает код без классов"},
18, "chapter": "Как работают концевые вызовы"},
19, "chapter": "Как работает чистота"},
20, "chapter": "Как работает событийное программирование"},
21, "chapter": "Как работает Date"},
22, "chapter": "Как работает JSON"},
23, "chapter": "Как работает тестирование"},
24, "chapter": "Как работает оптимизация"},
25, "chapter": "Как работает транспиляция"},
26, "chapter": "Как работает разбиение на лексемы"},
27, "chapter": "Как работает парсер"},
28, "chapter": "Как работает генерация кода"},
29, "chapter": "Как работает среда выполнения"},
30, "chapter": "Как работают нелепости, или Что такое Wat!"},
31, "chapter": "Как устроена эта книга"}

Tlgm: @it_boooks

Глава 0

Сначала прочитайте меня!
○ ○ ○ ○ ○

Некоторые образы кажутся таинственными и не
оставляют никаких сомнений в том, что являются
результатом последовательности случайных
событий, как будто созданы пресловутой обезьяной
за пишущей машинкой.
Джордж Марсалья (George Marsaglia)
Особой красотой JavaScript не блещет, но он работает.
Эта книга написана для людей, имеющих определенный опыт работы с JavaScript
и желающих приобрести более четкое и глубокое понимание того, как этот язык
работает и как добиться от него наибольшей отдачи. Она также подойдет опытным
программистам, желающим освоить еще один язык.
Издание не для начинающих. Надеюсь, что когда-нибудь напишу книгу и для
них. Но эта им не подойдет. Ее не назовешь легким чтивом. Беглый просмотр вам
ничего не даст.
Здесь не рассматриваются механизмы обработки кода JavaScript или виртуальные
машины. Книга — о самом языке и о том, что должен знать каждый программист.
В ней я попробую сделать радикальную переоценку JavaScript, того, как он работает, как его можно усовершенствовать и как лучше использовать. Речь идет о том,
как думать о JavaScript и как думать в JavaScript. Я планирую притвориться, что
текущая версия языка — единственная, и не собираюсь тратить ваше время на
демонстрацию того, как все работает в ES1, ES3 или ES5. Это не имеет никакого
значения. Основное внимание будет уделено тому, как JavaScript работает для нас
именно сейчас.
Эта книга не исчерпывающее руководство. В ней без какого-либо специального
упоминания будут проигнорированы довольно большие и сложные части языка.
Если не упомянута ваша самая любимая функция, то это, скорее всего, потому, что
она не представляет ни малейшей ценности. Синтаксис также не будет удостоен
особого внимания. Предполагается, что вы уже знаете, как написать инструкцию if.
Если же вам такие тонкости неизвестны, обратитесь к информационному ресурсу
JSLint по адресу jslint.com.

Tlgm: @it_boooks
Сначала прочитайте меня!

0.1

Некоторым весьма полезным составляющим языка, например большинству методов в базовых прототипах, также будет отведено не слишком много времени.
Для их изучения есть отличные справочные материалы в Интернете. Мой любимый
ресурс — Mozilla Foundation — находится по адресу developer.mozilla.org/en-US/docs/
Web/JavaScript/Reference.
Важная цель при разработке языка программирования — сделать так, чтобы он
был понятен, логичен и хорошо сформулирован, не приводил к возникновению
странных тупиковых ситуаций. Но JavaScript даже близко не подвели к достижению этой цели. С каждым выпуском языка его странности растут как снежный
ком, неизменно усугубляя ситуацию. В нем появляется множество тупиковых
и критических проблем. В книге рассматриваются только некоторые из этих странностей, просто чтобы показать наличие подобных безобразий. Держитесь подальше
от всего, что приводит к таким тупиковым и крайним ситуациям. Не углубляйтесь
в этот мрак. Оставайтесь в той части языка, где все просто и понятно. Там есть все,
что вам нужно для написания хороших программ.
Десять лет назад я написал небольшой памфлет о JavaScript с необычным посылом
о творящемся в нем явном бардаке, намекая на то, что глубоко внутри него прячется
очень хороший язык. А избегая проблемных функций, можно создавать вполне
приличные программы.
С моим мнением не согласилось немало маститых программистов, утверждавших,
что мастерство реально продемонстрировать, только используя все возможности
языка. Они были абсолютно убеждены, что странности для того и существуют,
чтобы демонстрировать мастерство программиста, поэтому плохих функций просто не бывает.
Похоже, что это в корне неверное мнение доминирует до сих пор. Истинное
мастерство проявляется в создании хороших программ, код которых легко
читается, сопровождается и не содержит ошибок. Если возникнет потребность
показать себя с лучшей стороны, попробуйте работать в таком ключе. Будучи
скромным программистом, я всегда критически отношусь к себе и своей работе,
стремясь к совершенствованию мастерства. Я усвоил трудный урок: оптимизация с целью использования характерных особенностей приводит к обратным
результатам.
Вот мой самый эффективный инструмент для наилучшего использования языка
программирования:
если функция в одних случаях полезна, а в других — опасна и есть более
подходящий вариант, нужно именно им и воспользоваться.

Взяв этот принцип на вооружение, я всегда стараюсь свести язык к его меньшей
и лучшей части, чтобы по возможности избежать тех функций, которые с большой
долей вероятности могут привести к возникновению ошибок. Я постоянно пересматриваю свои взгляды на то, что считать хорошей практикой, а что — нет. В книге
изложены самые последние размышления о JavaScript. Возможность рассматривать

Tlgm: @it_boooks

0.2

Сначала прочитайте меня!

самые удачные составляющие этого языка представилась мне именно потому, что
они в нем есть. Если сравнивать мои нынешние представления с теми, что были
десять лет назад, сейчас я полагаю: в использовании языка нужно исходить из
принципа «лучше меньше, да лучше».
JavaScript стал наиболее востребованным языком программирования в мире.
Извините, но отчасти в этом есть и моя вина. Выпуски новых редакций стандарта
ECMAScript не приводят к устранению серьезных проблем JavaScript, а иногда
и создают новые проблемы. Комитет по стандартам имеет ограниченные полномочия по коррекции языка. Его представители обладают практически безграничной
властью над развитием языка, делая его все более сложным и странным. У них есть
достаточные полномочия, чтобы не усугублять ситуацию, но заинтересованы ли
они в этом?
С каждым годом языки программирования все больше страдают от пристрастия
их разработчиков к некачественной пластической хирургии. В них лихорадочно
вводят новые функции в отчаянной надежде сохранить их популярность или по
крайней мере моду на их применение. Раздувание функциональных возможностей — такая же большая и глубокая проблема, как и раздувание кода. Полагаю,
что вместо этого нужно восхвалять внутреннюю красоту JavaScript.
Я рекомендую вам обратиться к стандарту ECMAScript. Читать его нелегко, но
он есть в свободном доступе по адресу ecma-international.org/publications/standards/
Ecma-262.htm.
Чтение стандарта ECMAScript буквально изменило мою жизнь. Как и многие
другие, я начал писать код на JavaScript, не потрудившись хорошенько изучить
язык. И пришел к выводу, что он недоработан, запутан и сильно раздражает меня.
Лишь когда нашлось время на изучение стандарта ECMAScript, раскрылось все
великолепие JavaScript.

Ересь
Возможно, у кого-то эта книга о языке программирования вызовет раздражение.
Я выступаю предвестником следующей парадигмы, и это угрожает хранителям
старой парадигмы. Я привык к этому. Нападки на меня начались, когда я обнаружил достоинства JavaScript, что стало первым важным открытием XXI века. Меня
атаковали за разработку JSON — на текущий момент самый популярный формат
обмена данными.
Сообщества формируются вокруг общих убеждений и способны приносить пользу
тем, кто в них входит, даже если убеждения ошибочны. Члены сообщества могут
чувствовать угрозу, когда эти убеждения подвергаются сомнению. Я еретик. Я ценю
стремление к истине, а не пользу от принадлежности к сообществу. Кто-то может
счесть это оскорблением.
Я всего лишь программист, который пытается найти лучший способ создания программ. Вероятно, в чем-то я ошибаюсь, но очень стараюсь все исправить. Во многом
образ мышления в нашей профессии сложился еще в эпоху Фортрана. Полагаю,

Tlgm: @it_boooks
Сначала прочитайте меня!

0.3

настало время выйти за рамки этого образа. Перемены же даются нелегко даже
в самых инновационных профессиях.
Если вас смущает такая ересь, поставьте эту книгу обратно на полку и уходите.

Код
Весь код, приведенный в этой книге, находится в свободном доступе. Его можно
применять для любых целей, но прошу не использовать его кому-нибудь во вред.
Попробуйте сделать из него что-нибудь хорошее, если, конечно, представится такая
возможность.
Я настоятельно рекомендую не заниматься копированием и вставкой непонятного вам кода, даже если он получен от меня. Похоже, при всей своей опасности
и безрассудности подобные действия уже стали вполне обычной практикой.
Это ничуть не глупее установки пакетов, на которые вы даже не смотрели, но все
равно здесь нет ничего хорошего. Учитывая современное состояние дел, самым
важным фильтром безопасности является ваше собственное осознанное поведение.
Воспользуйтесь им. Это важно.
Не берусь утверждать, что представленные в книге программы близки к идеалу.
Но уверен, что программы, которые я пишу сейчас, лучше, чем те, которые создавал
десять лет назад. Я усердно работаю над повышением своей квалификации и надеюсь, что мой солидный стаж позволит наконец получить желаемый результат.
Верю, что удача будет сопутствовать и вам. Тем не менее никто не застрахован от
ошибок. На латыни множественное число от слова «ошибка» (erratum) — errata,
поскольку это существительное среднего рода второго склонения в именительном
падеже. Но я пишу на современном английском, где существительные множественного числа формируются добавлением -s или, при избыточном количестве
шипящих, -es. Так что да, ошибки я обозначаю словом erratums. Выбирая между
прогрессом и традицией, я отдаю предпочтение прогрессу, основанному на уроках истории. Так будет лучше. Поэтому перечень ошибок можно найти по адресу
howjavascriptworks.com/erratums.
Пожалуйста, сообщайте обо всех допущенных мною грубых ошибках по адресу
erratum@howjavascriptworks.com.

Следующий язык
Эта книга о JavaScript, но порой я завожу речь о следующем языке, который придет на смену JavaScript. Я должен верить, что у JavaScript будет последователь,
поскольку даже сама мысль о том, что его никогда не будет, глубоко печалит.
Нам нужно найти путь к следующему языку хотя бы ради наших детей. Они заслуживают лучшего наследия, чем JavaScript.
Я верю, что наше будущее — дети. А также роботы.
Следующей парадигмой станет глобально распределенное безопасное программирование, определяемое конечными событиями. Интернет требует этого. Почти

Tlgm: @it_boooks

0.4

Сначала прочитайте меня!

все современные языки программирования, включая JavaScript, все еще прочно
прикованы к старой парадигме локального, небезопасного, последовательного
программирования. Я рассматриваю JavaScript как переходный язык. И внедрение
лучших приемов программирования в JavaScript станет хорошей подготовкой для
понимания следующей парадигмы.

Английский язык
Слово для обозначения цифры 1 пишется неправильно. Я использую правильное
написание wun. Произношение слова one не соответствует ни одному из стандартных или специальных правил английского произношения. И использовать
слово, обозначающее цифру 1, которое начинается с буквы, похожей на цифру 0, —
ошибка.
Написание wun вам незнакомо, поэтому может показаться ошибочным. Я же применяю его намеренно, чтобы приучить вас к мысли, что восприятие чего-то незнакомого как странного еще не доказательство неправильности.
Именно так и происходят реформы правописания. Например, кто-то решает, что
было бы лучше, если бы слово through превратилось в thru, поскольку нет смысла
в том, что половина букв в популярном слове не произносится, это крайне неэффективно и ложится ненужным бременем на тех, кто изучает язык. Реформы
правописания — это борьба между традицией и разумом, и иногда разум побеждает.
Я испытываю сходное чувство и в отношении языков программирования. Так что,
если wun имеет для вас больше смысла, чем one, пожалуйста, станьте сторонником
моих нововведений.
Когда обычные люди говорят о диапазоне, например, от 1 до 10 (1 to 10), считается,
что диапазон заканчивается на 10. Но программисты зачастую склонны к исключению 10. Эта путаница связана с принятой в программировании практикой нумерации с 0 вместо 1. Поэтому я использую to для обозначения того, что программисты
обычно имеют в виду под словом «до», и thru для обозначения того, что подразумевают под этим словом обычные люди. Таким образом, запись «от 0 до 3» в виде 0 to 3
означает диапазон, включающий 0, 1, 2, а запись «от 0 до 3» в виде 0 thru 3 означает
диапазон, включающий 0, 1, 2, 3. Под to подразумевается
представляет собой оператор сдвига вправо, выполняющий расширение знака,
а знак >>> — нет. В языке C расширение знака было определено типом, а в языке
Java — оператором. В JavaScript скопирован неудачный выбор, сделанный для Java.
Поэтому здесь нужно проявлять осмотрительность.
Единственным поразрядным унарным оператором является знак тильды (~), обозначающий поразрядное НЕ.
К поразрядным бинарным операторам относятся:

xx
xx
xx
xx
xx
xx

& (aмперсанд) — поразрядное И;
| (вертикальная черта) — поразрядное ИЛИ;
^ (циркумфлекс) — поразрядное исключающее ИЛИ;
>> (больше, больше, больше) — сдвиг вправо;
>> (больше, больше) — сдвиг вправо с расширением знака.

Объект Math
В объекте Math содержится важный набор функций, который следовало бы встроить
в Number. Это очередной пример плохого влияния языка Java. Помимо тригонометрических и логарифмических функций, в нем содержатся практические функции,
которые следовало бы предоставить в качестве операторов.
Обе функции, и Math.floor , и Math.trunc , производят из числа целое число.
Math.floor выдает наименьшее целое число, а Math.trunc — то целое число, которое
ближе к нулю. Какую из них использовать, зависит от того, что вы хотите получить
из отрицательных чисел:
Math.floor(-2.5) // -3
Math.trunc(-2.5) // -2

Функции Math.min и Math.max возвращают наименьший или наибольший из
аргументов.
Функция Math.random возвращает число в диапазоне от 0 до 1. Она вполне подойдет для игр, но только не для криптографических приложений или игр в казино.

Tlgm: @it_boooks

2.7

Как работают числа

Монстр
В JavaScript нет инструментария для разбора числа на компоненты, но написать такое
средство не составит труда. Тогда мы сможем познать истинную природу чисел.
Я собираюсь использовать вместо значимой части числа коэффициент, поскольку
хочу работать полностью в целочисленном пространстве, где все может быть
понятнее и точнее. Дробная значимая часть числа требует более пространных
объяснений.
function deconstruct(number) {

Эта функция выполняет разбор числа, переводя его в компоненты — знак, целочисленный коэффициент и экспоненту:
number = sign * coefficient * (2 ** exponent)
let sign = 1;
let coefficient = number;
let exponent = 0;

Убираем знак из коэффициента:
if (coefficient < 0) {
coefficient = -coefficient;
sign = -1;
}
if (Number.isFinite(number) && number !== 0) {

Выполняем перевод коэффициента: получить экспоненту можно делением числа
на 2 до тех пор, пока не будет получен нуль. Количество делений добавляем к –1128,
то есть к экспоненте Number.MIN_VALUE за вычетом количества разрядов в значимой
части числа и бонусного разряда:
exponent = -1128;
let reduction = coefficient;
while (reduction !== 0) {

Этот цикл гарантирует достижения нуля. Каждая операция деления будет уменьшать
экспоненту перевода. Когда она станет настолько малой, что ее невозможно будет
уменьшить на единицу, тогда вместо этого внутренняя субнормальная значимая
часть будет сдвинута вправо. В конечном итоге будут удалены все разряды:

}

exponent += 1;
reduction /= 2;

Переводим экспоненту: когда она равна нулю, число может рассматриваться как
целое. Если экспонента отлична от нуля, требуется корректировка коэффициента:
reduction = exponent;
while (reduction > 0) {

Tlgm: @it_boooks
Как работают числа

2.8

coefficient /= 2;
reduction -= 1;

}

}
while (reduction < 0) {
coefficient *= 2;
reduction += 1;
}

Возвращаем объект, содержащий три компонента и исходное число:

}

return {
sign,
coefficient,
exponent,
number
};

Теперь, имея оснащение, рассмотрим само чудовище.
Когда будет выполнен разбор Number.MAX_SAFE_INTEGER, мы получим:
{

}

"sign": 1,
"coefficient": 9007199254740991,
"exponent": 0,
"number": 9007199254740991

Number.MAX_SAFE_INTEGER — наибольшее число, помещающееся в 54-разрядное

целое число со знаком.

После разбора числа 1 мы получим:
{

}

"sign": 1,
"coefficient": 9007199254740992,
"exponent": -53,
"number": 1

Заметьте, что 1 * 9007199254740992 * (2 ** -53) равняется 1.
А теперь усложним задачу и разберем число 0.1. Одну десятую. Цент:
{

}

"sign": 1,
"coefficient": 7205759403792794,
"exponent": -56,
"number": 0.1

Если вычислить 1 * 7205759403792794 * 2 ** -56, то получится значение не 0.1,
а более точно: 0.1000000000000000055511151231257827021181583404541015625.

Tlgm: @it_boooks

2.9

Как работают числа

Хорошо известно, что JavaScript плохо справляется с обработкой десятичных
дробей, в частности денежных величин. Когда в программу вводится 0.1 или большинство других десятичных дробей, JavaScript не способен в точности представить
это значение, поэтому использует иные величины, подставляя значение, которое он
способен представить.
Когда в программе набирается десятичная точка или же она считывает значение
данных с десятичной точкой, вполне вероятно, что в программу вносится небольшая погрешность. Порой погрешности настолько малы, что их невозможно
заметить. Иногда они взаимопоглощаются, а в некоторых случаях накапливаются.
При разборе 0.3 мы получим результат, отличный от получаемого при разборе
0.1 + 0.2:
{

}
{

}

"sign": 1,
"coefficient":5404319552844595,
"exponent":-54,
"number": 0.3

"sign": 1,
"coefficient":5404319552844596,
"exponent":-54,
"number": 0.30000000000000004

Заметьте, что ни 0.299999999999999988897769753748434595763683319091796875,
ни 0.3000000000000000444089209850062616169452667236328125 не равны 0.3.
Рассмотрим еще один пример. При разборе 100 / 3 мы получим:
{

}

"sign": 1,
"coefficient": 9382499223688534,
"exponent": -48,
"number": 33.333333333333336

Заметьте, JavaScript сообщил, что число равно 33.333333333333336. Конечная цифра 6
свидетельствует о том, что JavaScript не в состоянии дать правильный или хотя бы
приемлемый ответ. На самом деле все еще печальнее. JavaScript фактически полагает,
что ответ в точности равен 33.33333333333333570180911920033395290374755859375.
Системы с плавающей точкой поставляются с функциями, выполняющими преобразования между внутренним двоичным представлением и внешним, более
привычным для людей десятичным представлением. Эти функции разработаны
таким образом, чтобы по возможности максимально скрыть истинное положение
дел. Есть вполне обоснованная обеспокоенность тем, что постоянное столкновение
с реалиями стандарта IEEE 754 вызвало бы настоящий бунт с выдвижением требований об использовании чего-то более подходящего. А на практике мы не желаем

Tlgm: @it_boooks
Как работают числа

2.10

видеть возникающие погрешности в получаемых результатах и не хотим показывать их своим клиентам. Это выставило бы напоказ всю нашу некомпетентность.
Лучше сделать вид, что все хорошо.
Первыми использовать двоичное представление чисел с плавающей точкой начали
математики и ученые. Математики хорошо понимали, что компьютеры, будучи
конечными устройствами, не могут точно представлять действительные числа, поэтому они полагались на методы численного анализа, чтобы получать от конечных
систем полезные результаты. Ученые работали над экспериментальными данными,
не лишенными погрешностей, поэтому неточность представления двоичных чисел
с плавающей точкой не создавала для них существенных проблем. Но первые бизнес-пользователи отказались от двоичного представления с плавающей точкой,
потому что их клиенты и закон требуют точных десятичных значений. По закону
при подсчете денег вы должны получить правильную сумму.
Это было более полувека назад. С тех пор мы, похоже, забыли о компромиссах,
связанных с двоичным представлением чисел с плавающей точкой. К настоящему
времени мы должны были перейти к чему-то более подходящему. Непростительно,
что в XXI веке мы не можем с достаточной степенью надежности сложить 0.1 и 0.2,
чтобы получить 0.3. Я надеюсь, что язык, который придет на смену JavaScript, будет
иметь один числовой тип, способный точно выражать десятичные дроби. Такая система до сих пор не способна точно представлять вещественные числа. И никакая
конечная система не сделает этого. Но она сможет точно представлять числа, наиболее важные для человечества, — числа, состоящие из десятичных цифр.
А пока нужно стремиться как можно больше работать в безопасном целочисленном
диапазоне. Я рекомендую переводить все денежные значения в центы, чтобы они
могли быть точно обработаны как целые числа. Опасность такого подхода заключается во взаимодействии с кодом или системами, которые не делают того же самого.
Подобные ошибки в организации взаимодействия могут привести к результатам,
стократно отличающимся от правильных в ту или иную сторону. Конечно, это
никуда не годилось бы. Возможно, здесь нас спасет инфляция, обесценивающая
центы и позволяющая их просто проигнорировать.
Когда работа протекает вне безопасного целочисленного диапазона, числа, содержащие десятичную точку (.) или десятичную экспоненту e, могут не получать истинного числового значения. Сложение одинаковых чисел дает меньшую ошибку,
чем сложение разных чисел. Поэтому вычисление общего итога из предварительно
вычисленных промежуточных итогов дает более точный результат, чем вычисление
общего итога на основе отдельно взятых значений.

Tlgm: @it_boooks

Глава 3

Как работают большие целые числа
○ ○ ○ ● ●

Он сказал пять, я сказал шесть. Он сказал восемь,
я сказал девять. Он сказал десять, я сказал
одиннадцать. Я ни за что не останавливаюсь.
Я повышаю ставку. Я иду все выше, выше, выше.
Чико Маркс (Chico Marx)
Одна из популярных претензий к JavaScript заключается в том, что этот язык
не имеет 64-разрядных целых чисел. Тип int64 способен содержать точные целые
числа вплоть до 9223372036854775807, что на три разряда больше получаемого
от чисел JavaScript с их жалким значением Number.MAX_SAFE_INTEGER , равным
9007199254740991.
Возникают вопросы по поводу необходимости простого введения нового числового типа. Казалось бы, здесь не должно быть никакой проблемы. В других языках
есть несколько числовых типов. Почему бы JavaScript не стать более похожим на
другие языки?
Когда в языке имеется один числовой тип, добавление еще одного является актом
насилия. Это повлечет за собой огромные потери в простоте и существенный рост
нового потенциала для формирования ошибок. Потенциальная ошибка — каждое
объявление типа и каждое преобразование типа.
Возникает также вопрос: а хватит ли 64 разрядов? Возможно, нам нужно заглядываться на 72, или 96, или 128, или 256 разрядов. Какое бы число вы ни выбрали,
есть не менее веский аргумент в пользу еще более высокого числа.
Полагаю, что добавление в язык больших целых чисел было бы ошибкой. Вместо
этого должна быть библиотека. Большинство пользователей языка в таких числах
не нуждаются, и они не решают самую серьезную проблему, которую приходится
решать при работе с нынешними числовыми данными. При небольшом объеме
программирования нет необходимости калечить язык. Точную целочисленную
арифметику в любом распределении разрядов можно получить с помощью текущей версии JavaScript. Существует множество способов решения этой задачи.

Tlgm: @it_boooks
Как работают большие целые числа

3.1

Реализация, представляемая в этой книге, не была оптимизирована в области
скорости или лаконичности, вместо этого вся ее оптимизация была направлена на
объяснимость. Я хочу представить полную библиотеку, не отведя на это слишком
много страниц.
Я собираюсь хранить большие целые числа в массивах. Массивы — удачный
выбор, поскольку они могут иметь любой размер. (Неплохим вариантом станут
строки, в которых каждый символ может рассматриваться как 16-разрядное целое
число без знака.) В каждом элементе массива хранится число, содержащее разряды большого целого числа. Важный конструктивный вопрос: сколько разрядов
будет приходиться на один элемент? Ответ: максимум 53, размер положительного
безопасного целого числа. Язык подсказывает, что разумнее выбрать 32 разряда
или еще меньше, поскольку это позволит задействовать поразрядные операторы.
Если размер нашего слова будет больше 32 разрядов, тогда поразрядные операторы
не смогут использоваться и реализация усложнится.
Но, учитывая потребности в умножении и делении, и 32 разряда будут слишком
большой величиной. При реализации умножения и деления больших целых чисел
хотелось бы использовать оператор умножения JavaScript, сохраняющий точность
только до 53 разрядов. Это означает, что размер нашего слова должен быть не более половины этой величины. Я выбрал 24 разряда. Мог бы остановиться и на 26,
но мне нравится число 24, оно мне ближе. (Моя первая программа работала на
24-разрядной универсальной машине Control Data Corporation 3150.) Назовем эти
24-разрядные блоки мегацифрами, потому что они могут представлять более чем
в миллион раз больше значений, чем обычные цифры.
Я выбрал представление числовой величины со знаком. Нулевой элемент массива
содержит знак числа — либо «+», либо «–». В первом элементе содержатся наименее значимые мегацифры. В последнем элементе — наиболее значимые мегацифры.
В результате 9000000000000000000 будет выглядеть так:
["+", 8650752, 7098594, 31974]

Особой красоты в этом представлении нет, но оно работает, поскольку:
9000000000000000000 = 8650752 + ((7098594 + (31974 * 16777216)) * 16777216)

Чтобы легче было добраться до внутреннего устройства большого целого числа,
я создал константы и функции.
const
const
const
const
const
const
const

radix = 16777216;
radix_squared = radix * radix;
log2_radix = 24;
plus = "+";
minus = "-";
sign = 0;
least = 1;

function last(array) {

Tlgm: @it_boooks

3.2

}

Как работают большие целые числа
return array[array.length — 1];

function next_to_last(array) {
return array[array.length — 2];
}

Создадим несколько констант. В них нет насущной потребности, но код с ними
будет проще читаться:
const
const
const
const
const

zero = Object.freeze([plus]);
wun = Object.freeze([plus, 1]);
two = Object.freeze([plus, 2]);
ten = Object.freeze([plus, 10]);
negative_wun = Object.freeze([minus, 1]);

Для обнаружения как положительных, так и отрицательных больших целых чисел
нам нужны предикативные функции:
function is_big_integer(big) {
return Array.isArray(big) && (big[sign] === plus || big[sign] === minus);
}
function is_negative(big) {
return Array.isArray(big) && big[sign] === minus;
}
function is_positive(big) {
return Array.isArray(big) && big[sign] === plus;
}
function is_zero(big) {
return !Array.isArray(big) || big.length < 2;
}

Функция mint удаляет последние слова из массива, если они нулевые. При наличии
соответствия она выполняет подстановку одной из констант. Если соответствия
нет, массив замораживается. Если позволить изменять массивы, то в некоторых
случаях реализация могла бы работать быстрее, но тогда она усложнится и станет
способна допускать больше ошибок. Наши большие целые числа неизменяемые,
как и числа JavaScript:
function mint(proto_big_integer) {

Создание большого целого числа из его прототипа. Удаление лидирующих нулевых
мегацифр. Подстановка по возможности популярных констант:
while (last(proto_big_integer) === 0) {
proto_big_integer.length -= 1;
}
if (proto_big_integer.length b.length) {
[a, b] = [b, a];
}
return mint(a.map(function (element, element_nr) {
return (
element_nr === sign
? plus
: element & b[element_nr]
);
}));

function or(a, b) {

Превращение a в самый длинный массив:

}

if (a.length < b.length) {
[a, b] = [b, a];
}
return mint(a.map(function (element, element_nr) {
return (
element_nr === sign
? plus
: element | (b[element_nr] || 0)
);
}));

Tlgm: @it_boooks

3.6

Как работают большие целые числа

function xor(a, b) {

Превращение a в самый длинный массив:

}

if (a.length < b.length) {
[a, b] = [b, a];
}
return mint(a.map(function (element, element_nr) {
return (
element_nr === sign
? plus
: element ^ (b[element_nr] || 0)
);
}));

Некоторые функции получают в качестве аргумента малое целое число. Функция
int упрощает работу как с числами, так и с большими целыми числами:
function int(big) {
let result;
if (typeof big === "number") {
if (Number.isSafeInteger(big)) {
return big;
}
} else if (is_big_integer(big)) {
if (big.length < 2) {
return 0;
}
if (big.length === 2) {
return (
is_negative(big)
? -big[least]
: big[least]
);
}
if (big.length === 3) {
result = big[least + 1] * radix + big[least];
return (
is_negative(big)
? –result
: result
);
}
if (big.length === 4) {
result = (
big[least + 2] * radix_squared
+ big[least + 1] * radix
+ big[least]
);
if (Number.isSafeInteger(result)) {
return (
is_negative(big)
? -result
: result
);
}
}
}
}

Tlgm: @it_boooks
Как работают большие целые числа

3.7

Функция shift_down уменьшает числа, удаляя наименее значимые разряды.
С ее помощью можно уменьшить большое целое число. Это похоже на деление на
степени числа 2. Данная операция обычно называется сдвигом вправо (>>>) и выглядит обманчиво, поскольку для уменьшения чисел использует знак «больше».
Нумерация разрядов — вещь совершенно произвольная, поэтому представление
о том, что разряды увеличиваются справа налево, сбивает с толку. Мы создаем
значения из массивов, которые в обычном представлении растут слева направо,
следовательно, рост происходит одновременно как справа налево, так и слева направо. В некоторых системах обычного письма строки идут слева направо, а в некоторых — справа налево, поэтому естественность прироста слева направо нельзя
считать универсальной. Проблема направления роста разрядов (Endian Problem)
уходит своими корнями именно в эту путаницу. Сдвиги влево и вправо дают менее
точные результаты, чем уменьшение и увеличение чисел.
Если количество сдвигов кратно числу 24, сдвиг дается легко. В противном случае
приходится перераспределять все разряды:
function shift_down(big, places) {
if (is_zero(big)) {
return zero;
}
places = int(places);
if (Number.isSafeInteger(places)) {
if (places === 0) {
return abs(big);
}
if (places < 0) {
return shift_up(big, -places);
}
let skip = Math.floor(places / log2_radix);
places -= skip * log2_radix;
if (skip + 1 >= big.length) {
return zero;
}
big = (
skip > 0
? mint(zero.concat(big.slice(skip + 1)))
: big
);
if (places === 0) {
return big;
}
return mint(big.map(function (element, element_nr) {
if (element_nr === sign) {
return plus;
}
return ((radix — 1) & (
(element >> places)
| ((big[element_nr + 1] || 0) (log2_radix — places);
}, 0);
if (carry > 0) {
result.push(carry);
}
return mint(result);
}
}

Нам не помешало бы иметь функцию not, выполняющую дополнение всех разрядов, но у нас отсутствуют ограничения на количество разрядов, поэтому непонятно,
сколько разрядов нужно переворачивать (flipped). Следовательно, есть функция
mask, создающая большое целое число из конкретного количества единичных разрядов. Затем можно будет использовать mask и xor для создания not, но функции
not нужно сообщить размер поля разрядов.
function mask(nr_bits) {

Создание строки единичных разрядов:
nr_bits = int(nr_bits);
if (nr_bits !== undefined && nr_bits >= 0) {
let mega = Math.floor(nr_bits / log2_radix);

Tlgm: @it_boooks
Как работают большие целые числа

}

}

3.9

let result = new Array(mega + 1).fill(radix — 1);
result[sign] = plus;
let leftover = nr_bits — (mega * log2_radix);
if (leftover > 0) {
result.push((1 = radix) {
carry = 1;
element -= radix;
} else {
carry = 0;
}
}
return element;
});

Если возникает переполнение, то для возможности переноса добавляется еще один
элемент:

}

if (carry > 0) {
result.push(carry);
}
return mint(result);

Вычитание также не составляет труда:
function sub(minuend, subtrahend) {
if (is_zero(subtrahend)) {
return minuend;
}
if (is_zero(minuend)) {
return neg(subtrahend);
}
let minuend_sign = minuend[sign];

Tlgm: @it_boooks
Как работают большие целые числа

3.11

Если знаки разные, код превращается в сложение:
if (minuend_sign !== subtrahend[sign]) {
return add(minuend, neg(subtrahend));
}

Вычитание меньшего из большего:

}

if (abs_lt(minuend, subtrahend)) {
[subtrahend, minuend] = [minuend, subtrahend];
minuend_sign = (
minuend_sign === minus
? plus
: minus
);
}
let borrow = 0;
return mint(minuend.map(function (element, element_nr) {
if (element_nr === sign) {
return minuend_sign;
}
let diff = element — ((subtrahend[element_nr] || 0) + borrow);
if (diff < 0) {
diff += 16777216;
borrow = 1;
} else {
borrow = 0;
}
return diff;
}));

Умножение несколько сложнее. Мы используем вложенные функции forEach, поскольку нужно перемножить каждый элемент multiplicand с каждым элементом
multiplier. Каждое из этих произведений может быть 48-разрядным, но в элементе
могут содержаться только 24 разряда, поэтому переполнение должно быть перенесено:
function mul(multiplicand, multiplier) {
if (is_zero(multiplicand) || is_zero(multiplier)) {
return zero;
}

Если знаки совпадают, знак результата будет «+»:
let result = [
multiplicand[sign] === multiplier[sign]
? plus
: minus
];

Перемножение каждого элемента multiplicand с каждым элементом multiplier
с распространением переноса:
multiplicand.forEach(function (
multiplicand_element,

Tlgm: @it_boooks

3.12

Как работают большие целые числа

) {

}

multiplicand_element_nr
if (multiplicand_element_nr !== sign) {
let carry = 0;
multiplier.forEach(function (
multiplier_element,
multiplier_element_nr
) {
if (multiplier_element_nr !== sign) {
let at = (
multiplicand_element_nr + multiplier_element_nr — 1
);
let product = (
(multiplicand_element * multiplier_element)
+ (result[at] || 0)
+ carry
);
result[at] = product & 16777215;
carry = Math.floor(product / radix);
}
});
if (carry > 0) {
result[multiplicand_element_nr + multiplier.length — 1] = carry;
}
}

});
return mint(result);

Функция divrem выполняет деление, возвращая как частное, так и остаток. Для удобства предоставим также функцию div, возвращающую только частное от деления:
function divrem(dividend, divisor) {
if (is_zero(dividend) || abs_lt(dividend, divisor)) {
return [zero, dividend];
}
if (is_zero(divisor)) {
return undefined;
}

Придадим операндам положительные значения:
let quotient_is_negative = dividend[sign] !== divisor[sign];
let remainder_is_negative = dividend[sign] === minus;
let remainder = dividend;
dividend = abs(dividend);
divisor = abs(divisor);

Выполним привычное по школьным временам деление в столбик. Оценим следующую цифру частного. Вычтем столько значений делителя, сколько получится
в результате оценки делимого, а затем повторим это действие. Вместо основания 10
применим основание 16 777 216 и воспользуемся более системным подходом в прогнозировании следующей цифры частного.

Tlgm: @it_boooks
Как работают большие целые числа

3.13

Чтобы улучшить прогнозы, сначала обработаем делитель функцией mint. Сдвиг влево выполняется до тех пор, пока его самый старший разряд не станет 1. На ту же величину выполняется сдвиг влево делимого. Описание алгоритма Algorithm 4.3.1D
можно найти в книге The Art of Computer Programming.
Для определения количества сдвигов находим количество лидирующих 0. Функция
clz32 выполняет вычисление в поле из 32 разрядов. Нас также интересует поле
из 24 разрядов, поэтому вычитаем 8:
let shift = Math.clz32(last(divisor)) — 8;
dividend = shift_up(dividend, shift);
divisor = shift_up(divisor, shift);
let place = dividend.length — divisor.length;
let dividend_prefix = last(dividend);
let divisor_prefix = last(divisor);
if (dividend_prefix < divisor_prefix) {
dividend_prefix = (dividend_prefix * radix) + next_to_last(dividend);
} else {
place += 1;
}
divisor = shift_up(divisor, (place — 1) * 24);
let quotient = new Array(place + 1).fill(0);
quotient[sign] = plus;
while (true) {

Оценка не будет слишком маленькой, но может оказаться слишком большой.
Если она слишком высока, вычитание из делимого произведения оценки и делителя дает отрицательный результат. Когда это происходит, нужно уменьшить оценку
и попробовать еще раз:
let estimated = Math.floor(dividend_prefix / divisor_prefix);
if (estimated > 0) {
while (true) {
let trial = sub(dividend, mul(divisor, [plus, estimated]));
if (!is_negative(trial)) {
dividend = trial;
break;
}
estimated -= 1;
}
}

Правильная оценка сохраняется в quotient. Если это был последний разряд, идем
дальше:
quotient[place] = estimated;
place -= 1;
if (place === 0) {
break;
}

Tlgm: @it_boooks

3.14

Как работают большие целые числа

Выполняем подготовку для следующего разряда. Обновляем dividend_prefix
с использованием первых двух слов оставшегося делимого dividend и уменьшаем
делитель divisor:

}

if (is_zero(dividend)) {
break;
}
dividend_prefix = last(dividend) * radix + next_to_last(dividend);
divisor = shift_down(divisor, 24);

Исправляем остаток:

}

quotient = mint(quotient);
remainder = shift_down(dividend, shift);
return [
(
quotient_is_negative
? neg(quotient)
: quotient
),
(
remainder_is_negative
? neg(remainder)
: remainder
)
];

function div(dividend, divisor) {
let temp = divrem(dividend, divisor);
if (temp) {
return temp[0];
}
}

Возведение целого числа в целочисленную степень сводится к простому использованию квадрата числа и метода умножения:
function power(big, exponent) {
let exp = int(exponent);
if (exp === 0) {
return wun;
}
if (is_zero(big)) {
return zero;
}
if (exp === undefined || exp < 0) {
return undefined;
}
let result = wun;
while (true) {
if ((exp & 1) !== 0) {
result = mul(result, big);
}

Tlgm: @it_boooks
Как работают большие целые числа

3.15

exp = Math.floor(exp / 2);
if (exp < 1) {
break;
}
big = mul(big, big);

}

}
return mint(result);

Для сокращения дробей применим функцию gcd:
function gcd(a, b) {
a = abs(a);
b = abs(b);
while (!is_zero(b)) {
let [ignore, remainder] = divrem(a, b);
a = b;
b = remainder;
}
return a;
}

Нам нужны функции для преобразования чисел и строк в большие целые числа
и обратно. При преобразовании в строки и из строк нужна явная поддержка десятичной системы записи, а также следующих сигнатур: двоичной, восьмеричной,
шестнадцатеричной и Base32. Дополнительные сведения о Base32 можно получить
по адресу crockford.com/wrmg/base32.html.
Строка digitset позволяет отображать числа на символы. Объект charset отображает символы не числа. Для шестнадцатеричных, десятичных, восьмеричных
и двоичных чисел может использоваться поднабор с аналогичным символьным
отображением:
const digitset = "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U";
const charset = (function (object) {
digitset.split("").forEach(function (element, element_nr) {
object[element] = element_nr;
});
return Object.freeze(object);
}(Object.create(null)));

Функция make получает число или строку и необязательное основание системы
счисления, а возвращает большое целое число. Преобразование выполняется
с абсолютной точностью для всех целочисленных значений:
function make(value, radix_2_37) {

Функция make возвращает большое целое число. Параметр value является строкой,
а необязательный параметр radix — либо целочисленным значением, либо значением большого целого числа (big_integer):
let result;
if (typeof value === "string") {

Tlgm: @it_boooks

3.16

Как работают большие целые числа
let radish;
if (radix_2_37 === undefined) {
radix_2_37 = 10;
radish = ten;
} else {
if (
!Number.isInteger(radix_2_37)
|| radix_2_37 < 2
|| radix_2_37 > 37
) {
return undefined;
}
radish = make(radix_2_37);
}
result = zero;
let good = false;
let negative = false;
if (value.toUpperCase().split("").every(
function (element, element_nr) {
let digit = charset[element];
if (digit !== undefined && digit < radix_2_37) {
result = add(mul(result, radish), [plus, digit]);
good = true;
return true;
}
if (element_nr === sign) {
if (element === plus) {
return true;
}
if (element === minus) {
negative = true;
returntrue;
}
}
return digit === "_";
}
) && good) {
if (negative) {
result = neg(result);
}
return mint(result);
}
return undefined;

}
if (Number.isInteger(value)) {
let whole = Math.abs(value);
result = [(
value < 0
? minus
: plus
)];
while (whole >= radix) {
let quotient = Math.floor(whole / radix);
result.push(whole — (quotient * radix));
whole = quotient;

Tlgm: @it_boooks
Как работают большие целые числа

3.17

}
if (whole > 0) {
result.push(whole);
}
return mint(result);

}

}
if (Array.isArray(value)) {
return mint(value);
}

Функция number преобразует большое целое число в число JavaScript. Это делается
с абсолютной точностью, если значение находится в безопасном целочисленном
диапазоне:
function number(big) {
let value = 0;
let the_sign = 1;
let factor = 1;
big.forEach(function (element, element_nr) {
if (element_nr === 0) {
if (element === minus) {
the_sign = -1;
}
} else {
value += element * factor;
factor *= radix;
}
});
return the_sign * value;
}

Функция string преобразует большое целое число в строку. Преобразование выполняется с абсолютной точностью:
function string(a, radix_2_thru_37 = 10) {
if (is_zero(a)) {
return "0";
}
radix_2_thru_37 = int(radix_2_thru_37);
if (
!Number.isSafeInteger(radix_2_thru_37)
|| radix_2_thru_37 < 2
|| radix_2_thru_37 > 37
) {
return undefined;
}
const radish = make(radix_2_thru_37);
const the_sign = (
a[sign] === minus
? "-"
: ""
);
a = abs(a);
let digits = [];

Tlgm: @it_boooks

3.18

}

Как работают большие целые числа
while (!is_zero(a)) {
let [quotient, remainder] = divrem(a, radish);
digits.push(digitset[number(remainder)]);
a = quotient;
}
digits.push(the_sign);
return digits.reverse().join("");

Функция подсчета заполнения подсчитывает в большом целом числе количество
разрядов, содержащих 1. Это может быть использовано для вычисления расстоя­
ния Хэмминга.
Получение общего количества единичных разрядов в 32-разрядном целом числе:
function population_32(int32) {

Подсчет 16 пар разрядов с получением 16 двухразрядных счетчиков (0, 1 или 2):
для каждой пары выполняется вычитание самого старшего разряда из пары, что
превращает два разряда в значение счетчика:
//
//
//
//
//

HL
00
01
10
11







H
0
0
1
1

=
=
=
=
=

count
00
01
01
10

int32 -= (int32 >>> 1) & 0x55555555;

Объединяя восемь пар двухразрядных счетчиков, получаем восемь четырехразрядных счетчиков в диапазоне от 0 до 4:
int32 = (int32 & 0x33333333) + ((int32 >>> 2) & 0x33333333);

Объединяя четыре пары четырехразрядных счетчиков, получаем четыре восьмиразрядных счетчика в диапазоне от 0 до 8. Переполнение с переходом в соседние
счетчики больше невозможно, поэтому после сложения нам понадобится только
одна операция наложения маски:
int32 = (int32 + (int32 >>> 4)) & 0x0F0F0F0F;

Объединяя две пары восьмиразрядных счетчиков, получаем два шестнадцатиразрядных счетчика в диапазоне от 0 до 16:
int32 = (int32 + (int32 >>> 8)) & 0x001F001F;

И наконец, объединяя два шестнадцатиразрядных счетчика, получаем число в диапазоне от 0 до 32:
}

return (int32 + (int32 >>> 16)) & 0x0000003F;

function population(big) {

Tlgm: @it_boooks
Как работают большие целые числа

3.19

Подсчет общего количества разрядов, содержащих 1:

}

return big.reduce(
function (reduction, element, element_nr) {
return reduction + (
element_nr === sign
? 0
: population_32(element)
);
},
0
);

function significant_bits(big) {

Подсчет общего количества разрядов, исключая лидирующие нули:

}

return (
big.length > 1
? make((big.length — 2) * log2_radix + (32 — Math.clz32(last(big))))
: zero
);

И в завершение все эти полезные объекты экспортируются в виде модуля:
export default Object.freeze({
abs,
abs_lt,
add,
and,
div,
divrem,
eq,
gcd,
is_big_integer,
is_negative,
is_positive,
is_zero,
lt,
make,
mask,
mul,
neg,
not,
number,
or,
population,
power,
random,
shift_down,
shift_up,
significant_bits,

Tlgm: @it_boooks

3.20

});

Как работают большие целые числа
signum,
string,
sub,
ten,
two,
wun,
xor,
zero

Доступ к объекту большого целого числа в вашем модуле можно получить после
его импортирования:
import big_integer from "./big_integer.js";

Tlgm: @it_boooks

Глава 4

Как работают большие числа
с плавающей точкой
○ ○ ● ○ ○

Не геройствуй, юноша. В этом нет никакого смысла.
Харлан Поттер (Harlan Potter)

Система, работающая с большими числами, способна решать множество задач, но
явно ограничена только целыми числами, а решение ряда задач неподвластно целым числам. Поэтому создадим систему с числами с плавающей точкой. Она имеет
дело с тремя числами: мантиссой, экспонентой, или порядком, и основанием. Значение определяется этими тремя числами:
значение = мантисса * (основание ** порядок)

У формата IEEE 754, используемого для чисел в JavaScript, основанием является
число 2. Это позволяло реализовать его на оборудовании 1950-х годов. Закон Мура
убрал ограничение, делающее 2 единственно разумным основанием, поэтому рассмотрим другие возможности.
Пакет больших целых чисел (big integer package) привязан к 2 ** 24. Если взять за
основание число 16 777 216, то некоторые операции выравнивания будут просто
вставлять элементы в массив или удалять их из него. Это позволит добиться весьма высокой производительности. И это согласуется с широко распространенной
практикой обмена точности на производительность.
Я полагаю, что основанием должно послужить число 10. При таком основании
все десятичные дроби могут получить точное представление. Важность такого
выбора обусловлена тем, что большинство людей используют десятичные дроби,
следовательно, система чисел с плавающей точкой, имеющая основание 10, станет
наиболее подходящей для них.
Большие целые числа являются идеальным представлением для мантиссы. Большинство странностей систем с плавающей точкой связаны с ограничением размера.

Tlgm: @it_boooks

4.1

Как работают большие числа с плавающей точкой

Они возникают, если размер не ограничен. Поскольку это способно вызывать
ошибки, следует по возможности избавиться от странностей.
Большие целые числа можно использовать также для экспоненты, но это будет излишним. Вполне хватит и чисел JavaScript. Гигабайты памяти были бы исчерпаны
еще до того, как свойство Number.MAX_SAFE_INTEGER стало бы ограничением.
Большие числа с плавающей точкой будут представлены в виде объектов со свойствами coefficient и exponent.
После того как большие целые числа взяты на вооружение, разобраться с числами
с плавающей точкой не составит особого труда:
import big_integer from "./big_integer.js";

Функция is_big_float используется для обнаружения объекта больших чисел
с плавающей точкой:
function is_big_float(big) {
return (
typeof big === "object"
&& big_integer.is_big_integer(big.coefficient)
&& Number.isSafeInteger(big.exponent)
);
}
function is_negative(big) {
return big_integer.is_negative(big.coefficient);
}
function is_positive(big) {
return big_integer.is_positive(big.coefficient);
}
function is_zero(big) {
return big_integer.is_zero(big.coefficient);
}

Отдельное значение zero представляет все нули:
const zero = Object.create(null);
zero.coefficient = big_integer.zero;
zero.exponent = 0;
Object.freeze(zero);
function make_big_float(coefficient, exponent) {
if (big_integer.is_zero(coefficient)) {
return zero;
}
const new_big_float = Object.create(null);
new_big_float.coefficient = coefficient;
new_big_float.exponent = exponent;
return Object.freeze(new_big_float);
}
const big_integer_ten_million = big_integer.make(10000000);

Tlgm: @it_boooks
Как работают большие числа с плавающей точкой

4.2

Функция number превращает большое число с плавающей точкой в число JavaScript.
Если число выходит за пределы безопасной зоны целых чисел, точность преобразования не гарантируется. Попробуем также разобраться с другими типами.
function number(a) {
return (
is_big_float(a)
? (
a.exponent === 0
? big_integer.number(a.coefficient)
: big_integer.number(a.coefficient) * (10 ** a.exponent)
)
: (
typeof a === "number"
? a
: (
big_integer.is_big_integer(a)
? big_integer.number(a)
: Number(a)
)
)
);
}

Нам нужны функция абсолютного значения и функция смены знака:
function neg(a) {
return make_big_float(big_integer.neg(a.coefficient), a.exponent);
}
function abs(a) {
return (
is_negative(a)
? neg(a)
: a
);
}

Сложение и вычитание даются легко: мы просто складываем мантиссы, но только
при одинаковых экспонентах. Если экспоненты неодинаковы, их нужно привести
в соответствие. Поскольку сложение и вычитание очень похожи, я создал функцию,
выполняющую как функцию сложения, так и функцию вычитания. Если функции
conform_op передать аргумент bi.add, получится функция сложения чисел с плавающей точкой. А если ей передать аргумент bi.sub, получится функция вычитания
чисел с плавающей точкой:
function conform_op(op) {
return function (a, b) {
const differential = a.exponent — b.exponent;
return (
differential === 0
? make_big_float(op(a.coefficient, b.coefficient), a.exponent)
: (
differential < 0
? make_big_float(

Tlgm: @it_boooks

4.3

Как работают большие числа с плавающей точкой
op(

big_integer.mul(
a.coefficient,
big_integer.power(big_integer.ten, -differential)
),
b.coefficient

),
b.exponent

);

)

)
: make_big_float(
op(
a.coefficient,
big_integer.mul(
b.coefficient,
big_integer.power(big_integer.ten, differential)
)
),
a.exponent
)

};
}
const add = conform_op(big_integer.add);
const sub = conform_op(big_integer.sub);

Умножение реализуется еще проще. Мы просто перемножаем мантиссы и складываем экспоненты:
function mul(multiplicand, multiplier) {
return make_big_float(
big_integer.mul(multiplicand.coefficient, multiplier.coefficient),
multiplicand.exponent + multiplier.exponent
);
}

Сложность деления заключается в том, чтобы не ошибиться с моментом остановки.
Проще всего остановиться в целочисленном делении. Остановка выполняется,
когда заканчиваются цифры. Также просто определить момент остановки, работая
с числами с плавающей точкой, имеющими фиксированный размер. Остановка
выполняется, когда заканчиваются разряды, но при работе с большими целыми числами такие ограничения отсутствуют. Деление может продолжаться до нахождения
точного результата, но нет никакой гарантии, что его получится достичь. Так что
оставим все это на усмотрение программиста. Функция div получает необязательный третий аргумент, указывающий на точность результата. Указывается место
десятичной точки. Младший разряд (разряд единиц) находится на нулевой позиции. Дробные позиции выражены отрицательными числами. Деление возвращает
как минимум столько знаков после точки, сколько было указано. По умолчанию
используется значение -4, то есть четыре цифры после точки:
function div(dividend, divisor, precision = -4) {
if (is_zero(dividend)) {
return zero;
}

Tlgm: @it_boooks
Как работают большие числа с плавающей точкой

4.4

if (is_zero(divisor)) {
return undefined;
}
let {coefficient, exponent} = dividend;
exponent -= divisor.exponent;

Масштабируем мантиссу до нужной точности:
if (typeof precision !== "number") {
precision = number(precision);
}
if (exponent > precision) {
coefficient = big_integer.mul(
coefficient,
big_integer.power(big_integer.ten, exponent — precision)
);
exponent = precision;
}
let remainder;
[coefficient, remainder] = big_integer.divrem(
coefficient,
divisor.coefficient
);

Округляем результат, если это необходимо:

}

if (!big_integer.abs_lt(
big_integer.add(remainder, remainder),
divisor.coefficient
)) {
coefficient = big_integer.add(
coefficient,
big_integer.signum(dividend.coefficient)
);
}
return make_big_float(coefficient, exponent);

Большое число с плавающей точкой нормализовано, если экспонента максимально
приблизилась к нулю без потери значимости:
function normalize(a) {
let {coefficient, exponent} = a;
if (coefficient.length < 2) {
return zero;
}

Если экспонента равна нулю, значит, число уже нормализовано:
if (exponent !== 0) {

Если экспонента — положительное число, выполняется умножение мантиссы
на 10 ** экспоненты:
if (exponent > 0) {
coefficient = big_integer.mul(
coefficient,
big_integer.power(big_integer.ten, exponent)

Tlgm: @it_boooks

4.5

Как работают большие числа с плавающей точкой
);
exponent = 0;
} else {
let quotient;
let remainder;

Если же экспонента — отрицательное число, а мантисса делится на 10, выполняется
деление и к экспоненте добавляется единица.
Чтобы ускорить эту работу, сначала попробуем взять из младших разрядов блоки
по 10 000 000, убирая сразу семь нулей:
while (exponent 0) {
coefficient = big_integer.mul(
coefficient,
big_integer.power(big_integer.two, exponent)
);
exponent = 0;
}
return make(coefficient, exponent);

}

}
if (is_big_float(a)) {
return a;
}

Функция string преобразует большое число с плавающей точкой в строку. Преобра­
зование выполняется с абсолютной точностью. Основная часть работы заключается
во вставке десятичного знака и заполнении нулевых позиций. Аналогичная функция для двоичной плавающей точки была бы значительно сложнее.
function string(a, radix) {
if (is_zero(a)) {
return "0";
}
if (is_big_float(radix)) {
radix = normalize(radix);
return (
(radix && radix.exponent === 0)
? big_integer.string(integer(a).coefficient, radix.coefficient)
: undefined
);
}
a = normalize(a);
let s = big_integer.string(big_integer.abs(a.coefficient));
if (a.exponent < 0) {
let point = s.length + a.exponent;
if (point 0) {
s += "0".repeat(a.exponent);
}
if (big_integer.is_negative(a.coefficient)) {
s = "-" + s;
}
return s;
}

Существует два соглашения по представлению десятичного знака: в виде точки (.)
и в виде запятой (,). В большинстве стран используется один из этих знаков. В преде-

Tlgm: @it_boooks
Как работают большие числа с плавающей точкой

4.8

лах той или иной страны вид знака не имеет значения. Оба они работают должным
образом. Но такое разночтение препятствует международным связям, поскольку
у числа вида 1,024 может быть различное толкование. Могу предсказать, что в итоге
все остановятся на использовании точки, приняв ее за международный стандарт, поскольку в языках программирования применяется точка, а основная часть информа­
ционных потоков, проходящих через наши программы, написана на этих языках.
Функция scientific преобразует большое число с плавающей точкой в строку
с e-нотацией:
function scientific(a) {
if (is_zero(a)) {
return "0";
}
a = normalize(a);
let s = big_integer.string(big_integer.abs(a.coefficient));
let e = a.exponent + s.length — 1;
if (s.length > 1) {
s = s.slice(0, 1) + "." + s.slice(1);
}
if (e !== 0) {
s += "e" + e;
}
if (big_integer.is_negative(a.coefficient)) {
s = "-" + s;
}
return s;
}

И наконец, все эти полезные объекты экспортируются в виде модуля:
export default Object.freeze({
abs,
add,
div,
eq,
fraction,
integer,
is_big_float,
is_negative,
is_positive,
is_zero,
lt,
make,
mul,
neg,
normalize,
number,
scientific,
string,
sub,
zero
});

Tlgm: @it_boooks

4.9

Как работают большие числа с плавающей точкой

Получается библиотека, подходящая для вычислений, обработки финансовых данных или любой другой работы, требующей корректного обращения с десятичными
дробями. На данной стадии в ней есть только самое необходимое, но ее можно дополнить всеми функциями и операторами, которые когда-либо могут понадобиться.
Я воспользовался этой библиотекой, чтобы установить истину в главе 2. Несмотря на ее высокую эффективность, я полагаю, что многим приложениям больше
подошел бы стандартный тип десятичных чисел с плавающей точкой, имеющих
фиксированный размер. Проблема с числовым типом, имеющимся в JavaScript,
кроется не в ограниченных диапазоне или точности. Дело в том, что он не в состоянии точно представить числа, наиболее интересные для практического применения
людьми, — числа, состоящие из десятичных цифр. Поэтому более удачным выбором
для следующего языка может стать что-то вроде DEC64: www.DEC64.com.
Ни двоичные числа с плавающей точкой, ни десятичные числа с плавающей точкой
не могут в точности дать представление, к примеру, о результате действия 100/3.
Эта проблема будет рассматриваться и далее.

Tlgm: @it_boooks

Глава 5

Как работают большие
рациональные числа
○ ○ ● ○ ●

Я изучал только обязательные предметы. …
Сначала мы, как полагается, Чихали и Пищали1.
А потом принялись за четыре действия
Арифметики: Скольжение, Причитание, Умиление
и Изнеможение2.
Черепаха Квази
(«Алиса в Стране чудес»3)

Рациональным называется число, которое может быть выражено отношением двух
целых чисел. Если два целых числа являются большими целыми числами, то это
способно стать весьма привлекательным представлением чисел. Ими можно точно выразить то же самое, что и двоичными числами с плавающей точкой. И они
позволяют точно выразить все рациональные числа, чего не могут сделать другие
представления.
Система рациональных чисел работает с двумя числами — числителем (numerator)
и знаменателем (denominator). Значение (value) определяется двумя числами:
значение = числитель / знаменатель

Наши рациональные числа — это объекты со свойствами numerator и denominator,
каждое из которых является большим целым числом. Знак значения определяется
знаком числителя. Знаменатель не должен быть отрицательным.
1

В английском созвучно словам «читали» и «писали». — Примеч. пер.

2

В английском созвучно словам «сложение», «вычитание», «умножение», «деление». —
Примеч. пер.

3

В переводе Н. Демуровой.

Tlgm: @it_boooks

5.1

Как работают большие рациональные числа

Эти обстоятельства допускают применение весьма привлекательных способов выполнения арифметических действий. Можно выстраивать их реализацию на основе
использования больших целых чисел:
import big_integer from "./big_integer.js";

Начнем с некоторых предикатных функций:
function is_big_rational(a) {
return (
typeof a === "object"
&& big_integer.is_big_integer(a.numerator)
&& big_integer.is_big_integer(a.denominator)
);
}
function is_integer(a) {
return (
big_integer.eq(big_integer.wun, a.denominator)
|| big_integer.is_zero(
big_integer.divrem(a.numerator, a.denominator)[1]
)
);
}
function is_negative(a) {
return big_integer.is_negative(a.numerator);
}

А это константы, которые я счел полезными. Можно без особого труда определить
дополнительные константы:
function make_big_rational(numerator, denominator) {
const new_big_rational = Object.create(null);
new_big_rational.numerator = numerator;
new_big_rational.denominator = denominator;
return Object.freeze(new_big_rational);
}
const zero = make_big_rational(big_integer.zero, big_integer.wun);
const wun = make_big_rational(big_integer.wun, big_integer.wun);
const two = make_big_rational(big_integer.two, big_integer.wun);

Понадобятся также функции абсолютного значения и отрицания. В соответствии
с соглашением знак определяется числителем. Знаменатель всегда положительное
число.
function neg(a) {
return make(big_integer.neg(a.numerator), a.denominator);
}
function abs(a) {
return (
is_negative(a)
? neg(a)
: a
);
}

Tlgm: @it_boooks
Как работают большие рациональные числа

5.2

Сложение и вычитание реализуются весьма просто. Если знаменатели равны,
можно складывать или вычитать числители. В противном случае дополнительно
выполняются два умножения, сложение и еще одно умножение, поскольку:
(a / b) + (c / d) = ((a * d) + (b * c)) / (b * d)

Схожесть сложения и вычитания позволила мне создать функцию, выполняющую
функции add и sub:
function conform_op(op) {
return function (a, b) {
try {
if (big_integer.eq(a.denominator, b.denominator)) {
return make(
op(a.numerator, b.numerator),
a.denominator
);
}
return normalize(make(
op(
big_integer.mul(a.numerator, b.denominator),
big_integer.mul(b.numerator, a.denominator)
),
big_integer.mul(a.denominator, b.denominator)
));
} catch (ignore) {
}
};
}
const add = conform_op(big_integer.add);
const sub = conform_op(big_integer.sub);

Рациональное число можно увеличить на единицу, добавив знаменатель к числителю.
function inc(a) {
return make(
big_integer.add(a.numerator, a.denominator),
a.denominator
);
}
function dec(a) {
return make(
big_integer.sub(a.numerator, a.denominator),
a.denominator
);
}

Умножение также не вызывает затруднений. Мы просто перемножаем числители
и знаменатели. Деление представляется тем же умножением, но с инвертированным
вторым аргументом. В системе рациональных чисел выполнять деление в столбик
практически не приходится. Мы просто увеличиваем делитель:
function mul(multiplicand, multiplier) {
return make(

Tlgm: @it_boooks

5.3

}

Как работают большие рациональные числа

);

big_integer.mul(multiplicand.numerator, multiplier.numerator),
big_integer.mul(multiplicand.denominator, multiplier.denominator)

function div(a, b) {
return make(
big_integer.mul(a.numerator, b.denominator),
big_integer.mul(a.denominator, b.numerator)
);
}
function remainder(a, b) {
const quotient = div(normalize(a), normalize(b));
return make(
big_integer.divrem(quotient.numerator, quotient.denominator)[1]
);
}
function reciprocal(a) {
return make(a.denominator, a.numerator);
}
function integer(a) {
return (
a.denominator === wun
? a
: make(big_integer.div(a.numerator, a.denominator), big_integer.wun)
);
}
function fraction(a) {
return sub(a, integer(a));
}

Функция нормализации normalize сокращает дробь, избавляя ее от общих множителей. Разложение на множители больших чисел — весьма непростая задача.
К счастью, нам не нужно выполнять разложение на множители для операции сокращения. Следует просто найти наибольший общий делитель, на который затем
и выполняется сокращение.
Фактически нормализация не нужна. Значение в результате нормализации не изменяется. Большие целые числа внутри рационального объекта могут быть уменьшены, и это способно сократить потребность в памяти (что редко играет важную
роль) и ускорить выполнение последующих арифметических операций.
function normalize(a) {

Нормализация большого рационального числа выполняется делением двух его
компонентов на наибольший общий делитель (НОД). Если НОД равен единице,
значит, число уже нормализовано:

Tlgm: @it_boooks
Как работают большие рациональные числа

5.4

let {numerator, denominator} = a;
if (big_integer.eq(big_integer.wun, denominator)) {
return a;
}
let g_c_d = big_integer.gcd(numerator, denominator);
return (
big_integer.eq(big_integer.wun, g_c_d)
? a
: make(
big_integer.div(numerator, g_c_d),
big_integer.div(denominator, g_c_d)
)
);
}

Чтобы определить равенство двух значений, нормализация не понадобится. Если:
a / b = c / d

то:
a * d = b * c

даже если они не нормализованы.
function eq(comparahend, comparator) {
return (
comparahend === comparator
? true
: (
big_integer.eq(comparahend.denominator, comparator.denominator)
? big_integer.eq(comparahend.numerator, comparator.numerator)
: big_integer.eq(
big_integer.mul(comparahend.numerator, comparator.denominator),
big_integer.mul(comparator.numerator, comparahend.denominator)
)
)
);
}
function lt(comparahend, comparator) {
return (
is_negative(comparahend) !== is_negative(comparator)
? is_negative(comparator)
: is_negative(sub(comparahend, comparator))
);
}

Функция make принимает два аргумента и создает объект, содержащий числитель
numerator и знаменатель denominator. Преобразование выполняется с абсолютной
точностью.
Функция принимает одно или два больших целых числа, а также строки вида
"33 1/3" и "98.6" и выполняет их правильное преобразование. Она также принимает

Tlgm: @it_boooks

5.5

Как работают большие рациональные числа

любое конечное число JavaScript и превращает его в рациональное без дополнительных потерь.
const number_pattern = /
^
( -? )
(?:
( \d+ )
(?:
(?:
\u0020 ( \d+ )
)?
\/
( \d+ )
|
(?:
\. ( \d* )
)?
(?:
e ( -? \d+ )
)?
)
|
\. (\d+)
)
$
/;
function make(numerator, denominator) {

Если получены два аргумента, то оба будут преобразованы в большие целые числа.
Возвращаемым значением станет объект, содержащий числитель и знаменатель.
При вызове с одним аргументом будет сделана попытка его осмысления. Если аргумент имеет строковое значение, будет предпринята попытка его парсинга в качестве
смешанной дроби или десятичного литерала. В противном случае предполагается,
что недостающим аргументом была единица.
if (denominator !== undefined) {

Создание рационального числа из числителя и знаменателя — можно передать
функции большие целые числа, простые целые числа или строки.
numerator = big_integer.make(numerator);

Если числитель равен нулю, то знаменатель не нужен.
if (big_integer.zero === numerator) {
return zero;
}
denominator = big_integer.make(denominator);
if (
!big_integer.is_big_integer(numerator)

Tlgm: @it_boooks
Как работают большие рациональные числа

5.6

|| !big_integer.is_big_integer(denominator)
|| big_integer.zero === denominator
) {
return undefined;
}

Если знаменатель — отрицательное число, знак переносится в числитель.

}

if (big_integer.is_negative(denominator)) {
numerator = big_integer.neg(numerator);
denominator = big_integer.abs(denominator);
}
return make_big_rational(numerator, denominator);

А если аргумент является строковым значением? В таком случае выполняется его
парсинг.
if (typeof numerator === "string") {
let parts = numerator.match(number_pattern);
if (!parts) {
return undefined;
}
//
//
//
//
//
//
//
//

Определение групп:
[1] sign
[2] integer
[3] top
[4] bottom
[5] frac
[6] exp
[7] naked frac
if (parts[7]) {
return make(
big_integer.make(parts[1] + parts[7]),
big_integer.power(big_integer.ten, parts[7].length)
);
}
if (parts[4]) {
let bottom = big_integer.make(parts[4]);
if (parts[3]) {
return make(
big_integer.add(
big_integer.mul(
big_integer.make(parts[1] + parts[2]),
bottom
),
big_integer.make(parts[3])
),
bottom
);
}
return make(parts[1] + parts[2], bottom);
}

Tlgm: @it_boooks

5.7

Как работают большие рациональные числа
let frac = parts[5] || "";
let exp = (Number(parts[6]) || 0) — frac.length;
if (exp < 0) {
return make(
parts[1] + parts[2] + frac,
big_integer.power(big_integer.ten, -exp)
);
}
return make(
big_integer.mul(
big_integer.make(parts[1] + parts[2] + parts[5]),
big_integer.power(big_integer.ten, exp)
),
big_integer.wun
);
}

Аргумент является числом? Тогда оно подвергается разбору и реконструкции.

}

if (typeof numerator === "number" && !Number.isSafeInteger(numerator)) {
let {sign, coefficient, exponent} = deconstruct(numerator);
if (sign < 0) {
coefficient = -coefficient;
}
coefficient = big_integer.make(coefficient);
if (exponent >= 0) {
return make(
big_integer.mul(
coefficient,
big_integer.power(big_integer.two, exponent)
),
big_integer.wun
);
}
return normalize(make(
coefficient,
big_integer.power(big_integer.two, -exponent)
));
}
return make(numerator, big_integer.wun);

Функция number преобразует большое рациональное число в число JavaScript.
Преобразование не гарантирует абсолютную точность, если число находится за
пределами безопасной целочисленной зоны:
function number(a) {
return big_integer.number(a.numerator) / big_integer.number(a.demoninator);
}

Функция string преобразует большое рациональное число в строку. Преобразование выполняется с абсолютной точностью:
function string(a, nr_places) {
if (a === zero) {

Tlgm: @it_boooks
Как работают большие рациональные числа

5.8

return "0";
}
let {numerator, denominator} = normalize(a);

Числитель делится на знаменатель. Если действие выполнено без остатка, значит,
получен нужный результат.
let [quotient, remains] = big_integer.divrem(numerator, denominator);
let result = big_integer.string(quotient);
if (remains !== big_integer.zero) {

Если предоставлен аргумент nr_places , результат имеет десятичный формат.
Остатки сводятся к масштабу степени 10, и выполняется целочисленное деление.
Если остаток составляет не менее половины знаменателя, производится округление
в бо' льшую сторону.
remains = big_integer.abs(remains);
if (nr_places === undefined) {
let [fractus, residue] = big_integer.divrem(
big_integer.mul(
remains,
big_integer.power(big_integer.ten, nr_places)
),
denominator
);
if (!big_integer.abs_lt(
big_integer.mul(residue, big_integer.two),
denominator
)) {
fractus = big_integer.add(fractus, big_integer.wun);
}
result += "." + big_integer.string(fractus).padStart(
big_integer.number(nr_places),
"0"
);
} else {

Результат будет в виде смешанной дроби.
result = (
(
result === "0"
? ""
: result + " "
)
+ big_integer.string(remains)
+ "/"
+ big_integer.string(denominator)
);

}

}
}
return result;

Tlgm: @it_boooks

5.9

Как работают большие рациональные числа

Все эти полезные объекты экспортируются в виде модуля:
export default Object.freeze({
abs,
add,
dec,
div,
eq,
fraction,
inc,
integer,
is_big_rational,
is_integer,
is_negative,
lt,
make,
mul,
neg,
normalize,
number,
wun,
reciprocal,
remainder,
string,
sub,
two,
zero
});

Получить в модуле доступ к большому рациональному числу можно путем его
импортирования:
import big_rational from "./big_rational.js";

Эта библиотека рациональных чисел весьма компактна, проста, медлительна и на
удивление эффективна. Она все еще не в состоянии в точности отобразить число π
или квадратный корень из 2, но способна приблизиться к их отображению в нужной вам степени. Например:
const pi = big_rational.make(
"3141592653589793238462643383279502884197169399375105820974944592307816406",
"1000000000000000000000000000000000000000000000000000000000000000000000000"
);
const sqrt_two = big_rational.make(
"1414213562373095048801688724209698078569671875376948073176679737990732478",
"1000000000000000000000000000000000000000000000000000000000000000000000000"
);

Такой формат подойдет для 72 знаков после десятичной точки. Если нужно
больше, можно получить и такой результат. С увеличением точности падает производительность. Зачастую мы не задумываясь жертвуем точностью ради производительности.

Tlgm: @it_boooks
Как работают большие рациональные числа

5.10

Эта библиотека позволяет выполнять в JavaScript вычисления, которые, как уже
говорилось, не могут быть произведены в чистом JavaScript. Поэтому фактически
не возникает надобности в добавлении в JavaScript новых числовых типов. Все, что
нужно, реально сделать средствами самого языка.
Да, при этом возникают некоторые неудобства. Вместо a + b приходится делать запись big_rational.add(a, b). Не думаю, что это кого-то очень напрягает, особенно
если основной интерес — получение качественных результатов, а не экономия времени при наборе текста. Роль синтаксиса сильно преувеличена. Но если вы чрезвычайно озабочены синтаксической поддержкой, транспилятор может с легкостью
переписать радующие ваш взор языковые конструкции в вызовы big_rational.

Tlgm: @it_boooks

Глава 6

Как работают булевы значения
○ ○ ● ● ○

Истина есть истина. У вас не может быть
собственных представлений об истине.
Питер Шикели (Peter Schickele)
Булев тип был назван в честь Джорджа Буля (George Boole), английского математика, разработавшего систему алгебраической логики. Клод Шеннон (Claude
Shannon) адаптировал эту систему для проектирования цифровых схем. Именно
поэтому мы называем компьютерные схемы логикой.
Булев тип состоит всего из двух значений: истины (true) и лжи (false). Обычно
булевы значения генерируются операторами сравнения, манипулируют ими с помощью логических операторов, а потребляют их тернарный оператор и условная
часть инструкций if, do, for и while.
Оператор typeof возвращает "boolean", когда его операнд имеет значение true
или false.
Операторы отношений

=== (знак равенства, знак равенства, знак равенства)

Равно

!== (восклицательный знак, знак равенства, знак равенства)

Не равно

< (знак меньше)

Меньше

(знак больше)

Больше

>= (знак больше, знак равенства)

Больше или равно

К великому сожалению, оператор равенства имеет вид ===, а не =. Еще печальнее
то, что оператор неравенства имеет вид !==, а не ≠. По сути, все эти операторы
работают ожидаемо, за исключением того, что при их использовании возникает
масса абсурдных моментов:
undefined < null
undefined > null
undefined === null

// false
// false
// false

Tlgm: @it_boooks
Как работают булевы значения
NaN === NaN
NaN !== NaN

// false
// true

"11" < "2"
"2" < 5
5 < "11"

// true
// true
// true

6.1

В целом операторы === и !== работают правильно, за исключением случая, когда
оба операнда имеют значение NaN. Операторы === и !== могут применяться для
определения нулевых (null) или неопределенных (undefined) значений или любого
значения, отличного от NaN. Чтобы протестировать x на значение NaN, нужно всегда
использовать выражение Number.isNaN(x).
Оператор === не должен применяться для тестирования условия завершения цикла,
если переменная инициирования очередной итерации не относится к безопасному
целочисленному диапазону. И даже тогда безопаснее воспользоваться оператором >=.
Операторы является антиподом = b, поскольку:
!(a
!(a
!(a
!(a

===

>=

b)
b)
b)
b)

===
===
===
===

(a
(a
(a
(a

!==
>
= NaN

// false
// false
// false

Было бы разумнее, чтобы значение NaN было меньше всех остальных чисел или сравнение всех прочих чисел с NaN выдавало исключение. Но вместо этого в JavaScript

Tlgm: @it_boooks

6.4

Как работают булевы значения

получается бессмыслица. Единственной значимой операцией в отношении NaN
является Number.isNaN(NaN). Во всех других контекстах от использования NaN нужно
отказываться.
В упрощении логических выражений могут помочь законы де Моргана. Я ими
пользуюсь довольно часто:
!(p && q) === !p || !q
!(p || q) === !p && !q

В JavaScript эти законы имеют силу, только если p и q не пострадали от бессмыслицы. Выражений, не являющихся булевыми, но выдающих булевы результаты,
следует избегать. Используйте вместо них булевы выражения.

Tlgm: @it_boooks

Глава 7

Как работают массивы
○ ○ ● ● ●

Здесь все пронумеровано. Монстр проходит под
нулевым номером.
Контролер планеты Х
Массивы — это самая почтенная структура данных. Массив представляет собой
непрерывную область памяти, разделенную на равные части, каждая из которых
связана с целым числом, по которому к ней можно получить быстрый доступ.
В первый выпуск JavaScript массивы включить не удалось. Но весьма высокая
эффективность объектов JavaScript практически нивелировала это упущение. Если
проигнорировать вопросы производительности, объектам подвластно все, на что
способны массивы.
С тех пор ситуация не изменилась. Индексом для массива способна послужить любая
строка. В действительности массивы JavaScript являются объектами. В современном
JavaScript массивы слегка отличаются от объектов четырьмя особенностями.

xx

У массивов есть волшебное свойство — их длина (length). Длина массива
не обязательно отражает количество его элементов. Вместо этого она определяется как самое большое порядковое число элементов плюс один. Тем самым подтверждается то, что массивы JavaScript являются настоящими массивами, что позволяет им подвергаться обработке с использованием той же
самой архаичной инструкции for, которую можно отыскать в программе на
языке С полувековой давности.

xx

Массивы наследуются из прототипа Array.prototype, который содержит
намного более богатую коллекцию методов, чем прототип Object.prototype.

xx

Массивы создаются с применением литералов массивов, а не литералов
объектов. Литералы массивов синтаксически намного проще: от нуля и более выражений, разделенных запятыми (,), помещаются между левой ([)
и правой (]) квадратными скобками.

xx

Отношение к массивам в JSON совершенно иное, нежели к объектам, хотя
в JavaScript к ним в основном одинаковое отношение.

В самом JavaScript массивы создают некоторую путаницу. Когда оператору определения типа typeof попадается массив, он возвращает строку "object", что в корне

Tlgm: @it_boooks

7.1

Как работают массивы

неверно. Чтобы определить принадлежность значения к массиву, вместо него
следует использовать функцию Array.isArray(значение):
const what_is_it = new Array(1000);
typeof what_is_it
Array.isArray(what_is_it)

// "object"
// true

Начало отсчета
Как только был изобретен счет, люди стали нумеровать предметы, начиная с тех
слов, которые использовали для обозначения единицы. В середине 1960-х годов
небольшая, но весьма влиятельная группа программистов установила, что мы
вместо этого должны начинать отсчет с нуля. Все остальное человечество, включая
большинство математиков, по-прежнему начинало считать с единицы. В начале
отсчета математики обычно ставили цифру 0, но первый элемент упорядоченного
набора большинство из них обозначало цифрой 1. Как так у них получилось, до
сих пор остается загадкой.
Аргумент в пользу начала отсчета с нуля имеется, но он настолько слаб, что им
можно пренебречь. Есть еще аргумент, согласно которому начало отсчета с нуля
более корректно, так как уменьшает количество ошибок, занижения или завышения
на единицу. Однако он также весьма сомнителен. Похоже, для объяснения того,
почему программисты считают иначе, нежели все остальные, должна быть веская
причина. Вероятно, однажды мы ее узнаем.
В JavaScript это обстоятельство отражается на нумерации элементов массива
и в меньшей степени — на нумерации символов в строке. Мысль о том, что массивы
должны обрабатываться поэлементно, возникла не позднее, чем был разработан
Фортран. Более современная идея заключается в обработке массивов функциональным образом. Это упрощает код за счет упразднения явного управления циклом и создает потенциал для распределения работы по нескольким процессорам.
Качественно написанная программа на качественно разработанном языке не должна отвлекаться на то, с чего начинается подсчет элементов в массиве, с нуля или
с единицы. Не стану уверять вас в том, что JavaScript — качественно разработанный
язык, но все же он существенно улучшен, по сравнению с Фортраном, и, отказавшись по факту от модели Фортрана, нам не следует беспокоиться о том, как именно
нумеруются элементы.
Но иногда приходится обращать на это внимание. Слово «первый» становится
проблемным, поскольку ассоциируется со словом «один», поэтому вместо него
я использую слово «нулевой» (zeroth). Это делает мою систему начала отсчета
понятнее:
[0]
[1]
[2]
[3]

нулевой
первый
второй
третий

0th
1th
2th
3th

Tlgm: @it_boooks

7.2

Как работают массивы

Если продолжить, то кажется, что на четвертом, пятом и шестом смысл несколько
теряется, но со временем, когда наименьшие целые числа останутся позади, станет
яснее, каким было начало отсчета.

Инициализация
Новый массив можно создать двумя способами:

xx
xx

указав литерал массива;
применив выражение new Array(целочисленное_значение).

let my_little_array = new Array(10).fill(0);
// получился my_little_array вида [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

Обратите внимание на то, что my_little_array и same_thing абсолютно одинаковы.
Но они также являются двумя отдельными, уникальными значениями. В отличие
от строк, но подобно объектам, идентичные массивы считаются равными только
в том случае, если это фактически один и тот же массив.

Стеки и очереди
У массивов есть методы, которые ведут себя с массивом как со стеком. Метод pop
удаляет последний элемент массива и возвращает его значение. Метод push добавляет к массиву новый элемент.
Стеки зачастую используются в интерпретаторах и калькуляторах.
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);

//
//
//
//

my_little_stack
my_little_stack
my_little_stack
my_little_stack

имеет
имеет
имеет
имеет

значение
значение
значение
значение

[]
[3]
[3, 5]
[3, 5, 7]

Tlgm: @it_boooks

7.3

Как работают массивы

mulop(my_little_stack);
addop(my_little_stack);
let answer = my_little_stack.pop();

//
//
//
//

my_little_stack
my_little_stack
my_little_stack
answer

имеет
имеет
имеет
имеет

значение
значение
значение
значение

[3, 35]
[38]
[],
38

Метод shift аналогичен методу pop, за исключением того, что он удаляет нулевой
элемент и возвращает его значение. Метод со странным названием unshift подобен методу push, за исключением того, что он вставляет новый элемент не в конец,
а в начало массива. Методы shift и unshift могут работать намного медленнее методов pop и push, особенно с большим массивом. Совместное использование shift
и push создает очередь, где новые элементы добавляются в конец, а забираются
в конечном счете с начала.

Выполнение поиска
В JavaScript имеются методы для сквозного поиска в массиве. Метод indexOf получает значение, которое сравнивает с каждым элементом массива с самого начала.
Если значение совпадает с элементом, поиск останавливается и возвращается порядковый номер элемента.
Если совпадение не обнаружено, метод возвращает –1. Заметьте, что это предпосылка для ошибки типа, поскольку число –1 весьма похоже на любое другое число.
Если воспользоваться возвращаемым значением indexOf без предварительной
проверки на –1, то вычисления, в которых это значение применяется, могут пойти
не так, как задумано, без какого-либо предупреждения. В JavaScript есть множество
ничтожно малых (bottom) значений. Одно из них и нужно взять.
Функция lastIndexOf похожа на indexOf, за исключением того, что начинает свою
работу с конца массива и ведет поиск в обратном направлении. В ней в качестве
кода неудачного завершения также используется -1.
Функция includes похожа на indexOf, но она возвращает true, если значение найдено, и false — в противном случае.

Свертка
Метод reduce свертывает массив в одно значение. Он получает функцию, которая
получает два аргумента. Метод reduce раз за разом вызывает эту функцию с парами
значений, пока не останется всего одно значение, которое и станет результатом.
Метод reduce можно создать двумя способами. Один из них заключается в том,
чтобы заставить его вызывать функцию для каждого элемента массива. Для начала
нужно предоставить исходное значение:
function add(reduction, element) {
return reduction + element;
}
let my_little_array = [3, 5, 7, 11];
let total = my_little_array.reduce(add, 0);

// total имеет значение 26

Tlgm: @it_boooks
Как работают массивы

7.4

Значение свертки передается функции add для каждого элемента наряду с текущим
элементом массива. В качестве исходного значения свертки было явно передано
число 0. Функция add видит аргументы в такой последовательности:
(0, 3)
(3, 5)
(8, 7)
(15, 11)

//
//
//
//

3
8
15
26

Каждое значение, возвращаемое add, становится значением свертки для следующего
вызова add.
Исходное значение свертки не всегда должно быть нулевым. Если reduce передается функция multiply, то исходным значением свертки должно быть число 1.
Если reduce передается Math.max, исходным значением свертки должно быть
-Infinity . Здесь есть шанс допустить ошибку, поэтому при выборе исходного
значения свертки нужно быть внимательными.
Другой способ создания reduce не требует исходного значения свертки. Вместо
этого функция вызывается на один раз меньше. При первом вызове она получает
нулевой и первый элементы. Нулевой элемент становится ее исходным значением.
total = my_little_array.reduce(add);

// 26

Теперь функция add видит аргументы в такой последовательности:
(3, 5)
(8, 7)
(15, 11)

// 8
// 15
// 26

Функция вызывается на один раз меньше, и возможность ошибки при выборе неверного исходного значения свертки исключается.
У JavaScript есть замечательная особенность, заключающаяся в том, что его метод
reduce работает в любом случае. Если ему передается исходное значение свертки,
функция вызывается для каждого элемента. Если исходное значение свертки
не передается, то для первого элемента функция не вызывается. Вместо этого первый элемент становится исходным значением свертки. Таким образом, работают
оба примера свертки путем сложения, показанные ранее.
Функция reduceRight действует практически аналогично, за исключением того,
что начинает свою работу с конца массива. Мне хотелось бы, чтобы она называлась
reduce_reverse.
Я использую метод reduce для вычисления контрольной цифры ISBN-номера
данной книги:
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

Tlgm: @it_boooks

7.5

Как работают массивы
? Number(digit)
: Number(digit) * 3

);
},
0
) % 10;
return (
check > 0
? 10 — check
: check
);

}
}
isbn_13_check_digit("978-1-94-981500") // 9

Итерация
Одной из наиболее распространенных операций над массивами является определенная работа с каждым из его элементов. Исторически это делалось с помощью
инструкции for. В JavaScript предлагается более современный подход.
Метод forEach получает массив и функцию, которую он вызывает для каждого
элемента массива. У функции может быть три параметра: element, element_nr
и array. Параметр element — это элемент массива. Параметр element_nr — порядковый номер элемента, который может пригодиться, если вычисление проводится
над другим массивом и нужна координация. Параметр array был придуман ошибочно, его здесь быть не должно. Это приглашение к модификации обрабатыва­
емого массива, что зачастую является весьма неудачной затеей.
К сожалению, мы не располагаем методом, способным обрабатывать массив в обратном порядке, как это делает reduceRight. Но есть метод reverse, который можно
вызвать предварительно, правда, он имеет разрушительный характер и не применяется к замороженным массивам.
Метод forEach игнорирует значение вызываемой им функции. Если уделить внимание возвращаемому значению, можно создать весьма интересные методы.

xx

Метод every проверяет возвращаемое значение. Если оно лживое, метод
останавливает обработку и возвращает false, если правдивое — продолжает
обработку. Если метод every дойдет до конца массива, он возвращает true.

xx

Метод some настолько похож на метод every, что непонятно, зачем он вообще
нужен в языке. Если функция возвращает правдивое значение, метод останавливает обработку и возвращает true, если значение лживое — продолжает
обработку. Если метод some доходит до конца массива, он возвращает false.

xx

Метод find похож на метод some, но вместо возвращения true или false он
возвращает элемент, который был обработан, когда функция возвратила
что-либоправдивое.

Tlgm: @it_boooks
Как работают массивы

7.6

xx

Метод findIndex похож на метод find, но вместо возвращения обработанного
элемента, когда функция возвращает что-либо правдивое, он возвращает его
порядковый номер.

xx

Метод filter также похож на метод find, но он всегда ведет обработку до
конца и возвращает массив, собирающий все элементы, для которых функция возвратила правдивое значение. Стало быть, метод find возвращает
первый соответствующий условию элемент, а метод filter — все соответствующие ему элементы.

xx

Метод map похож на метод forEach, но он собирает все возвращаемые значения и возвращает их в новом массиве. Этот метод — идеальный способ
выполнения преобразований путем создания нового массива, являющегося
усовершенствованной или развитой версией оригинала.

Эти методы обеспечивают более совершенный способ обработки массивов без использования цикла for. Но этот набор методов еще не полон.
Методы forEach и find способны на выход на ранних стадиях. (Формами forEach
с возможностью выхода являются every и some.) А методы map, reduce и filter
не имеют вариантов с ранним выходом. У метода reduce есть вариант, работающий
в обратном направлении, — reduceRight, а у методов forEach, map, filter и find
таких вариантов нет.
Эти функциональные пробелы не позволяют отказаться от применения инструкции for.

Сортировка
В JavaScript имеется метод sort. К сожалению, с ним возникает ряд проблем.
Он выполняет сортировку на месте, изменяя сортируемый массив. Это означает,
что замороженный массив отсортировать невозможно, также небезопасно сортировать совместно используемый массив.
let my_little_array = ["unicorns", "rainbows", "butterflies", "monsters"];
my_little_array.sort()
// my_little_array имеет значение ["butterflies", "monsters",
//
"rainbows", "unicorns"]

Используемая им по умолчанию функция сравнения выстраивает значения таким
образом, будто это строки, даже если они являются числами, например:
let my_little_array = [11, 2, 23, 13, 3, 5, 17, 7, 29, 19];
my_little_array.sort();
// my_little_array имеет значение [11, 13, 17, 19, 2, 23, 29, 3, 5, 7]

Это не только абсолютно неэффективно, но и неверно. К счастью, этот негатив можно сгладить, передав методу sort функцию сравнения. Этой функции передаются

Tlgm: @it_boooks

7.7

Как работают массивы

два элемента. Ожидается, что она возвратит отрицательное число, если первый
элемент должен быть первым, положительное число, если первым должен быть
второй элемент, и нуль, если функции сравнения будет нечего сказать.
Эта функция способна правильно отсортировать числовой массив, если, конечно,
все элементы будут конечными числами:
function compare(first, second) {
return first — second;
}

Если нужно сравнивать неконечные числа вроде NaN и Infinity, функции сравнения придется поработать интенсивнее.
Следующим в списке проблем, связанных с функцией sort , станет дефицит
стабильности. Функция sort стабильна, если порядок равных сравниваемых элементов (для которых функция сравнения возвратила нуль) остается неизменным.
JavaScript не гарантирует стабильности. При сортировке массива строк или
массива чисел это неактуально, а вот при сортировке массива объектов и массива
массивов становится серьезной проблемой. Сложная сортировка может потребовать сортировки по фамилиям, а при их совпадении — по именам. Один из
способов решения задачи — предварительная сортировка по именам, а затем
повторная — по фамилиям. Но этот прием не работает, поскольку сортировка
нестабильна и информация, добавленная в ходе сортировки по именам, может
быть утрачена.
Уменьшить нестабильность реально за счет применения более сложной функции
сравнения. Чтобы упростить задачу, создадим фабрику по производству функций
сравнения:
function refine(collection, path) {

Она получает массив или объект и путь в форме массива строк и возвращает значение в конце пути. Если значения нет, возвращается неопределенность.

}

return path.reduce(
function (refinement, element) {
try {
return refinement[element];
} catch (ignore) {}
},
collection
);

function by(...keys) {

Эта фабрика создает функцию сравнения, чтобы помочь в сортировке массива объектов или массива массивов. Аргументами служат одна или несколько строк или
целых чисел, определяющих сравниваемые свойства или элементы. Если первый
аргумент видит связь, пробуется второй, затем третий…

Tlgm: @it_boooks
Как работают массивы

7.8

Преобразование каждого ключа в массив строк:
const paths = keys.map(function (element) {
return element.toString().split(".");
});

Сравнение каждой пары значений, пока не будет найдено несоответствие — если
несоответствия не будет, значит, два значения равны:
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;
}

Если два значения относятся к одному и тому же типу, мы сможем сравнить их.
Если типы разные, то нужна некая политика, позволяющая справиться со странностями. Нашей простой политикой будет сравнение имен типов по принципу
boolean < number < string < undefined. (Возможно, было бы лучше отказаться от
сортировки элементов, не совпадающих по типу.)

};

}

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

Пример:
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"));
// [
//

{"first": "Ferd", "last": "Berfel"},

Tlgm: @it_boooks

7.9
//
//
//
//
//
//
//
// ]

Как работают массивы
{"first":
{"first":
{"first":
{"first":
{"first":
{"first":
{"first":

"Charcoal", "last": "Farkel"},
"Fanny", "last": "Farkel"},
"Frank", "last": "Farkel"},
"Gar", "last": "Farkel"},
"Mark", "last": "Farkel"},
"Simon", "last": "Farkel"},
"Sparkle", "last": "Farkel"}

Попурри
Метод concat получает два и более массива и объединяет их для создания нового
массива:
let part_zero = ["unicorns", "rainbows"];
let part_wun = ["butterflies", "monsters"];
let whole = part_zero.concat(part_wun);
// whole имеет значение ["unicorns", "rainbows", "butterflies", "monsters"]

Метод join получает массив, содержащий строковые значения и строку-разделитель. Он создает длинную строку, объединяющую все элементы. Если разделение
не требуется, следует в качестве разделителя воспользоваться пустой строкой.
Этот метод — антипод метода split, работающего со строками.
let string = whole.join(" & ");
// string имеет значение "unicorns & rainbows & butterflies & monsters"

Метод reverse разворачивает элементы в массиве, чтобы они оказались в нем стоящими в обратном порядке. Этот метод разрушающий, как и функция sort.
whole.reverse();
// whole имеет значение ["monsters", "butterflies", "rainbows", "unicorns"]

Метод slice способен создать копию массива или его части. Нулевой параметр
определяет, с какого порядкового номера начинать. Первый параметр — это нулевой параметр плюс количество копируемых элементов. Если первый параметр
не указан, будут скопированы все оставшиеся элементы.
let element_nr = whole.indexOf("butterflies");
let good_parts;
if (element_nr !== -1) {
good_parts = whole.slice(element_nr);
}
// good_parts имеет значение ["butterflies", "rainbows", "unicorns"]

Чистый и нечистый
Некоторые из методов работы с массивами относятся к чистым, не изменяющим
введенные в них данные. Другие — нет. Некоторые из них должны быть чистыми,
но таковыми не являются. Методы, которые не могут быть чистыми по своей природе, все же представляют определенную ценность.

Tlgm: @it_boooks
Как работают массивы

7.10

При работе с двойственностью «чистый — нечистый» важно знать, что является
чистым, а что — нет.
Чистые методы:
concat
every
filter
find
findIndex
forEach
indexOf
join
lastIndexOf
map
reduce
reduceRight
slice
some

Нечистые методы:
fill
pop
push
shift
splice
unshift

Нечистые методы, которые должны быть чистыми:
reverse
sort

Tlgm: @it_boooks

Глава 8

Как работают объекты
○ ● ○ ○ ○

Избавляйтесь от вещей с чувством благодарности.
Испытывая благодарность, вы завершаете отношения с этим объектом, и это чувство облегчает вам
расставание с ним.
Мари Кондо (Marie Kondo)
Слово «объект» в JavaScript сильно перегружено. В этом языке абсолютно все
(за исключением двух ничтожно малых значений, null и undefined ) является
объектом. Но обычно, особенно в этой главе, слово «объект» означает нечто более
конкретное.
Первичная структура данных в JavaScript называется объектом. Объект является
контейнером для свойств или элементов. У каждого свойства есть имя и значение.
Имя является строкой. Значение может быть любого типа. В других языках такой
тип объекта называется хеш-таблицей, отображением, записью, структурой, ассоциативным массивом, словарем, а в некоторых слишком грубых языках — диктом
(dict).
Новый объект создается с помощью литерала объекта. Этот литерал создает значение, которое может храниться в переменной, или в объекте, или в массиве, или же
передаваться функции, или возвращаться из нее.
Литерал объекта заключается в фигурные скобки ({ }). Внутри них должно быть
ноль или более свойств, разделенных запятыми (,). Свойствами могут быть:

xx

строковое значение, за которым следуют двоеточие (:) и выражение. Имя свойства определяется строковым значением. Значением свойства является
значение выражения;

xx

имя, за которым следуют двоеточие и выражение. Именем свойства является
имя, преобразованное в строковое значение, значением свойства — значение
выражения;

xx

имя. Именем свойства является имя, преобразованное в строковое значение. Значением свойства является значение переменной или параметра
с таким же именем;

Tlgm: @it_boooks

8.1

Как работают объекты

xx

имя, за которым следуют список параметров, заключенный в круглые скобки (( )), и тело функции, заключенное в фигурные скобки ({ }). Это сокращение для имени, после которого стоит двоеточие, а затем выражение
функции. Оно позволяет не указывать : function, но при этом разборчивость
кода ухудшается, так что пользоваться им, пожалуй, не стоит.

Например:
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.";
}
};

Обратиться к свойству объекта можно с помощью точечной записи с указанием
имени:
my_little_object.foo === my_little_object.bar

// true

Можно также обратиться к свойству объекта, воспользовавшись скобочной
записью. Ее применяют для доступа к свойству, чье имя не является допустимым идентификатором. Скобочная запись используется также для вычисляемых
имен свойств. Если нужно, выражение в скобках вычисляется и преобразуется
в строку.
my_little_object["0/0"] === 0

// true

Если с помощью точечной или скобочной записи запрашивается имя, которое
не может быть найдено в объекте, то вместо свойства предоставляется ничтожно
малое значение undefined. Запрос несуществующего или утраченного свойства
не считается ошибкой. Это обычная операция, выдающая undefined:
my_little_object.rainbow
my_little_object[0]

// undefined
// undefined

Новое свойство можно добавить к объекту с помощью операции присваивания.
Таким же образом могут быть заменены значения существующих свойств.
my_little_object.age = 39;
my_little_object.foo = "slightly frilly or fancy";

Я рекомендую не хранить undefined в объектах. JavaScript это допускает и корректно возвращает значение undefined, но это undefined ничем не отличается от
того, что означает отсутствие свойства. Стоит ли создавать путаницу, без которой
легко обойтись? Желательно, чтобы сохранение undefined приводило к удалению
свойства, но этого не происходит. Свойство можно удалить с помощью оператора
delete:
delete my_little_object["0/0"];

Tlgm: @it_boooks

8.2

Как работают объекты

Из объектов можно выстраивать сложные структуры данных, поскольку в объектах могут храниться ссылки на объекты. Можно построить разнообразные графы
и циклические структуры. Глубина вложения не лимитирована, но благоразумие
здесь не помешает.
Когда оператору typeof предоставляется объект, он выдает строку "object":
typeof my_little_object === "object" // true

Регистр
Проверка на соответствие ключевому значению чувствительна к регистру символов. Поэтому my_little_object.cat — это не то же самое, что my_little_object.Cat
или my_little_object.CAT. Соответствие строковых значений имен свойств определяется оператором ===.

Копирование
Функция Object.assign способна скопировать свойства из одного объекта в другой.
Копию объекта можно создать, присвоив его пустому объекту.
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

Объекту может присваиваться материал из множества объектов. Таким образом,
сложный объект реально сконструировать путем сбора материала из более простых.

Наследование
В JavaScript объект может быть создан наследованием из другого объекта. Это совершенно иной тип наследования, чем тот, что практикуется в языках, имеющих
такие тесно связанные программные структуры, как классы. В JavaScript это связанные данные, которые могут существенно уменьшить уязвимость, способную
охватить всю структуру приложения.
Метод Object.create(прототип) получает существующий объект и возвращает
в качестве его наследника новый объект. Существующий объект становится прототипом нового объекта. Прототипом может стать любой объект. Объект — наследник
прототипа может также стать прототипом еще более новых объектов. Ограничений
на длину цепочки прототипов не существует, но лучше делать ее покороче.
При попытке обращения к несуществующему свойству перед возвращением
undefined система сначала обращается к прототипу, а затем к его прототипу и т. д.

Tlgm: @it_boooks

8.3

Как работают объекты

Если свойство с таким же именем будет найдено в цепочке прототипов, оно будет
выдано, как будто было найдено в интересовавшем нас объекте.
При обращении к объекту изменяется только самый близкий объект. Объектов по
цепочке прототипов изменения не касаются.
let my_clone = Object.create(my_little_object);
my_clone.bar
// "a long rod or rigid
my_clone.age
my_clone.age += 1;
my_clone.age
delete my_clone.age;
my_clone.age

piece of wood or metal"
// 39
// 40
// 39

Чаще всего прототипы задействуются в качестве места для хранения функций.
Этот прием используется и в самом JavaScript. Объект, созданный с помощью литерала объекта, является наследником Object.prototype. Аналогично этому массивы
наследуют методы от Array.prototype. Числа наследуют методы от Number.prototype.
Строки наследуют методы от String.prototype. Даже функции наследуют методы
от Function.prototype. Методы массивов и строк весьма полезны, а вот методы
в Object.prototype в большинстве своем бесполезны.
Поскольку речь идет о наследовании, теперь у нас есть два типа свойств: собственные, присущие самому верхнему объекту, и унаследованные, присущие цепочке
прототипов. В большинстве случаев они работают одинаково. Иногда необходимо
знать, действительно ли свойство принадлежит самому объекту. Большинство
объектов наследуют функцию hasOwnProperty(строка), но, к сожалению, ее надежность оставляет желать лучшего. Она получает строковое значение и возвращает
true, если объект содержит свойство с указанным именем и это свойство неуна­
следованное. Если же у объекта есть свойство с именем hasOwnProperty, то вместо
унаследованного метода Object.prototype.hasOwnProperty будет вызвано именно
оно. Это может привести к сбоям или путанице. Было бы лучше, чтобы hasOwnPro­
perty был не методом, а оператором, тогда состояние объекта не смогло бы привести к сбойному вызову hasOwnProperty. Еще лучше, если бы не было унаследованных свойств, что сделало бы этот проблемный метод ненужным.
my_little_object.hasOwnProperty("bar")
my_copy.hasOwnProperty("bar")
my_clone.hasOwnProperty("bar")
my_clone.hasOwnProperty = 7;
my_clone.hasOwnProperty("bar")

// true
// false
// false
// ИСКЛЮЧЕНИЕ!

При наличии свойства hasOwnProperty воспользоваться унаследованным методом
hasOwnProperty невозможно. Вместо него вызов будет обращен к собственному
свойству объекта.
Тот же риск сбоев характерен и для метода Object.prototype.toString. Но, даже
когда он работает, без разочарований не обходится.
my_clone.toString

// "[object Object]"

Tlgm: @it_boooks

8.4

Как работают объекты

Не нужно говорить, что объект является объектом. Это не секрет. Хотелось бы посмотреть на его содержимое. Если нужно преобразовать объект в строку, JSON.stringify
справится с этим намного лучше.
Преимущество Object.create(прототип) над Object.assign(Object.create({}),
прототип) заключается в меньшей потребности в памяти. В большинстве случаев
экономия незначительна. Прототипы могут привнести ряд странностей, не дав
особых дополнительных преимуществ.
Существует также проблема непреднамеренного наследования. Возможно, вам
захочется воспользоваться объектом наподобие хеш-таблицы, но объект наследует имена, например "toString", "constructor" и т. д., которые могут зависеть от
реализации. Потенциально их можно спутать с принадлежащими вам именами.
К счастью, Object.create(null) создает объект, не обремененный наследственностью.
Не возникает никакой путаницы ни с унаследованными свойствами, ни с непреднамеренным наследованием. В объекте нет ничего, кроме того, что в него помещено
явным образом. Я сейчас пользуюсь Object.create(null) весьма часто.

Ключи
Функция Object.keys(объект) может взять все собственные (неунаследованные)
имена свойств, имеющихся у объекта, и возвратить их в виде строкового массива.
Это позволяет воспользоваться методами работы с массивами для обработки
свойств объекта.
Строковые значения в массиве будут располагаться в том порядке, в котором они были
вставлены. Если нужен иной порядок, можно воспользоваться Array-методом sort.

Заморозка
Функция Object.freeze(объект) способна получить объект и заморозить его,
сделав неизменяемым. Неизменяемость позволяет создавать более надежные
системы. Если объект создается в соответствии с вашими предпочтениями, он
может быть заморожен, что гарантирует невозможность его повреждения или
вмешательства в его содержимое. Заморозка неглубока, она воздействует только
на объекты верхнего уровня.
Неизменяемые объекты могут когда-нибудь обрести ценные характеристики
производительности. Если известно, что объект нельзя изменить, в реализации
языка появляется возможность применения весьма эффективного набора средств
оптимизации.
У неизменяемых объектов имеются великолепные средства обеспечения безопасности. Их важность определяется общепринятой современной практикой,

Tlgm: @it_boooks

8.5

Как работают объекты

позволяющей устанавливать в ваши системы сомнительный код. Когда-нибудь
неизменяемость поможет превратить все это в безопасный метод работы. Неизменяемые объекты можно будет снабдить высококачественными интерфейсами,
способными их защитить.
Функция Object.freeze(объект) и инструкция const действуют абсолютно поразному. Object.freeze оперирует значениями, а const — переменными. Если поместить изменяемый объект в const-переменную, вероятность изменения объекта
сохранится, но вы не сможете заменить объект каким-либо другим значением.
Если поместить неизменяемый объект в обычную переменную, вы не измените
объект, но сможете присвоить переменной другое значение.
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;

//
//
//
//

разрешено
СИНТАКСИЧЕСКАЯ ОШИБКА!
ИСКЛЮЧЕНИЕ!
разрешено

Прототипы и заморозка не смешиваются
Одна из задач, решаемых с помощью прототипов, — это создание легких копий
объектов. У нас есть объект, заполненный данными. Нам нужен другой точно такой же объект, но с одним измененным свойством. Как уже было показано, сделать
это реально с помощью метода Object.create. Это позволит сэкономить немного
времени на создании нового объекта, но извлечение свойств может обойтись дороже, поскольку придется просматривать цепочку прототипов.
К сожалению, это не сработает, если прототип заморожен. Если свойство в прототипе неизменяемое, экземпляр не способен иметь собственную версию этого
свойства. В некоторых стилях функционального программирования для повышения надежности на основе неизменяемости требуется, чтобы все объекты были
заморожены. Поэтому вместо создания копии для внесения изменения было бы
неплохо получить возможность создать экземпляр, получающий наследство от
замороженного прототипа, изменить его, а затем заморозить. Но работоспособных вариантов просто нет. При обновлении экземпляра выдается исключение.
Кроме того, в целом замедляется вставка новых свойств. Когда вставляется новое
свойство, то, чтобы определить, что у предка нет неизменяемого свойства, в поисках
ключа нужно просмотреть всю цепочку прототипов. Избежать этого при создании
объектов позволяет использование функции Object.create(null).
Одна из конструктивных ошибок в JavaScript заключается в том, что именами свойств в объектах должны быть строки. Бывают ситуации, когда требуется
в качестве ключа воспользоваться объектом или массивом. К сожалению, объекты JavaScript в таком случае ведут себя неправильно, преобразуя ключ-объект

Tlgm: @it_boooks

8.6

Как работают объекты

в ключ-строку с помощью метода toString, что, как мы уже видели, приводит
к разочарованиям.
Вместо предоставления нам объекта, правильно работающего для всех ключей,
JavaScript дает другой тип объектов — WeakMap, который позволяет быть ключами
объектам, а не строкам и у которого совершенно иной интерфейс.
Объект

WeakMap

object = Object.create(null);

weakmap = new WeakMap();

object[ключ]

weakmap.get(ключ)

object[ключ] = значение;

weakmap.set(ключ, значение);

delete object[ключ];

weakmap.delete(ключ);

В таком существенном синтаксическом различии двух типов объектов, работающих одинаково, нет никакого смысла. Также нет смысла в том, что их два,
а не один. Вместо одного типа объекта, который позволяет задействовать в качестве ключей только строки, и другого, позволяющего использовать только объекты, должен быть один тип объекта, позволяющий брать в качестве ключей как
строки, так и объекты.
При всем этом WeakMap — великолепный тип объекта. Рассмотрим два примера его
использования.
Нам нужно поместить в объект секретное свойство. Для получения доступа к секретному свойству нужны доступ к объекту и секретный ключ. Получить доступ
к свойству невозможно, пока не будет и того и другого. Это можно сделать с WeakMap,
рассматривая его в качестве секретного ключа:
const secret_key = new WeakMap();
secret_key.set(object, secret);
secret = secret_key.get(object);

Чтобы раскрыть секрет, нужен доступ как к объекту, так и к секретному ключу.
У этого приема есть весьма полезное качество: секретные свойства могут запросто
добавляться к замороженным объектам.
Нам нужно получить возможность предоставления объекта некоторому коду, который способен сделать что-то полезное, например внести его в список или сохранить
для дальнейшего извлечения, но при этом не хочется давать этому коду возможность изменить объект или вызвать какой-либо из его методов. В реальном мире
нам хочется, чтобы работник запарковал машину, но при этом не рылся в бардачке
и багажнике и не продал ее. В реальном мире доверять людям можно, но относиться
с доверием к коду в компьютерной сети крайне неразумно.
Поэтому создадим устройство, называемое установщиком пломбы (sealer). Мы дадим ему объект для установки пломбы, а он вернет контейнер, который нельзя

Tlgm: @it_boooks
Как работают объекты

8.7

будет открыть. Этот контейнер может быть отдан работнику. Чтобы получить
обратно исходный объект, нужно отдать контейнер соответствующему съемщику
пломбы (unsealer). Все эти функции довольно легко создаются с помощью WeakMap:
function sealer_factory() {
const weakmap = new WeapMap();
return {
sealer(object) {
const box = Object.freeze(Object.create(null));
weakmap.set(box, object);
return box;
},
unsealer(box) {
return weakmap.get(box);
}
};
}

Объект weakmap не позволяет проверять свое содержимое. Из него невозможно
извлечь значение, пока не будет ключа. WeakMap хорошо взаимодействует со сборщиком мусора JavaScript. Если копий ключа все еще не существует, свойство этого
ключа в weakmap автоматически удаляется. Это помогает избежать утечек памяти.
В JavaScript имеется нечто похожее под названием Map, но там не обеспечены
безопасность и защита от утечек памяти, имеющиеся у WeakMap. И хотя WeakMap
(слабое отображение) далеко не самое подходящее название, Map (отображение)
вносит еще бо' льшую путаницу. Это название никак не связано с Array-методом
map, и его нетрудно спутать с функцией картографии. Поэтому я не рекомендую
использовать Map. Но мне нравятся WeakMap и Array-функция map.
В JavaScript имеется также функция под названием Symbol, способная выполнять
одно из действий, присущих WeakMap. Пользоваться Symbol не стоит, поскольку
в этом нет необходимости. Я стремлюсь к упрощению, избавляясь от ненужных
мне избыточных функций.

Tlgm: @it_boooks

Глава 9

Как работают строки
○ ● ○ ○ ●

— Нечестно, нечестно! — зашипел он. — Ведь правда,
моя прелесть, ведь нечестно спрашивать нас, что
у него в мерзком кармашке?
Голлум

Компьютеры хороши для манипуляции хитросплетениями битов. Людям же это
несвойственно. Строки перебрасывают мост через пропасть между компьютерами
и людьми. Отображение символов на целые числа было одним из важных достижений в разработке цифровых вычислительных устройств. Это был первый важный
шаг к разработке пользовательских интерфейсов.
Мы не знаем, почему этот тип называется строкой. Почему он не назван текстом?
Никто не знает. Строка JavaScript не похожа на фрагмент обычной строки. Можно
говорить о строке символов и с такой же легкостью — о строке инструкций, или
строке битов, или даже о строке сбоев. В реальном мире мы строки не объединяем.
Мы их связываем.

Основы
Строка представляет собой неизменяемый массив из 16-разрядных целых чисел в диапазоне от 0 до 65 535. Строка может быть создана с помощью функции
String.fromCharCode, получающей любое количество чисел. Обратиться к элементу
строки можно с помощью метода charCodeAt. Элементы не могут быть изменены,
поскольку строки всегда заморожены. У строк, как и у массивов, есть свойство
длины — length.
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"

Tlgm: @it_boooks

9.1

Как работают строки

Для получения из строки отдельных значений можно применить скобочную запись,
но она не предоставляет число, как в мире массивов. Вместо этого возвращается
новая строка единичной длины, содержимым которой является число.
my_little_string[0] === my_little_array[0]
my_little_string[0] === String.fromCharCode(99)

// false
// true

В прототипе String.prototype содержатся методы, работающие со строками.
Методы concat и slice работают практически так же, как и аналогичные методы
работы с массивами. А вот метод indexOf работает совершенно иначе. Аргументом
для indexOf служит строка, а не число. Этот метод пытается сопоставить все элементы в аргументе последовательно со всеми элементами в первой строке:
my_little_string.indexOf(String.fromCharCode(111, 114))
my_little_string.indexOf(String.fromCharCode(111, 110))

// 1
// -1

Методы startsWith, endsWith и contains — всего лишь оболочки, заключающие
в себе indexOf и lastIndexOf.
Оператор === считает строки с одинаковым содержимым равными. А подобные
массивы равны только при условии, что это один и тот же массив.
my_little_array === my_little_array
my_little_array === [99, 111, 114, 110]
my_little_string === String.fromCharCode(99, 111, 114, 110)

// true
// false
// true

Равенство строк — весьма эффективный механизм. Он исключает необходимость
в символьном типе, поскольку одинаковые строки считаются одним и тем же объектом, что совершенно не свойственно менее востребованным языкам типа Java.

Юникод
В качестве элементов строк JavaScript позволяет задействовать все 65 536 бит
16-разрядной раскладки. Но на практике каждый элемент считается символом,
а стандарт Юникод определяет кодировку символов. В JavaScript имеется серьезная
синтаксическая и методическая поддержка Юникода. Согласно стандарту Юникод некоторые коды не являются символами и использоваться не должны. Но для
JavaScript это ничего не значит, и в нем допускается применение всех 16-битных
кодов. Если предполагается взаимодействие ваших систем с системами, написанными на менее востребованных языках, то злоупотреблять Юникодом не стоит.
Строковые литералы записываются путем заключения нуля и более символов
Юникода в двойные кавычки ("). (Можно использовать и одинарные кавычки ('),
но лучше этого не делать, поскольку в этом нет необходимости.) Каждый символ
представлен 16-разрядным элементом.
Для объединения применяется оператор в виде знака «плюс» (+). Нам уже встречался
знак +, используемый и для сложения. Перед тем как выполнить объединение, следует
убедиться, что хотя бы один операнд — это строка. Один из способов выполнения

Tlgm: @it_boooks

9.2

Как работают строки

этого условия предусматривает задействование в качестве одного из операндов строкового литерала. Другой способ предполагает передачу значения функции String:
my_little_string === "corn"
"uni" + my_little_string
3 + 4
String(3) + 4
3 + String(4)

//
//
//
//
//

true
"unicorn"
7
34
34

Строковый литерал должен полностью помещаться в одной строчке кода. Обратный
слеш (\) используется в качестве символа отмены действия специального символа,
что позволяет включать в строковые литералы двойную кавычку ("), сам обратный
слеш и символ перевода строки.
Для извлечения символов из строки могут применяться скобки. Представлением
символа является строка единичной длины. Символьный тип отсутствует. Символ
может быть представлен как число или строка.
Символьный тип есть во многих языках, и зачастую для его обозначения используется термин char, но, похоже, произношение этого термина в соответствии с его
видом стандартным не стало. Мне приходилось слышать, что его произносят так
же, как слова car, care, chair, char и share.
Все строки замораживаются при создании. Созданная строка не может быть изменена. Фрагменты строк можно скопировать. Каждый фрагмент является новой
строкой. Новые строки могут создаваться путем объединения строк.
const mess = "monster";
const the_four = "uni" + my_little_string + " rainbow butterfly " + mess;
// the_four имеет значение "unicorn rainbow butterfly monster"
const first = the_four.slice(0, 7);
const last = the_four.slice(-7);
const parts = the_four.split(" ");

parts[2][0] === "b"
"*".repeat(5)
parts[1].padStart(10, "/")

//
//
//
//
//
//
//
//
//
//
//

first имеет значение "unicorn"
last имеет значение "monster"
parts имеет значение [
"unicorn"
"rainbow"
"butterfly"
"monster"
]
true
"*****"
"///rainbow"

И снова Юникод
Исходной задачей Юникода было представление всех существующих на свете
языков в 16 битах. Затем его характер изменился и он стал представлять все языки
мира в 21 бите.
К сожалению, JavaScript был разработан в эпоху 16-битного Юникода. Символ
JavaScript Юникодом разбивается на два новых способа выражения: кодовую еди-

Tlgm: @it_boooks

9.3

Как работают строки

ницу, или кодовое значение (code unit), и кодовую точку, или позицию (code point).
Кодовая единица — это один из 16-битных символов. А кодовая точка — это число,
соответствующее символу. Кодовая точка формируется из одной и более кодовых
единиц.
В Юникоде определены 1 114 112 кодовых точек, разделенных на 17 плоскостей
(planes) из 65 536 кодовых точек. Исходная плоскость называется основной многоязычной плоскостью — Basic Multilingual Plane (BMP). Остальные 16 плоскостей —
дополнительные. JavaScript способен без каких-либо трудностей использовать символы в BMP, поскольку там кодовая точка может быть идентифицирована с одной
кодовой единицей. Работать с дополнительными символами труднее.
JavaScript обращается к дополнительным символам, применяя суррогатные пары.
Такие пары составляются из двух специализированных кодовых единиц. Существуют 1024 старшие суррогатные кодовые единицы и 1024 младшие суррогатные кодовые единицы. У старших суррогатных кодовых единиц коды ниже, чем у младших:

xx
xx

от 0xD800 до 0xDBFF — старшие суррогатные кодовые единицы;
от 0xDC00 до 0xDFFF — младшие суррогатные кодовые единицы.

Когда пары составлены правильно, каждая суррогатная кодовая единица вносит
10 бит для формирования 20-разрядного смещения, к которому добавляется 65 536
для формирования кодовой точки. (Дополнение было сделано, чтобы избавиться от
путаницы, вызванной наличием двух разных последовательностей кодовых единиц,
которые могли бы создавать одну и ту же кодовую точку. Полагаю, что это решение
привнесло еще больше путаницы, чем та, которой удалось избежать. Более простым
решением было бы запрещение использования суррогатных пар для представления
кодовых точек в BMP. Это дало бы не 21-битный, а 20-битный набор символов.)
Рассмотрим кодовую точку U+1F4A9 (или в десятичном представлении 128169).
Вычтем 65 536, что даст 62 633, или 0xF4A9. Старшие 10 бит — 0x03D. Младшие
10 бит — 0x0A9. Прибавим 0xD800 к старшим битам и 0xDC00 к младшим битам
и получим суррогатную пару. Таким образом, JavaScript хранит U+1F4A9 как
U+D83D U+DCA9. С точки зрения JavaScript U+1F4A9 — это два 16-битных символа.
Если операционная система в курсе, то они будут отображаться как один символ.
Фактически JavaScript ничего не знает о дополнительных плоскостях, но не проявляет по отношению к ним открытой враждебности.
Существует два способа записи кодовой точки U+1F4A9 в строковом литерале с применением эскейп-символа:
"\uD83D\uDCA9" === "\u{1F4A9}"

// true

Оба они приводят к созданию одной и той же строки длиной 2.
Есть функция String.fromCodePoint, способная создавать суррогатные пары:
String.fromCharCode(55357, 56489) === String.fromCodePoint(128169)

// true

Tlgm: @it_boooks

9.4

Как работают строки

Метод codePointAt аналогичен методу charCodeAt, за исключением того, что он
может выполнять поиск также в следующем символе и, если символы формируют
суррогатную пару, возвращает дополнительную кодовую точку:
"\uD83D\uDCA9".codePointAt(0)

// 128169

В BMP может быть найдено большинство тех символов, которые люди используют
для глобального общения, поэтому суррогатные пары обычно не нужны. Но даже
при этом дополнительные символы могут появиться в любой момент, и вашей программе следует ожидать их возникновения.
В Юникоде содержатся комбинированные и модифицирующие символы, добавляющие к буквам диакритические знаки и вносящие другие изменения. В нем
также содержатся символы, управляющие направлением письма и другими сервисами. Иногда это способно усложнить ситуацию, и получится, что две строки,
казалось бы содержащие одинаковые символы, могут не совпасть. В Юникоде
существуют правила нормализации, которые определяют порядок появления
символов и те случаи, когда базовые символы с модификаторами должны заменяться составными символами. Люди такими правилами не связаны, поэтому вам,
возможно, придется работать с ненормативным материалом. В JavaScript правила
нормализации заключены в методе normalize:
const combining_diaeresis = "\u0308";
const u_diaeresis = "u" + combining_diaeresis;
const umlaut_u = "\u00FC";
u_diaeresis === umlaut_u
u_diaeresis.normalize() === umlaut_u.normalize()

// false
// true

В Юникоде содержится множество дубликатов и схожих символов, поэтому сохраняется вероятность наличия строк, одинаковых по виду, но неодинаковых по коду,
даже после нормализации. Это вызывает путаницу и создает угрозу безопасности.

Шаблонные строковые литералы
Применение шаблонов — весьма популярная практика при разработке вебприложений. Поскольку имеющийся в веб-браузере DOM API недоразвит и чреват
появлением ошибок, обычной практикой стало создание вместо него HTMLпредставлений с использованием шаблонов. Это может существенно упростить
работу по сравнению с борьбой с DOM-моделью, однако способно также стать
причиной XSS-атак и других неприятностей в области веб-безопасности.
Имеющиеся в JavaScript шаблонные строковые литералы были предназначены для
поддержки шаблонов и смягчения проблем безопасности, а иногда и частичного
их решения.
Шаблонные строковые литералы — это строковые литералы, способные размещаться сразу на нескольких строках текста. В качестве разделителя используется
символ ` (гравис).

Tlgm: @it_boooks
Как работают строки

9.5

const old_form = (
"Can you"
+ "\nbelieve how"
+ "\nincredibly"
+ "\nlong this"
+ "\nstring literal"
+ "\nis?"
);
const new_form = `Can you
believe how
incredibly
long this
string literal
is?`;
old_form === new_form // true

Дает ли новая форма существенные преимущества по сравнению со старой? Есть небольшое преимущество в обозначениях, но оно обходится недешево: самые большие
синтаксические структуры в языке представлены самыми незаметными символами
клавиатуры. Из-за этого возрастает вероятность совершения ошибок. Добавляется
также визуальная неразборчивость. Может ли случиться что-либо невероятное?
При использовании старой формы ответ однозначно отрицательный.
В большинстве случаев текстовый материал крупных форм не должен храниться
в программах. Он должен обрабатываться соответствующим инструментарием,
например текстовым редактором, или редактором JSON, или инструментарием
баз данных, а затем становиться доступным программе в виде привязки к ресурсу.
Новый синтаксис поощряет выработку вредных привычек.
Зачастую строки применяются для записи слов естественного языка. Чтобы обслуживать всемирную аудиторию, такие тексты обычно должны быть переведены.
Нам не хочется поддерживать отдельную версию каждого программного файла
для каждого языка. Хочется иметь единую версию каждого программного файла,
способную получать локализованный текст. Новая форма усложняет решение этой
задачи, поскольку ограничивает способы представления программам шаблонных
строк.
Преимущество нового синтаксиса в следующем: он позволяет использовать вставки.
Допустимое выражение можно поместить внутрь конструкции в составе шаблонного строкового литерала, состоящей из знака доллара ($), открывающей фигурной
скобки ({) и закрывающей фигурной скобки (}). Выражения вычисляются, преобразуются в строки и вставляются в шаблонный строковый литерал:
let fear = "monsters";
const old_way = "The only thing we have to fear is " + fear + ".";
const new_way = `The only thing we have to fear is ${fear}.`;
old_way === new_way // true

Tlgm: @it_boooks

9.6

Как работают строки

Опасность с обеих сторон в том, что контент может быть чем-то угрожающим, например:
fear = "";

В веб-технологиях существует масса способов внедрения вредоносного материала.
И шаблоны, похоже, усиливают уязвимость. Большинство инструментов работы
с шаблонами предоставляют механизмы уменьшения опасности, но все же позволяют проявляться вредоносному коду. И что хуже всего, средства, подобные шаблонным строковым литералам с возможностью применения вставок, изначально
небезопасны.
В этом случае возможность смягчения последствий выражается в особой функции,
называемой функцией отличительного признака (тег-функцией). Функциональное выражение, предшествующее шаблонному строковому литералу, инициирует
вызов функции с шаблонной строкой и значениями выражений в качестве ее аргументов. Смысл заключается в том, что тег-функции передаются все части, которые
затем ею фильтруются, декодируются, собираются и возвращаются.
Функция dump — это тег-функция, которая просто возвращает все введенные в нее
материалы в удобочитаемой форме:
function dump(strings, ...values) {
return JSON.stringify({
strings,
values
}, undefined, 4);
}
const what = "ram";
const where = "rama lama ding dong";
`Who put the ${what} in the ${where}?`
// "Who put the ram in the rama lama ding dong?"
const result = dump`Who put the ${what} in the ${where}?`;
// result имеет значение `{
//
"strings": [
//
"Who put the ",
//
" in the ",
//
"?"
//
],
//
"values": [
//
"ram",
//
"rama lama ding dong"
//
]
// }`

Странно, что строки передаются вместе в виде массива, а значения — в виде отдельных аргументов. При должной разработке тег-функции способны смягчить угрозы
со стороны XSS и других уязвимостей безопасности, конечно, если эти функции
действительно применяются. Но напомню еще раз: шаблонные строковые литералы
с возможностью использования вставок изначально небезопасны.

Tlgm: @it_boooks
Как работают строки

9.7

Шаблонные строковые литералы добавили множество новых элементов синтаксиса
и механизмов и усложнили язык. Они подталкивают к выработке вредных привычек,
но дают некоторые преимущества в предоставлении больших строковых литералов.

Регулярные выражения
Строковые функции match, replace, search, split способны принимать в качестве
аргументов объекты регулярных выражений. Эти объекты также могут содержать
собственные методы exec и test.
Объекты регулярных выражений проверяют строки на соответствие шаблону.
Объекты регулярных выражений лаконичны, что затрудняет их понимание, но
очень выразительны. Они эффективны, однако их возможности далеко не безграничны. Например, эффективности объектов регулярных выражений не хватает
для разбора текста в формате JSON. Дело в том, что для парсинга JSON требуется какое-то хранилище наподобие стека, позволяющее работать с вложенными
структурами. У объектов регулярных выражений таких хранилищ нет. Объекты
регулярных выражений могут разбивать текст в формате JSON на лексемы, что
существенно упрощает создание JSON-парсера.

Разбор данных на лексемы
Компиляторы выполняют разбор исходных программ на лексемы, что является
частью процесса компиляции. Есть и другие программы, также зависящие от разбора данных на лексемы: редакторы, оформители кода, анализаторы статистики,
макропроцессоры и минификаторы. Программы с повышенной интерактивностью,
такие как редакторы, нуждаются в быстром разборе на лексемы. К сожалению, язык
JavaScript слишком сложен для разбора на лексемы.
Это связано со взаимными помехами, создаваемыми литералами регулярных выражений и автоматической вставкой точек с запятой. В результате возникают
неоднозначности, затрудняющие интерпретацию программы, например:
return /a/i;

// возвращает объект регулярного выражения

return b.return /a/i;

// возвращает ((b.return) / a) / i

Из этого следует, что в целом корректный разбор программ JavaScript невозможен
без одновременного проведения полного парсинга.
Ситуация еще больше усложняется при использовании шаблонных строковых
литералов. В литерал может быть встроено выражение, а в него вложен еще один
шаблонный строковый литерал. И такое построение способно уходить на любую
глубину. В выражениях могут также содержаться литералы регулярных выражений
и строки с грависами (backticks). Иногда очень сложно определить, закрывает гравис текущий литерал или же открывает вложенный литерал. Бываеттакже сложно
определить, какова структура какой-либо строчки кода:
`${`${"\"}`"}`}`

Tlgm: @it_boooks

9.8

Как работают строки

Может быть и еще хуже. Эти выражения могут также содержать функции, которые,
в свою очередь, могут содержать литералы регулярных выражений и вместо точек
с запятыми еще какие-нибудь шаблонные строковые литералы.
Инструментальные средства, такие как редакторы, станут предполагать, что с программами все в порядке. Имеется поднабор JavaScript, который можно разобрать на
лексемы без сопутствующего полного парсинга. Если от всего этого у вас наступает
помрачение рассудка, то нужно ожидать, что ваш инструментарий не сработает.
Надеюсь, что следующий язык не создаст проблем с разбором на лексемы. А пока
он не появился, я рекомендую не использовать вставки с шаблонными строковыми
литералами.

Функция fulfill
Рассмотрим альтернативу шаблонным строковым литералам — функцию fulfill.
Она получает строку, в которую могут включаться символические переменные,
и объект или массив, содержащий значения, которые требуется заменить символическими переменными. Она также может принимать либо функцию, либо объект,
состоящий из функций и способный кодировать замены.
Потенциальная проблема с шаблонными строками заключается в том, что все
находящееся в области видимости доступно для включения кода. Это очень
удобно, но одновременно угрожает безопасности. Функция fulfill имеет доступ
только к контейнеру значений, переданных ей в явном виде, поэтому безопасна.
Исходный кодировщик удаляет из определенного контекста символы, известные
как источники угроз, поэтому контекст HTML изначально безопасен, в отличие
от изначально небезопасных шаблонных строк. Но, как и включение шаблонных
строк, такое включение способно стать опасным при неправильном использовании
кодировщиков.
Строка может поступать из любого источника, такого как информационное наполнение JSON или строковый литерал. Ей не нужно находиться в том же самом
файле JavaScript, что и программа. Это лучше вписывается в потребности локализованных приложений.
Символическая переменная заключается в фигурные скобки ({ }). Она не может
содержать пробелов или скобок. В ней могут быть путь и дополнительно двоеточие,
за которым следует название системы кодирования. Путь состоит из одного или
нескольких имен или чисел, разделенных точками (.):
{a.very.long.path:hexify}

Контейнер является объектом или массивом, содержащим значения, заменяющие
символические переменные. В нем могут содержаться вложенные объекты и массивы. Значения будут найдены с использованием пути. Если заменяемым значением является функция, она вызывается и ее возвращаемое значение становится

Tlgm: @it_boooks
Как работают строки

9.9

заменяющим значением. Сам контейнер может быть функцией. Она вызывается
с передачей пути и системы кодирования, чтобы получить заменяющее значение.
Затем заменяющее значение может быть преобразовано кодировщиком. Это делается главным образом для смягчения проблем безопасности, исходящих от шаблонов,
но способно служить и другим целям.
Необязательный аргумент кодировщика может быть объектом, содержащим
функции кодирования. Та часть символической переменной, которая относится
к кодировщику, выбирает один из кодировщиков. Если в объекте кодировщика
нет подходящей функции, исходная символическая переменная не заменяется.
Аргумент кодировщика может быть также функцией кодирования, вызываемой со
всем содержимым символических переменных. Функции кодировщика передаются
кандидат для замены, путь и система кодирования. Если она не возвращает строку
или число, исходная символическая переменная не заменяется.
Если в символической переменной не указана система кодирования, предполагается, что она имеет вид "".
Путь содержит число, соответствующее элементу в массиве значений, или имя
свойства в объекте значений. Путь может содержать точки, позволяющие находить
свойства вложенных объектов и массивов.
Символические переменные заменяются, только если они правильно сформированы, имеется значение для замены и указана и правильно реализована система
кодирования. Если что-то не так, символическая переменная остается на месте.
Скобки, используемые в качестве литералов, можно помещать в строку, не устанавливая перед ними эскейп-символы. Если они не являются частью символической
переменной, их оставляют в покое.
const example = fulfill(
"{greeting}, {my.place:upper}! :{",
{
greeting: "Hello",
my: {
fabulous: "Unicorn",
insect: "Butterfly",
place: "World"
},
phenomenon : "Rainbow"
},
{
upper: function upper(string) {
return string.toUpperCase();
},
"": function identity(string) {
return string;
}
}
); // example имеет значение "Hello, WORLD! :{"

Tlgm: @it_boooks

9.10

Как работают строки

Функция entitiyify делает текст безопасным для вставки его в HTML:
function entityify(text) {
return text.replace(
/&/g,
"&amp;"
).replace(
//g,
"&gt;"
).replace(
/\\/g,
"&bsol;"
).replace(
/"/g,
"&quot;"
);
}

Теперь заполним шаблон опасными данными:
const template = "Lucky {name.first} {name.last} won ${amount}.";
const person = {
first: "Da5id",
last: ""
};
// Теперь вызовем функцию fulfill.
fulfill(
template,
{
name: person,
amount: 10
},
entityify
)
// "Lucky Da5id &lt;script src=enemy.evil/pwn.js/&gt; won $10."

Кодировщик entityify сделал потенциально вредоносный тег сценария безопасным для HTML.
А теперь посмотрим на код, который я использовал для подготовки оглавления
книги, которую вы сейчас читаете!
Оглавление представлено в виде JSON-текста. В шаблонах имеются литералы
скобок, но их наличие не вызывает никаких проблем. Здесь показаны вложенные
вызовы функции fulfill, что позволяет избавиться от лексической сложности
вложенных шаблонных строк:
const chapter_names = [
"Сначала прочитайте меня!",

Tlgm: @it_boooks
Как работают строки
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как
"Как

работают
работают
работают
работают
работают
работают
работают
работают
работают
работают
работают
работают
работают
работают
работают
работает
работает
работают
работает
работает
работает
работает
работает
работает
работает
работает
работает
работает
работает
работают
устроена

имена",
числа",
большие целые числа",
большие числа с плавающей точкой",
большие рациональные числа",
булевы значения",
массивы",
объекты",
строки",
ничтожно малые значения",
инструкции",
функции",
генераторы",
исключения",
программы",
this",
код без классов"
концевые вызовы",
чистота",
событийное программирование",
Date",
JSON",
тестирование",
оптимизация",
транспиляция",
разбиение на лексемы",
парсер",
генерация кода",
среда выполнения",
нелепости, или Что такое Wat!",
эта книга"

];
const chapter_list = "[{chapters}]";
const chapter_list_item = `{comma}
{"номер": {index}, "глава": "{chapter}"}`;
fulfill(
chapter_list,
{
chapters: chapter_names.map(function (chapter, chapter_nr) {
return fulfill(
chapter_list_item,
{
chapter,
chapter_nr,
comma: (chapter_nr > 0)
? ","
: ""
}
).join("");
})
},
entityify
)
// См. оглавление в начале книги

9.11

Tlgm: @it_boooks

9.12

Как работают строки

Функция fulfill не очень большая:
const rx_delete_default = /[ < > & % " \\ ]/g;
const rx_syntactic_variable = /
\{
(
[^ { } : \s ]+
)
(?:
:
(
[^ { } : \s ]+
)
)?
\}
/g;
// Определение групп:
//
[0] исходник (символическая переменная, заключенная в фигурные скобки)
//
[1] путь
//
[2] система кодирования
function default_encoder(replacement) {
return String(replacement).replace(rx_delete_default, "");
}
export default Object.freeze(function fulfill(
string,
container,
encoder = default_encoder
) {

Функция fulfill получает строку, содержащую символические переменные, функцию-генератор, или объект, или массив со значениями для замены символических
переменных, и необязательную функцию кодировщика или объект, содержащий
функции-кодировщики. Кодировщик, используемый по умолчанию, убирает все
угловые скобки.
Основную часть работы выполняет принадлежащий string метод replace, который
находит символические переменные, представляя их в виде исходной подстроки,
строки пути и необязательной строки системы кодирования:
return string.replace(
rx_syntactic_variable,
function (original, path, encoding = "") {
try {

Он использует путь для получения одиночной замены из контейнера значений.
Путь содержит одно или несколько имен (или чисел), разделенных точками:
let replacement = (
typeof container === "function"
? container
: path.split(".").reduce(
function (refinement, element) {

Tlgm: @it_boooks
Как работают строки

)

);

9.13

return refinement[element];
},
container

Если значением для замены является функция, ее вызывают, чтобы получить это
значение.
if (typeof replacement === "function") {
replacement = replacement(path, encoding);
}

Если предоставлен объект кодировщика, вызывается одна из его функций. Если
кодировщик представлен функцией, то выполняется ее вызов.
replacement = (
typeof encoder === "object"
? encoder[encoding]
: encoder
)(replacement, path, encoding);

Если значения для замены относятся к булеву типу, они преобразуются в строку.
if (
) {
}

typeof replacement === "number"
|| typeof replacement === "boolean"
replacement = String(replacement);

Если значение для замены — строка, выполняется ее подстановка. В противном
случае символическая переменная остается неизменной.
return (
typeof replacement === "string"
? replacement
: original
);

Если что-то пойдет не так, символическая переменная будет оставлена в исходном
состоянии.

});

);

}

} catch (ignore) {
return original;
}

Tlgm: @it_boooks

Глава 10

Как работают ничтожно малые
значения
○ ● ○ ● ○

Ты — романс,
Ты — степи России,
Ты — штаны на билетере Рокси.
Я же — сломанная кукла, безделушка, пустячок.
И если я ничтожество,
То ты — само совершенство!
Коул Портер (Cole Porter)

Ничтожно малыми (bottom) называются специальные значения, показывающие
окончание рекурсивной структуры данных или отсутствие значения. В языках программирования ничтожно малые значения могут носить имена nil, none, nothing
и null.
В JavaScript два ничтожно малых значения — null и undefined. Значение NaN также может считаться ничтожно малым, так как оно показывает отсутствие числа.
Обилие таких значений можно рассматривать как ошибку разработки.
Ничтожно малые значения null и undefined — это единственные значения в JavaScript,
не являющиеся объектами в абстрактном смысле. Попытка дать свойству значение
из этой пары приводит к выдаче исключения.
В чем-то null и undefined очень похожи друг на друга, а в чем-то ведут себя
по-разному. Они частично, но не полностью взаимозаменяемы. Вызывает путаницу
само наличие двух ничтожно малых значений, которые можно считать одним и тем
же, но которые иногда действуют по-разному. Принятие решения, какое из них
следует использовать, — пустая трата времени, а связанные с ними беспочвенные
теории приводят к еще большей путанице, которая, в свою очередь, вызывает появление ошибок.

Tlgm: @it_boooks
Как работают ничтожно малые значения

10.1

Удаление одного из них помогает создавать более качественные программы.
Мы не в состоянии удалить одно из этих значений из языка, но можем получить
выгоду, убрав его из своих программ. Следует удалить null и пользоваться исключительно undefined.
Обычно, когда нужно выбирать между двумя словами, я предпочитаю то, что короче. У null есть известное значение в контексте программирования и структуры
данных, а у undefined его нет. Более того, undefined звучит непонятно. С математическим понятием неопределенности оно не имеет ничего общего. Оно даже
не означает то, что программисты подразумевают под неопределенностью.
Получается, что null выглядит более подходящим именем, так почему же я предпочел undefined? Дело в том, что undefined — это значение, используемое самим
JavaScript. Если определить переменную, применяя let или var, и не инициализировать ее явным образом, JavaScript инициализирует ее значением undefined, что
вызывает путаницу, поскольку только что определенная переменная приобретает
значение undefined, означающее, что она не определена. Если не передать функции
достаточное количество аргументов, дополнительные параметры устанавливаются
в undefined. При попытке получить у объекта отсутствующее свойство будет получено значение undefined. При попытке получить у массива отсутствующий элемент
будет получено значение undefined.
Единственный случай, когда я применяю null, — использование метода Object.crea­
te(null) для создания нового пустого объекта. Ошибка спецификации не позволяет задействовать Object.create() и Object.create(undefined).
Значения null и undefined могут быть протестированы с помощью оператора
равенства:
function stringify_bottom(my_little_bottom) {
if (my_little_bottom === undefined) {
"undefined";
}
if (my_little_bottom === null) {
return "null";
}
if (Number.isNaN(my_little_bottom)) {
return "NaN";
}
}

Иногда можно увидеть, что программисты со стажем пишут (typeof my_little_
bottom === "undefined"), что также работает. Но (typeof my_little_bottom ===
"null") дает сбой, поскольку typeof null возвращает "object", а не "null". Это плохо, потому что (typeof my_little_object === "object") дает ложный положительный ответ, когда my_little_object имеет значение null, что приводит к ошибке,
которую тест должен был предотвратить. Это еще одна причина избегать применения null.

Tlgm: @it_boooks

10.2

Как работают ничтожно малые значения

Значение undefined, несомненно, лучше подходит для использования, чем null, но
оно также страдает от проблемы пути. Вспомним, что значением отсутствующих
свойств является undefined, что могло бы считаться положительным моментом,
если бы undefined был замороженным пустым объектом, но он таковым не является. Значение undefined не объект, поэтому попытки получения из него свойства
приводят к выдаче исключения. Это существенно усложняет написание путевых
выражений. Например:
my_little_first_name = my_little_person.name.first;

выдает исключение, если в my_little_person нет свойства name или my_little_person имеет значение undefined. Поэтому нельзя рассматривать цепочку точек (.)
и список индексов [ ] в качестве пути. Можно считать это последовательностью
отдельно взятых операций, любая из которых способна дать сбой. Это приводит
к созданию примерно следующего кода:
my_little_first_name = (
my_little_person
&& my_little_person.name
&& my_little_person.name.first
);

Логический оператор И (&&) используют, чтобы избежать вычисления того, что
находится справа от него, если то, что стоит слева, имеет лживое значение. Этот
код весьма пространный, уродливый и медленный, но он избавляет от выдачи исключений и обычно делает то, что должен был бы делать код:
my_little_first_name = my_little_person.name.first;

если бы значение undefined работало как объект, а не как антиобъект.

Tlgm: @it_boooks

Глава 11

Как работают инструкции
○ ● ○ ● ●

Когда я кивну головой, ты ударишь по ней молотком.
Буллвинкль Дж. Лось (Bullwinkle J. Moose)
Большинство языков программирования можно разбить на два класса: языки
выражений и языки инструкций. Язык инструкций имеет инструкции (их еще называют операторами) и выражения. В языке выражений есть только выражения.
У сторонников языков выражений в ходу совокупность теорий, где приводятся
доводы о несомненном превосходстве этих языков. А вот обилия теорий с обоснованиями преимуществ языков инструкций что-то не наблюдается. Несмотря на это
все популярные языки, включая JavaScript, — это языки инструкций.
Ранние программы были просто списками инструкций, иногда напоминающими
предложения и даже заканчивающимися в некоторых языках точками. К сожалению, эти точки путали с десятичными, поэтому в более поздних языках символами
завершения инструкций стали точки с запятой.
Структурированное программирование разрушило идею простого списка инструкций, позволив этим спискам быть вложенными в другие инструкции. Язык
ALGOL 60 был одним из первых языков структурированного поколения, который
в качестве разделителей блоков инструкций использовал BEGIN и END. В языке
BCPL для замены BEGIN и END были введены фигурные скобки ({ и }). Эта мода,
пришедшая из 1960-х, сохраняла популярность многие десятилетия.

Объявления
В JavaScript имеется три инструкции, используемые для объявления переменных
в функции или модуле: let, function и const. Есть также устаревшая инструкция
var, применяемая с нелюбимым многими браузером Internet Explorer.
Поговорим об инструкции let. Она объявляет новую переменную в текущей области видимости. Каждый блок (строка инструкций, заключенная в фигурные скобки) создает область видимости. Переменные, объявленные в области видимости,
невидимы за ее пределами. Инструкция let также допускает инициализацию, но
не требует ее проведения. Если переменная не инициализирована, ей по умолчанию

Tlgm: @it_boooks

11.1

Как работают инструкции

дается значение undefined. Инструкции let позволено объявлять сразу несколько
переменных, но я рекомендую объявлять каждую переменную ее собственной инструкцией let. Это упрощает чтение кода и поддержку программы.
Инструкция let допускает деструктурирование. Это довольно хитрый синтаксис,
определяющий и инициализирующий сразу несколько переменных содержимым
объекта или массива. Таким образом:
let {huey, dewey, louie} = my_little_object;

это сокращение для:
let huey = my_little_object.huey;
let dewey = my_little_object.dewey;
let louie = my_little_object.louie;

Аналогично:
let [zeroth, wunth, twoth] = my_little_array;

является сокращением для:
let zeroth = my_little_array[0];
let wunth = my_little_array[1];
let twoth = my_little_array[2];

Деструктурирование — не самое важное свойство, но оно способно улучшить некоторые шаблоны. Однако, применяя его, можно довольно легко ошибиться.
Существуют синтаксические возможности для переименований и задания значений
по умолчанию, но им сопутствует слишком много визуальных сложностей.
Объявление function создает объект функции и переменную, в которой она содержится. К сожалению, у этого объявления такой же синтаксис, как и у функцийвыражений, что создает путаницу.
function my_little_function() {
return "So small.";
}

является сокращением для:
let my_little_function = undefined;
my_little_function = function my_little_function() {
return "So small.";
};

Следует заметить, что объявление function не заканчивается точкой с запятой,
а let и инструкции присваивания заканчиваются этим символом.
Объявление function получает возможность подтянуться к началу. Части кода
удаляются из того места, куда вы поместили объявление, и перемешаются к началу
тела функции или модуля. Все инструкции let, созданные объявлением function,
подтягиваются к началу, а за ними следуют все присваивания функциональных объектов для этих переменных. Поэтому объявление function не должно помещаться

Tlgm: @it_boooks
Как работают инструкции

11.2

в блок. Допустимо помещать объявление в тело функции или модуля, но не нужно —
в инструкции if, switch, while, do или for. Функции рассматриваются в главе 12.
Инструкция const похожа на инструкцию let, но имеет два весьма важных отличия: для нее требуется обязательная инициализация, а откладывать присваивание
значения переменной нельзя. Когда у меня есть выбор, я отдаю предпочтение использованию инструкции const, поскольку это позволяет программировать более
чисто (см. главу 19).
Вполне очевидно, что const — сокращение от слова constant. Применять его было бы
нежелательно, поскольку оно подразумевает постоянство или неподвластность
времени. Const — это нечто эфемерное, способное исчезать, когда функция возвращает управление. При каждом запуске программы или вызове функции у нее
могут быть разные значения. Следует также понимать, что если значение const
изменяемое, подобное незамороженному объекту или массиву, то ей должно быть
присвоено значение, а переменную ей присвоить невозможно. Object.freeze влияет
на значения, а не на переменные, а const влияет на переменные, а не на значения.
Важно понимать разницу между переменными и значениями: переменные содержат
ссылки на значения. Значения никогда не содержат переменных.
let my_littlewhitespace_variable = {};
const my_little_constant = my_littlewhitespace_variable;
my_little_constant.butterfly = "free";
// {butterfly: "free"}
Object.freeze(my_littlewhitespace_variable);
my_little_constant.monster = "free";
// СБОЙ!
my_little_constant.monster
// undefined
my_little_constant.butterfly
// "free"
my_littlewhitespace_variable = Math.PI;
// my_littlewhitespace_variable имеет
// значение, приближающееся к числу π
my_little_constant = Math.PI;
// СБОЙ!

Выражения
В JavaScript допускается появление на месте инструкций любого выражения. Это
весьма небрежный, но довольно популярный прием в конструкции языка. В Java­
Script есть три типа выражений, имеющих смысл на месте инструкций: присваивания, вызовы и удаления. К сожалению, на месте инструкций допустимо применение
и всех остальных типов выражений, что ослабляет возможности компилятора по
выявлению ошибок.
Инструкция присваивания заменяет ссылку в переменной или вносит правку
в изменяемый объект или массив. Инструкция присваивания состоит из четырех
частей.
0. Левая часть — выражение, получающее значение. Это может быть переменная или выражение, производящее значение объекта или массива, и детализация — точка (.), за которой следует имя свойства, или открывающая
квадратная скобка ([), за которой следует выражение, производящее имя
свойства или номер элемента, и далее закрывающая квадратная скобка (]).

Tlgm: @it_boooks

11.3

Как работают инструкции

1. Оператор присваивания:

‚‚
‚‚
‚‚
‚‚
‚‚
‚‚
‚‚
‚‚
‚‚
‚‚
‚‚
‚‚
‚‚

= — присваивание;
+= — присваивание со сложением;
-= — присваивание с вычитанием;
*= — присваивание с умножением;
/= — присваивание с делением;
%= — присваивание с остатком от деления;
**= — присваивание с возведением в степень;
>>>= — присваивание с расширением знака и со сдвигом вправо;
>>= — присваивание со сдвигом вправо;
2
);
})
throw make_reason(
factory_name,
"Bad requestors array.",
requestor_array
);

Функция run является сердцевиной Parseq. Она запускает запросчики и управляет
соблюдением лимита времени, отменой и регулятором.
function run(
factory_name,
requestor_array,

Tlgm: @it_boooks

20.13

) {

Как работает событийное программирование
initial_value,
action,
timeout,
time_limit,
throttle = 0

Функция run выполняет работу, общую для всех фабрик Parseq. Она принимает
имя фабрики, массив запросчиков, исходное значение, то или иное действие обратного вызова, функцию обратного вызова по истечении лимита времени, лимит
времени в миллисекундах и регулятор.
Если все получается, вызываются все функции-запросчики, содержащиеся в массиве.
Каждая из них может возвратить функцию отмены, хранящуюся в cancel_array:
let cancel_array = new Array(requestor_array.length);
let next_number = 0;
let timer_id;

Нам нужны функции cancel и start_requestor:
function cancel(reason = make_reason(factory_name, "Cancel.")) {

Остановка всех незавершенных дел — эта функция может быть вызвана, когда
запросчик дает сбой. Ее также вызывают, когда запросчик завершает свою работу
успешно, например, когда в состязательном режиме останавливается работа всех,
кто проиграл, или в параллельном режиме останавливается работа тех необязательных запросчиков, которые еще не завершили работу.
Если таймер запущен, его следует остановить.
if (timer_id !== undefined) {
clearTimeout(timer_id);
timer_id = undefined;
}

Если происходит что-то еще, это действие нужно отменить.

}

if (cancel_array !== undefined) {
cancel_array.forEach(function (cancel) {
try {
if (typeof cancel === "function") {
return cancel(reason);
}
} catch (ignore) {}
});
cancel_array = undefined;
}

function start_requestor(value) {

Теперь функция start_requestor совсем не рекурсивная. Она не вызывает сама
себя напрямую, но возвращает функцию, которая может вызвать start_requestor.

Tlgm: @it_boooks
Как работает событийное программирование

20.14

Запуск выполнения запросчика, если таковой его еще ожидает:
if (
) {

cancel_array !== undefined
&& next_number < requestor_array.length

У каждого запросчика есть номер:
let number = next_number;
next_number += 1;

Вызов следующего запросчика, передача функции обратного вызова, сохранение
функции отмены, которую может возвратить запросчик:
const requestor = requestor_array[number];
try {
cancel_array[number] = requestor(
function start_requestor_callback(value, reason) {

Функция обратного вызова вызывается запросчиком, когда он завершает свою
работу. Если работы больше нет, вызов игнорируется. Например, это может быть
результат, отправленный назад по истечении лимита времени. Данная функция
обратного вызова может быть вызвана только один раз:
if (
) {

cancel_array !== undefined
&& number !== undefined

Больше нам не нужна отмена, связанная с этим запросчиком:
cancel_array[number] = undefined;

Вызов функции action для информирования запросчика о случившемся:
action(value, reason, number);

Очистка номера, чтобы этот обратный вызов не мог быть использован еще раз:
number = undefined;

Если есть еще какие-нибудь запросчики, ожидающие запуска, то происходит запуск
следующего запросчика. Если следующий запросчик встроен в последовательность,
осуществляется передача ему самого последнего значения. Другие запросчики
получают исходное значение initial_value:

);

}
},
value

return start_requestor(
factory_name === "sequence"
? value
: initial_value
);

Tlgm: @it_boooks

20.15

Как работает событийное программирование

Запросчики должны сообщать о своем сбое через обратный вызов. Им не разрешено выдавать исключения. Если будет перехвачено исключение, это будет
считаться сбоем:

}

}

} catch (exception) {
action(undefined, exception, number);
number = undefined;
start_requestor(value);
}

Теперь, располагая функциями cancel и start_requestor, можно приступать к работе.
Если запрошено отслеживание превышения лимита времени, запускаем таймер:
if (time_limit !== undefined) {
if (typeof time_limit === "number" && time_limit >= 0) {
if (time_limit > 0) {
timer_id = setTimeout(timeout, time_limit);
}
} else {
throw make_reason(factory_name, "Bad time limit.", time_limit);
}
}

Если есть намерение запустить параллельный или состязательный режим, нужно
запустить все запросчики одновременно. Но если установлен регулятор, запускается разрешенное им количество запросчиков, а когда завершит работу каждый
запросчик, запускается следующий.
Фабрики sequence и fallback устанавливают регулятор throttle равным 1, поскольку они обрабатывают запросчики поочередно и всегда запускают другой
запросчик, когда завершает работу его предшественник:
if (!Number.isSafeInteger(throttle) || throttle < 0) {
throw make_reason(factory_name, "Bad throttle.", throttle);
}
let repeat = Math.min(throttle || Infinity, requestor_array.length);
while (repeat > 0) {
setTimeout(start_requestor, 0, initial_value);
repeat -= 1;
}

Возвращаем cancel, что позволяет запросчику завершить работу:
}

return cancel;

Теперь рассмотрим четыре открытые функции.
Функция parallel самая сложная из них из-за массива необязательных запросчиков:
function parallel(
required_array,

Tlgm: @it_boooks
Как работает событийное программирование

) {

20.16

optional_array,
time_limit,
time_option,
throttle,
factory_name = "parallel"

Фабрика parallel самая сложная из этих фабрик. Она может принимать второй
массив запросчиков, получающих более щадящую политику сбоев. И возвращает
запросчик, создающий массив значений:
let number_of_required;
let requestor_array;

Возможны четыре варианта, поскольку как массив required_array, так и массив
optional_array может быть пустым:
if (required_array === undefined || required_array.length === 0) {
number_of_required = 0;
if (optional_array === undefined || optional_array.length === 0) {

Если оба массива пустые, то это, вероятно, ошибка:

}

throw make_reason(
factory_name,
"Missing requestor array.",
required_array
);

Если присутствует только optional_array, то он становится requestor_array:
requestor_array = optional_array;
time_option = true;
} else {

Если имеется только required_array, то этот массив становится requestor_array:
number_of_required = required_array.length;
if (optional_array === undefined || optional_array.length === 0) {
requestor_array = required_array;
time_option = undefined;

Если предоставлены оба массива, мы их объединяем:
} else {

}

}

requestor_array = required_array.concat(optional_array);
if (time_option !== undefined && typeof time_option !== "boolean") {
throw make_reason(
factory_name,
"Bad time_option.",
time_option
);
}

Tlgm: @it_boooks

20.17

Как работает событийное программирование

Проверяем массив и возвращаем запросчик:
check_requestor_array(requestor_array, factory_name);
return function parallel_requestor(callback, initial_value) {
check_callback(callback, factory_name);
let number_of_pending = requestor_array.length;
let number_of_pending_required = number_of_required;
let results = [];

run его запускает:
let cancel = run(
factory_name,
requestor_array,
initial_value,
function parallel_action(value, reason, number) {

Функция действия получает результат работы каждого запросчика, имеющегося
в массиве. Функции parallel требуется возвращение массива всех видимых ею
значений:
results[number] = value;
number_of_pending -= 1;

Если запросчик был одним из обязательных, нужно убедиться, что он завершил
свою работу успешно. Если же выдал сбой, параллельная операция считается
сбойной. Если выдал сбой необязательный запросчик, можно продолжить работу:
if (number < number_of_required) {
number_of_pending_required -= 1;
if (value === undefined) {
cancel(reason);
callback(undefined, reason);
callback = undefined;
return;
}
}

Если все было обработано или все обязательные запросчики завершили работу
успешно и у нас не было time_option, значит, мы справились с задачей:
if (

) {

number_of_pending < 1
|| (
time_option === undefined
&& number_of_pending_required < 1
)
cancel(make_reason(factory_name, "Optional."));
callback(
factory_name === "sequence"
? results.pop()
: results
);

Tlgm: @it_boooks
Как работает событийное программирование

}

20.18

callback = undefined;

},
function parallel_timeout() {

Когда срабатывает таймер, работа прекращается, если только она не велась под
параметром времени со значением false . Это значение не накладывает лимит
времени на работу обязательных запросчиков, позволяя необязательным работать
до тех пор, пока не завершится работа обязательных запросчиков или не истечет
лимит времени в зависимости от того, что наступит позже.
const reason = make_reason(
factory_name,
"Timeout.",
time_limit
);
if (time_option === false) {
time_option = undefined;
if (number_of_pending_required < 1) {
cancel(reason);
callback(results);
}
} else {

Время истекло. Если все запросчики завершили свою работу успешно, то параллельная операция будет считаться успешно завершенной.
cancel(reason);
if (number_of_pending_required < 1) {
callback(results);
} else {
callback(undefined, reason);
}
callback = undefined;

}
},
time_limit,
throttle

}

};

);
return cancel;

Функция race намного проще, чем parallel, поскольку она не нуждается в аккумулировании всех результатов. Ей нужен всего лишь один результат.
function race(requestor_array, time_limit, throttle) {

Фабрика race возвращает запросчик, запускающий одновременно все запросчики,
имеющиеся в массиве requestor_array. Первый же успешно завершивший работу
запросчик считается победителем.
const factory_name = (
throttle === 1

Tlgm: @it_boooks

20.19

Как работает событийное программирование

);

? "fallback"
: "race"

check_requestor_array(requestor_array, factory_name);
return function race_requestor(callback, initial_value) {
check_callback(callback, factory_name);
let number_of_pending = requestor_array.length;
let cancel = run(
factory_name,
requestor_array,
initial_value,
function race_action(value, reason, number) {
number_of_pending -= 1;

У нас есть победитель. Отменяем работу проигравших и передаем значение обратному вызову.
if (value !== undefined) {
cancel(make_reason(factory_name, "Loser.", number));
callback(value);
callback = undefined;
}

Победителя нет. Сигнализируем о сбое.
if (number_of_pending < 1) {
cancel(reason);
callback(undefined, reason);
callback = undefined;
}

},
function race_timeout() {
let reason = make_reason(
factory_name,
"Timeout.",
time_limit
);
cancel(reason);
callback(undefined, reason);
callback = undefined;
},
time_limit,
throttle

}

};

);
return cancel;

Альтернативный режим (fallback) — это всего лишь регулируемый состязательный
режим (race):
function fallback(requestor_array, time_limit) {

Tlgm: @it_boooks
Как работает событийное программирование

20.20

Фабрика fallback возвращает запросчик, поочередно пробующий запускать каждый запросчик, имеющийся в массиве requestor_array, пока не найдет запросчик,
успешно завершивший свою работу:
}

return race(requestor_array, time_limit, 1);

Последовательный режим (sequence) представляет собой все тот же регулируемый
параллельный режим (parallel) с распространяемыми значениями:
function sequence(requestor_array, time_limit) {

В последовательном режиме запускается по порядку каждый запросчик, который
передает результаты следующему запросчику, если его работа завершается успешно. Последовательный режим — это регулируемый параллельный режим.

}

return parallel(
requestor_array,
undefined,
time_limit,
undefined,
1,
"sequence"
);

Доступ к библиотеке Parseq в вашем модуле можно получить, импортировав ее.
import parseq from "./parseq.js";

Тонкости английского языка
Эдсгер Дейкстра (Edsger Dijkstra) был одним из первых, кто еще в 1962 году
распознал проблему с потоками. Он разработал первый механизм взаимного исключения — семафоры. Семафор был реализован в виде двух функций — P и V.
Функция P будет пытаться заблокировать критическую секцию, блокируя затем
код, пытающийся установить блокировку, если эта секция уже заблокирована.
Функция V будет снимать блокировку, позволяя ожидающему потоку заблокировать критическую секцию и запуститься.
Пер Бринч Хансен (Per Brinch Hansen) и Чарльз Энтони Ричард Хоар (C. A. R. Hoare)
интегрировали семафоры в классы, чтобы создать мониторы — синтаксическую
форму, которая, как они надеялись, будет удобнее и устойчивее к ошибкам.
В языке JavaScript есть нечто похожее под названием synchronized («синхронизировано»). Ключевое слово synchronized применяется для декорирования кода, нуждающегося во вставке семафоров. К сожалению, выбранное слово не имеет смысла.
«Синхронный» (synchronous) означает «существующий в то же время или использующий те же часы». Музыканты в оркестре синхронизированы, потому что все

Tlgm: @it_boooks

20.21

Как работает событийное программирование

играют в ритме, задаваемом дирижером. Когда был создан язык Java, конструкторы
искали слово, которое было бы связано с временем. К сожалению, похоже, при этом
они были недостаточно усердны.
Когда компания Microsoft изыскивала возможность добавить совершенно неприемлемую событийную поддержку в C#, ее специалисты обратились к Java: взяли
неправильное слово и превратили его в еще более неправильное, снабдив префи­
ксом a- для присвоения противоположного значения. Получившееся слово async
не имеет никакого смысла.
У большинства программистов недостаточно образования или опыта работы
в сфере параллельного программирования. Одновременное управление несколькими потоками активности им незнакомо и непривычно. Внедрение в их сознание
непонятного языка, который в корне неверен, не облегчает решения задачи. Неосведомленность приводит к еще большему невежеству.

Tlgm: @it_boooks

Глава 21

Как работает Date
● ○ ● ○ ●

С чем вы столкнетесь, открывая дверь, будет ли это
таинственное свидание мечтой или подделкой?
Милтон Брэдли (Milton Bradley)
Наш календарь отображает движение Солнца и Луны на постоянный ход времени,
но он был разработан задолго до того, как мы поняли, как работает Солнечная
система. Он претерпел множество коррекций, но никогда не подвергался полному
обновлению. Наш уродливый календарь в ходе завоеваний и торговли навязывали все большему и большему количеству сообществ. В итоге его всучили всему
мировому сообществу без предварительной корректировки или замены более
подходящей конструкцией. Он работает, и весь мир использует его, но он может
быть лучше.
Современный календарь основан на римском календаре, в котором сначала было
десять месяцев и бонус зимнего сезона из оставшихся дней года. Первым месяцем
был март (назван по имени Марса, бога войны). Десятым был декабрь, что и означает десятый месяц. Зимний сезон заменили двумя новыми месяцами — январем
и февралем. Иногда в политических целях у февраля крали дни и использовали их
для продления других месяцев. Каждые четыре года в календаре дрейфовал примерно один день, поскольку число дней в году не является целым числом. Римляне
не виноваты в том, что число дней в тропическом году не целое число. Они попытались рационально перестроить свой календарь, объявив январь первым месяцем.
При этом декабрь, который все еще означает десятый месяц, стал двенадцатым.
Юлий Цезарь ввел стандарт добавления високосного дня к февралю каждые четыре
года. Это уменьшило сезонный дрейф, но полностью его не устранило. По указанию
Папы Римского Григория XIII привели в соответствие со стандартом лучший алгоритм високосного года, но более простой и точный алгоритм так и не был принят.
Григорианский алгоритм таков:
добавлять високосный день в те годы, которые делятся на 4, за исключением того случая, когда они также делятся на 100, но все равно добавлять високосный день, если год делится на 400.

Tlgm: @it_boooks

21.1

Как работает Date

В результате средняя продолжительность года получилась 365,2425 дня, что доволь­но
близко к тропическому году, продолжающемуся около 365,242 188 792 дня.
Есть и более удачный алгоритм:
добавлять високосный день в те годы, которые делятся на 4, за исключением того случая, когда они также делятся на 128.

Он дает год, состоящий из 365,242 187 5 дня, что намного ближе к продолжительности тропического года. Мне, как программисту, представляется просто чудом
включение в алгоритм двух степеней числа 2, хотя я был бы гораздо больше впечатлен точным соответствием продолжительности реального года. Если бы число дней
в году было целым числом, то я мог бы подумать, что в теории разумного начала,
возможно, кроется некая истина.
Следующим годом несоответствия алгоритмов 4|100|400 и 4|128 станет 2048-й.
Нужно ввести алгоритм 4|128 в качестве стандарта до его наступления.
Високосный день добавлялся в конце года. К сожалению, когда январь стал первым
месяцем, високосный день не был перенесен с февраля на декабрь (по-прежнему
означающий десятый месяц).
У минут и секунд нулевые исходные значения, и это хорошо, но и те и другие подсчитываются по модулю 60, что уже не так хорошо. Часы имеют нулевое исходное
значение, но в часах в 12-часовом формате 0 заменяется на 12, что абсолютно неестественно.
(Будь моя воля, вместо этого я использовал бы десятичасовой день, где час равен
100 минутам, а минута — 100 секундам. Моя секунда была бы немного короче, составив 86,4 % от нынешней секунды. Темпом секунд было бы приятное адажио, что
стало бы легким ускорением темпа жизни по сравнению с нынешним ларго.)
Для месяцев и дней в качестве исходного значения используется единица, поскольку этот стандарт был введен до открытия нуля. Месяцы подсчитываются по модулю 12. Римляне пытались сделать месяцы по модулю 10, но не смогли ввести это
в обиход. Дни в месяце подсчитываются по модулю 30 или 31 либо 28 или 29,
в зависимости от месяца и года. Отсчет лет начинался с единицы, но ныне существующая нумерация была принята спустя множество веков после наступления
нашей эры, поэтому данное неудобство можно спокойно проигнори­ровать.

Функция Date
Странности в стандартах учета времени становятся помехой для программ, в которых должно обрабатываться время. Класс Date в Java обеспечивает поддержку дат.
Все должно было быть просто, но получилось очень сложно, продемонстрировав
один из худших шаблонов проектирования классического программирования.
В JavaScript могло бы быть сделано что-то более удачное, но вместо этого просто
скопирован весьма неудачный пример из языка Java.

Tlgm: @it_boooks

21.2

Как работает Date

Нынешние объекты JavaScript Date содержат множество методов. Большинство из
них — это просто получатели и установщики (get- и set-методы):
getDate
getDay
getFullYear
getHours
getMilliseconds
getMinutes
getMonth
getSeconds
getTime
getTimezoneOffset
getUTCDate
getUTCDay
getUTCFullYear
getUTCHours
getUTCMilliseconds
getUTCMinutes
getUTCMonth
getUTCSeconds
getYear

setDate
setFullYear
setHours
setMilliseconds
setMinutes
setMonth
setSeconds
setTime
setUTCDate
setUTCFullYear
setUTCHours
setUTCMilliseconds
setUTCMinutes
setUTCMonth
setUTCSeconds
setYear

toDateString
toISOString
toJSON
toLocaleDateString
toLocaleString
toLocaleTimeString
toString
toTimeString
toUTCString

Есть метод getDate, который возвращает день месяца из объекта Date. Сразу бросается в глаза то, что слово Date имеет в одном и том же методе два совершенно разных
значения. Усугубляет путаницу метод getDay, который возвращает день недели.
Метод getMonth вносит правку в месяц, делая исходным нулевое значение, поскольку
программистам нравится работать со всем, что начинается с нуля. Получается, что
getMonth возвращает числа от 0 до 11. Метод getDate не вносит правку в день, поэтому
он возвращает числа от 1 до 31. Это разночтение порождает массу ошибок.
Методы getYear и setYear после 1999 года работают неправильно и больше
не должны использоваться. Язык Java был выпущен в 1995 году и содержал методы
дат, которые должны были дать сбой в 2000 году. Неужели разработчики языка ничего не слышали о проблеме 2000 года? Или они сомневались, что Java переползет
на рынке через этот рубеж? Возможно, это так и останется тайной. Но нам уже известно, что язык Java необъяснимым образом выжил и что в языке JavaScript была
допущена точно такая же ошибка. Вместо этих методов всегда нужно использовать
методы getFullYear и setFullYear.
Date демонстрирует весьма неудачные приемы классического программирования.

В объекте что-то должно инкапсулироваться. Формами взаимодействия с объектами должны быть транзакции и другие высокоуровневые действия. Date дает
весьма низкоуровневое представление с get- и set-методами для каждого отдельно
взятого компонента времени. При таком подходе объекты используются крайне
нерационально.

Tlgm: @it_boooks

21.3

Как работает Date

ISO 8601
Новый конструктор Date может принимать строку, представляющую дату, и создавать объект для этой даты. К сожалению, в стандарте ECMAScript правила синтаксического анализа и распознавания не определены, поэтому в соответствии со
стандартом не гарантируется, что они будут работать.
За исключением дат ISO.
ISO 8601 — это международный стандарт представления даты и времени. Для правильного разбора дат ISO, например 2018-11-06, необходим JavaScript. Гораздо
логичнее ставить наиболее значимые объекты на первое, а наименее значимые — на
последнее место, в этом больше смысла, чем в стандарте США: 11/06/2018. Например, строки даты ISO хорошо поддаются сортировке.

Другой подход
Разумеется, уже слишком поздно что-либо исправлять. JavaScript не должен был
копировать Java. Подобные действия всегда ошибочны. Многие из неправильностей JavaScript пришли из Java. Если создавать правильный вариант, то я предлагаю следующее.
Должно существовать три представления даты:

xx
xx

xx

количество миллисекунд, прошедших с начала эпохи;
объект даты, в котором могут содержаться следующие свойства:

‚‚
‚‚
‚‚
‚‚
‚‚
‚‚
‚‚
‚‚
‚‚

year;
month;
day;
hour;
minute;
second;
zone;
week;
weekday;

строка в каком-либо стандартном формате.

Нам не нужен классический объект Date с методами. Нужны лишь простые функции для преобразований между тремя представлениями.

xx

Date.now() возвращает текущее время в виде числа. Эта функция уже су-

ществует. JavaScript открывает к ней доступ для всего кода, но я полагаю,
что она должна быть привилегией, предоставляемой толькодоверенному

Tlgm: @it_boooks
Как работает Date

21.4

коду. Часть нечистого вредоносного кода может воспользоваться Date.now()
или Math.random() для изменения ее поведения, позволяя избежать ее обнаружения.

xx

Date.object() принимает либо число, либо строку и возвращает объект, со-

xx

Date.string () будет принимать число или объект данных, а также необя-

xx

Date.number() будет принимать объект данных или строку, а также необяза-

держащий информацию, доступную для извлечения. Если аргумент является
строкой, могут существовать дополнительные необязательные аргументы,
определяющие часовой пояс и стандарт форматирования, управляющий
синтаксическим анализом.
зательный стандарт форматирования и часовой пояс и возвращать удобное
для прочтения строковое представление времени.
тельный стандарт форматирования и часовой пояс и возвращать числовое
представление времени.

Этот подход был бы намного проще и легче в использовании, более устойчив
к ошибкам и, что уже не имеет никакого значения, готов к 2000 году. Вместо массы
нечистых методов низкого уровня предлагаются всего лишь одна нечистая и три
чистые функции. Сделать такое в Java было невозможно, поскольку в этом языке
отсутствовали замечательные объектные литералы JavaScript. Я не знаю, почему
так не сделано в JavaScript. Надеюсь, все это появится в следующем языке.
Эпоха, которая применяется в JavaScript, — это эпоха Unix с точкой начала отсчета 1970-01-01. Есть 32-разрядные системы Unix, которые выйдут из строя
в 2038 году, когда все биты будут использованы и часы переполнены. Оказывается, 32 разрядов недостаточно для запуска системных часов с секундным разрешением.
Я бы предпочел взять в качестве точки начала отсчета эпохи дату 0000-01-01. Числа JavaScript не дадут сбоя в суммировании миллисекунд вплоть до 285 426 года.
К тому времени либо будет разработан более удачный календарь, либо все мы вымрем. Хорошего вам дня.

Tlgm: @it_boooks

Глава 22

Как работает JSON
● ○ ● ● ○

XML должен стать основой для формата
информационного наполнения не из-за того,
что он замечателен с технической точки зрения,
а из-за достигнутой им необычайной широты
внедрения. Именно на этом обстоятельстве теряется
суть аргументации приверженцев JSON (или
YAML), которые гордо указывают на технические
преимущества своего формата: создать более
удачный формат данных, чем XML, может самый
непроходимый тупица.
Джеймс Кларк (James Clark)
Здесь впервые в печатной форме я расскажу правдивую историю о происхождении
самого любимого в мире формата обмена данными.

Изобретение
Формат JSON был придуман в 2001 году в сарае за домом Чипа Морнингстара
(Chip Morningstar). Мы с Чипом основали компанию по разработке платформы
для одностраничных веб-приложений. Замысел заключался в том, что с хорошей
библиотекой JavaScript (которую написал я) и эффективным и масштабируемым
сервером сеансов (который создал Чип) можно было создавать веб-страницы, которые работали бы так же хорошо, как и устанавливаемые приложения. Или даже
еще лучше, потому что наша платформа, кроме прочего, поддерживает многопользовательскую совместную работу, а это то, чего еще не достигали в сети.
Я все еще нахожусь под впечатлением от созданного Чипом сервера сеансов.
За прошедшие годы он подвергался многочисленным переделкам. Самая свежая
версия называется Elko (elkoserver.org).
Единственный недостаток Elko — то, что он написан на Java. Я мечтаю, чтобы
вскоре кто-нибудь заплатил Чипу, чтобы он переделал его еще раз, теперь уже

Tlgm: @it_boooks
Как работает JSON

22.1

на JavaScript. Это стало бы существенным продвижением вперед по сравнению
с тем, где мы сейчас находимся с Node.js. Мы получили бы повышенный уровень
безопасности, лучшую масштабируемость и поддержку более широкого спектра
приложений.
Но вернемся в сарай. Нам нужен был способ передачи информации между браузером и сервером. В то время индустрия программного обеспечения испытывала
полную приверженность формату XML. Такие гиганты, как Microsoft, IBM,
Oracle, Sun, HP и другие, решили, что следующее поколение программного обеспечения будет построено на XML, и с ними согласились как их последователи,
так и клиенты.
Нам же нужен был обмен данными между программами, написанными на двух разных языках. Мы посмотрели на XML и решили, что он плохо подходит для нашей
задачи. Модель работы с XML заключалась в том, что сначала отправлялся запрос
на сервер, который отвечал XML-документом. Затем, чтобы получить данные, делались дополнительные запросы к документу XML. Почему сервер не может просто
отправить данные в форме, которую программы способны бы сразу использовать?
Наши данные просто не были похожи на документы.
В то время доступно было множество улучшенных XML-вариантов и замен этого
формата, но ни один из них не получил поддержки. Мы думали о том, чтобы пойти
собственным путем. И тут на меня нашло прозрение. Мы могли бы воспользоваться объектными литералами JavaScript. Их встроенность в JavaScript создавала
реальные удобства на стороне этого языка. А в отношении поднабора, в котором
мы нуждались, нетрудно было выполнить синтаксический анализ на стороне Java.
Для этого понадобилось бы куда меньше усилий, чем при работе с XML.
Нам хотелось, чтобы наша платформа работала как с Microsoft Internet Explorer,
так и с Netscape Navigator. Это было совсем не просто, поскольку обе компании,
производящие эти браузеры, изобретали свои собственные функции. Там было
мало общего. Microsoft добавила функцию XMLHttpRequest, которую можно было
использовать для связи с сервером. К сожалению, в компании Netscape не было
ничего подобного, поэтому задействовать ее браузер было невозможно.
У обоих браузеров был JavaScript (ES3), и оба они имели наборы фреймов. И мы
применяли их вместе, чтобы создать канал обмена данными. Первое сообщение
JSON, отправленное в браузер, имело следующий вид:

document.domain = "fudco.com";
parent.session.receive({to: "session", do: "test", text: "Hello world"});


Веб-страница содержала скрытый фрейм, по которому можно было перемещаться.
Выполнение POST-запроса на скрытом фрейме приводило к отправке сообщения
на сервер. Сервер отвечал, передавая документ, содержащий сценарий, который

Tlgm: @it_boooks

22.2

Как работает JSON

вызвал метод receive объекта session в главном фрейме. Чтобы включить межфреймовый обмен данными, нам пришлось изменить document.domain. Синтаксический анализ сообщения проводился компилятором JavaScript.
Как бы ни хотелось мне сказать, что первое сообщение было доставлено успешно,
но ничего из этого не вышло. Сбой произошел из-за ужасной политики зарезервированных слов ES3. В то время были зарезервированы следующие слова:
abstract boolean break byte case catch char class const continue debugger
default delete do double else enum export extends false final finally float
for function goto if implements import in instanceof int interface long native
new null package private protected public return short static super switch
synchronized this throw throws transient true try typeof var void volatile
while with

В соответствии с политикой зарезервированных слов ES3 ни одно из этих слов
нельзя использовать в качестве имен переменных, или имен параметров, или имен
свойств в точечной позиции, или имен свойств в литералах объекта. В сообщении
было свойство do, поэтому сценарий содержал синтаксическую ошибку и не запускался.
С радостью сообщаю, что в ES5 ситуация была исправлена. Список зарезервированных слов был сокращен, а ограничения на имена свойств были сняты, но
в 2001 году нам пришлось поставить кавычки вокруг слова do, чтобы оно заработало. Чип поместил зарезервированный список слов в свой кодировщик, и больше
проблем у нас с ним не было.
Мы выяснили, что строка, содержащая последовательность символов