425

Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

  • Upload
    xydope

  • View
    451

  • Download
    22

Embed Size (px)

DESCRIPTION

PDF версия самоучителя JavaScript

Citation preview

Page 1: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015
Page 2: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

1. Введение2. Опрограммировании3. Величины,типыиоператоры4. Структурапрограмм5. Функции6. Структурыданных:объектыимассивы7. Функциивысшегопорядка8. Тайнаяжизньобъектов9. Проект:электроннаяжизнь10. Поискиобработкаошибок11. Регулярныевыражения12. Модули13. Проект:языкпрограммирования14. JavaScriptибраузер15. DocumentObjectModel16. Обработкасобытий17. Проект:игра-платформер18. Рисованиенахолсте19. HTTP20. Формыиполяформ21. Проект:Paint22. Node.js23. Проект:веб-сайтпообменуопытом

Содержание

ВыразительныйJavascript

2

Page 3: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

2-еиздание

Автор:MarijnHaverbekeПеревод:ВячеславГолованов

РаспространяетсяподлицензиейCreativeCommonsAttribution-Noncommercial.ИсходныйкодвкнигераспростроняетсяподлицензиейMIT.

СборкавGitbook:АнтонКармазин

ВыразительныйJavascript

ВыразительныйJavascript

3

Page 4: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Этокнигарассказывает,какзаставитькомпьютерыделатьто,чтовамотнихнужно.Компьютерысегоднятакжераспространены,какотвёртки–носодержатгораздобольшескрытыхсложностей,ипоэтомуихсложнеепонятьиснимисложнееработать.Длямногихониостаютсячуждыми,слегкаугрожающимиштуками.

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

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

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

Введение

ВыразительныйJavascript

4

Page 5: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Запоследние20летработаскомпьютеромсталаоченьраспространённымявлением,иинтерфейсы,построенныенаязыке(акогда-тоэтобылединственныйспособобщенияскомпьютером)почтивытесненыграфическими.Ноонивсёещёесть–есливызнаете,гдеихискать.Одинизтакихязыков,JavaScript,встроенпочтивлюбойвеб-браузер,ипотомудоступенпочтинакаждомвычислительномустройстве.

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

ВыразительныйJavascript

5

Page 6: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Конфуций

КромеобъясненияJavaScriptятакжехочуобъяснитьосновныепринципыпрограммирования.Каквыясняется,программироватьтяжело.Обычнобазовыепринципыпростыипонятны.Нопрограммы,построенныенаэтихпринципах,становятсясложныминастолько,чтовводятсвоисобственныеправилаиуровнисложности.Выстроитесвойсобственныйлабиринт,иможетевнёмпотеряться.

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

Высамидолжныобосноватьнеобходимостьэтихусилий.Есливамтяжелопродиратьсячерезкнигу,ненужнодуматьосебеплохо.Свамивсёвпорядке–вамнужнопростопродолжатьдвижение.Сделайтеперерыв,вернитесьназад–ивсегдаудостоверяйтесь,чтовыпрочлиипонялипримерыпрограмм.Обучение–этосложнаяработа,норазвычто-товыучили,оноужепринадлежитвам,иоблегчаетдальнейшиешаги.

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

ДжозефВайзенбаум,«СилакомпьютеровиРазумлюдей»

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

Опрограммировании

ВыразительныйJavascript

6Опрограммировании

Page 7: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

самойпамятью.Аналогии,которыепытаютсясравниватьпрограммысознакомыминамобъектамиобычнонесправляютсясэтим.Однаболее-менееподходящая–аналогиясмашиной.Множествоотдельныхчастейсоставляютодноцелое,ичтобызаставитьеёработать,намнужнопредставлятьсебеспособы,которымиэтичастивзаимодействуютичтоонипривносятвработуцелоймашины.

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

Длянекоторыхизнаспрограммирование–этоувлекательнаяигра.Программа–этомысленнаяконструкция.Ничегонестоитеёпостроить,онаничегоневесит,ионалегковырастаетподнашимипальцами.

Еслинебытьосторожным,размерисложностьвыходятиз-подконтроля,запутываядажетого,ктоеёпишет.Этоосновнаяпроблемапрограммирования:сохранятьконтрольнадпрограммами.Когдапрограммаработает–этопрекрасно.Искусствопрограммирования–этоумениеконтролироватьсложность.Большаяпрограмманаходитсяподконтролем,ивыполненапростовсвоейсложности.

Многиепрограммистыверят,чтоэтойсложностьюлучшевсегоуправлять,используявпрограммахнебольшойнаборхорошоизвестныхтехник.Ониописалистрогиеправила(«наилучшиепрактики»)того,какуюформупрограммыдолжныиметь.Исамыеревностныесрединихсчитаюттех,ктоотклоняетсяотэтихпрактик,плохимипрограммистами.

Чтозавраждебностьпоотношениюкбогатствупрограммирования–попыткипринизитьегодочего-топрямолинейногоипредсказуемого,наложитьтабунавсякиестранныеипрекрасныепрограммы!Ландшафттехникпрограммированияогромен,увлекателенсвоимразнообразием,идосихпоризученмало.Этоопасноепутешествие,заманивающееизапутывающеенеопытногопрограммиста,ноэтовсеголишьозначает,чтовыдолжныследоватьэтимпутёмосторожноидуматьголовой.Помереобучениявамвсегдабудутвстречатьсяновыезадачииновыенеизведанныетерритории.

ВыразительныйJavascript

7Опрограммировании

Page 8: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Программисты,неизучающиеновое,стагнируют,забываютсвоюрадость,ихработанаскучиваетим.

Вначале,призарождениикомпьютерныхдисциплин,небылоязыковпрограммирования.Программывыгляделитак:

001100010000000000000000

001100010000000100000001

001100110000000100000010

010100010000101100000010

001000100000001000001000

010000110000000100000000

010000010000000100000001

000100000000001000000000

011000100000000000000000

Этопрограмма,складывающаячислаот1до10,ивыводящаярезультат(1+2+…+10=55).Онаможетвыполнятьсянаоченьпростойгипотетическоймашине.Дляпрограммированияпервыхкомпьютеровбылонеобходимоустанавливатьбольшиемассивыпереключателейвнужныепозиции,илипробиватьдыркивперфокартахискармливатьихкомпьютеру.Можетепредставить,какаяэтобылаутомительная,подверженнаяошибкампроцедура.Написаниедажепростыхпрограммтребовалобольшогоумаидисциплины.Сложныепрограммыбылипрактическинемыслимы.

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

Каждаястрокауказаннойпрограммысодержитоднуинструкцию.Наобычномязыкеихможноописатьтак:

1. записать0вячейкупамяти02. записать1вячейкупамяти13. записатьзначениеячейки1вячейку24. вычесть11иззначенияячейки25. еслиуячейке2значение0,тогдапродолжитьспункта9.

Почемуязыкимеетзначение

ВыразительныйJavascript

8Опрограммировании

Page 9: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

6. добавитьзначениеячейки1кячейке07. добавить1кячейке18. продолжитьспункта3.9. вывестизначениеячейки0

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

установить‘total’в0

установить‘count’в1

[loop]

установить‘compare’в‘count’

вычесть11из‘compare’

если‘compare’равнонулю,перейтина[end]

добавить‘count’к‘total’

добавить1к‘count’

перейтина[loop]

[end]

вывести‘total’

Воттеперьуженетаксложнопонять,какработаетпрограмма.Справитесь?Первыедвестрокиназначаютдвумобластямпамятиначальныезначения.totalбудетиспользоватьсядляподсчётарезультатавычисления,аcountбудетследитьзачислом,скоторыммыработаемвданныймомент.Строчки,использующие‘compare’,наверно,самыестранные.Программенужнопонять,неравнолиcount11,чтобыпрекратитьподсчёт.Таккакнашавоображаемаямашинадовольнопримитивна,онаможеттольковыполнитьпроверкунаравенствопеременнойнулю,ипринятьрешениеотом,надолиперепрыгнутьнадругуюстроку.Поэтомуонаиспользуетобластьпамятиподназванием‘compare’,чтобыподсчитатьзначениеcount–11ипринятьрешениенаоснованииэтогозначения.Следующеедвестрокидобавляютзначениеcountвсчетчикрезультатаиувеличиваютcountна1каждыйраз,когдапрограммарешает,чтоещёнедостиглазначения11.

ВоттажепрограмманаJavaScript:

vartotal=0,count=1;

while(count<=10){

total+=count;

ВыразительныйJavascript

9Опрограммировании

Page 10: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

count+=1;

}

console.log(total);

//→55

Ещенесколькоулучшений.Главное–нетнеобходимостивручнуюобозначатьпереходымеждустроками.Конструкцияязыкаwhileделаетэтосама.Онапродолжаетвычислятьблок,заключённыйвфигурныескобки,покаусловиевыполняется(count<=10),тоестьзначениеcountменьшеилиравно10.Ужененужносоздаватьвременноезначениеисравниватьегоснулём.Этобылоскучно,исилаязыковпрограммированиявтом,чтоонипомогаютизбавитьсяотскучныхдеталей.

Вконцепрограммыпозавершениюwhileкрезультатуприменяетсяоперацияconsole.logсцельювывода.

Инаконец,воттакмоглабывыглядетьпрограмма,еслибунасбылиудобныеоперацииrangeиsum,которые,соответственно,создавалибынаборномероввзаданномпромежуткеиподсчитывалисуммунабора:

console.log(sum(range(1,10)));

//→55

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

Хорошийязыкпрограммированияпомогаетпрограммистусообщатькомпьютеруонеобходимыхоперацияхнавысокомуровне.Позволяетопускатьскучныедетали,даётудобныестроительныеблоки(whileиconsole.log),позволяетсоздаватьсвоисобственныеблоки(sumиrange),иделаетпростымкомбинированиеблоков.

JavaScriptбылпредставленв1995годукакспособдобавлятьпрограммына

ЧтотакоеJavaScript?

ВыразительныйJavascript

10Опрограммировании

Page 11: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

веб-страницывбраузереNetscapeNavigator.Стехпорязыкприжилсявовсехосновныхграфическихбраузерах.Ондалвозможностьпоявитьсясовременнымвеб-приложениям–браузерныее-мейл-клиенты,карты,социальныесети.Аещёониспользуетсянаболеетрадиционныхсайтахдляобеспеченияинтерактивностиивсякихнаворотов.

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

Послетого,какязыквышелзапределыNetscape,былсоставлендокумент,описывающийработуязыка,чтобыразныепрограммы,заявляющиеоегоподдержке,работалиодинаково.ОнназываетсястандартECMAScriptпоимениорганизацииECMA.НапрактикеможноговоритьоECMAScriptиJavaScriptкакободномитомже.

МногиеругаютJavaScriptиговорятонёммногоплохого.Имногоеизэтого–правда.КогдамнепервыйразпришлосьписатьпрограммунаJavaScript,ябыстропочувствовалотвращение–языкпринималпрактическивсё,чтояписал,приэтоминтерпретировалэтововсенетак,какяподразумевал.Восновномэтобылоиз-затого,чтоянеимелпонятияотом,чтоделаю,нотутестьипроблема:JavaScriptслишкомлиберален.Задумывалосьэтокакоблегчениепрограммированиядляначинающих.Вреальности,этозатрудняетрозыскпроблемвпрограмме,потомучтосистемаонихнесообщает.

Гибкостьимеетсвоипреимущества.Онаоставляетместодляразныхтехник,невозможныхвболеестрогихязыках.Иногда,какмыувидимвглаве«модули»,еёможноиспользоватьдляпреодолениянекоторыхнедостатковязыка.Послетого,какяпонастоящемуизучилипоработалсним,янаучилсялюбитьJavaScript.

ВышлоуженескольковерсийязыкаJavaScript.ECMAScript3быладоминирующей,распространённойверсиейвовремяподъёмаязыка,примернос2000до2010.Вэтовремяготовиласьамбициозная4-яверсия,вкоторойбылозапланированонесколькорадикальныхулучшенийи

ВыразительныйJavascript

11Опрограммировании

Page 12: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

расширенийязыка.Однакополитическиепричинысделалиизменениеживогопопулярногоязыкаоченьсложным,иработанад4-йверсиейбылапрекращенав2008.Вместонеёвышламенееамбициозная5-яверсияв2009.Сейчасбольшинствобраузеровподдерживает5-юверсию,которуюмыибудемиспользоватьвкниге.

JavaScriptподдерживаютнетолькобраузеры.БазыданныхтипаMongoDBandCouchDBиспользуютеговкачествескриптовогоязыкаиязыказапросов.Естьнесколькоплатформдлядекстоповисерверов,наиболееизвестнаяизкоторыхNode.js,предоставляютмощноеокружениедляпрограммированиявнебраузера.

Код–этотекст,изкоторогосостоятпрограммы.Вбольшинствеглавкнигиестькод.Чтениеинаписаниекода–этонеотъемлемаячастьобученияпрограммированию.Старайтесьнепростопробегатьглазамипримеры–читайтеихвнимательноиразбирайтесь.Сначалаэтобудетпроисходитьмедленноинепонятно,новыбыстроовладеетенавыками.Тоже–насчётупражнений.Неподразумевайте,чтовывнихразобрались,поканенапишетеработающийвариант.

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

ВыможетеустановитьNode.jsивыполнятьпрограммысегопомощью.Такжевыможетеделатьэтовконсолибраузера.В12главебудетобъяснено,каквстраиватьпрограммывHTML-страницы.Такжеестьсайтытипаjsbin.com,позволяющиепростозапускатьпрограммывбраузере.Насайтекнигиестьпесочницадлякода.

Код,ичтоснимделать

ВыразительныйJavascript

12Опрограммировании

Page 13: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Подповерхностьюмашиныдвижетсяпрограмма.Безусилий,онарасширяетсяисжимается.Находясьввеликойгармонии,электронырассеиваютсяисобираются.Формынамониторе–лишьрябьнаводе.Сутьостаётсяскрытойвнутри…

МастерЮан-Ма,Книгапрограммирования

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

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

Кпримеру,номер13.Вместодесятичнойсистемы,состоящейиз10цифр,увасестьдвоичнаясистемасдвумяцифрами.Значениекаждойпозициичислаудваиваетсяпридвижениисправаналево.Биты,составляющиечисло13,вместесихвесами:

00001101

1286432168421

Получаетсядвоичноечисло00001101,или8+4+1,чторавно13.

Представьтеокеанбит.Типичныйсовременныйкомпьютерхранитболее30миллиардовбитвоперативнойпамяти.Постояннаяпамять(жёсткийдиск)обычноещёнапарупорядковобъёмнее.

Величины,типыиоператоры

Величины.

ВыразительныйJavascript

13Величины,типыиоператоры

Page 14: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Длясозданиявеличинывамнужноуказатьеёимя.Этоудобно.Вамненадособиратьстройматериалыилиплатитьзаних.Нужнопростопозвать–иоп-па,готово.Онинесоздаютсяизвоздуха–каждаявеличинагде-тохранится,иесливыхотитеиспользоватьогромноеихколичество,увасмогутзакончитьсябиты.Ксчастью,этотолькоеслионивсенужнывамодновременно.Когдавеличинавамстанетненужна,онарастворяется,ииспользованныееюбитыпоступаютвпереработкукакстройматериалдляновыхвеличин.

ВэтойглавемызнакомимсясатомамипрограммJavaScript–простыетипывеличиниоператоры,которыекнимприменимы.

Величинычисловыхтипов,это–сюрприз–числа.ВпрограммеJavaScriptонизаписываютсякак

13

Используйтеэтузаписьвпрограмме,ионавызоветкжизнивкомпьютерной

Числа

ВыразительныйJavascript

14Величины,типыиоператоры

Page 15: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

памятицепочкубит,представляющуючисло13.

JavaScriptиспользуетфиксированноечислобит(64)дляхранениячисленныхвеличин.Числовеличин,которыеможновыразитьприпомощи64бит,ограничено–тоестьисамичислатожеограничены.ДляNдесятичныхцифрколичествочисел,которыеимиможнозаписать,равно10встепениN.Аналогично,64битамиможновыразить2в64степеничисел.Этодовольномного.

Раньшеукомпьютеровпамятибыломеньше,итогдадляхранениячиселиспользовалигруппыиз8или16бит.Былолегкослучайнопревыситьмаксимальноечислодлятакихнебольшихчисел–тоесть,использоватьчисло,котороенепомещалосьвэтотнаборбит.Сегодняукомпьютеровпамятимного,можноиспользоватькускипо64бит,изначитвамнадобеспокоитьсяобэтомтолько,есливыработаетесастрономическимичислами.

Правда,невсечисламеньше2^64помещаютсявчислоJavaScript.Вэтихбитахтакжехранятсяотрицательныечисла–поэтому,одинбитхранитзнакчисла.Крометого,намнужноиметьвозможностьхранитьдроби.Дляэтогочастьбитиспользуетсядляхраненияпозициидесятичнойточки.Реальныймаксимумдлячисел–примерно10^15,чтовобщемвсёравнодовольномного.

Дробизаписываютсяспомощьюточки.

9.81

Оченьбольшиеилималенькиечислазаписываютсянаучнойзаписьюсбуквой“e”(exponent),закоторойследуетстепень:

2.998e8

Это2.998×10^8=299800000.

Вычислениясцелымичислами(которыетакженазываютсяinteger),меньшими,чем10^15,гарантированобудутточными.Вычислениясдробямиобычнонет.Также,какчислоπ(пи)нельзяпредставитьточноприпомощи

ВыразительныйJavascript

15Величины,типыиоператоры

Page 16: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Главное,чтоможноделатьсчислами–этоарифметическиевычисления.Сложенияиумноженияиспользуютдвачислаивыдаюттретье.КакэтозаписываетсявJavaScript:

100+4*11

Символы+и*называютсяоператорами.Первый–сложение,второй–умножение.Помещаемоператормеждудвумявеличинамииполучаемзначениевыражения.

Авпримереполучается«сложить4и100изатемумножитьрезультатна11»илиумножениевыполняетсясначала?Каквымоглидогадаться,умножениевыполняетсяпервым.Нокакивматематике,этоможноизменитьприпомощискобок:

(100+4)*11

Длявычитанияиспользуетсяоператор-,адляделения-/

Когдаоператорыиспользуютсябезскобок,порядокихвыполненияопределяетсяихприоритетом.Уоператоров*и/приоритетодинаковый,выше,чему+и-,которыемеждусобойравныпоприоритету.Привычисленииоператоровсравнымприоритетомонивычисляютсяслеванаправо:

1-2+1

вычисляетсякак(1-2)+1

Покабеспокоитьсяоприоритетахненадо.Еслисомневаетесь–используйте

Арифметика

ВыразительныйJavascript

16Величины,типыиоператоры

Page 17: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

скобки.

Естьещёодиноператор,которыйвынесразуузнаете.Символ%используетсядляполученияостатка.X%Y–остатокотделенияXнаY.314%100даёт14,и144%12даёт0.Приоритетуоператоратакойже,какуумноженияиделения.Егоещёчастоназывают«делениепомодулю»,хотяболееправильно«состатком».

ВJavaScriptестьтриспециальныхзначения,которыесчитаютсячислами,новедутсебянекакобычныечисла.

ЭтоInfinityи-Infinity,которыепредставляютположительнуюиотрицательнуюбесконечности.Infinity-1=Infinity,итакдалее.Ненадейтесьсильнонавычислениясбесконечностями,онинеслишкомстрогие.

Третьечисло:NaN.Обозначает«notanumber»(нечисло),хотяэтовеличиначисловоготипа.Выможетеполучитьеёпослевычисленийтипа0/0,Infinity–Infinity,илидругихопераций,которыеневедуткточнымосмысленнымрезультатам.

Следующийбазовыйтипданных–строки.Онииспользуютсядляхранениятекста.Записываютсяонивкавычках:

"Чтопосеешь,тоизпруда"

'Бабасвозу,потехечас'

Можноиспользоватькакдвойные,такиодинарныекавычки–главноеиспользоватьихвместе.Почтивсёможнозаключитьвкавычкиисделатьизэтогостроку.Нонекоторыесимволывызываютпроблемы.Например,сложнозаключитькавычкивкавычки.Переводстрокитоженельзяпростотакзаключитьвних–строкадолжнаидтиоднойстрокой.

Длязаключенияспециальныхсимволовиспользуетсяобратныйслеш.Он

Специальныечисла

Строки

ВыразительныйJavascript

17Величины,типыиоператоры

Page 18: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

обозначает,чтосимвол,идущийзаним,имеетспециальноезначение–этоназывается«экранированиесимволов»(escapecharacter).\”можнозаключатьвдвойныекавычки.\nобозначаетпереводстроки,\t–табуляцию.

Строка“Междупервойивторой\nсимволбудетнебольшой”насамомделебудетвыглядетьтак:

Междупервойивторой

символбудетнебольшой

Есливамнужновключитьвстрокуобратныйслеш,еготоженужноэкранировать:\.Инструкцию“Символновойстроки—это“\n””нужнобудетнаписатьтак:

"Символновойстроки–это\"\\n\""

Строкинельзяделить,умножатьискладывать.Однакоснимиможноиспользоватьоператор+,которыйбудетсоединятьихдругсдругом.Следующеевыражениевыдастслово«соединение»:

"сое"+"ди"+"н"+"ение"

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

Невсеоператорызаписываютсясимволами–некоторыесловами.Одинизтакихоператоров–typeof,которыйвыдаётназваниетипавеличины,ккоторойонприменяется.

console.log(typeof4.5)

//→number

console.log(typeof"x")

//→string

Унарныеоператоры

ВыразительныйJavascript

18Величины,типыиоператоры

Page 19: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Будемиспользоватьвызовconsole.logвпримерах,когдазахотимувидетьрезультатнаэкране.Какименнобудетвыданрезультат–зависитотокружения,вкоторомвызапускаетескрипт.

Предыдущиеоператорыработалисдвумявеличинами,однакоtypeofиспользуеттолькоодну.Операторы,работающиесдвумявеличинами,называютсябинарными,асодной–унарными.Минус(вычитание)можноиспользоватьикакунарный,икакбинарный.

console.log(-(10-2))

//→-8

Частовамнужнавеличина,котораяпростопоказываетоднуиздвухвозможностей–типа«да»и«нет»,или«вкл»и«выкл».ДляэтоговJavaScriptестьтипBoolean,укоторогоестьвсегодвазначения–trueиfalse(правдаиложь).

Одинизспособовполучитьбулевскиевеличины:

console.log(3>2)

//→true

console.log(3<2)

//→false

Знаки<и>традиционнообозначают«меньше»и«больше».Этобинарныеоператоры.Врезультатеихиспользованиямыполучаембулевскуювеличину,котораяпоказывает,являетсялинеравенствоверным.

Строкиможносравниватьтакже:

console.log("Арбуз"<"Яблоко")

//→true

Булевскиевеличины

Сравнения

ВыразительныйJavascript

19Величины,типыиоператоры

Page 20: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Строкисравниваютсяпоалфавиту:буквывверхнемрегистревсегда«меньше»букввнижнемрегистре.СравнениеоснованонастандартеUnicode.Этотстандартприсваиваетномерпрактическилюбомусимволуизлюбогоязыка.ВовремясравнениястрокJavaScriptпроходитпоихсимволамслеванаправо,сравниваяномерныекодыэтихсимволов.

Другиесходныеоператоры–это>=(большеилиравно),<=(меньшеилиравно),==(равно),!=(неравно).

console.log("Хочется"!="Колется")

//→true

ВJavaScriptестьтолькооднавеличина,котораянеравнасамойсебе–NaN(«нечисло»).

console.log(NaN==NaN)

//→false

NaN–эторезультатлюбогобессмысленноговычисления,поэтомуоннеравенрезультатукакого-тодругогобессмысленноговычисления.

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

Оператор&&—логическое«и».Онбинарный,иегорезультат–правда,толькоеслиобевеличины,ккоторымонприменяется,тожеправда.

console.log(true&&false)

//→false

console.log(true&&true)

//→true

Оператор||—логическое«или».Выдаётtrue,еслиоднаизвеличинtrue.

console.log(false||true)

//→true

console.log(false||false)

//→false

ВыразительныйJavascript

20Величины,типыиоператоры

Page 21: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

«Нет»записываетсяприпомощивосклицательногознака“!”.Этоунарныйоператор,которыйобращаетданнуювеличинунаобратную.!trueполучаетсяfalse,!falseполучаетсяtrue.

Прииспользованиилогическихиарифметическихоператоровневсегдаясно,когданужныскобки.Напрактикевыможетесправитьсясэтим,зная,чтоу||приоритетнижевсех,потомидёт&&,потомоператорысравнения,потомвсеостальные.Такойпорядокбылвыбрандлятого,чтобыввыраженияхтипаследующегоможнобылоиспользоватьскобокпоминимуму:

1+1==2&&10*10>50

Последнийлогическийоператорнеунарныйинебинарный–онтройной.Записываетсяприпомощивопросительногознакаидвоеточия:

console.log(true?1:2);

//→1

console.log(false?1:2);

//→2

Этоусловныйоператор,укотороговеличинаслеваотвопросительногознакавыбираетоднуиздвухвеличин,разделённыхдвоеточием.Когдавеличинаслеваtrue,выбираемпервоезначение.Когдаfalse,второе.

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

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

Неопределённыезначения

Автоматическоепреобразованиетипов

ВыразительныйJavascript

21Величины,типыиоператоры

Page 22: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Ранееяупоминал,чтоJavaScriptпозволяетвыполнятьлюбые,подчасоченьстранныепрограммы.Кпримеру:

console.log(8*null)

//→0

console.log("5"-1)

//→4

console.log("5"+1)

//→51

console.log("пять"*2)

//→NaN

console.log(false==0)

//→true

Когдаоператорприменяется«нектому»типувеличин,JavaScriptвтихуюпреобразовываетвеличинукнужномутипу,используянаборправил,которыеневсегдасоответствуютвашиможиданиям.Этоназываетсяприведениемтипов(coercion).Впервомвыраженииnullпревращаетсяв0,а“5”становится5(изстроки–вчисло).Однаковтретьемвыражении+выполняетконкатенацию(объединение)строк,из-зачего1преобразовываетсяв“1’(изчиславстроку).

Когдачто-тонеочевидноепревращаетсявномер(кпримеру,“пять”илиundefined),возвращаетсязначениеNaN.ПоследующиеарифметическиеоперациисNaNопятьполучаютNaN.Есливыполучилитакоезначение,поищите,гдепроизошлослучайноепреобразованиетипов.

Присравнениивеличинодноготипачерез==,легкопредсказать,чтовыдолжныполучитьtrue,еслиониодинаковые(исключаяслучайсNaN).Нокогдатипыразличаются,JavaScriptиспользуетсложныйизапутанныйнаборправилдлясравнений.Обычноонпытаетсяпреобразоватьтиподнойизвеличинвтипдругой.Когдасоднойизстороноператоравозникаетnullилиundefined,онвыдаётtrue,еслиобестороныимеютзначениеnullилиundefined.

console.log(null==undefined);

//→true

console.log(null==0);

//→false

ВыразительныйJavascript

22Величины,типыиоператоры

Page 23: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Последнийпримердемонстрируетполезныйприём.Когдавамнадопроверить,имеетливеличинареальноезначениевместоnullилиundefined,выпростосравниваетееёсnullприпомощи==или!=.

Ночто,есливамнадосравнитьнечтосточнойвеличиной?Правилапреобразованиятиповвбулевскиезначенияговорят,что0,NaNипустаястрока“”считаютсяfalse,авсеостальные–true.Поэтому0==falseи“”==false.Вслучаях,когдавамненужноавтоматическоепреобразованиетипов,можноиспользоватьещёдваоператора:===и!==.Первыйпроверяет,чтодвевеличиныабсолютноидентичны,второй–наоборот.Итогдасравнение“”===falseвозвращаетfalse.

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

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

Кпримеру,||вернётзначениеслевойчасти,когдаегоможнопреобразоватьвtrue–аиначевернётправуючасть.

console.log(null||"user")

//→user

console.log("Karl"||"user")

//→Karl

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

Короткоевычислениелогическихоператоров

ВыразительныйJavascript

23Величины,типыиоператоры

Page 24: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

случай.

Оператор&&работаетсходнымобразом,нонаоборот.Есливеличинаслевапреобразовываетсявfalse,онвозвращаетэтувеличину,аиначе–величинусправа.

Ещёодноважноеихсвойство–выражениевправойчастивычисляетсятолькопринеобходимости.Вслучаеtrue||Xневажно,чемуравноX.Дажееслиэтокакое-тоужасноевыражение.РезультатвсегдаtrueиXневычисляется.Такжеработаетfalse&&X–Xпростоигнорируется.Этоназываетсякороткимвычислением.

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

МырассмотреличетыретипавеличинJavaScript:числа,строки,булевскиеинеопределённые.

Этивеличиныполучаются,когдамыпишемихимена(true,null)илизначения(13,“ёпрст”).Ихможнокомбинироватьиизменятьприпомощиоператоров.Дляарифметикиестьбинарныеоператоры(+,-,*,/,and%),объединениестрок(+),сравнение(==,!=,===,!==,<,>,<=,>=),илогическиеоператоры(&&,||),также,какинесколькоунарныхоператоров(-дляотрицательногозначения,!длялогическогоотрицания,иtypeofдляопределениятипавеличины).

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

Итог

ВыразительныйJavascript

24Величины,типыиоператоры

Page 25: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Сердцемоёсияетярко-краснымсветомподмоейтонкой,прозрачнойкожей,иимприходитсявколотьмнедесятькубиковJavaScript,чтобывернутьменякжизни(яхорошореагируюнатоксинывкрови).Отэтойфигниувасвразжабрыпобледнеют!

_why,Why's(Poignant)GuidetoRuby

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

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

Фрагменткода,результатомработыкоторогоявляетсянекаявеличина,называетсявыражением.Каждаявеличина,записаннаябуквально(например,22или“психоанализ”)тожеявляетсявыражением.Выражение,записанноевскобках,такжеявляетсявыражением,какибинарныйоператор,применяемыйкдвумвыражениямилиунарный–кодному.

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

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

Простейшаяинструкция–этовыражениесточкойсзапятойпосленего.Это—программа:

Структурапрограмм

Выраженияиинструкции

ВыразительныйJavascript

25Структурапрограмм

Page 26: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

1;

!false;

Правда,этобесполезнаяпрограмма.Выражениеможноиспользоватьтолькодляполучениявеличины,котораяможетбытьиспользованавдругомвыражении,охватывающемэто.Инструкциястоитсамапосебеиеёприменениеизменяетчто-товмирепрограммы.Онаможетвыводитьчто-тонаэкран(изменениевмире),илименятьвнутреннеесостояниемашинытакимобразом,чтоэтоповлияетнаследующиезаниминструкции.Этиизмененияназываютсяпобочнымиэффектами.Инструкциивпредыдущемпримерепростовыдаютвеличины1иtrue,исразуихвыбрасывают.Онинеоказываютникакоговлияниянамирпрограммы.Привыполнениипрограммыничегозаметногонепроисходит.

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

Какжепрограммахранитвнутреннеесостояние?Каконаегозапоминает?Мыполучалиновыевеличиныизстарых,ностарыевеличиныэтонеменяло,ановыенужнобылоиспользоватьсразу,илижеониисчезали.Чтобызахватитьихранитьих,JavaScriptпредлагаетнечтоподназванием«переменная».

varcaught=5*5;

Иэтодаётнамвторойвидинструкций.Специальноеключевоеслово(keyword)varпоказывает,чтовэтойинструкциимыобъявляемпеременную.Занимидётимяпеременной,и,еслимысразухотимназначитьейзначение–оператор=ивыражение.

Переменные

ВыразительныйJavascript

26Структурапрограмм

Page 27: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Примерсоздаётпеременнуюподименемcaughtииспользуетеёдлязахватачисла,котороеполучаетсяврезультатеперемножения5и5.

Послеопределенияпеременнойеёимяможноиспользоватьввыражениях.Величинапеременнойбудеттакой,какоезначениевнейсейчассодержится.Пример:

varten=10;

console.log(ten*ten);

//→100

Переменныеможноназыватьлюбымсловом,котороенеявляетсяключевым(типаvar).Нельзяиспользоватьпробелы.Цифрытожеможноиспользовать,нонепервымсимволомвназвании.Нельзяиспользоватьзнакипунктуации,кромесимволов$и_.

Переменнойприсваиваютзначениененавсегда.Оператор=можноиспользоватьнасуществующихпеременныхвлюбоевремя,чтобыприсвоитьимновоезначение.

varmood="лёгкое";

console.log(mood);

//→лёгкое

mood="тяжёлое";

console.log(mood);

//→тяжёлое

Представляйтесебепеременныеневвидекоробочек,аввидещупалец.Онинесодержатзначения–онихватаютих.Двепеременныемогутссылатьсянаоднозначение.Программаимеетдоступтолькокзначениям,которыеонисодержат.Когдавамнужночто-тозапомнить,выотращиваетещупальцеидержитесьзаэто,иливыиспользуетесуществующеещупальце,чтобыудержатьэто.

ВыразительныйJavascript

27Структурапрограмм

Page 28: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Пример.Длязапоминанияколичестваденег,которыевамдолженВасилий,высоздаётепеременную.Затем,когдаонвыплачиваетчастьдолга,выдаётеейновоезначение.

varvasyaDebt=140;

vasyaDebt=vasyaDebt-35;

console.log(vasyaDebt);

//→105

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

Однаинструкцияvarможетсодержатьнесколькопеременных.Определениянужноразделятьзапятыми.

varone=1,two=2;

console.log(one+two);

//→3

Словасоспециальнымсмыслом,типаvar–ключевые.Ихнельзя

Переменныекакщупальца

Ключевыеизарезервированныеслова

ВыразительныйJavascript

28Структурапрограмм

Page 29: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

использоватькакименапеременных.Такжеестьнесколькослов,«зарезервированныхдляиспользования»вбудушихверсияхJavaScript.Ихтоженельзяиспользовать,хотявнекоторыхсредахисполненияэтовозможно.Полныйихсписокдостаточнобольшой.

breakcasecatchcontinuedebuggerdefaultdeletedoelsefalsefinally

forfunctionifimplementsininstanceofinterfaceletnewnullpackage

privateprotectedpublicreturnstaticswitchthrowtruetrytypeofvar

voidwhilewithyieldthis

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

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

Многиевеличиныизстандартногоокруженияимеюттипfunction(функция).Функция–отдельныйкусочекпрограммы,которыйможноиспользоватьвместесдругимивеличинами.Кпримеру,вбраузерепеременнаяalertсодержитфункцию,котораяпоказываетнебольшоеокноссообщением.Используютеготак:

alert("Сдобрымутром!");

Окружение

Функции

ВыразительныйJavascript

29Структурапрограмм

Page 30: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Функцияalertможетиспользоватьсякаксредствовыводаприэкспериментах,нозакрыватькаждыйразэтоокновамскоронадоест.Впрошлыхпримерахмыиспользовалифункциюconsole.logдлявыводазначений.БольшинствосистемJavaScript(включаявсесовременныебраузерыиNode.js)предоставляютфункциюconsole.log,котораявыводитвеличинынакакое-либоустройствовывода.ВбраузерахэтоконсольJavaScript.Этачастьбраузераобычноскрыта–большинствобраузеровпоказываютеёпонажатиюF12,илиCommand-Option-IнаМаке.Еслиэтонесработало,поищитевменю“webconsole”или“developertools”.

Впримерахэтойкнигирезультатывыводапоказанывкомментариях:

varx=30;

console.log("thevalueofxis",x);

//→thevalueofxis30

Хотявименахпеременныхнельзяиспользоватьточку–она,очевидно,содержитсявназванииconsole.log.Этооттого,чтоconsole.log–непростаяпеременная.Этовыражение,возвращающеесвойствоlogпеременнойconsole.Мыпоговоримобэтомвглаве4.

Диалогalert

Функцияconsole.log

ВыразительныйJavascript

30Структурапрограмм

Page 31: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Показдиалоговогоокнаиливыводтекстанаэкран–этопобочныйэффект.Множествофункцийполезныоттого,чтоонипроизводятэтиэффекты.Функциитакжемогутпроизводитьзначения,ивэтомслучаеимненуженпобочныйэффектдлятого,чтобыбытьполезной.Кпримеру,функцияMath.maxпринимаетлюбоеколичествопеременныхивозвращаетзначениесамойбольшой:

console.log(Math.max(2,4));

//→4

Когдафункцияпроизводитзначение,говорят,чтоонавозвращаетзначение.Всё,чтопроизводитзначение–этовыражение,тоестьвызовыфункцийможноиспользоватьвнутрисложныхвыражений.Кпримеру,возвращаемоефункциейMath.min(противоположностьMath.max)значениеиспользуетсякакодинизаргументовоператорасложения:

console.log(Math.min(2,4)+100);

//→102

Вследующейглавеописано,какписатьсобственныефункции.

Окружениебраузерасодержитдругиефункции,кромеalert,которыепоказываютвсплывающиеокна.МожновызватьокносвопросомикнопкамиOK/Cancelприпомощифункцииconfirm.Онавозвращаетбулевскоезначение–true,еслинажатоOK,иfalse,еслинажатоCancel.

confirm("Нучто,поехали?");

Возвращаемыезначения

promptиconfirm

ВыразительныйJavascript

31Структурапрограмм

Page 32: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Функциюpromptможноиспользовать,чтобызадатьоткрытыйвопрос.Первыйаргумент–вопрос,второй–текст,скоторогопользовательначинает.Вдиалоговоеокноможновписатьстрокутекста,ифункциявернётеговвидестроки.

prompt("Расскажимневсё,чтознаешь.","...");

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

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

vartheNumber=Number(prompt("Выберичисло",""));

alert("Твоёчисло–квадратныйкореньиз"+theNumber*theNumber);

ФункцияNumberпреобразовываетвеличинувчисло.Намэтонужно,потомучтоpromptвозвращаетстроку.ЕстьсходныефункцииStringиBoolean,преобразующиевеличинывсоответствующиетипы.

Простаясхемапрямогопорядкаисполненияпрограммы:

Условноевыполнение

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

Управлениепорядкомвыполненияпрограммы

ВыразительныйJavascript

32Структурапрограмм

Page 33: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

издвухвозможныхпутей,основываясьнабулевскойвеличине:

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

vartheNumber=prompt("Выберичисло","");

if(!isNaN(theNumber))

alert("Твоёчисло–квадратныйкореньиз"+theNumber*theNumber);

Теперь,введя«сыр»,вынеполучитевывод.

Ключевоесловоifвыполняетилипропускаетинструкцию,взависимостиотзначениябулевоговыражения.Этовыражениезаписываетсяпослеifвскобках,изанимидётнужнаяинструкция.

ФункцияisNaN–стандартнаяфункцияJavaScript,котораявозвращаетtrue,толькоеслиеёаргумент–NaN(нечисло).ФункцияNumberвозвращаетNaN,еслизадатьейстроку,котораянепредставляетсобойдопустимоечисло.Врезультате,условиезвучиттак:«выполнить,еслитолькоtheNumberнеявляетсяне-числом».

Частонужнонаписатькоднетолькодляслучая,когдавыражениеистинно,ноидляслучая,когдаоноложно.Путьсвариантами–этовтораястрелочкадиаграммы.Ключевоесловоelseиспользуетсявместесifдлясозданиядвухраздельныхпутейвыполнения.

vartheNumber=Number(prompt("Выберичисло",""));

if(!isNaN(theNumber))

alert("Твоёчисло–квадратныйкореньиз"+theNumber*theNumber);

else

alert("Нутычточисло-тоневвёл?");

Есливамнужнобольшеразныхпутей,можноиспользоватьнесколькопар

ВыразительныйJavascript

33Структурапрограмм

Page 34: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

if/elseпоцепочке.

varnum=Number(prompt("Выберичисло","0"));

if(num<10)

alert("Маловато");

elseif(num<100)

alert("Нормально");

else

alert("Многовато");

Программапроверяет,действительнолиnumменьше10.Еслида–выбираетэтуветку,ипоказывает«Маловато».Еслинет,выбираетдругую–накоторойещёодинif.Еслиследующееусловиевыполняется,значитномербудетмежду10и100,ивыводится«Нормально».Еслинет–значит,выполняетсяпоследняяветка.

Последовательностьвыполненияпримернотакая:

Представьтепрограмму,выводящуювсечётныечислаот0до12.Можнозаписатьеётак:

console.log(0);

console.log(2);

console.log(4);

console.log(6);

console.log(8);

console.log(10);

console.log(12);

Этоработает–носмыслпрограммированиявтом,чтобыработатьменьше,чемкомпьютер,аненаоборот.Еслибнампонадобилисьвсечисладо1000,эторешениебылобынеприемлемым.Намнужнавозможностьповторения.

Циклыwhileиdo

ВыразительныйJavascript

34Структурапрограмм

Page 35: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Этотвидконтролянадпорядкомвыполненияназываетсяциклом.

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

varnumber=0;

while(number<=12){

console.log(number);

number=number+2;

}

//→0

//→2

//…ит.д.

Инструкция,начинающаясясключевогословаwhile–этоцикл.Заwhileследуетвыражениевскобках,изатеминструкция(телоцикла)–также,какуif.Циклвыполняетинструкцию,покавыражениевыдаётистинныйрезультат.

Вцикленамнужновыводитьзначениеиприбавлятькнему.Еслинамнужновыполнятьвцикленесколькоинструкций,мызаключаемеговфигурныескобки{}.Фигурныескобкидляинструкций–каккруглыескобкидлявыражений.Онигруппируютихипревращаютвединое.Последовательностьинструкций,заключённаявфигурныескобки,называетсяблоком.

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

Переменнаяnumberпоказывает,какпеременнаяможетотслеживатьпрогресспрограммы.Прикаждомповторениициклаnumberувеличиваетсяна2.Передкаждымповторениемоносравниваетсяс12,чтобыпонять,сделалалипрограммавсё,чтотребовалось.

ВыразительныйJavascript

35Структурапрограмм

Page 36: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Дляпримераболееполезнойработымыможемнаписатьпрограммувычисления2в10степени.Мыиспользуемдвепеременные:однудляслежениязарезультатом,авторую–дляподсчётаколичестваумножений.Циклпроверяет,достиглаливтораяпеременная10,изатемобновляетобе.

varresult=1;

varcounter=0;

while(counter<10){

result=result*2;

counter=counter+1;

}

console.log(result);

//→1024

Можноначинатьcounterс1ипроверятьегона<=10,нопопричинам,которыестанутясныдалее,всегдалучшеначинатьсчётчикис0.

Циклdoпохожнациклwhile.Отличаетсятольководном:циклdoвсегдавыполняеттелохотябыодинраз,апроверяетусловиепослепервоговыполнения.Поэтомуитестируемоевыражениезаписываютпослетелацикла:

do{

varname=prompt("Whoareyou?");

}while(!name);

console.log(name);

Этапрограммазаставляетввестиимя.Онаспрашиваетегосноваиснова,поканеполучитчто-токромепустойстроки.Добавление"!"превращаетзначениевбулевскоеизатемприменяетлогическоеотрицание,авсестроки,кромепустой,преобразуютсявбулевскоеtrue.

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

ВыразительныйJavascript

36Структурапрограмм

Page 37: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Многоцикловстроятсяпотакомушаблону,каквпримере.Создаётсяпеременная-счётчик,потомидётциклwhile,гдепроверочноевыражениеобычнопроверяет,недостиглилимыкакой-нибудьграницы.Вконцетелацикласчётчикобновляется.

Посколькуэтотакойчастыйслучай,вJavaScriptестьвариантпокороче,циклfor.

for(varnumber=0;number<=12;number=number+2)

console.log(number);

//→0

//→2

//…ит.д.

Этапрограммаэквивалентнапредыдущей.Толькотеперьвсеинструкции,относящиесякотслеживаниюсостоянияцикла,сгруппированы.

Скобкипослеforсодержатдветочкисзапятой,разделяяинструкциюнатричасти.Перваяинициализируетцикл,обычнозадаваяначальноезначениепеременной.Вторая–выражениепроверкинеобходимостипродолженияцикла.Третья–обновляетсостояниепослекаждогопрохода.Вбольшинствеслучаевтакаязаписьболеекороткаяипонятная,чемwhile.

Вычисляем2^10припомощиfor:

varresult=1;

for(varcounter=0;counter<10;counter=counter+1)

result=result*2;

console.log(result);

//→1024

Хотяянеписалфигурныхскобок,яотделяютелоциклапробелами.

Циклыfor

ВыразительныйJavascript

37Структурапрограмм

Page 38: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Дождаться,покаусловиецикланестанетложным–неединственныйспособзакончитьцикл.Специальнаяинструкцияbreakприводиткнемедленномувыходуизцикла.

Вследующемпримеремыпокидаемцикл,когданаходимчисло,большее20иделящеесяна7безостатка.

for(varcurrent=20;;current++){

if(current%7==0)

break;

}

console.log(current);

//→21

Конструкцияforнеимеетпроверочнойчасти–поэтомуциклнеостановится,поканесработаетинструкцияbreak.

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

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

Ключевоесловоcontinueтакжевлияетнаисполнениецикла.Когдаэтослововстречаетсявцикле,оннемедленнопереходитнаследующуюитерацию.

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

counter=counter+1;

Выходизцикла

Короткоеобновлениепеременных

ВыразительныйJavascript

38Структурапрограмм

Page 39: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ВJavaScriptестьдляэтогокороткаязапись:

counter+=1;

Подобныезаписиработаютдлямногихдругихоператоров,кпримеруresult*=2дляудвоения,илиcounter-=1дляобратногоотсчёта.

Этопозволяетнамсократитьпрограммувыводачётныхчисел:

for(varnumber=0;number<=12;number+=2)

console.log(number);

Дляcounter+=1иcounter-=1естьещёболеекороткиезаписи:counter++andcounter--.

Частокодвыглядиттак:

if(variable=="value1")action1();

elseif(variable=="value2")action2();

elseif(variable=="value3")action3();

elsedefaultAction();

Существуетконструкцияподназваниемswitch,котораяупрощаетподобнуюзапись.Ксожалению,синтаксисJavaScriptвэтомслучаедовольностранный–частоцепочкаif/elseвыглядитлучше.Пример:

switch(prompt("Какпогодка?")){

case"дождь":

console.log("Незабудьзонт.");

break;

case"снег":

console.log("Блин,мывРоссии!");

break;

case"солнечно":

console.log("Оденьсяполегче.");

case"облачно":

Работаемспеременнымиприпомощиswitch

ВыразительныйJavascript

39Структурапрограмм

Page 40: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

console.log("Идигуляй.");

break;

default:

console.log("Непонятнаяпогода!");

break;

}

Вблокswitchможнопоместитьлюбоеколичествометокcase.Программаперепрыгиваетнаметку,соответствующуюзначениюпеременнойвswitch,илинаметкуdefault,еслиподходящихметокненайдено.Послеэтогоинструкцииисполняютсядопервойинструкцииbreak–дажееслимыужепрошлидругуюметку.Иногдаэтоможноиспользоватьдляисполненияодногоитогожекодавразныхслучаях(вобоихслучаях«солнечно»и«облачно»программапорекомендуетпойтипогулять).Однако,оченьлегкозабытьзаписьbreak,чтоприведётквыполнениюнежелательногоучасткакода.

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

fuzzylittleturtle

fuzzy_little_turtle

FuzzyLittleTurtle

fuzzyLittleTurtle

Первыйдовольносложночитать.Мненравятсяподчёркивания,хотяихнеоченьудобнопечатать.СтандартныефункцииJavaScriptибольшинствопрограммистовиспользуютпоследнийвариант–каждоесловосбольшойбуквы,кромепервого.

Внекоторыхслучаях,напримервслучаефункцииNumber,первуюбуквутожепишутбольшой–когданужновыделитьфункциюкакконструктор.Оконструкторахмыпоговоримвглаве6.Сейчаспростонеобращайтенаэтовнимания.

Регистримён

ВыразительныйJavascript

40Структурапрограмм

Page 41: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Частокоднесодержитвсюинформацию,которуюхотелосьбыпередатьчитателям-людям,илидоноситеёвнепонятномвиде.Иногдавычувствуетепоэтическоевдохновение,илипростохотитеподелитьсямыслямивсвоейпрограмме.Дляэтогослужаткомментарии.

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

varaccountBalance=calculateBalance(account);

//Издалекадолго

accountBalance.adjust();

//ТечётрекаВолга

varreport=newReport();

//ТечётрекаВолга

addToReport(accountBalance,report);

//Концаикраянет

Комментарийпродолжаетсятолькодоконцастроки.Кодмеждусимволами/и/будетигнорироватьсявместесвозможнымипереводамистроки.Этоподходитдлявключенияцелыхинформационныхблоковвпрограмму:

/*

Этотгород–самыйлучший

ГороднаЗемле.

Онкакбудтонарисован

Меломнастене.

*/

varmyCity=‘Челябинск’;

Теперьвызнаете,чтопрограммасостоитизинструкций,которыесамимогутсодержатьинструкции.Винструкцияхсодержатсявыражения,которыемогутсостоятьизвыражений.

Записываяинструкцииподряд,мыполучаемпрограмму,которая

Комментарии

Итог

ВыразительныйJavascript

41Структурапрограмм

Page 42: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

выполняетсясверхувниз.Выможетеизменятьэтотпотоквыполнения,используяусловные(if,else,иswitch)операторыиоператорыцикла(while,do,иfor).

Переменныеможноиспользоватьдляхранениякусочковданныхподопределённымназваниемидляотслеживаниясостоянияпрограммы.Окружение–наборопределённыхпеременных.Системы,исполняющиеJavaScript,всегдадобавляютнесколькостандартныхпеременныхввашеокружение.

Функции–особыепеременные,включающиечастипрограммы.ИхможновызватькомандойfunctionName(argument1,argument2).Такойвызов–этовыражение,иможетвыдаватьзначение.

Каждоеупражнениеначинаетсясописаниязадачи.Прочтитеипостарайтесьвыполнить.Всложныхситуацияхобращайтеськподсказкам.Готовыерешениязадачможнонайтинасайтекнигиeloquentjavascript.net/code/.Чтобыобучениебылоэффективным,незаглядывайтевответы,поканерешитезадачусами,илихотябынепопытаетесьеёрешитьдостаточнодолгодлятого,чтобыувасслегказаболелаголова.Тамжеможнописатькодпрямовбраузереивыполнятьего.

Напишитецикл,которыйза7вызововconsole.logвыводиттакойтреугольник:

#

##

###

####

#####

######

#######

Будетполезнознать,чтодлинустрокиможноузнать,приписавкпеременной.length.

Упражнения

Треугольниквцикле

ВыразительныйJavascript

42Структурапрограмм

Page 43: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

varabc="abc";

console.log(abc.length);

//→3

Напишитепрограмму,котораявыводитчерезconsole.logвсецифрыот1до100,сдвумяисключениями.Длячисел,нацелоделящихсяна3,онадолжнавыводить‘Fizz’,адлячисел,делящихсяна5(нонена3)–‘Buzz’.

Когдасумеете–исправьтееётак,чтобыонавыводила«FizzBuzz»длявсехчисел,которыеделятсяина3ина5.

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

Напишитепрограмму,создающуюстроку,содержащуюрешётку8х8,вкоторойлинииразделяютсясимволаминовойстроки.Накаждойпозициилибопробел,либо#.Врезультатедолжнаполучитьсяшахматнаядоска.

####

####

####

####

####

####

####

####

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

FizzBuzz

Шахматнаядоска

ВыразительныйJavascript

43Структурапрограмм

Page 44: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

ДональдКнут

Выужевиделивызовыфункций,такихкакalert.Функции–этохлебсмасломпрограммированиянаJavaScript.Идеяоборачиваниякускапрограммыивызоваеёкакпеременнойоченьвостребована.Этоинструментдляструктурированиябольшихпрограмм,уменьшенияповторений,назначенияимёнподпрограммам,иизолированиеподпрограммдруготдруга.

Самоеочевидноеиспользованиефункций–созданиеновогословаря.Придумыватьсловадляобычнойчеловеческойпрозы–дурнойтон.Вязыкепрограммированияэтонеобходимо.

Среднийвзрослыйрусскоговорящийчеловекзнаетпримерно10000слов.Редкийязыкпрограммированиясодержит10000встроенныхкоманд.Исловарьязыкапрограммированияопределёнчётче,поэтомуонменеегибок,чемчеловеческий.Поэтомунамобычноприходитсядобавлятьвнегосвоислова,чтобыизбежатьизлишнихповторений.

Определениефункции–обычноеопределениепеременной,гдезначение,котороеполучаетпеременная,являетсяфункцией.Например,следующийкодопределяетпеременнуюsquare,котораяссылаетсянафункцию,подсчитывающуюквадратзаданногочисла:

varsquare=function(x){

returnx*x;

};

Функции

Определениефункции

ВыразительныйJavascript

44Функции

Page 45: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

console.log(square(12));

//→144

Функциясоздаётсявыражением,начинающимсясключевогословаfunction.Уфункцийестьнаборпараметров(вданномслучае,толькоx),итело,содержащееинструкции,которыенеобходимовыполнитьпривызовефункции.Телофункциивсегдазаключаютвфигурныескобки,дажееслионосостоитизоднойинструкции.

Уфункцииможетбытьнесколькопараметров,иливообщеихнебыть.ВследующемпримереmakeNoiseнеимеетспискапараметров,ауpowerихцелыхдва:

varmakeNoise=function(){

console.log("Хрясь!");

};

makeNoise();

//→Хрясь!

varpower=function(base,exponent){

varresult=1;

for(varcount=0;count<exponent;count++)

result*=base;

returnresult;

};

console.log(power(2,10));

//→1024

Некоторыефункциивозвращаютзначение,какpowerиsquare,другиеневозвращают,какmakeNoise,котораяпроизводиттолькопобочныйэффект.Инструкцияreturnопределяетзначение,возвращаемоефункцией.Когдаобработкапрограммыдоходитдоэтойинструкции,онасразужевыходитизфункции,ивозвращаетэтозначениевтоместокода,откудабылавызванафункция.returnбезвыражениявозвращаетзначениеundefined.

Параметрыфункции–такиежепеременные,ноихначальныезначения

Параметрыиобластьвидимости

ВыразительныйJavascript

45Функции

Page 46: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

задаютсяпривызовефункции,аневеёкоде.

Важноесвойствофункцийвтом,чтопеременные,созданныевнутрифункции(включаяпараметры),локальнывнутриэтойфункции.Этоозначает,чтовпримересpowerпеременнаяresultбудетсоздаватьсякаждыйразпривызовефункции,иэтиотдельныееёинкарнацииникакдругсдругомнесвязаны.

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

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

varx="outside";

varf1=function(){

varx="insidef1";

};

f1();

console.log(x);

//→outside

varf2=function(){

x="insidef2";

};

f2();

console.log(x);

//→insidef2

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

ВыразительныйJavascript

46Функции

Page 47: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

Кпримеру,следующаядовольнобессмысленнаяфункциясодержитвнутриещёдве:

varlandscape=function(){

varresult="";

varflat=function(size){

for(varcount=0;count<size;count++)

result+="_";

};

varmountain=function(size){

result+="/";

for(varcount=0;count<size;count++)

result+="'";

result+="\\";

};

flat(3);

mountain(4);

flat(6);

mountain(1);

flat(1);

returnresult;

};

console.log(landscape());

//→___/''''\______/'\_

Функцииflatиmountainвидятпеременнуюresult,потомучтоонинаходятсявнутрифункции,вкоторойонаопределена.Ноонинемогутвидеть

Вложенныеобластивидимости

ВыразительныйJavascript

47Функции

Page 48: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

переменныеcountдругдруга,потомучтопеременныеоднойфункциинаходятсявнеобластивидимостидругой.Аокружениеснаружифункцииlandscapeневидитниоднойизпеременных,определённыхвнутриэтойфункции.

Корочеговоря,вкаждойлокальнойобластивидимостиможноувидетьвсеобласти,которыееёсодержат.Наборпеременных,доступныхвнутрифункции,определяетсяместом,гдеэтафункцияописанавпрограмме.Всепеременныеизблоков,окружающихопределениефункции,видны–включаяите,чтоопределенынаверхнемуровневосновнойпрограмме.Этотподходкобластямвидимостиназываетсялексическим.

Люди,изучавшиедругиеязыкипрограммирования,могутподумать,чтолюбойблок,заключённыйвфигурныескобки,создаётсвоёлокальноеокружение.НовJavaScriptобластьвидимостисоздаюттолькофункции.Выможетеиспользоватьотдельностоящиеблоки:

varsomething=1;

{

varsomething=2;

//Делаемчто-либоспеременнойsomething...

}

//Вышлиизблока...

Ноsomethingвнутриблока–этотажепеременная,чтоиснаружи.Хотятакиеблокииразрешены,имеетсмыслиспользоватьихтолькодлякомандыifициклов.

Еслиэтокажетсявамстранным–таккажетсянетольковам.ВверсииJavaScript1.7появилосьключевоесловоlet,котороеработаеткакvar,носоздаётпеременные,локальныедлялюбогоданногоблока,анетолькодляфункции.

Именафункцийобычноиспользуюткакимядлякусочкапрограммы.Такаяпеременнаяоднаждызадаётсяинеменяется.Такчтолегкоперепутатьфункциюиеёимя.

Функциикакзначения

ВыразительныйJavascript

48Функции

Page 49: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Ноэто–дверазныевещи.Вызовфункцииможноиспользовать,какпростуюпеременную–например,использоватьихвлюбыхвыражениях.Возможнохранитьвызовфункциивновойпеременной,передаватьеёкакпараметрдругойфункции,итакдалее.Такжепеременная,хранящаявызовфункции,остаётсяобычнойпеременнойиеёзначениеможнопоменять:

varlaunchMissiles=function(value){

missileSystem.launch("пли!");

};

if(safeMode)

launchMissiles=function(value){/*отбой*/};

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

Естьболеекороткаяверсиявыражения“varsquare=function…”.Ключевоесловоfunctionможноиспользоватьвначалеинструкции:

functionsquare(x){

returnx*x;

}

Этообъявлениефункции.Инструкцияопределяетпеременнуюsquareиприсваиваетейзаданнуюфункцию.Покавсёок.Естьтолькоодинподводныйкаменьвтакомопределении.

console.log("Thefuturesays:",future());

functionfuture(){

return"WeSTILLhavenoflyingcars.";

}

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

Объявлениефункций

ВыразительныйJavascript

49Функции

Page 50: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

functionexample(){

functiona(){}//Нормуль

if(something){

functionb(){}//Ай-яй-яй!

}

}

Полезнымбудетприсмотретьсяктому,какпорядоквыполненияработаетсфункциями.Вотпростаяпрограммаснесколькимивызовамифункций:

functiongreet(who){

console.log("Привет,"+who);

}

greet("Семён");

console.log("Покеда");

Обрабатываетсяонапримернотак:вызовgreetзаставляетпроходпрыгнутьнаначалофункции.Онвызываетвстроеннуюфункциюconsole.log,котораяперехватываетконтроль,делаетсвоёделоивозвращаетконтроль.Потомондоходитдоконцаgreet,ивозвращаетсякместу,откудаеговызвали.Следующаястрочкаопятьвызываетconsole.log.

Схематичноэтоможнопоказатьтак:

top

Стеквызовов

ВыразительныйJavascript

50Функции

Page 51: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

greet

console.log

greet

top

console.log

top

Посколькуфункциядолжнавернутьсянатоместо,откудаеёвызвали,компьютердолжензапомнитьконтекст,изкоторогобылавызванафункция.Водномслучае,console.logдолжнавернутьсяобратновgreet.Вдругом,онавозвращаетсявконецпрограммы.

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

Хранениестекатребуетместавпамяти.Когдастекслишкомсильноразрастается,компьютерпрекращаетвыполнениеивыдаётчто-товроде“stackoverflow”или“toomuchrecursion”.Следующийкодэтодемонстрирует–онзадаёткомпьютеруоченьсложныйвопрос,которыйприводиткбесконечнымпрыжкаммеждудвумяфункциями.Точнее,этобылибыбесконечныепрыжки,еслибыукомпьютерабылбесконечныйстек.Вреальностистекпереполняется.

functionchicken(){

returnegg();

}

functionegg(){

returnchicken();

}

console.log(chicken()+"camefirst.");

//→??

Следующийкодвполнеразрешёнивыполняетсябезпроблем:

alert("Здрасьте","Добрыйвечер","Всемпривет!");

Необязательныеаргументы

ВыразительныйJavascript

51Функции

Page 52: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Официальнофункцияпринимаетодинаргумент.Однако,притакомвызовеонанежалуется.Онаигнорируетостальныеаргументыипоказывает«Здрасьте».

JavaScriptоченьлояленпоповодуколичествааргументов,передаваемыхфункции.Есливыпередадитеслишкоммного,лишниебудутпроигнорированы.Слишкоммало–отсутствующимбудетназначенозначениеundefined.

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

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

functionpower(base,exponent){

if(exponent==undefined)

exponent=2;

varresult=1;

for(varcount=0;count<exponent;count++)

result*=base;

returnresult;

}

console.log(power(4));

//→16

console.log(power(4,3));

//→64

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

console.log("R",2,"D",2);

//→R2D2

ВыразительныйJavascript

52Функции

Page 53: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Следующийпримериллюстрируетэтотвопрос.ВнёмобъявляетсяфункцияwrapValue,котораясоздаётлокальнуюпеременную.Затемонавозвращаетфункцию,котораячитаетэтулокальнуюпеременнуюивозвращаетеёзначение.

functionwrapValue(n){

varlocalVariable=n;

returnfunction(){returnlocalVariable;};

}

varwrap1=wrapValue(1);

varwrap2=wrapValue(2);

console.log(wrap1());

//→1

console.log(wrap2());

//→2

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

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

Снебольшимизменениеммыпревращаемнашпримервфункцию,умножающуючисланалюбоезаданноечисло.

functionmultiplier(factor){

returnfunction(number){

Замыкания

ВыразительныйJavascript

53Функции

Page 54: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

returnnumber*factor;

};

}

vartwice=multiplier(2);

console.log(twice(5));

//→10

ОтдельнаяпеременнаявродеlocalVariableизпримерасwrapValueужененужна.Таккакпараметр–сампосебелокальнаяпеременная.

Потребуетсяпрактика,чтобыначатьмыслитьподобнымобразом.Хорошийвариантмысленноймодели–представлять,чтофункциязамораживаеткодвсвоёмтелеиобёртываетеговупаковку.Когдавывидитеreturnfunction(...){...},представляйте,чтоэтопультуправлениякускомкода,замороженнымдляупотребленияпозже.

Внашемпримереmultiplierвозвращаетзамороженныйкусоккода,которыймысохраняемвпеременнойtwice.Последняястрокавызываетфункцию,заключённуювпеременной,всвязисчемактивируетсясохранённыйкод(returnnumber*factor;).Унеговсёещёестьдоступкпеременнойfactor,котораяопределяласьпривызовеmultiplier,ктомужеунегоестьдоступкаргументу,переданномувовремяразморозки(5)вкачествечисловогопараметра.

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

functionpower(base,exponent){

if(exponent==0)

return1;

else

returnbase*power(base,exponent-1);

}

console.log(power(2,3));

//→8

Рекурсия

ВыразительныйJavascript

54Функции

Page 55: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Однако,утакойреализацииестьпроблема–вобычнойсредеJavaScriptонаразв10медленнее,чемверсиясциклом.Проходпоциклувыходитдешевле,чемвызовфункции.

Дилемма«скоростьпротивэлегантности»довольноинтересна.Естьнекийпромежутокмеждуудобствомдлячеловекаиудобствомдлямашины.Любуюпрограммуможноускорить,сделавеёбольшеизамысловатее.Отпрограммистатребуетсянаходитьподходящийбаланс.

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

Основноеправило,котороеуженеразповторяли,искоторымяполностьюсогласен–небеспокойтесьнасчётбыстродействия,покавыточнонеуверены,чтопрограмматормозит.Еслитак,найдитетечасти,которыеработаютдольшевсех,именяйтетамэлегантностьнаэффективность.

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

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

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

Вотвамзагадка:можнополучитьбесконечноеколичествочисел,начинаяс

ВыразительныйJavascript

55Функции

Page 56: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

числа1,ипотомлибодобавляя5,либоумножаяна3.Какнамнаписатьфункцию,которая,получивчисло,пытаетсянайтипоследовательностьтакихсложенийиумножений,которыеприводяткзаданномучислу?Кпримеру,число13можнополучить,сначалаумножив1на3,азатемдобавив5двараза.Ачисло15вообщенельзятакполучить.

Рекурсивноерешение:

functionfindSolution(target){

functionfind(start,history){

if(start==target)

returnhistory;

elseif(start>target)

returnnull;

else

returnfind(start+5,"("+history+"+5)")||

find(start*3,"("+history+"*3)");

}

returnfind(1,"1");

}

console.log(findSolution(24));

//→(((1*3)+5)*3)

Этотпримернеобязательнонаходитсамоекороткоерешение–онудовлетворяетсялюбым.Неожидаю,чтовысразупоймёте,какпрограммаработает.Нодавайтеразбиратьсявэтомотличномупражнениинарекурсивноемышление.

Внутренняяфункцияfindзанимаетсярекурсией.Онапринимаетдвааргумента–текущеечислоистроку,котораясодержитзаписьтого,какмыпришликэтомуномеру.Ивозвращаетлибострочку,показывающуюнашупоследовательностьшагов,либоnull.

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

ВыразительныйJavascript

56Функции

Page 57: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

онвозвращается.Вдругомслучаевозвращаетсявторой.

Чтобылучшепонять,какфункциядостигаетнужногоэффекта,давайтепросмотримеёвызовы,которыепроисходятвпоискахрешениядлячисла13.

find(1,"1")

find(6,"(1+5)")

find(11,"((1+5)+5)")

find(16,"(((1+5)+5)+5)")

toobig

find(33,"(((1+5)+5)*3)")

toobig

find(18,"((1+5)*3)")

toobig

find(3,"(1*3)")

find(8,"((1*3)+5)")

find(13,"(((1*3)+5)+5)")

found!

Отступпоказываетглубинустекавызовов.Впервыйразфункцияfindвызываетсамасебядважды,чтобыпроверитьрешения,начинающиесяс(1+5)и(13).Первыйвызовищетрешение,начинающеесяс(1+5),иприпомощирекурсиипроверяетвсерешения,выдающиечисло,меньшееилиравноетребуемому.Ненаходит,ивозвращаетnull.Тогда-тооператор||ипереходитквызовуфункции,которыйисследуетвариант(13).Здесьнасждётудача,потомучтовтретьемрекурсивномвызовемыполучаем13.Этотвызоввозвращаетстроку,икаждыйизоператоров||попутипередаётэтустрокувыше,врезультатевозвращаярешение.

Существуетдваболее-менееестественныхспособавводафункцийвпрограмму.

Первый–выпишетесхожийкоднесколькораз.Этогонужноизбегать–большекодаозначаетбольшеместадляошибокибольшематериаладлячтениятех,ктопытаетсяпонятьпрограмму.Такчтомыберёмповторяющуюсяфункциональность,подбираемейхорошееимяипомещаемеёвфункцию.

Выращиваемфункции

ВыразительныйJavascript

57Функции

Page 58: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

То,насколькосложновамподобратьимядляфункции,показывает,какхорошовыпредставляетесебееёфункциональность.Возьмёмпример.Намнужнонаписатьпрограмму,выводящуюдвачисла,количествокоровикурицнаферме,закоторымиидутслова«коров»и«куриц».Кчисламнужноспередидобавитьнулитак,чтобыкаждоезанималоровнотрипозиции.

007Коров

011Куриц

Очевидно,чтонампонадобитсяфункциясдвумяаргументами.Начинаемкодить.

//вывестиИнвентаризациюФермы

functionprintFarmInventory(cows,chickens){

varcowString=String(cows);

while(cowString.length<3)

cowString="0"+cowString;

console.log(cowString+"Коров");

varchickenString=String(chickens);

while(chickenString.length<3)

chickenString="0"+chickenString;

console.log(chickenString+"Куриц");

}

printFarmInventory(7,11);

Еслимыдобавимкстроке.length,мыполучимеёдлину.Получается,чтоциклыwhileдобавляютнулиспередикчислам,поканеполучатстрочкув3символа.

Готово!Нотолькомысобралисьотправитьфермерукод(вместесизряднымчеком,разумеется),онзвонитиговоритнам,чтоунеговхозяйствепоявилисьсвиньи,инемоглибымыдобавитьвпрограммувыводколичествасвиней?

ВыразительныйJavascript

58Функции

Page 59: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Можно,конечно.Нокогдамыначинаемкопироватьивставлятькодизэтихчетырёхстрочек,мыпонимаем,чтонадоостановитьсяиподумать.Долженбытьспособлучше.Пытаемсяулучшитьпрограмму:

//выводСДобавлениемНулейИМеткой

functionprintZeroPaddedWithLabel(number,label){

varnumberString=String(number);

while(numberString.length<3)

numberString="0"+numberString;

console.log(numberString+""+label);

}

//вывестиИнвентаризациюФермы

functionprintFarmInventory(cows,chickens,pigs){

printZeroPaddedWithLabel(cows,"Коров");

printZeroPaddedWithLabel(chickens,"Куриц");

printZeroPaddedWithLabel(pigs,"Свиней");

}

printFarmInventory(7,11,3);

Работает!НоназваниеprintZeroPaddedWithLabelнемногостранное.Онообъединяеттривещи–вывод,добавлениенулейиметку–воднуфункцию.Вместотого,чтобывставлятьвфункциювесьповторяющийсяфрагмент,давайтевыделимоднуконцепцию:

//добавитьНулей

functionzeroPad(number,width){

varstring=String(number);

while(string.length<width)

string="0"+string;

returnstring;

}

//вывестиИнвентаризациюФермы

functionprintFarmInventory(cows,chickens,pigs){

console.log(zeroPad(cows,3)+"Коров");

console.log(zeroPad(chickens,3)+"Куриц");

console.log(zeroPad(pigs,3)+"Свиней");

}

printFarmInventory(7,16,3);

Функциясхорошим,понятнымименемzeroPadоблегчаетпониманиекода.И

ВыразительныйJavascript

59Функции

Page 60: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

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

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

Перваявспомогательнаяфункциявпримересфермой,printZeroPaddedWithLabel,вызываетсяиз-запобочногоэффекта:онавыводитстроку.Вторая,zeroPad,из-завозвращаемогозначения.Иэтонесовпадение,чтовтораяфункцияпригождаетсячащепервой.Функции,возвращающиезначения,легчекомбинироватьдругсдругом,чемфункции,создающиепобочныеэффекты.

Чистаяфункция–особыйвидфункции,возвращающейзначения,котораянетольконеимеетпобочныхэффектов,ноинезависитотпобочныхэффектовостальногокода–кпримеру,неработаетсглобальнымипеременными,которыемогутбытьслучайноизмененыгде-тоещё.Чистаяфункция,будучивызваннойсоднимиитемижеаргументами,возвращаетодинитотжерезультат(ибольшеничегонеделает)–чтодовольноприятно.Снейпростоработать.Вызовтакойфункцииможномысленнозаменятьрезультатомеёработы,безизменениясмыслакода.Когдавыхотитепроверитьтакуюфункцию,выможетепростовызватьеё,ибытьуверенным,чтоеслиона

Функцииипобочныеэффекты

ВыразительныйJavascript

60Функции

Page 61: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

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

//Создаёмfсоссылкойнафункцию

varf=function(a){

console.log(a+2);

};

//Объявляемфункциюg

functiong(a,b){

returna*b*3.5;

}

Ключевоймоментвпониманиифункций–локальныеобластивидимости.Параметрыипеременные,объявленныевнутрифункции,локальныдлянеё,пересоздаютсякаждыйразприеёвызове,иневидныснаружи.Функции,объявленныевнутридругойфункции,имеютдоступкеёобластивидимости.

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

Итог

ВыразительныйJavascript

61Функции

Page 62: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ВпредыдущейглавебылаупомянутафункцияMath.min,возвращающаясамыймаленькийизаргументов.Теперьмыможемнаписатьтакуюфункциюсами.Напишитефункциюmin,принимающуюдвааргумента,ивозвращающуюминимальныйизних.

console.log(min(0,10));

//→0

console.log(min(0,-10));

//→-10

Мывидели,чтооператор%(остатокотделения)можетиспользоватьсядляопределениятого,чётноеличисло(%2).Авотещёодинспособопределения:

Нольчётный.Единицанечётная.УлюбогочислаNчётностьтакаяже,какуN-2.

НапишитерекурсивнуюфункциюisEvenсогласноэтимправилам.Онадолжнаприниматьчислоивозвращатьбулевскоезначение.

Потестируйтееёна50и75.Попробуйтезадатьей-1.Почемуонаведётсебятакимобразом?Можнолиеёкак-тоисправить?

Testiton50and75.Seehowitbehaveson-1.Why?Canyouthinkofawaytofixthis?

console.log(isEven(50));

//→true

console.log(isEven(75));

//→false

console.log(isEven(-1));

//→??

Упражнения

Минимум

Рекурсия

ВыразительныйJavascript

62Функции

Page 63: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

СимволномерNстрокиможнополучить,добавивкней.charAt(N)(“строчка”.charAt(5))–схожимобразомсполучениемдлиныстрокиприпомощи.length.Возвращаемоезначениебудетстроковым,состоящимизодногосимвола(кпримеру,“к”).Упервогосимволастрокипозиция0,чтоозначает,чтоупоследнегосимволапозициябудетstring.length–1.Другимисловами,устрокииздвухсимволовдлина2,апозицииеёсимволовбудут0и1.

НапишитефункциюcountBs,котораяпринимаетстрокувкачествеаргумента,ивозвращаетколичествосимволов“B”,содержащихсявстроке.

ЗатемнапишитефункциюcountChar,котораяработаетпримернокакcountBs,толькопринимаетвторойпараметр—символ,которыймыбудемискатьвстроке(вместотого,чтобыпростосчитатьколичествосимволов“B”).ДляэтогопеределайтефункциюcountBs.

Считаембобы.

ВыразительныйJavascript

63Функции

Page 64: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Дваразаменяспрашивали:«Скажите,м-рБэббидж,аесливывведётевмашинунеправильныеданные,получитсялиправильныйответ?».Непостижиматапутаницавголовах,котораяприводитктакимвопросам.

ЧарльзБэббидж,«Отрывкиизжизнифилософа»(1864)

Числа,булевскиезначенияистроки–кирпичики,изкоторыхстроятсяструктурыданных.Нонельзясделатьдомизодногокирпича.Объектыпозволяютнамгруппироватьзначения(втомчислеидругиеобъекты)вместе–истроитьболеесложныеструктуры.

Написаниепрограмм,которыммыдосегомоментазанимались,сильнозатруднялтотфакт,чтоониработалитолькоспростымиданными.Этаглавадобавитвамвинструментарийпониманиеструктурданных.Кеёконцувыбудетезнатьдостаточнодлятого,чтобыначатьписатьполезныепрограммы.

Главапройдётсяпоболее-менеереалистичномупримерупрограммирования,вводяпонятияпомеренеобходимости.Кодпримеровбудетстроитьсяизфункцийипеременных,которыемыопределялиранее.

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

Соднойстороны,Жакрад,чтооннепревращаетсявклассическоговолка.Превращениевбелкувлечётменьшепроблем.Вместотого,чтобыволноватьсяотом,несъешьлитысоседа(этобылобынеловко),онволнуется,какбыегонесъелсоседскийкот.Послетого,какондваждыпросыпалсянаоченьтонкойветкевкронедуба,голыйидезориентированный,онприучилсязапиратьокнаидверивсвоейкомнате

Структурыданных:объектыимассивы

Белка-оборотень

ВыразительныйJavascript

64Структурыданных:объектыимассивы

Page 65: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

наночь,икластьнесколькоорешковнапол,чтобычем-тозанятьсебя.

Такрешаютсяпроблемыскотомидубом.НоЖаквсёещёстрадаетотсвоегозаболевания.Нерегулярныеобращениянаводятегонамысль,чтоонидолжныбытьчем-товызваны.Сначалаондумал,чтоэтопроисходиттольковтедни,когдаонприкасалсякдеревьям.Онпересталэтоделать,идажестализбегатьподходитькним.Нопроблеманеисчезла.

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

Сперваонрешилразработатьструктуруданныхдляхраненияэтойинформации.

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

2,3,5,7,11

Можнопоигратьсостроками–строкимогутбытьлюбойдлины,внихможнопоместитьмногоданных,ииспользоватьдляпредставленияэтогонабора«235711».Ноэтонеудобно.Намнужнобудеткак-товыниматьоттудачислаиливставлятьновыевстроку.

Наборыданных

ВыразительныйJavascript

65Структурыданных:объектыимассивы

Page 66: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Ксчастью,JavaScriptпредлагаеттипданныхспециальнодляхраненияпоследовательностейчисел.Онназываетсямассивом(array),изаписывается,каксписокзначенийвквадратныхскобках,разделённыхзапятыми:

varlistOfNumbers=[2,3,5,7,11];

console.log(listOfNumbers[1]);

//→3

console.log(listOfNumbers[1-1]);

//→2

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

Номерпервогоэлемента–ноль,анеодин.Поэтомупервыйэлементможнополучитьтак:listOfNumbers[0].Есливыраньшенепрограммировали,придётсяпривыкнутьктакойнумерации.Ноонаимеетдавнюютрадицию,ивсёвремя,покаеёпоследовательнособлюдают,онапрекрасноработает.

МывиделимногоподозрительныхвыраженийвродеmyString.length(получениедлиныстроки)иMath.max(получениемаксимума)враннихпримерах.Этивыраженияиспользуютсвойствавеличин.Впервомслучае,мыполучаемдоступксвойствуlength(длина)переменнойmyString.Вовтором—доступксвойствуmaxобъектаMath(которыйявляетсянаборомфункцийипеременных,связанныхсматематикой).

ПочтиувсехпеременныхвJavaScriptестьсвойства.Исключения—nullиundefined.Есливыпопробуетеполучитьдоступкнесуществующимсвойствамэтихне-величин,получитеошибку:

null.length;

//→TypeError:Cannotreadproperty'length'ofnull

Дваосновныхспособадоступаксвойствам–точкаиквадратныескобки.

Свойства

ВыразительныйJavascript

66Структурыданных:объектыимассивы

Page 67: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

value.xиvalue[x]получаютдоступксвойствуvalue–нонеобязательнокодномуитомуже.Разницавтом,какинтерпретируетсяx.Прииспользованииточкизаписьпослеточкидолжнабытьименемсуществующейпеременной,ионатакимобразомнапрямуювызываетсвойствопоимени.Прииспользованииквадратныхскобоквыражениевскобкахвычисляетсядляполученияименисвойства.value.xвызываетсвойствоподименем“x”,аvalue[x]вычисляетвыражениеxииспользуетрезультатвкачествеименисвойства.

Есливызнаете,чтоинтересующеевассвойствоназывается“length”,выпишетеvalue.length.Есливыхотитеизвлечьимясвойстваизпеременнойi,выпишетеvalue[i].Апосколькусвойствоможетиметьлюбоеимя,длядоступаксвойствупоимени“2”или“JonDoe”вампридётсяиспользоватьквадратныескобки:value[2]илиvalue[«JohnDoe»].Этонеобходимодажекогдавызнаететочноеимясвойства,потомучто“2”или«JohnDoe»неявляютсядопустимымиименамипеременных,поэтомукнимнельзяобратитьсяприпомощизаписичерезточку.

Элементымассивахранятсявсвойствах.Таккакименаэтихсвойств–числа,инамчастоприходитсяполучатьихименаиззначенийпеременных,нужноиспользоватьквадратныескобкидлядоступакним.Свойствоlengthмассиваговоритотом,скольковнёмэлементов.Имяэтогосвойства–допустимоеимяпеременной,имыегознаемзаранее,поэтомуобычномыпишемarray.length,потому,чтоэтопроще,чемписатьarray[“length”].

Объектыstringиarrayсодержат,вдополнениексвойствуlength,несколькосвойств,ссылающихсянафункции.

vardoh="Дык";

console.log(typeofdoh.toUpperCase);

//→function

console.log(doh.toUpperCase());

//→ДЫК

УкаждойстрокиестьсвойствоtoUpperCase.Привызовеоновозвращаеткопиюстроки,вкоторойвсебуквызамененынапрописные.Естьтакжеи

Методы

ВыразительныйJavascript

67Структурыданных:объектыимассивы

Page 68: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

toLowerCase–можетедогадаться,чтооноделает.

Чтоинтересно,хотявызовtoUpperCaseнепередаётникакихаргументов,функциякаким-тообразомполучаетдоступкстрочке“Дык”,свойствокотороймывызывали.Какэтоработает,описановглаве6.

Свойства,содержащиефункции,обычноназываютметодамитойпеременной,которойонипринадлежат.Тоесть,toUpperCase–этометодстроки.

Вследующемпримередемонстрируютсянекоторыеметоды,имеющиесяумассивов:

varmack=[];

mack.push("Трест,");

mack.push("который","лопнул");

console.log(mack);

//→["Трест,","который","лопнул"]

console.log(mack.join(""));

//→Трест,которыйлопнул

console.log(mack.pop());

//→лопнул

console.log(mack);

//→["Трест,","который"]

Методpushиспользуетсядлядобавлениязначенийвконецмассива.popделаетобратное:удаляетзначениеизконцамассиваивозвращаетего.Массивстрокможносплющитьводнустрокуприпомощиметодаjoin.Вкачествеаргументаjoinпередаютстроку,котораябудетвставленамеждуэлементамимассива.

Вернёмсякнашейбелке.Наборжурнальныхзаписейможнопредставитьввидемассива.Нозаписинесостояттольколишьизномеровилистрок–каждаядолжнахранитьсписоктого,чтосделалнашгерой,ибулевскоезначение,показывающее,превратилсялиЖаквбелку.Видеаленамбыхотелосьгруппироватькаждуюиззаписейвкакую-тооднупеременную,ипотомдобавлятьихвмассив.

Переменныетипаobject(объект)–коллекциипроизвольныхсвойств,имы

Объекты

ВыразительныйJavascript

68Структурыданных:объектыимассивы

Page 69: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

можемдобавлятьиудалятьсвойстваобъектапожеланию.Одинизспособовсоздатьобъект–использоватьфигурныескобки:

varday1={

squirrel:false,

events:["работа","тронулдерево","пицца","пробежка","телевизор"]

};

console.log(day1.squirrel);

//→false

console.log(day1.wolf);

//→undefined

day1.wolf=false;

console.log(day1.wolf);

//→false

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

vardescriptions={

work:"Пошёлнаработу",

"тронулдерево":"Дотронулсядодерева"

};

Получается,уфигурныхскобоквJavaScriptдвазначения.Употреблённыевначалеинструкции,ониначинаютновыйблокинструкций.Влюбомдругомместеониописываютобъект.Обычнонетсмысланачинатьинструкциюсописанияобъекта,ипоэтомувпрограммахобычнонетдвусмысленностейпоповодуэтихдвухпримененийфигурныхскобок.

Есливыпопытаетесьпрочестьзначениенесуществующегосвойства,выполучитеundefined–каквпримере,когдамыпервыйразпопробовалипрочестьсвойствоwolf.

Свойствуможноназначатьзначениечерезоператор=.Еслиунегоранеебылозначение,онобудетзаменено.Еслисвойствоотсутствовало,онобудетсоздано.

ВыразительныйJavascript

69Структурыданных:объектыимассивы

Page 70: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Операторdeleteотрезаетщупальце.Этоунарныйоператор,применяемыйквыражениюдоступаксвойству.Этоделаетсяредко,новполневозможно.

varanObject={left:1,right:2};

console.log(anObject.left);

//→1

deleteanObject.left;

console.log(anObject.left);

//→undefined

console.log("left"inanObject);

//→false

console.log("right"inanObject);

//→true

Бинарныйоператорinпринимаетстрокуиимяобъекта,ивозвращаетбулевскоезначение,показывающее,естьлиуобъектасвойствостакимименем.Естьразницамеждуустановкойзначениясвойствавundefinedиудалениемсвойства.Впервомслучаесвойствосохраняетсяуобъекта,простоонопустое.Вовтором–свойствабольшенет,итогдаinвозвращаетfalse.

Получается,чтомассивы–эторазновидностьобъектов,которыеспециализируютсянахранениипоследовательностей.Выражениеtypeof[1,2]вернёт“object”.Ихможнорассматриватькакдлинныхплоскихосьминогов,укоторыхвсещупальцарасположеныровнымрядомиразмеченыномерами.

ПоэтомужурналЖакаможнопредставитьввидемассиваобъектов:

varjournal=[

{events:["работа","тронулдерево","пицца","пробежка","телевизор"],

squirrel:false},

ВыразительныйJavascript

70Структурыданных:объектыимассивы

Page 71: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

{events:["работа","мороженое","цветнаякапуста","лазанья","тронулдерево","почистилзубы"],

squirrel:false},

{events:["выходной","велик","перерыв","арахис","пивасик"],

squirrel:true},

/*итакдалее...*/

];

Скоромыужеидопрограммированиядоберёмся.Апоканамнужнопонятьпоследнюючастьтеории.

Мыувидели,чтозначенияобъектаможноменять.Типызначений,которыемырассматривалиранее,–числа,строки,булевскиезначения,-неизменяемы.Нельзяпоменятьсуществующеезначениезаданноготипа.Ихможнокомбинироватьивыводитьизнихновыезначения,нокогдавыработаетеснекоторымзначениемстроки,этозначениеостаётсяпостоянным.Текствнутристрокинельзяпоменять.Еслиувасестьссылканастроку«кошка»,вкоденельзяпоменятьвнейсимвол,чтобыполучилось«мошка».

Авотуобъектовсодержимоеможноменять,изменяязначенияихсвойств.

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

varobject1={value:10};

varobject2=object1;

varobject3={value:10};

console.log(object1==object2);

//→true

console.log(object1==object3);

//→false

object1.value=15;

console.log(object2.value);

//→15

Изменчивость(Mutability)

ВыразительныйJavascript

71Структурыданных:объектыимассивы

Page 72: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

console.log(object3.value);

//→10

Переменныеobject1иobject2держатсязаодинитотжеобъект,поэтомуизмененияobject1приводяткизменениямвobject2.Переменнаяobject3показываетнадругойобъект,которыйизначальносодержиттежесвойства,чтоиobject1,ноживётсвоейсобственнойжизнью.

Оператор==присравненииобъектоввозвращаетtrueтолько,еслисравниваемыеобъекты–этооднаитажепеременная.Сравнениеразныхобъектоввернётfalse,дажееслиунихидентичноесодержимое.Оператора«глубокого»сравнения,которыйбысравнивалсодержимоеобъектов,вJavaScriptнепредусмотрено,ноеговозможносделатьсамостоятельно(этобудетоднимизупражненийвконцеглавы).

Итак,ЖакзапускаетсвойлюбимыйинтерпретаторJavaScriptисоздаётокружение,необходимоедляхраненияжурнала.

varjournal=[];

functionaddEntry(events,didITurnIntoASquirrel){

journal.push({

events:events,

squirrel:didITurnIntoASquirrel

});

}

Каждыйвечер,часоввдесять,–аиногдаиназавтраутром,спускаясьсверхнейполкишкафа,-онзаписываетсвойдень.

addEntry(["работа","тронулдерево","пицца","пробежка","телевизор"],false);

addEntry(["работа","мороженое","цветнаякапуста","лазанья","тронулдерево","почистилзубы"],false);

addEntry(["выходной","велик","перерыв","арахис","пивасик"],true);

Кактолькоунегобудетдостаточноданных,онсобираетсявычислитькорреляциюмеждуегооборачиваниямиисобытиямикаждогоиздней,ив

Журналоборотня

ВыразительныйJavascript

72Структурыданных:объектыимассивы

Page 73: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

идеалеузнатьизихкорреляцийчто-тополезное.

Корреляция–этомеразависимостимеждупеременнымивеличинами(переменнымивстатистическомсмысле,аневсмыслеJavaScript).Онаобычновыражаетсяввидекоэффициента,принимающегозначенияот-1до1.Нулеваякорреляцияобозначает,чтопеременныевообщенесвязаны,акорреляция1означает,чтоониполностьюсвязаны–есливызнаетеодну,выавтоматическизнаетедругую.Минусодинтакжеозначаетпрочнуюсвязьпеременных,ноиихпротивоположность–когдаоднаtrue,втораявсегдаfalse.

Дляизмерениякорреляциибулевскихпеременныххорошоподходиткоэффициентфи(ϕ),ктомуже,егосравнительнолегкоподсчитать.Дляэтогонамнужнатаблица,содержащаяколичествораз,когданаблюдалисьразличныекомбинациидвухпеременных.Кпримеру,мыможемвзятьсобытия«поелпиццы»и«обращение»ипредставитьихвследующейтаблице:

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

ВыразительныйJavascript

73Структурыданных:объектыимассивы

Page 74: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

n01обозначаетколичествоизмерений,когдапервоесобытие(пицца)false(0),автороесобытие(обращение)true(1).Внашемпримереn01=4.

Записьn1•обозначаетсуммувсехизмерений,гдепервоесобытиебылоtrue,чтодлянашегопримераравно10.Соответственно,n•0–суммавсехизмерений,гдесобытие«обращение»былоfalse.

Значит,длятаблицыспиццейчислительформулыбудет1×76—9×4=40,азнаменатель–кореньиз10×80×5×85,или√340000.Получается,чтоϕ≈0.069,чтодовольномало.Непохоже,чтобыпиццавлияланаобращениявбелку.

Таблицу2х2можнопредставитьмассивомизчетырёхэлементов([76,9,4,1]),массивомиздвухэлементов,каждыйизкоторыхявляетсятакжедвухэлементныммассивом([76,9],[4,1]]),илижеобъектомсосвойствамиподименами“11”или“01”.Нодлянасодномерныймассивпрощеивыражениедлядоступакнемубудеткороче.Мыбудемобрабатыватьиндексымассивакакдвузначныедвоичныечисла,гделевыйзнакобозначаетпеременнуюоборачиваемости,аправый–события.Кпримеру,10обозначаетслучай,когдаЖакобратилсявбелку,нособытие(кпримеру,«пицца»)неимеломеста.Такслучилось4раза.Ипосколькудвоичное10–этодесятичное2,мыбудемхранитьэтовмассивепоиндексу2.

Функция,вычисляющаякоэффициентϕизтакогомассива:

functionphi(table){

return(table[3]*table[0]-table[2]*table[1])/

Math.sqrt((table[2]+table[3])*

(table[0]+table[1])*

(table[1]+table[3])*

(table[0]+table[2]));

}

console.log(phi([76,9,4,1]));

//→0.068599434

ЭтопростопрямаяреализацияформулыϕнаязыкеJavaScript.Math.sqrt–этофункцияизвлеченияквадратногокорняобъектаMathизстандартного

Вычисляемкорреляцию

ВыразительныйJavascript

74Структурыданных:объектыимассивы

Page 75: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

окруженияJavaScript.Намнужносложитьдваполятаблицыдляполученияполейтипаn1•,потомучтомынехранимвявномвидесуммыстолбцовилистрок.

Жаквёлжурналтримесяца.Результатдоступеннасайтекнигиeloquentjavascript.net/code/jacques_journal.js

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

functionhasEvent(event,entry){

returnentry.events.indexOf(event)!=-1;

}

functiontableFor(event,journal){

vartable=[0,0,0,0];

for(vari=0;i<journal.length;i++){

varentry=journal[i],index=0;

if(hasEvent(event,entry))index+=1;

if(entry.squirrel)index+=2;

table[index]+=1;

}

returntable;

}

console.log(tableFor("pizza",JOURNAL));

//→[76,9,4,1]

ФункцияhasEventпроверяет,содержитлизаписьнужныйэлемент.УмассивовестьметодindexOf,которыйищетзаданноезначение(внашемслучае–имясобытия)вмассиве.ивозвращаетиндексегоположениявмассиве(-1,еслиеговмассивенет).Значит,есливызовindexOfневернул-1,тособытиевзаписиесть.

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

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

ВыразительныйJavascript

75Структурыданных:объектыимассивы

Page 76: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

выдаётсяличтоизсписка.Нокакхранитьэтикорреляции?

Одинизспособов–хранитькорреляциивмассиве,используяобъектысосвойствамиnameиvalue.Однакопоисккорреляцийвмассивебудетдовольногромоздким:нужнобудетпройтисьповсемумассиву,чтобынайтиобъектснужнымименем.Можнобылобыобернутьэтотпроцессвфункцию,нокодпришлосьбыписатьвсёравно,икомпьютервыполнялбыбольшеработы,чемнеобходимо.

Способлучше–использоватьсвойстваобъектовсименамисобытий.Мыможемиспользоватьквадратныескобкидлясозданияичтениясвойствиоператорinдляпроверкисуществованиясвойства.

varmap={};

functionstorePhi(event,phi){

map[event]=phi;

}

storePhi("пицца",0.069);

storePhi("тронулдерево",-0.081);

console.log("пицца"inmap);

//→true

console.log(map["тронулдерево"]);

//→-0.081

Карта(map)–способсвязатьзначенияизоднойобласти(вданномслучае–названиясобытий)созначениямивдругой(внашемслучае–коэффициентыϕ).

Стакимиспользованиемобъектовестьпарапроблем–мыобсудимихвглаве6,нопокаволноватьсянебудем.

Что,еслинамнадособратьвсесобытия,длякоторыхсохраненыкоэффициенты?Онинесоздаютпредсказуемуюпоследовательность,какбылобывмассиве,поэтомуциклforиспользоватьнеполучится.JavaScriptпредлагаетконструкциюцикласпециальнодляобходавсехсвойствобъекта.Онапохожанациклfor,ноиспользуеткомандуin.

Объектыкаккарты(map)

ВыразительныйJavascript

76Структурыданных:объектыимассивы

Page 77: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

for(vareventinmap)

console.log("Кореляциядля'"+event+

"'получается"+map[event]);

//→Кореляциядля'пицца'получается0.069

//→Кореляциядля'тронулдерево'получается-0.081

Чтобынайтивсетипысобытий,представленныхвнабореданных,мыобрабатываемкаждоевхождениепоочереди,изатемсоздаёмциклповсемсобытиямвхождения.Мыхранимобъектphis,вкоторомсодержатсякорреляционныекоэффициентыдлявсехтиповсобытий,которыемыуженашли.Еслимывстречаемновыйтип,которогоещёнебыловphis,мыподсчитываемегокорреляциюидобавляемеёвобъект.

functiongatherCorrelations(journal){

varphis={};

for(varentry=0;entry<journal.length;entry++){

varevents=journal[entry].events;

for(vari=0;i<events.length;i++){

varevent=events[i];

if(!(eventinphis))

phis[event]=phi(tableFor(event,journal));

}

}

returnphis;

}

varcorrelations=gatherCorrelations(JOURNAL);

console.log(correlations.pizza);

//→0.068599434

Смотрим.чтополучилось:

for(vareventincorrelations)

console.log(event+":"+correlations[event]);

//→морковка:0.0140970969

//→упражнения:0.0685994341

//→выходной:0.1371988681

//→хлеб:-0.0757554019

//→пудинг:-0.0648203724

//итакдалее...

Итоговыйанализ

ВыразительныйJavascript

77Структурыданных:объектыимассивы

Page 78: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

for(vareventincorrelations){

varcorrelation=correlations[event];

if(correlation>0.1||correlation<-0.1)

console.log(event+":"+correlation);

}

//→выходной:0.1371988681

//→чистилзубы:-0.3805211953

//→конфета:0.1296407447

//→работа:-0.1371988681

//→спагетти:0.2425356250

//→читал:0.1106828054

//→арахис:0.5902679812

Ага!Удвухфакторовкорреляциизаметнобольшеостальных.Арахиссильновлияетнавероятностьпревращениявбелку,тогдакакчистказубовнаоборот,препятствуетэтому.

Интересно.Попробуемвотчто:

for(vari=0;i<JOURNAL.length;i++){

varentry=JOURNAL[i];

if(hasEvent("арахис",entry)&amp;&amp;

!hasEvent("чистказубов",entry))

entry.events.push("арахисзубы");

}

console.log(phi(tableFor("арахисзубы",JOURNAL)));

//→1

Ошибкибытьнеможет!Феноменслучаетсяименнотогда,когдаЖакестьарахисинечиститзубы.Еслибонтольконебылтакимнеряхойотносительнооральнойгигиены,онбывообщенезаметилсвоегонесчастья.

Знаяэто,Жакпростоперестаётестьарахисиобнаруживает,чтотрансформациипрекратились.

УЖакакакое-товремявсёхорошо.Ночерезнескольколетонтеряетработу,

ВыразительныйJavascript

78Структурыданных:объектыимассивы

Page 79: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ивконцеконцовемуприходитсянанятьсявцирк,гдеонвыступаеткакУдивительныйЧеловек-белка,набираяполныйротарахисовогомаслапередшоу.Однажды,уставотстольжалкогосуществования,Жакнеобращаетсяобратновчеловека,пробираетсячерездырувцирковомтентеиисчезаетвлесу.Большеегониктоневидел.

Вконцеглавыхочупознакомитьвасещёснесколькимиконцепциями,относящимисякобъектам.Начнёмсполезныхметодов,имеющихсяумассивов.

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

vartodoList=[];

functionrememberTo(task){

todoList.push(task);

}

functionwhatIsNext(){

returntodoList.shift();

}

functionurgentlyRememberTo(task){

todoList.unshift(task);

}

Даннаяпрограммауправляетспискомдел.Выдобавляетеделавконецсписка,вызываяrememberTo(«поесть»),акогдавыготовызанятьсячем-то,вызываетеwhatIsNext(),чтобыполучить(иудалить)первыйэлементсписка.ФункцияurgentlyRememberToтожедобавляетзадачу,нотольковначалосписка.

УметодаindexOfестьродственникпоимениlastIndexof,которыйначинаетпоискэлементавмассивесконца:

console.log([1,2,3,2,1].indexOf(2));

//→1

console.log([1,2,3,2,1].lastIndexOf(2));

//→3

Дальнейшаямассивология

ВыразительныйJavascript

79Структурыданных:объектыимассивы

Page 80: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Обаметода,indexOfиlastIndexOf,принимаютнеобязательныйвторойаргумент,которыйзадаётначальнуюпозициюпоиска.

Ещёодинважныйметод–slice,которыйпринимаетномераначального(start)иконечного(end)элементов,ивозвращаетмассив,состоящийтолькоизэлементов,попадающихвэтотпромежуток.Включаятот,чтонаходитсяпоиндексуstart,ноисключаятот,чтопоиндексуend.

console.log([0,1,2,3,4].slice(2,4));

//→[2,3]

console.log([0,1,2,3,4].slice(2));

//→[2,3,4]

Когдаиндексendнезадан,sliceвыбираетвсеэлементыпослеиндексаstart.Устрокестьсхожийметод,которыйработаеттакже.

Методconcatиспользуетсядлясклейкимассивов,примернокакоператор+склеиваетстроки.Впримерепоказаныметодыconcatиsliceвделе.Функцияпринимаетмассивarrayииндексindex,ивозвращаетновыймассив,которыйявляетсякопиейпредыдущего,заисключениемудалённогоэлемента,находившегосяпоиндексуindex.

functionremove(array,index){

returnarray.slice(0,index).concat(array.slice(index+1));

}

console.log(remove(["a","b","c","d","e"],2));

//→["a","b","d","e"]

Мыможемполучатьзначениясвойствстрок,напримерlengthиtoUpperCase.Нопопыткадобавитьновоесвойствоникчемунеприведёт:

varmyString="Шарик";

myString.myProperty="значение";

console.log(myString.myProperty);

//→undefined

Строкииихсвойства

ВыразительныйJavascript

80Структурыданных:объектыимассивы

Page 81: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Ноунихестьсвоивстроенныесвойства.Укаждойстрокиестьнаборметодов.Самыеполезные,пожалуй–sliceиindexOf,напоминающиетежеметодыумассивов.

console.log("кокосы".slice(3,6));

//→осы

console.log("кокос".indexOf("с"));

//→4

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

console.log("раздватри".indexOf("ва"));

//→5

Методtrimудаляетпробелы(атакжепереводыстрок,табуляциюипрочиеподобныесимволы)собоихконцовстроки.

console.log("ладно\n".trim());

//→ладно

Мыужесталкивалисьсосвойствомстрокиlength.ДоступкотдельнымсимволамстрочкиможнополучитьчерезметодcharAt,атакжепросточерезнумерациюпозиций,каквмассиве:

varstring="abc";

console.log(string.length);

//→3

console.log(string.charAt(0));

//→a

console.log(string[1]);

//→b

ВыразительныйJavascript

81Структурыданных:объектыимассивы

Page 82: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

functionnoArguments(){}

noArguments(1,2,3);//Пойдёт

functionthreeArguments(a,b,c){}

threeArguments();//Итакможно

Уобъектаargumentsестьсвойствоlength,котороесодержитреальноеколичествопереданныхфункцииаргументов.Такжеунегоестьсвойствадлякаждогоаргументаподименами0,1,2ит.д.

Есливамкажется,чтоэтооченьпохоженамассив–выправы.Этооченьпохоженамассив.Ксожалению,уэтогообъектанетметодовтипаsliceилиindexOf,чтоделаетдоступкнемутруднее.

functionargumentCounter(){

console.log("Тыдалмне",arguments.length,"аргумента.");

}

argumentCounter("Дядя","Стёпа","Милиционер");

//→Тыдалмне3аргумента.

Некоторыефункциирассчитаныналюбоеколичествоаргументов,какconsole.log.Ониобычнопроходятцикломпосвойствамобъектаarguments.Этоможноиспользоватьдлясозданияудобныхинтерфейсов.Кпримеру,вспомните,какмысоздавализаписидляжурналаЖака:

addEntry(["работа","тронулдерево","пицца","пробежка","телевизор"],false);

Таккакмычастовызываемэтуфункцию,мыможемсделатьальтернативу,которуюпрощевызывать:

Объектarguments

ВыразительныйJavascript

82Структурыданных:объектыимассивы

Page 83: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functionaddEntry(squirrel){

varentry={events:[],squirrel:squirrel};

for(vari=1;i<arguments.length;i++)

entry.events.push(arguments[i]);

journal.push(entry);

}

addEntry(true,"работа","тронулдерево","пицца","пробежка","телевизор");

Этаверсиячитаетпервыйаргументкакобычно,апоостальнымпроходитвцикле(начинаясиндекса1,пропускаяпервыйаргумент)исобираетихвмассив.

Мыужевидели,чтоMath–наборинструментовдляработысчислами,такими,какMath.max(максимум),Math.min(минимум),иMath.sqrt(квадратныйкорень).

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

Слишкомбольшоечислоглобальныхпеременных«загрязняет»пространствоимён.Чембольшеимёнзанято,тембольшевероятностьслучайноиспользоватьодноизнихвкачествепеременной.Кпримеру,весьмавероятно,чтовызахотитеиспользоватьимяmaxдлячего-товсвоейпрограмме.ПосколькувстроеннаявJavaScriptфункцияmaxбезопасноупакованавобъектMath,намненужноволноватьсяпоповодутого,чтомыеёперезапишем.

Многиеязыкиостановятвас,илихотябыпредупредят,когдавыбудетеопределятьпеременнуюсименем,котороеужезанято.JavaScriptнебудетэтогоделать,поэтомубудьтеосторожны.

ВозвращаяськобъектуMath.Есливамнужнатригонометрия,онвампоможет.Унегоестьcos(косинус),sin(синус),иtan(тангенс),ихобратныефункции—acos,asin,иatan.Числоπ(pi)–или,покрайнеймере,его

ОбъектMath

ВыразительныйJavascript

83Структурыданных:объектыимассивы

Page 84: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

близкаяаппроксимация,помещающаясявчислоJavaScript,-такжедоступнакакMath.PI.(Естьтакаястараятрадициявпрограммировании—записыватьименаконстантвверхнемрегистре).

functionrandomPointOnCircle(radius){

varangle=Math.random()*2*Math.PI;

return{x:radius*Math.cos(angle),

y:radius*Math.sin(angle)};

}

console.log(randomPointOnCircle(2));

//→{x:0.3667,y:1.966}

Есливынезнакомыссинусамиикосинусами–неотчаивайтесь.Мыихбудемиспользоватьв13главе,итогдаяихобъясню.

ВпредыдущемпримереиспользуетсяMath.random.Этофункция,возвращающаяприкаждомвызовеновоепсевдослучайноечисломеждунулёмиединицей(включаяноль).

console.log(Math.random());

//→0.36993729369714856

console.log(Math.random());

//→0.727367032552138

console.log(Math.random());

//→0.40180766698904335

Хотякомпьютеры–машиныдетерминистские(онивсегдареагируютодинаковонаодниитежевходныеданные),возможнозаставитьихвыдаватькажущиесяслучайныминомера.Дляэтогомашинахранитусебявовнутреннемсостояниинесколькономеров.Каждыйраз,когдаидётзапроснаслучайныйномер,онавыполняетразныесложныедетерминистскиевычисленияивозвращаетчастьрезультатавычислений.Этотрезультатонаиспользуетдлятого,чтобыизменитьсвоёвнутреннеесостояние,поэтомуследующий«случайный»номерполучаетсядругим.

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

console.log(Math.floor(Math.random()*10));

ВыразительныйJavascript

84Структурыданных:объектыимассивы

Page 85: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

//→2

Умножаяслучайноечислона10,получаемномеротнулядо10(включаяноль).ТаккакMath.floorокругляетвниз,мыполучимчислоот0до9включительно.

ЕстьтакжефункцияMath.ceil(«ceiling»–потолок),котораяокругляетвверхдоближайшегоцелого)иMath.round(округляетдоближайшегоцелого).

Кглобальнойобластивидимости,гдеживутглобальныепеременные,vожнополучитьдоступтакже,каккобъекту.Каждаяглобальнаяпеременнаяявляетсясвойствомэтогообъекта.Вбраузерахглобальнаяобластьвидимостихранитсявпеременнойwindow.

varmyVar=10;

console.log("myVar"inwindow);

//→true

console.log(window.myVar);

//→10

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

УбольшинствавеличинвJavaScriptестьсвойства,заисключениемnullиundefined.Мыполучаемдоступкнимчерезvalue.propNameилиvalue[«propName»].Объектыиспользуютименадляхранениясвойствихранятболее-менеефиксированноеихколичество.Массивыобычносодержатпеременноеколичествосходныхпотипувеличин,ииспользуютчисла(начинаяснуля)вкачествеимёнэтихвеличин.

Объектglobal

Итог

ВыразительныйJavascript

85Структурыданных:объектыимассивы

Page 86: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Такжевмассивахестьименованныесвойства,такиекакlength,инесколькометодов.Методы–этофункции,живущиесредисвойстви(обычно)работающиенадтойвеличиной,чьимсвойствомониявляются.

Объектытакжемогутработатькаккарты,ассоциируязначениясименами.Операторinиспользуетсядлявыяснениятого,содержитлиобъектсвойствосданнымименем.Этожеключевоесловоиспользуетсявциклеfor(for(varnameinobject))дляпереборавсехсвойствобъекта.

Вовведениибылупомянутудобныйспособподсчётасуммдиапазоновчисел:

console.log(sum(range(1,10)));

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

Затемнапишитефункциюsum,принимающуюмассивчиселивозвращающуюихсумму.Запуститеуказаннуювышеинструкциюиубедитесь,чтоонавозвращает55.

Вкачествебонусадополнитефункциюrange,чтобыонамоглаприниматьнеобязательныйтретийаргумент–шагдляпостроениямассива.Еслионнезадан,шагравенединице.Вызовфункцииrange(1,10,2)долженбудетвернуть[1,3,5,7,9].Убедитесь,чтоонаработаетсотрицательнымшагомтак,чтовызовrange(5,2,-1)возвращает[5,4,3,2].

console.log(sum(range(1,10)));

//→55

console.log(range(5,2,-1));

//→[5,4,3,2]

Упражнения

Суммадиапазона

ВыразительныйJavascript

86Структурыданных:объектыимассивы

Page 87: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Умассивовестьметодreverse,меняющийпорядокэлементоввмассивенаобратный.Вкачествеупражнениянапишитедвефункции,reverseArrayиreverseArrayInPlace.Перваяполучаетмассивкакаргументивыдаётновыймассив,собратнымпорядкомэлементов.Втораяработаеткакоригинальныйметодreverse–онаменяетпорядокэлементовнаобратныйвтоммассиве,которыйбылейпереданвкачествеаргумента.Неиспользуйтестандартныйметодreverse.

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

console.log(reverseArray(["A","B","C"]));

//→["C","B","A"];

vararrayValue=[1,2,3,4,5];

reverseArrayInPlace(arrayValue);

console.log(arrayValue);

//→[5,4,3,2,1]

Объектымогутбытьиспользованыдляпостроенияразличныхструктурданных.Частовстречающаясяструктура–список(непутайтесмассивом).Список–связанныйнаборобъектов,гдепервыйобъектсодержитссылкунавторой,второй–натретий,ит.п.

varlist={

value:1,

rest:{

value:2,

rest:{

value:3,

rest:null

}

}

};

Врезультатеобъектыформируютцепочку:

Обращаемвспятьмассив

Список

ВыразительныйJavascript

87Структурыданных:объектыимассивы

Page 88: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Спискиудобнытем,чтоонимогутделитьсячастьюсвоейструктуры.Например,можносделатьдвасписка,{value:0,rest:list}и{value:-1,rest:list},гдеlist–этоссылканаранееобъявленнуюпеременную.Этодванезависимыхсписка,приэтомунихестьобщаяструктураlist,котораявключаеттрипоследнихэлементакаждогоизних.Крометого,оригинальныйсписоктакжесохраняетсвоисвойствакакотдельныйсписокизтрёхэлементов.

НапишитефункциюarrayToList,котораястроиттакуюструктуру,получаявкачествеаргумента[1,2,3],атакжефункциюlistToArray,котораясоздаётмассивизсписка.Такженапишитевспомогательнуюфункциюprepend,котораяполучаетэлементисоздаётновыйсписок,гдеэтотэлементдобавленспередикпервоначальномусписку,ифункциюnth,котораявкачествеаргументовпринимаетсписокичисло,авозвращаетэлементназаданнойпозициивсписке,илижеundefinedвслучаеотсутствиятакогоэлемента.

Есливашаверсияnthнерекурсивна,тогданапишитееёрекурсивнуюверсию.

console.log(arrayToList([10,20]));

//→{value:10,rest:{value:20,rest:null}}

console.log(listToArray(arrayToList([10,20,30])));

//→[10,20,30]

console.log(prepend(10,prepend(20,null)));

//→{value:10,rest:{value:20,rest:null}}

console.log(nth(arrayToList([10,20,30]),1));

//→20

Оператор==сравниваетпеременныеобъектов,проверяя,ссылаютсялионинаодинобъект.Ноиногдаполезнобылобысравнитьобъектыпосодержимому.

Глубокоесравнение

ВыразительныйJavascript

88Структурыданных:объектыимассивы

Page 89: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

НапишитефункциюdeepEqual,котораяпринимаетдвазначенияивозвращаетtrue,толькоеслиэтодваодинаковыхзначенияилиэтообъекты,свойствакоторыхимеютодинаковыезначения,еслиихсравниватьрекурсивнымвызовомdeepEqual.

Чтобыузнать,когдасравниватьвеличинычерез===,акогда–объектыпосодержимому,используйтеоператорtypeof.Еслионвыдаёт“object”дляобеихвеличин,значитнужноделатьглубокоесравнение.Незабудьтеободномдурацкомисключении,случившемсяиз-заисторическихпричин:“typeofnull”тожевозвращает“object”.

varobj={here:{is:"an"},object:2};

console.log(deepEqual(obj,obj));

//→true

console.log(deepEqual(obj,{here:1,object:2}));

//→false

console.log(deepEqual(obj,{here:{is:"an"},object:2}));

//→true

ВыразительныйJavascript

89Структурыданных:объектыимассивы

Page 90: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Цу-лииЦу-супохвалялисьразмерамисвоихновыхпрограмм.«Двеститысячстрок»,-сказалЦу-ли,-«несчитаякомментариев!»Цу-суответил:«Пф-ф,моя–почтимиллионстрок».МастерЮнь-Масказал:«Моялучшаяпрограммазанимаетпятьсотстрок».Услышавэто,Цу-лииЦу-суиспыталипросветление.

МастерЮнь-Ма,Книгапрограммирования

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

ЭнтониХоар,1980лекциянавручениипремииТьюринга

Большаяпрограмма–затратнаяпрограмма,инетолькоиз-завремениеёнаписания.Большойразмеробычноозначаетсложность,асложностьсбиваетстолкупрограммистов.Сбитыестолкупрограммистыделаютошибкивпрограммах.Большаяпрограммаозначает,чтобагаместьгдеспрятатьсяиихполучаетсятруднееотыскать.

Вернёмсяненадолгокдвумпримерамизвведения.Первыйсамодостаточен,изанимаетшестьстрок.

vartotal=0,count=1;

while(count<=10){

total+=count;

count+=1;

}

console.log(total);

Второйоснованнадвухвнешнихфункцияхизанимаетоднустроку.

console.log(sum(range(1,10)));

Вкакомизнихскореевстретитсяошибка?

Функциивысшегопорядка

ВыразительныйJavascript

90Функциивысшегопорядка

Page 91: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Еслимыдобавимразмеропределенийsumиrange,втораяпрограмматожеполучитсябольшой–большепервой.Ноявсёравноутверждаю,чтоонаскореевсегобудетправильной.

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

Определенияэтогословаря(функцииsumиrange)будутвключатьциклы,счётчикиидругиеслучайныедетали.Нопотому,чтоонивыражаютболеепростыеконцепции,чемвсяпрограмма,ихпрощесделатьправильно.

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

Сравнитедварецептагороховогосупа:

Добавьтевёмкостьпооднойчашкесухогогороханапорцию.Добавьтеводытак,чтобыонапокрылагорох.Оставьтееготакминимумна12часов.Выньтегорохизводыипоместитеихнасковороду.Добавьте4чашкиводынапорцию.Закройтесковородуитушитегорохдвачаса.Возьмитепополовинелуковицынапорцию.Порежьтенакускиножом,добавьтекгороху.Возьмитепоодномустеблюсельдереянапорцию.Порежьтенакускиножом,добавьтекгороху.Возьмитепоморковкенапорцию.Порежьтенакускиножом,добавьтекгороху.Готовьтеещё10минут.

Второйрецепт:

Напорцию:1чашкасухогогороха,половиналуковицы,стебельсельдерея,морковка.Вымачивайтегорох12часов.Тушите2часав4чашкахводынапорцию.Порежьтеидобавьтеовощи.Готовьтеещё10минут.

Второй–корочеипроще.Новамнужнознатьчутьбольшепонятий,связанныхсготовкой–вымачивание,тушение,резка(иовощи).

Абстракции

ВыразительныйJavascript

91Функциивысшегопорядка

Page 92: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

Простыефункции,которыемыиспользовалираньше,хорошидляпостроенияабстракций.Ноиногдаихбываетнедостаточно.

Впредыдущейглавемынесколькоразвстречалитакойцикл:

vararray=[1,2,3];

for(vari=0;i<array.length;i++){

varcurrent=array[i];

console.log(current);

}

Кодпытаетсясказать:«длякаждогоэлементавмассиве–вывестиеговконсоль».Ноониспользуетобходнойпуть–спеременнойдляподсчётаi,проверкойдлинымассива,иобъявлениемдополнительнойпеременнойcurrent.Малотого,чтооннеоченькрасив,онещёиявляетсяпочвойдляпотенциальныхошибок.Мыможемслучайноповторноиспользоватьпеременнуюi,вместоlengthнаписатьlenght,перепутатьпеременныеiиcurrent,ит.п.

Давайтеабстрагируемеговфункцию.Можетепридуматьспособсделатьэто?

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

functionlogEach(array){

for(vari=0;i<array.length;i++)

console.log(array[i]);

}

Абстрагируемобходмассива

ВыразительныйJavascript

92Функциивысшегопорядка

Page 93: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Ночто,еслинамнадоделатьчто-тодругое,нежеливыводитьэлементывконсоль?Поскольку«делатьчто-то»можнопредставитькакфункцию,афункции–этопростопеременные,мыможемпередатьэтодействиекакаргумент:

functionforEach(array,action){

for(vari=0;i<array.length;i++)

action(array[i]);

}

forEach(["Тили","Мили","Трямдия"],console.log);

//→Тили

//→Мили

//→Трямдия

ЧастоможнонепередаватьзаранееопределённуюфункциювforEach,асоздаватьфункциюпрямонаместе.

varnumbers=[1,2,3,4,5],sum=0;

forEach(numbers,function(number){

sum+=number;

});

console.log(sum);

//→15

Выглядитпохоженаклассическийциклfor,стеломцикла,записаннымвблоке.Однако,теперьтелонаходитсявнутрифункции,итакжевнутрискобоквызоваforEach.Поэтомуегонужнозакрытькакфигурной,такикруглойскобкой.

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

Вообще,намдажененужнописатьсамимforEach.Этостандартныйметодмассивов.Таккакмассивужепереданвкачествепеременной,надкотороймыработаем,forEachпринимаеттолькоодинаргумент–функцию,которуюнужновыполнитьдлякаждогоэлемента.

Длядемонстрацииудобстваэтогоподходавернёмсякфункцииизпредыдущейглавы.Онасодержитдвацикла,проходящихпомассивам:

ВыразительныйJavascript

93Функциивысшегопорядка

Page 94: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functiongatherCorrelations(journal){

varphis={};

for(varentry=0;entry<journal.length;entry++){

varevents=journal[entry].events;

for(vari=0;i<events.length;i++){

varevent=events[i];

if(!(eventinphis))

phis[event]=phi(tableFor(event,journal));

}

}

returnphis;

}

ИспользуяforEachмыделаемзаписьчутькорочеигораздочище.

functiongatherCorrelations(journal){

varphis={};

journal.forEach(function(entry){

entry.events.forEach(function(event){

if(!(eventinphis))

phis[event]=phi(tableFor(event,journal));

});

});

returnphis;

}

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

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

functiongreaterThan(n){

returnfunction(m){returnm>n;};

Функциивысшегопорядка

ВыразительныйJavascript

94Функциивысшегопорядка

Page 95: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

}

vargreaterThan10=greaterThan(10);

console.log(greaterThan10(11));

//→true

Можносделатьфункцию,меняющуюдругиефункции.

functionnoisy(f){

returnfunction(arg){

console.log("callingwith",arg);

varval=f(arg);

console.log("calledwith",arg,"-got",val);

returnval;

};

}

noisy(Boolean)(0);

//→callingwith0

//→calledwith0-gotfalse

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

functionunless(test,then){

if(!test)then();

}

functionrepeat(times,body){

for(vari=0;i<times;i++)body(i);

}

repeat(3,function(n){

unless(n%2,function(){

console.log(n,"iseven");

});

});

//→0iseven

//→2iseven

Правилалексическихобластейвидимости,которыемыобсуждаливглаве3,работаютнамнапользувтакихслучаях.Впоследнемпримерепеременнаяn–этоаргументвнешнейфункции.Посколькувнутренняяфункцияживётвокружениивнешней,онаможетиспользоватьn.Телатакихвнутреннихфункцийимеютдоступкпеременным,окружающимих.Онимогутигратьрольблоков{},используемыхвобычныхциклахиусловныхвыражениях.

ВыразительныйJavascript

95Функциивысшегопорядка

Page 96: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Важноеотличиевтом,чтопеременные,объявленныевнутривнутреннихфункций,непопадаютвокружениевнешней.Иобычноэтотолькоклучшему.

Функцияnoisy,объявленнаяранее,котораяпередаётсвойаргументвдругуюфункцию,несовсемудобна.

functionnoisy(f){

returnfunction(arg){

console.log("callingwith",arg);

varval=f(arg);

console.log("calledwith",arg,"-got",val);

returnval;

};

}

Еслиfпринимаетбольшеодногопараметра,онаполучиттолькопервый.Можнобылобыдобавитькучуаргументовквнутреннейфункции(arg1,arg2ит.д.)ипередатьвсехихвf,новедьнеизвестно,какогоколичестванамхватит.Крометого,функцияfнемоглабыкорректноработатьсarguments.length.Таккакмывсёвремяпередавалибыодинаковоечислоаргументов,былобынеизвестно,сколькоаргументовнамбылозаданоизначально.

ДлятакихслучаевуфункцийвJavaScriptестьметодapply.Емупередаютмассив(илиобъектввидемассива)изаргументов,аонвызываетфункциюсэтимиаргументами.

functiontransparentWrapping(f){

returnfunction(){

returnf.apply(null,arguments);

};

}

Даннаяфункциябесполезна,ноонадемонстрируетинтересующийнасшаблон–возвращаемаяеюфункцияпередаётвfвсеполученныееюаргументы,нонеболеетого.Происходитэтоприпомощипередачиеёсобственныхаргументов,хранящихсявобъектеarguments,вметодapply.

Передачааргументов

ВыразительныйJavascript

96Функциивысшегопорядка

Page 97: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Первыйаргументметодаapply,которомумывданномслучаеприсваиваемnull,можноиспользоватьдляэмуляциивызоваметода.Мывернёмсякэтомувопросувследующейглаве.

Функциивысшегопорядка,которыекаким-тообразомприменяютфункциюкэлементаммассива,широкораспространенывJavaScript.МетодforEach–однаизсамыхпримитивныхподобныхфункций.Вкачествеметодовмассивовнамдоступномногодругихвариантовфункций.Длязнакомстваснимидавайтепоиграемсдругимнаборомданных.

Нескольколетназадкто-тообследовалмногоархивовисделалцелуюкнигупоисториимоейфамилии.Яоткрылеё,надеясьнайтитамрыцарей,пиратовиалхимиков…Нооказалось,чтооназаполненавосновномфламандскимифермерами.Дляразвлеченияяизвлёкинформациюпомоимнепосредственнымпредкамиоформилвформате,пригодномдлячтениякомпьютером.

Файлвыглядитпримернотак:

[

{"name":"EmmadeMilliano","sex":"f",

"born":1876,"died":1956,

"father":"PetrusdeMilliano",

"mother":"SophiavanDamme"},

{"name":"CarolusHaverbeke","sex":"m",

"born":1832,"died":1905,

"father":"CarelHaverbeke",

"mother":"MariavanBrussel"},

…итакдалее

]

ЭтотформатназываетсяJSON,чтоозначаетJavaScriptObjectNotation(разметкаобъектовJavaScript).Онширокоиспользуетсявхраненииданныхисетевыхкоммуникациях.

JSONпохожнаJavaScriptпоспособузаписимассивовиобъектов–снекоторымиограничениями.Всеименасвойствдолжныбытьзаключеныв

JSON

ВыразительныйJavascript

97Функциивысшегопорядка

Page 98: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

JavaScriptпредоставляетфункцииJSON.stringifyиJSON.parse,которыепреобразовываютданныеизэтогоформатаивэтотформат.ПерваяпринимаетзначениеивозвращаетстрочкусJSON.Втораяпринимаеттакуюстрочкуивозвращаетзначение.

varstring=JSON.stringify({name:"X",born:1980});

console.log(string);

//→{"name":"X","born":1980}

console.log(JSON.parse(string).born);

//→1980

ПеременнаяANCESTRY_FILEдоступназдесь.ОнасодержитJSONфайлввидестроки.Давайтееёраскодируемипосчитаемколичествоупомянутыхлюдей.

varancestry=JSON.parse(ANCESTRY_FILE);

console.log(ancestry.length);

//→39

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

functionfilter(array,test){

varpassed=[];

for(vari=0;i<array.length;i++){

if(test(array[i]))

passed.push(array[i]);

}

returnpassed;

}

console.log(filter(ancestry,function(person){

returnperson.born>1900&amp;&amp;person.born<1925;

Фильтруеммассив

ВыразительныйJavascript

98Функциивысшегопорядка

Page 99: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

}));

//→[{name:"PhilibertHaverbeke",…},…]

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

Вфайлеоказалосьтричеловека,которыебылимолодыв1924–дедушка,бабушкаидвоюроднаябабушка.

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

КакиforEach,filter–этоодинизстандартныхметодовмассива.Впримеремыописалитакуюфункцию,толькочтобыпоказать,чтоонаделаетвнутри.Отнынемыбудемиспользоватьеёпросто:

c```onsole.log(ancestry.filter(function(person){returnperson.father=="CarelHaverbeke";}));//→[{name:"CarolusHaverbeke",…}]

##Преобразованияприпомощиmap

Допустим,естьунасархивобъектов,представляющихлюдей,которыйбылполученфильтрациеймассивапредков.Нонамнуженмассивимён,которыйбылобыпрощепрочесть.

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

functionmap(array,transform){varmapped=[];for(vari=0;i<array.length;i++)mapped.push(transform(array[i]));returnmapped;}

varoverNinety=ancestry.filter(function(person){returnperson.died-person.born>90;});console.log(map(overNinety,function(person){returnperson.name;}));//→["ClaraAernoudts","EmileHaverbeke",//"MariaHaverbeke"]

Чтоинтересно,люди,которыепрожилихотябыдо90лет–этотесамые,чтомывиделиранее,которыебылимолодыв1920-хгодах.Этокакразсамоеновоепоколениевмоихзаписях.Видимо,медицинасерьёзноулучшилась.

КакиforEachиfilter,mapтакжеявляетсястандартнымметодомумассивов.

ВыразительныйJavascript

99Функциивысшегопорядка

Page 100: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

##Суммированиесreduce

Другойпопулярныйпримерработысмассивами–получениеодиночногозначениянаосноведанныхвмассиве.Одинпример–ужезнакомоенамсуммированиесписканомеров.Другой–поискчеловека,родившегосяраньшевсех.

Операциявысшегопорядкатакоготипаназываетсяreduce(уменьшение;илииногдаfold,свёртывание).Можнопредставитьеёввидескладываниямассива,поодномуэлементузараз.Присуммированиичиселмыначиналиснуля,идлякаждогоэлементакомбинировалиегостекущейсуммойприпомощисложения.

Параметрыфункцииreduce,кромемассива–комбинирующаяфункцияиначальноезначение.Этафункциячутьменеепонятная,чемfilterилиmap,поэтомуобратитенанеёпристальноевнимание.

functionreduce(array,combine,start){varcurrent=start;for(vari=0;i<array.length;i++)current=combine(current,array[i]);returncurrent;}

console.log(reduce([1,2,3,4],function(a,b){returna+b;},0));//→10

Стандартныйметодмассивовreduce,который,конечно,работаеттакже,ещёболееудобен.Еслимассивсодержитхотябыодинэлемент,выможетенеуказыватьаргументstart.Методвозьмётвкачествестартовогозначенияпервыйэлементмассиваиначнётработусовторого.

Чтобыприпомощиreduceнайтисамогодревнегоизизвестныхмоихпредков,мыможемнаписатьнечтовроде:

console.log(ancestry.reduce(function(min,cur){if(cur.born<min.born)returncur;elsereturnmin;}));//→{name:"PauwelsvanHaverbeke",born:1535,…}

##Компонуемость

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

varmin=ancestry[0];for(vari=1;i<ancestry.length;i++){varcur=ancestry[i];if(cur.born<min.born)min=cur;}console.log(min);//→{name:"PauwelsvanHaverbeke",born:1535,…}

Чутьбольшепеременных,надвестрочкидлиннее–нопокадостаточнопонятныйкод.

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

functionaverage(array){functionplus(a,b){returna+b;}returnarray.reduce(plus)/array.length;}functionage(p){returnp.died-p.born;}functionmale(p){returnp.sex=="m";}functionfemale(p){returnp.sex=="f";}

ВыразительныйJavascript

100Функциивысшегопорядка

Page 101: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

console.log(average(ancestry.filter(male).map(age)));//→61.67console.log(average(ancestry.filter(female).map(age)));//→54.56

(Глупо,чтонамприходитсяопределятьсложениекакфункциюplus,нооператорывJavaScriptнеявляютсязначениями,поэтомуихнепередашьвкачествеаргументов).

Вместотогочтобывпутыватьалгоритмвбольшойцикл,всёраспределенопоконцепциям,которыенасинтересуют–определениепола,подсчётвозрастаиусреднениечисел.Мыприменяемихпоочередидляполучениярезультата.

Длянаписанияпонятногокодаэтопрямо-такисказочнаявозможность.Конечно,ясностьнедостаётсябесплатно.

##Цена

ВсчастливомкраюэлегантногокодаикрасивыхрадугживётгадскоечудищепоимениНеэффективность.

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

Точнотакже,передачафункциивforEach,чтобытапрошласьпомассивузанас,удобнаипроставпонимании.НовызовфункцийвJavaScriptобходитсядорожепосравнениюсциклами.

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

Ксчастью,большинствокомпьютеровбезумнобыстрые.Есливашнаборданныхнеслишкомвелик,иливремяработыдолжнобытьвсеголишьдостаточнобыстрымсточкизрениячеловека(например,делатьчто-токаждыйраз,когдапользовательжмётнакнопку)—тогданеимеетзначения,написаливыкрасивоерешение,котороеработаетполовинумиллисекунды,илиоченьоптимизированное,котороеработаетоднудесятуюмиллисекунды.

Удобнопримерноподсчитывать,какчастобудетвызыватьсяданныйкусочеккода.Еслиувасестьциклвцикле(напрямую,илижечерезвызоввциклефункции,котораявнутритакжеработаетсциклом),токодбудетвыполненN*Mраз,гдеN–количествоповторенийвнешнегоцикла,аM–внутреннего.Есливовнутреннемциклеестьещёодинцикл,повторяющийсяPраз,тогдамыужеполучимN*M*P–итакдалее.Этоможетприводитькбольшимчислам,икогдапрограмматормозит,проблемучастоможносвестикнебольшомукусочкукода,находящемусявнутрисамоговнутреннегоцикла.

##Пра-пра-пра-пра-пра-…

Мойдед,ФилибертХавербеке,упомянутвфайлесданными.Начинаяснего,ямогуотследитьсвойродвпоискахсамогодревнегоизпредков,ПаувелсаванХавербеке,моегопрямогопредка.Теперьяхочуподсчитать,какойпроцентДНКуменяотнего(втеории).

Чтобыпройтиотименипредкадообъекта,представляющегоего,мыстроимобъект,которыйсопоставляетименаилюдей.

varbyName={};ancestry.forEach(function(person){byName[person.name]=person;});

console.log(byName["PhilibertHaverbeke"]);//→{name:"PhilibertHaverbeke",…}

Задача–непростонайтиукаждойиззаписейотцаипосчитать,сколькошаговполучаетсядоПаувелса.Висториисемьибылонесколькобраковмеждудвоюроднымиродственниками(ну,маленькиедеревниит.д.).Всвязисэтимветвисемейногодеревавнекоторыхместахсоединяютсясдругими,поэтомугеновуменяполучаетсябольше,чем1/2G(G–количествопоколениймеждуПаувелсомимною).Этаформулаисходитизпредположения,чтокаждоепоколениерасщепляетгенетическийфонднадвое.

Разумнобудетпровестианалогиюсreduce,гдемассивнизводитсядоединственногозначенияпутёмпоследовательногокомбинированияданныхслеванаправо.Здесьнамтоженадополучитьединственноечисло,ноприэтомнужноследоватьлиниямнаследственности.Аониформируютнепростойсписок,адерево.

Мысчитаемэтозначениедляконкретногочеловека,комбинируяэтизначенияегопредков.Этоможносделатьрекурсивно.Еслинамнуженкакой-точеловек,намнадоподсчитатьнужнуювеличинудляегородителей,чтовсвоюочередьтребуетподсчётаеёдляегопрародителей,ит.п.Поидеенампридётсяобойтибесконечноемножествоузловдерева,нотаккакнашнаборданныхконечен,намнадобудетгде-тоостановиться.Мыпростоназначимзначениепоумолчаниюдлявсехлюдей,которыхнетвнашемсписке.Логичнобудетназначитьимнулевоезначение–люди,которыхнетвсписке,ненесутвсебеДНКнужногонампредка.

Принимаячеловека,функциюдлякомбинированиязначенийотдвухпредковизначениепоумолчанию,функцияreduceAncestors«конденсирует»значениеизсемейногодрева.

functionreduceAncestors(person,f,defaultValue){functionvalueFor(person){if

ВыразительныйJavascript

101Функциивысшегопорядка

Page 102: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

(person==null)returndefaultValue;elsereturnf(person,valueFor(byName[person.mother]),valueFor(byName[person.father]));}returnvalueFor(person);}

ВнутренняяфункцияvalueForработаетсоднимчеловеком.Благодарярекурсивноймагиионаможетвызватьсебядляобработкиотцаиматериэтогочеловека.Результатывместесобъектомpersonпередаютсявf,котораяивычисляетнужноезначениедляэтогочеловека.

ТеперьмыможемиспользоватьэтодляподсчётапроцентаДНК,котороемойдедушкаразделилсПаувелсомваннХавербеке,иподелитьэтоначетыре.

functionsharedDNA(person,fromMother,fromFather){if(person.name=="PauwelsvanHaverbeke")return1;elsereturn(fromMother+fromFather)/2;}varph=byName["PhilibertHaverbeke"];console.log(reduceAncestors(ph,sharedDNA,0)/4);//→0.00049

ЧеловекпоимениПаувелсомваннХавербеке,очевидно,делит100%ДНКсПаувелсомваннХавербеке(полныхтёзоквспискеданныхнет),поэтомудлянегофункциявозвращает1.Всеостальныеделятсреднийпроцентихродителей.

Статистическиуменяпримерно0.05%ДНКсовпадаетсмоимпредкомиз16века.Это,конечно,приблизительноечисло.Этодовольномало,нотаккакнашгенетическийматериалсоставляетпримерно3миллиардабазовыхпар,вомнеестьчто-тоотмоегопредка.

МожнобылобыподсчитатьэточислоибезиспользованияreduceAncestors.Норазделениеобщегоподхода(обходдрева)иконкретногослучая(подсчётДНК)позволяетнамписатьболеепонятныйкодииспользоватьвновьчастикодадлядругихзадач.Например,следующийкодвыясняетпроцентизвестныхпредковданногочеловека,дожившихдо70лет.

functioncountAncestors(person,test){functioncombine(person,fromMother,fromFather){varthisOneCounts=test(person);returnfromMother+fromFather+(thisOneCounts?1:0);}returnreduceAncestors(person,combine,0);}functionlongLivingPercentage(person){varall=countAncestors(person,function(person){returntrue;});varlongLiving=countAncestors(person,function(person){return(person.died-person.born)>=70;});returnlongLiving/all;}console.log(longLivingPercentage(byName["EmileHaverbeke"]));//→0.145

Ненужноотноситьсяктакимрасчётамслишкомсерьёзно,таккакнашнаборсодержитпроизвольнуювыборкулюдей.Нокоддемонстрирует,чтоreduceAncestors–полезнаячастьобщегословарядляработысоструктуройданныхтипафамильногодрева.

##Связывание

Методbind,которыйестьувсехфункций,создаётновуюфункцию,котораявызоветоригинальную,носнекоторымификсированнымиаргументами.

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

vartheSet=["CarelHaverbeke","MariavanBrussel","DonaldDuck"];function

ВыразительныйJavascript

102Функциивысшегопорядка

Page 103: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

isInSet(set,person){returnset.indexOf(person.name)>-1;}

console.log(ancestry.filter(function(person){returnisInSet(theSet,person);}));//→[{name:"MariavanBrussel",…},//{name:"CarelHaverbeke",…}]console.log(ancestry.filter(isInSet.bind(null,theSet)));//→…sameresult

Вызовbindвозвращаетфункцию,котораявызоветisInSetспервымаргументомtheSet,ипоследующимиаргументамитакимиже,какиебылипереданывbind.

Первыйаргумент,которыйсейчасустановленвnull,используетсядлявызововметодов–также,какбыловapply.Мыпоговоримобэтомпозже.

##Итог

Возможностьпередаватьвызовфункциидругимфункциям–непростоигрушка,нооченьполезноесвойствоJavaScript.Мыможемписатьвыражения«спробелами»вних,которыезатембудутзаполненыприпомощизначений,возвращаемыхфункциями.

Умассивовестьнесколькополезныхметодоввысшегопорядка–forEach,чтобысделатьчто-тоскаждымэлементом,filter–чтобыпостроитьновыймассив,гденекоторыезначенияотфильтрованы,map–чтобыпостроитьновыймассив,каждыйэлементкоторогопропущенчерезфункцию,reduce–длякомбинациивсехэлементовмассиваводнозначение.

Уфункцийестьметодapply,дляпередачиимаргументовввидемассива.Такжеунихестьметодbindдлясозданиякопиифункциисчастичнозаданнымиаргументами.

##Упражнения

###Свёртка

Используйтеметодreduceвкомбинациисconcatдлясвёрткимассивамассивовводинмассив,укоторогоестьвсеэлементывходныхмассивов.

vararrays=[[1,2,3],[4,5],[6]];//Вашкодтут//→[1,2,3,4,5,6]

###Разницаввозрастематерейиихдетей

Используянаборданныхизпримера,подсчитайтесреднююразницуввозрастемеждуматерямииихдетьми(этовозрастматеривовремяпоявленияребёнка).Можноиспользоватьфункциюaverage,приведённуювглаве.

Обратитевнимание–невсематери,упомянутыевнаборе,присутствуютвнём.ЗдесьможетпригодитьсяобъектbyName,которыйупрощаетпроцедурупоискаобъектачеловекапоимени.

functionaverage(array){functionplus(a,b){returna+b;}returnarray.reduce(plus)/array.length;}

varbyName={};ancestry.forEach(function(person){byName[person.name]=person;});

//Вашкодтут

//→31.2

###Историческаяожидаемаяпродолжительностьжизни

ВыразительныйJavascript

103Функциивысшегопорядка

Page 104: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Мысчитали,чтотолькопоследнеепоколениелюдейдожилодо90лет.Давайтерассмотримэтотфеноменпоподробнее.Подсчитайтесреднийвозрастлюдейдлякаждогоизстолетий.Назначаемстолетиюлюдей,беряихгодсмерти,деляегона100иокругляя:Math.ceil(person.died/100).

functionaverage(array){functionplus(a,b){returna+b;}returnarray.reduce(plus)/array.length;}

//Тутвашкод

//→16:43.5//17:51.2//18:52.8//19:54.8//20:84.7//21:94

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

###Everyиsome

Умассивовестьещёстандартныеметодыeveryиsome.Онипринимаюткакаргументнекуюфункцию,которая,будучивызваннойсэлементоммассивавкачествеаргумента,возвращаетtrueилиfalse.Также,как&amp;&amp;возвращаетtrue,толькоесливыражениясобеихстороноператоравозвращаютtrue,методeveryвозвращаетtrue,когдафункциявозвращаетtrueдлявсехэлементовмассива.Соответственно,someвозвращаетtrue,когдазаданнаяфункциявозвращаетtrueприработеслюбымизэлементовмассива.Онинеобрабатываютбольшеэлементов,чемнеобходимо–например,еслиsomeполучаетtrueдляпервогоэлемента,оннеобрабатываетоставшиеся.

Напишитефункцииeveryиsome,которыеработаюттакже,какэтиметоды,толькопринимаютмассиввкачествеаргумента.

//Вашкодтут

console.log(every([NaN,NaN,NaN],isNaN));//→trueconsole.log(every([NaN,NaN,4],isNaN));//→falseconsole.log(some([NaN,3,4],isNaN));//→trueconsole.log(some([2,3,4],isNaN));//→false```

ВыразительныйJavascript

104Функциивысшегопорядка

Page 105: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Проблемаобъектно-ориентированныхязыковвтом,чтоонитащатссобойвсёсвоёнеявноеокружение.Вамнуженбылбанан–авыполучаетегориллусбананом,ицелыеджунгливпридачу.

ДжоАрмстронг,винтервьюCodersatWork

Термин«объект»впрограммированиисильноперегружензначениями.Вмоейпрофессииобъекты–стильжизни,темасвященныхвойнилюбимоезаклинание,нетеряющийсвоеймагическойсилы.

Стороннемучеловекувсёэтонепонятно.Начнёмжескраткойисторииобъектовкакконцепциивпрограммировании.

Этаистория,какбольшинствоисторийопрограммировании,начинаетсяспроблемысложности.Однаизидейговорит,чтосложностьможносделатьуправляемой,разделивеёненебольшиечасти,изолированныедруготдруга.Этичастисталиназыватьобъектами.

Объект–твёрдаяскорлупа,скрывающаялипкуюсложностьвнутри,ивместонеёпредлагающаянамнесколькоручекнастройкииконтактов(вродеметодов),представляющихинтерфейс,посредствомкоторогообъектнужноиспользовать.Идеявтом,чтоинтерфейсотносительнопрост,иприработеснимпозволяетигнорироватьвсесложныепроцессы,происходящиевнутриобъекта.

Простойинтерфейсможетспрятатьмногосложного.

Тайнаяжизньобъектов

История

ВыразительныйJavascript

105Тайнаяжизньобъектов

Page 106: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Дляпримерапредставьтеобъект,обеспечивающийинтерфейскучасткуэкрана.Сегопомощьюможнорисоватьфигурыиливыводитьтекстнаэтотучасток,ноприэтомвседетали,касающиесяпревращениятекстаилифигурвпиксели,скрыты.Увасестьнаборметодов,кпримеруdrawCircle,иэтовсё,чтовамнужнознатьдляиспользованиятакогообъекта.

Такиеидеиполучилиразвитиев70-80годах,ав90-хихвынесланаповерхностьрекламнаяволна–революцияобъектно-ориентированногопрограммирования.Внезапнобольшойкланлюдейобъявил,чтообъекты–этоправильныйспособпрограммирования.Авсё,чтонеимеетобъектов,являетсяустаревшейерундой.

Такойфанатизмвсегдаприводитккучебесполезнойчуши,истехпоридётчто-товродеконтрреволюции.Внекоторыхкругахобъектывообщеимеюткрайнеплохуюрепутацию.

ВыразительныйJavascript

106Тайнаяжизньобъектов

Page 107: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Япредпочитаюрассматриватьихспрактической,анеидеологическойточкизрения.Естьнесколькополезныхидей,вчастностиинкапсуляция(различиемеждувнутреннейсложностьюивнешнейпростотой),которыебылипопуляризованыобъектно-ориентированнойкультурой.Ихстоитизучать.

ЭтаглаваописываетдовольноэксцентричныйподходJavaScriptкобъектам,ито,каконисоотносятсясклассическимиобъектно-ориентированнымитехниками.

Методы–свойства,содержащиефункции.Простойметод:

varrabbit={};

rabbit.speak=function(line){

console.log("Кроликговорит'"+line+"'");

};

rabbit.speak("Яживой.");

//→Кроликговорит'Яживой.'

Обычнометоддолженчто-тосделатьсобъектом,черезкоторыйонбылвызван.Когдафункциювызываютввидеметода–каксвойствообъекта,напримерobject.method()–специальнаяпеременнаявеётелебудетуказыватьнавызвавшийеёобъект.

functionspeak(line){

console.log("А"+this.type+"кроликговорит'"+line+"'");

}

varwhiteRabbit={type:"белый",speak:speak};

varfatRabbit={type:"толстый",speak:speak};

whiteRabbit.speak("Ушкимоииусики,"+"яженавернякаопаздываю!");

//→Абелыйкроликговорит'Ушкимоииусики,яженавернякаопаздываю!'

fatRabbit.speak("Мнебысейчасморковочки.");

//→Атолстыйкроликговорит'Мнебысейчасморковочки.'

Кодиспользуетключевоесловоthisдлявыводатипаговорящегокролика.

Вспомните,чтометодыapplyиbindпринимаютпервыйаргумент,который

Методы

ВыразительныйJavascript

107Тайнаяжизньобъектов

Page 108: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

можноиспользоватьдляэмуляциивызоваметодов.Этотпервыйаргументкакраздаётзначениепеременнойthis.

Естьметод,похожийнаapply,подназваниемcall.Онтожевызываетфункцию,методомкоторойявляется,толькопринимаетаргументыкакобычно,аневвидемассива.Какapplyиbind,вcallможнопередатьзначениеthis.

speak.apply(fatRabbit,["Отрыжка!"]);

//→Атолстыйкроликговорит'Отрыжка!'

speak.call({type:"старый"},"О,господи.");

//→Астарыйкроликговорит'О,господи.'

Следитезаруками.

varempty={};

console.log(empty.toString);

//→functiontoString(){…}

console.log(empty.toString());

//→[objectObject]

Ядосталсвойствопустогообъекта.Магия!

Ну,немагия,конечно.Япростоневсёрассказалпрото,какработаютобъектывJavaScript.Вдополнениекнаборусвойств,почтиувсехтакжеестьпрототип.Прототип–этоещёодинобъект,которыйиспользуетсякакзапаснойисточниксвойств.Когдаобъектполучаетзапроснасвойство,которогоунегонет,этосвойствоищетсяуегопрототипа,затемупрототипапрототипа,ит.д.

Нуактожепрототиппустогообъекта?Этовеликийпредоквсехобъектов,Object.prototype.

console.log(Object.getPrototypeOf({})==Object.prototype);

//→true

console.log(Object.getPrototypeOf(Object.prototype));

//→null

Прототипы

ВыразительныйJavascript

108Тайнаяжизньобъектов

Page 109: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Какиследовалоожидать,функцияObject.getPrototypeOfвозвращаетпрототипобъекта.

ПрототипическиеотношениявJavaScriptвыглядяткакдерево,авегокорненаходитсяObject.prototype.Онпредоставляетнесколькометодов,которыепоявляютсяувсехобъектов,типаtoString,которыйпреобразуетобъектвстроковыйвид.

ПрототипоммногихобъектовслужитненепосредственноObject.prototype,акакой-тодругойобъект,которыйпредоставляетсвоисвойствапоумолчанию.ФункциипроисходятотFunction.prototype,массивы–отArray.prototype.

console.log(Object.getPrototypeOf(isNaN)==Function.prototype);

//→true

console.log(Object.getPrototypeOf([])==Array.prototype);

//→true

Утакихпрототиповбудетсвойпрототип–частоObject.prototype,поэтомуонвсёравно,хотьиненапрямую,предоставляетимметодытипаtoString.

ФункцияObject.getPrototypeOfвозвращаетпрототипобъекта.МожноиспользоватьObject.createдлясозданияобъектовсзаданнымпрототипом.

varprotoRabbit={

speak:function(line){

console.log("А"+this.type+"кроликговорит'"+line+"'");

}

};

varkillerRabbit=Object.create(protoRabbit);

killerRabbit.type="убийственный";

killerRabbit.speak("ХРЯЯЯСЬ!");

//→Аубийственныйкроликговорит'ХРЯЯЯСЬ!'

Прото-кроликработаетвкачествеконтейнерасвойств,которыеестьувсехкроликов.Конкретныйобъект-кролик,напримерубийственный,содержитсвойства,применимыетолькокнему,-например,свойтип,-инаследуетразделяемыесдругимисвойстваотпрототипа.

Конструкторы

ВыразительныйJavascript

109Тайнаяжизньобъектов

Page 110: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Болееудобныйспособсозданияобъектов,наследуемыхотнекоегопрототипа–конструктор.ВJavaScriptвызовфункцииспредшествующимключевымсловомnewприводитктому,чтофункцияработаеткакконструктор.Уконструкторабудетвраспоряжениипеременнаяthis,привязаннаяксвежесозданномуобъекту,иеслионаневернётнепосредственнодругоезначение,содержащееобъект,этотновыйобъектбудетвозвращёнвместонего.

Говорят,чтообъект,созданныйприпомощиnew,являетсяэкземпляромконструктора.

Вотпростойконструкторкроликов.Именаконструкторовпринятоначинатьсзаглавнойбуквы,чтобыотличатьихотдругихфункций.

functionRabbit(type){

this.type=type;

}

varkillerRabbit=newRabbit("убийственный");

varblackRabbit=newRabbit("чёрный");

console.log(blackRabbit.type);

//→чёрный

Конструкторы(авообще-то,ивсефункции)автоматическиполучаютсвойствоподименемprototype,котороепоумолчаниюсодержитпростойипустойобъект,происходящийотObject.prototype.Каждыйэкземпляр,созданныйэтимконструктором,будетиметьэтотобъектвкачествепрототипа.Поэтому,чтобыдобавитькроликам,созданнымконструкторомRabbit,методspeak,мыпростоможемсделатьтак:

Rabbit.prototype.speak=function(line){

console.log("А"+this.type+"кроликговорит'"+line+"'");

};

blackRabbit.speak("Всемкапец...");

//→Ачёрныйкроликговорит'Всемкапец...'

Важноотметитьразницумеждутем,какпрототипсвязансконструктором(черезсвойствоprototype)итем,какуобъектовестьпрототип(которыйможнополучитьчерезObject.getPrototypeOf).Насамомделепрототипконструктора—Function.prototype,посколькуконструкторы–этофункции.

ВыразительныйJavascript

110Тайнаяжизньобъектов

Page 111: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Егосвойствоprototypeбудетпрототипомэкземпляров,созданныхим,нонеегопрототипом.

Когдавыдобавляетесвойствообъекту,естьоновпрототипеилинет,онодобавляетсянепосредственноксамомуобъекту.Теперьэтоегосвойство.Есливпрототипеестьодноимённоесвойство,онобольшеневлияетнаобъект.Сампрототипнеменяется.

Rabbit.prototype.teeth="мелкие";

console.log(killerRabbit.teeth);

//→мелкие

killerRabbit.teeth="длинные,острыеиокровавленные";

console.log(killerRabbit.teeth);

//→длинные,острыеиокровавленные

console.log(blackRabbit.teeth);

//→мелкие

console.log(Rabbit.prototype.teeth);

//→мелкие

Надиаграмменарисованаситуацияпослепрогонакода.ПрототипыRabbitиObjectнаходятсязаkillerRabbitнаманерфона,иунихможнозапрашиватьсвойства,которыхнетусамогообъекта.

Перезагрузкасвойств,существующихвпрототипе,частоприноситпользу.Каквпримересзубамикроликаеёможноиспользоватьдлявыражениякаких-тоисключительныххарактеристикуболееобщихсвойств,втовремя

Перезагрузкаунаследованныхсвойств

ВыразительныйJavascript

111Тайнаяжизньобъектов

Page 112: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

какобычныеобъектыпростоиспользуютстандартныезначения,взятыеупрототипов.

ТакжеонаиспользуетсядляназначенияфункциямимассивамразныхметодовtoString.

console.log(Array.prototype.toString==Object.prototype.toString);

//→false

console.log([1,2].toString());

//→1,2

ВызовtoStringмассивавыводитрезультат,похожийна.join(",")–получаетсясписок,разделённыйзапятыми.ВызовObject.prototype.toStringнапрямуюдлямассиваприводиткдругомурезультату.Этафункциянезнаетничегоомассивах:

console.log(Object.prototype.toString.call([1,2]));

//→[objectArray]

Прототиппомогаетвлюбоевремядобавлятьновыесвойстваиметодывсемобъектам,которыеоснованынанём.Кпримеру,нашимкроликамможетпонадобитьсятанец.

Rabbit.prototype.dance=function(){

console.log("А"+this.type+"кроликтанцуетджигу.");

};

killerRabbit.dance();

//→Аубийственныйкроликтанцуетджигу.

Этоудобно.Новнекоторыхслучаяхэтоприводиткпроблемам.Впредыдущихглавахмыиспользовалиобъекткакспособсвязатьзначениясименами–мысоздавалисвойствадляэтихимён,идавалиимсоответствующиезначения.Вотпримериз4-йглавы:

Нежелательноевзаимодействиепрототипов

ВыразительныйJavascript

112Тайнаяжизньобъектов

Page 113: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

varmap={};

functionstorePhi(event,phi){

map[event]=phi;

}

storePhi("пицца",0.069);

storePhi("тронулдерево",-0.081);

Мыможемперебратьвсезначенияфивобъектечерезциклfor/in,ипроверитьналичиевнёмименичерезоператорin.Ксожалению,наммешаетсяпрототипобъекта.

Object.prototype.nonsense="ку";

for(varnameinmap)

console.log(name);

//→пицца

//→тронулдерево

//→nonsense

console.log("nonsense"inmap);

//→true

console.log("toString"inmap);

//→true

//Удалитьпроблемноесвойство

deleteObject.prototype.nonsense;

Этоженеправильно.Нетсобытияподназванием“nonsense”.Итемболеенетсобытияподназванием“toString”.

Занятно,чтоtoStringневылезловциклеfor/in,хотяоператорinвозвращаетtrueнаегосчёт.Этопотому,чтоJavaScriptразличаетсчётныеинесчётныесвойства.

Всесвойства,которыемысоздаём,назначаяимзначение–счётные.ВсестандартныесвойствавObject.prototype–несчётные,поэтомуониневылезаютвциклахfor/in.

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

Object.defineProperty(Object.prototype,"hiddenNonsense",{enumerable:false,value:"ку"});

ВыразительныйJavascript

113Тайнаяжизньобъектов

Page 114: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

for(varnameinmap)

console.log(name);

//→пицца

//→тронулдерево

console.log(map.hiddenNonsense);

//→ку

Теперьсвойствоесть,авциклеононевылезает.Хорошо.Нонамвсёещёмешаетпроблемасоператоромin,которыйутверждает,чтосвойстваObject.prototypeприсутствуютвнашемобъекте.ДляэтогонампонадобитсяметодhasOwnProperty.

console.log(map.hasOwnProperty("toString"));

//→false

Онговорит,являетсялисвойствосвойствомобъекта,безоглядкинапрототипы.Частоэтоболееполезнаяинформация,чемвыдаётоператорin.

Есливыволнуетесь,чтокто-тодругой,чейкодвызагрузиливсвоюпрограмму,испортилосновнойпрототипобъектов,ярекомендуюписатьциклыfor/inтак:

for(varnameinmap){

if(map.hasOwnProperty(name)){

//...этонашеличноесвойство

}

}

Нокроличьяноранаэтомнезаканчивается.Аесликто-тозарегистрировалимяhasOwnPropertyвобъектеmapиназначилемузначение42?Теперьвызовmap.hasOwnPropertyобращаетсяклокальномусвойству,вкоторомсодержитсяномер,анефункция.

Втакомслучаепрототипытолькомешаются,инамбыхотелосьиметьобъектывообщебезпрототипов.МывиделифункциюObject.create,чтопозволяетсоздаватьобъектсзаданнымпрототипом.Мыможемпередать

Объектыбезпрототипов

ВыразительныйJavascript

114Тайнаяжизньобъектов

Page 115: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

nullдляпрототипа,чтобысоздатьсвеженькийобъектбезпрототипа.Этото,чтонамнужнодляобъектовтипаmap,гдемогутбытьлюбыесвойства.

varmap=Object.create(null);

map["пицца"]=0.069;

console.log("toString"inmap);

//→false

console.log("пицца"inmap);

//→true

Так-толучше!НамужененужнаприблудаhasOwnProperty,потомучтовсесвойстваобъектазаданыличнонами.Мыспокойноиспользуемциклыfor/inбезоглядкинато,чтолюдитворилисObject.prototype

КогдавывызываетефункциюString,преобразующуюзначениевстроку,дляобъекта–онвызоветметодtoString,чтобысоздатьосмысленнуюстрочку.Яупомянул,чтонекоторыестандартныепрототипыобъявляютсвоиверсииtoStringдлясозданиястрок,болееполезных,чемпросто"[objectObject]".

Этопростойпримермощнойидеи.Когдакусоккоданаписантак,чтобыработатьсобъектамичерезопределённыйинтерфейс,–внашемслучаечерезметодtoString,-любойобъект,поддерживающийэтотинтерфейс,можноподключитьккоду–ивсёбудетпростоработать.

Такаятехниканазываетсяполиморфизм–хотяниктоинеменяетсвоейформы.Полиморфныйкодможетработатьсозначениямисамыхразныхформ,покаониподдерживаютодинаковыйинтерфейс.

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

Полиморфизм

Форматируемтаблицу

ВыразительныйJavascript

115Тайнаяжизньобъектов

Page 116: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

nameheightcountry

-------------------------------

Kilimanjaro5895Tanzania

Everest8848Nepal

MountFuji3776Japan

MontBlanc4808Italy/France

Vaalserberg323Netherlands

Denali6168UnitedStates

Popocatepetl5465Mexico

Работатьонабудеттак:основнаяфункциябудетспрашиватькаждуюячейку,какойонашириныивысоты,ипотомиспользуетэтуинформациюдляопределенияшириныколонокивысотырядов.Затемонапопроситячейкинарисоватьсебя,исоберётрезультатыводнустроку.

Программабудетобщатьсясобъектамиячеекчерезхорошоопределённыйинтерфейс.Типыячеекнебудутзаданыжёстко.Мысможемдобавлятьновыестилиячеек–кпримеру,подчёркнутыеячейкиузаголовка.Иеслионибудутподдерживатьнашинтерфейс,онипростозаработают,безизмененийвпрограмме.Интерфейс:

minHeight()возвращаетчисло,показывающееминимальнуювысоту,которуютребуетячейка(выраженнуювстрочках)

minWidth()возвращаетчисло,показывающееминимальнуюширину,которуютребуетячейка(выраженнуювсимволах)

draw(width,height)возвращаетмассивдлиныheight,содержащийнаборыстрок,каждаяизкоторыхширинойвwidthсимволов.Этосодержимоеячейки.

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

Перваячастьпрограммывычисляетмассивыминимальныхширинколонокивысотстрокдляматрицыячеек.Переменнаяrowsбудетсодержатьмассивмассивов,гдекаждыйвнутренниймассив–этострокаячеек.

functionrowHeights(rows){

returnrows.map(function(row){

returnrow.reduce(function(max,cell){

ВыразительныйJavascript

116Тайнаяжизньобъектов

Page 117: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

returnMath.max(max,cell.minHeight());

},0);

});

}

functioncolWidths(rows){

returnrows[0].map(function(_,i){

returnrows.reduce(function(max,row){

returnMath.max(max,row[i].minWidth());

},0);

});

}

Используяпеременную,укоторойимяначинаетсяс(илиполностьюсостоитиз)подчёркивания(_),мыпоказываемтому,ктобудетчитатькод,чтоэтотаргументнебудетиспользоваться.

ФункцияrowHeightsнедолжнавызватьзатруднений.Онаиспользуетreduceдляподсчётамаксимальнойвысотымассиваячеек,изаворачиваетэтовmap,чтобыпройтивсестрокивмассивеrows.

СитуациясcolWidthsпосложнее,потомучтовнешниймассив–этомассивстрок,анестолбцов.Язабылупомянуть,чтоmap(какиforEach,filterипохожиеметодымассивов)передаётвзаданнуюфункциювторойаргумент–индекстекущегоэлемента.Проходяприпомощиmapэлементыпервойстрокиииспользуятольковторойаргументфункции,colWidthsстроитмассивсоднимэлементомдлякаждогоиндексастолбца.Вызовreduceпроходитповнешнемумассивуrowsдлякаждогоиндекса,ивыбираетширинуширочайшейячейкивэтоминдексе.

Коддлявыводатаблицы:

functiondrawTable(rows){

varheights=rowHeights(rows);

varwidths=colWidths(rows);

functiondrawLine(blocks,lineNo){

returnblocks.map(function(block){

returnblock[lineNo];

}).join("");

}

functiondrawRow(row,rowNum){

varblocks=row.map(function(cell,colNum){

ВыразительныйJavascript

117Тайнаяжизньобъектов

Page 118: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

returncell.draw(widths[colNum],heights[rowNum]);

});

returnblocks[0].map(function(_,lineNo){

returndrawLine(blocks,lineNo);

}).join("\n");

}

returnrows.map(drawRow).join("\n");

}

ФункциияdrawTableиспользуетвнутреннююфункциюdrawRowдлярисованиявсехстрок,исоединяетихместечерезсимволыновойстроки.

ФункцияdrawRowспервапревращаетобъектыячеекстрокивблоки,которыеявляютсямассивамистрок,представляющимисодержимоеячеек,разделённыелиниями.Однаячейка,содержащаячисло3776,можетбытьпредставленамассивомизодногоэлемента[«3776»],аподчёркнутаяячейкаможетзанятьдвестрокиивыглядетькакмассив[«name»,"----"].

Блокидлястроки,укоторыходинаковаявысота,должнывыводитьсярядомдругсдругом.ВторойвызовmapвdrawRowстроитэтустрокувыводалиниязалинией,начинаяслинийсамоголевогоблока,изатемдлякаждойизнихдополняястрокудополнойширинытаблицы.Этилиниизатемсоединяютсячерезсимволновойстроки,создаваяцелыйряд,которыйвозвращаетdrawRow.

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

Давайтенапишемконструктордляячеек,содержащихтекст,которыйпредоставляетинтерфейсдляячеек.Онразбиваетстрочкувмассивстрокприпомощиметодаsplit,которыйрежетстрочкукаждыйраз,когдавнейвстречаетсяегоаргумент,ивозвращаетмассивэтихкусочков.МетодminWidthнаходитмаксимальнуюширинулиниивмассиве.

functionrepeat(string,times){

varresult="";

for(vari=0;i<times;i++)

result+=string;

returnresult;

ВыразительныйJavascript

118Тайнаяжизньобъектов

Page 119: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

}

functionTextCell(text){

this.text=text.split("\n");

}

TextCell.prototype.minWidth=function(){

returnthis.text.reduce(function(width,line){

returnMath.max(width,line.length);

},0);

};

TextCell.prototype.minHeight=function(){

returnthis.text.length;

};

TextCell.prototype.draw=function(width,height){

varresult=[];

for(vari=0;i<height;i++){

varline=this.text[i]||"";

result.push(line+repeat("",width-line.length));

}

returnresult;

};

Используетсявспомогательнаяфункцияrepeat,котораястроитстрочкусзаданнымзначением,повторённымзаданноеколичествораз.Методdrawиспользуетеёдлясозданияотступоввстроках,чтобыонивсебылинеобходимойдлины.

Давайтенарисуемдляопыташахматнуюдоску5х5.

varrows=[];

for(vari=0;i<5;i++){

varrow=[];

for(varj=0;j<5;j++){

if((j+i)%2==0)

row.push(newTextCell("##"));

else

row.push(newTextCell(""));

}

rows.push(row);

}

console.log(drawTable(rows));

//→######

//####

//######

//####

//######

ВыразительныйJavascript

119Тайнаяжизньобъектов

Page 120: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Работает!Нотаккакувсехячеекодинразмер,кодформатированиятаблицынеделаетничегоинтересного.

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

Намнужновыделитьверхнююстроку,содержащуюназваниястолбцов,припомощиподчёркивания.Никакихпроблем–мыпростозадаёмтипячейки,которыйэтимзанимается.

functionUnderlinedCell(inner){

this.inner=inner;

};

UnderlinedCell.prototype.minWidth=function(){

returnthis.inner.minWidth();

};

UnderlinedCell.prototype.minHeight=function(){

returnthis.inner.minHeight()+1;

};

UnderlinedCell.prototype.draw=function(width,height){

returnthis.inner.draw(width,height-1)

.concat([repeat("-",width)]);

};

Подчёркнутаяячейкасодержитдругуюячейку.Онавозвращаеттакиежеразмеры,какиуячейкиinner(черезвызовыеёметодовminWidthиminHeight),нодобавляетединичкуквысотеиз-заместа,занятогочёрточками.

Рисоватьеёпросто–мыберёмсодержимоеячейкиinnerидобавляемоднустроку,заполненнуючёрточками.

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

functiondataTable(data){

varkeys=Object.keys(data[0]);

varheaders=keys.map(function(name){

returnnewUnderlinedCell(newTextCell(name));

});

varbody=data.map(function(row){

returnkeys.map(function(name){

returnnewTextCell(String(row[name]));

ВыразительныйJavascript

120Тайнаяжизньобъектов

Page 121: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

});

});

return[headers].concat(body);

}

console.log(drawTable(dataTable(MOUNTAINS)));

//→nameheightcountry

//-------------------------------

//Kilimanjaro5895Tanzania

//…итакдалее

СтандартнаяфункцияObject.keysвозвращаетмассивимёнсвойствобъекта.Верхняястрокатаблицыдолжнасодержатьподчёркнутыеячейкисназваниямистолбцов.Значениявсехобъектовизнабораданныхвыглядятподзаголовкомкакнормальныеячейки–мыизвлекаемихпроходомфункцииmapпомассивуkeys,чтобыбытьувереннымвсохраненииодногопорядкаячееквкаждойстроке.

Итоговаятаблицанапоминаеттаблицуизпримера,тольковотчисланевыровненыпоправомукраю.Мызаймёмсяэтимчутьпозже.

Присозданииинтерфейсаможноввестисвойства,неявляющиесяметодами.МымоглипростоопределитьminHeightиminWidthкакпеременныедляхраненияномеров.Ноэтопотребовалобыотнасписатькодвычисленияихзначенийвконструкторе–аэтоплохо,посколькуконструированиеобъектанесвязаносниминапрямую.Могливозникнутьпроблемы,когданапримервнутренняяячейкаилиподчёркнутаяячейкаизменяются–итогдаихразмертожедолженменяться.

Этисоображенияпривеликтому,чтосвойства,неявляющиесяметодами,многиеневключаютвинтерфейс.Вместопрямогодоступаксвойствам-значениям,используютсяметодытипаgetSomethingиsetSomethingдлячтенияизаписизначенийсвойств.Ноестьиминус–приходитсяписать(ичитать)многодополнительныхметодов.

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

Геттерыисеттеры

ВыразительныйJavascript

121Тайнаяжизньобъектов

Page 122: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

обыкновенными,новтайнеимеютсвязанныеснимиметоды.

varpile={

elements:["скорлупа","кожура","червяк"],

getheight(){

returnthis.elements.length;

},

setheight(value){

console.log("Игнорируемпопыткузадатьвысоту",value);

}

};

console.log(pile.height);

//→3

pile.height=100;

//→Игнорируемпопыткузадатьвысоту100

Вобъявленииобъектазаписиgetилиsetпозволяютзадатьфункцию,котораябудетвызванапричтенииилизаписисвойства.Можнотакжедобавитьтакоесвойствовсуществующийобъект,кпримеру,вprototype,используяфункциюObject.defineProperty(раньшемыеёужеиспользовали,создаваянесчётныесвойства).

Object.defineProperty(TextCell.prototype,"heightProp",{

get:function(){returnthis.text.length;}

});

varcell=newTextCell("да\nну");

console.log(cell.heightProp);

//→2

cell.heightProp=100;

console.log(cell.heightProp);

//→2

Такжеможнозадаватьсвойствоsetвобъекте,передаваемомвdefineProperty,длязаданияметода-сеттера.Когдагеттересть,асеттеранет–попытказаписивсвойствопростоигнорируется.

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

Наследование

ВыразительныйJavascript

122Тайнаяжизньобъектов

Page 123: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Мымоглибынаписатьновыйконструкторсовсемитремяметодамивпрототипе.Нопрототипымогутсамииметьпрототипы,ипоэтомумыможемпоступитьумнее.

functionRTextCell(text){

TextCell.call(this,text);

}

RTextCell.prototype=Object.create(TextCell.prototype);

RTextCell.prototype.draw=function(width,height){

varresult=[];

for(vari=0;i<height;i++){

varline=this.text[i]||"";

result.push(repeat("",width-line.length)+line);

}

returnresult;

};

МыповторноиспользоваликонструкториметодыminHeightиminWidthизобычногоTextCell.ИRTextCellтеперьвобщемэквивалентенTextCell,заисключениемтого,чтовметодеdrawнаходитсядругаяфункция.

Такаясхеманазываетсянаследованием.Мыможемстроитьвчем-тоотличныетипыданныхнаосновесуществующих,нетратямногосил.Обычноновыйконструкторвызываетстарый(черезметодcall,чтобыпередатьемуновыйобъектиегозначение).Послеэтогомыможемпредположить,чтовсеполя,которыедолжныбытьвстаромобъекте,добавлены.Мынаследуемпрототипконструктораотстароготак,чтоэкземплярыэтоготипабудутиметьдоступксвойствамстарогопрототипа.Инаконец,мыможемпереопределитьнекоторыесвойства,добавляяихкновомупрототипу.

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

functiondataTable(data){

varkeys=Object.keys(data[0]);

varheaders=keys.map(function(name){

ВыразительныйJavascript

123Тайнаяжизньобъектов

Page 124: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

returnnewUnderlinedCell(newTextCell(name));

});

varbody=data.map(function(row){

returnkeys.map(function(name){

varvalue=row[name];

//Тутпоменяли:

if(typeofvalue=="number")

returnnewRTextCell(String(value));

else

returnnewTextCell(String(value));

});

});

return[headers].concat(body);

}

console.log(drawTable(dataTable(MOUNTAINS)));

//→…красивоотформатированнаятаблица

Наследование–основнаячастьобъектно-ориентированнойтрадиции,вместесинкапсуляциейиполиморфизмом.Но,втовремякакпоследниедвевоспринимаюткакотличныеидеи,перваявызываетспоры.

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

Мыможемиспользоватьполиморфизмбезнаследования.Янесоветуювамполностьюизбегатьнаследования–яегоиспользуюрегулярновсвоихпрограммах.Ноотноситеськнемукаккболеехитромутрюку,которыйпозволяетопределятьновыетипысминимумомкода–анекаккосновномупринципуорганизациикода.Предпочтительнорасширятьтипыприпомощикомпозиции–какUnderlinedCellпостроеннаиспользованиидругогообъектаячейки.Онпростохранитеговсвойствеиперенаправляетвызовыизсвоихвегометоды.

Иногдаудобнознать,произошёллиобъектотконкретногоконструктора.Для

Операторinstanceof

ВыразительныйJavascript

124Тайнаяжизньобъектов

Page 125: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

этогоJavaScriptдаётнамбинарныйоператорinstanceof.

console.log(newRTextCell("A")instanceofRTextCell);

//→true

console.log(newRTextCell("A")instanceofTextCell);

//→true

console.log(newTextCell("A")instanceofRTextCell);

//→false

console.log([1]instanceofArray);

//→true

Операторпроходитичерезнаследованныетипы.RTextCellявляетсяэкземпляромTextCell,посколькуRTextCell.prototypeпроисходитотTextCell.prototype.ОператортакжеможноприменятькстандартнымконструкторамтипаArray.Практическивсеобъекты–экземплярыObject.

Получается,чтообъектычутьболеесложны,чемяихподавалсначала.Унихестьпрототипы–этодругиеобъекты,иониведутсебятак,какбудтоунихестьсвойство,которогонасамомделенет,еслиэтосвойствоестьупрототипа.ПрототипомпростыхобъектовявляетсяObject.prototype/

Конструкторы,–функции,именакоторыхобычноначинаютсясзаглавнойбуквы,-можноиспользоватьсоператоромnewдлясозданияобъектов.Прототипомновогообъектабудетобъект,содержащийсявсвойствеprototypeконструктора.Этоможноиспользовать,помещаясвойства,которыеделятмеждусобойвсевеличиныданноготипа,вихпрототип.Операторinstanceof,еслиемудатьобъектиконструктор,можетсказать,являетсялиобъектэкземпляромэтогоконструктора.

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

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

Итог

ВыразительныйJavascript

125Тайнаяжизньобъектов

Page 126: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

полезнаяштука.

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

НапишитеконструкторVector,представляющийвекторвдвумерномпространстве.Онпринимаетпараметрыxиy(числа),которыехранятсяводноимённыхсвойствах.

ДайтепрототипуVectorдваметода,plusиminus,которыепринимаютдругойвекторвкачествепараметра,ивозвращаютновыйвектор,которыйхранитвxиyсуммуилиразностьдвух(одинthis,второй—аргумент)

Добавьтегеттерlengthвпрототип,подсчитывающийдлинувектора–расстояниеот(0,0)до(x,y).

//Вашкод

console.log(newVector(1,2).plus(newVector(2,3)));

//→Vector{x:3,y:5}

console.log(newVector(1,2).minus(newVector(2,3)));

//→Vector{x:-1,y:-1}

console.log(newVector(3,4).length);

//→5

СоздайтетипячейкиStretchCell(inner,width,height),соответствующийинтерфейсуячеектаблицыизэтойглавы.Ондолженоборачиватьдругуюячейку(какделаетUnderlinedCell),иубеждаться,чторезультирующаяячейкаимееткакминимумзаданныеширинуивысоту,дажеесливнутренняяячейка

Упражнения

Векторныйтип

Ещёоднаячейка

ВыразительныйJavascript

126Тайнаяжизньобъектов

Page 127: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

былабыменьше.

//Вашкод.

varsc=newStretchCell(newTextCell("abc"),1,2);

console.log(sc.minWidth());

//→3

console.log(sc.minHeight());

//→2

console.log(sc.draw(3,2));

//→["abc",""]

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

Задавинтерфейс,попробуйтесделатьфункциюlogFive,котораяпринимаетобъект-последовательностьивызываетconsole.logдляпервыхеёпятиэлементов–илидляменьшегоколичества,еслиихменьшепяти.

ЗатемсоздайтетипобъектаArraySeq,оборачивающиймассив,ипозволяющийпроходпомассивусиспользованиемразработанноговамиинтерфейса.Создайтедругойтипобъекта,RangeSeq,которыйпроходитподиапазонучисел(егоконструктордолженприниматьаргументыfromиto).

//Вашкод.

logFive(newArraySeq([1,2]));

//→1

//→2

logFive(newRangeSeq(100,1000));

//→100

//→101

//→102

//→103

//→104

Интерфейскпоследовательностям

ВыразительныйJavascript

127Тайнаяжизньобъектов

Page 128: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Вопросотом,могутлимашиныдуматьтакжеуместен,каквопросотом,могутлиподводныелодкиплавать.

ЭдсгерДейкстра,Угрозывычислительнойнауке

Вглавах-проектахяперестанузакидыватьвастеорией,ибудуработатьвместесваминадпрограммами.Теориянезаменимаприобучениипрограммированию,ноонадолжнасопровождатьсячтениемипониманиемнетривиальныхпрограмм.

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

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

Такимобразом,мыпорубимвремяипространствонаединицыфиксированногоразмера:клеткидляпространстваиходыдлявремени.Конечно,этогрубоеинеаккуратноеприближение.Нонашасимуляциядолжнабытьразвлекательной,анеаккуратной,поэтомумысвободно«срезаемуглы».

Определитьмирмыможемприпомощиплана–массивастрок,которыйраскладываетмировуюсетку,используяодинсимволнаклетку.

varplan=["############################",

"###o##",

"##",

"#######",

"#######",

"#######",

Проект:электроннаяжизнь

Определение

ВыразительныйJavascript

128Проект:электроннаяжизнь

Page 129: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

"######",

"######",

"###o#",

"#o#o####",

"###",

"############################"];

Символ“#”обозначаетстеныикамни,“o”–существо.Пробелы–пустоепространство.

Планможноиспользоватьдлясозданияобъектамира.Онследитзаразмеромисодержимыммира.УнегоестьметодtoString,которыйпреобразовываетмирввыводимуюстрочку(такую,какплан,накоторомоноснован),чтобымымоглинаблюдатьзапроисходящимвнутринего.Уобъектмираестьметодturn(ход),позволяющийвсемсуществамсделатьодинходиобновляющийсостояниемиравсоответствиисихдействиями.

Усетки,моделирующеймир,заданыширинаивысота.Клеткиопределяютсякоординатамиxиy.МыиспользуемпростойтипVector(изупражненийкпредыдущейглаве)дляпредставленияэтихпаркоординат.

functionVector(x,y){

this.x=x;

this.y=y;

}

Vector.prototype.plus=function(other){

returnnewVector(this.x+other.x,this.y+other.y);

};

Потомнамнужентипобъекта,моделирующийсамусетку.Сетка–частьмира,номыделаемизнеёотдельныйобъект(которыйбудетсвойствоммировогообъекта),чтобынеусложнятьмировойобъект.Мирдолжензагружатьсебявещами,относящимисякмиру,асетка–вещами,относящимисяксетке.

Дляхранениясеткизначенийунасестьнескольковариантов.Можноиспользоватьмассивизмассивов-строк,ииспользоватьдвухступенчатый

Изображаемпространство

ВыразительныйJavascript

129Проект:электроннаяжизнь

Page 130: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

доступксвойствам:

vargrid=[["topleft","topmiddle","topright"],

["bottomleft","bottommiddle","bottomright"]];

console.log(grid[1][2]);

//→bottomright

Илимыможемвзятьодинмассив,размераwidth×height,ирешить,чтоэлемент(x,y)находитсявпозицииx+(y×width).

vargrid=["topleft","topmiddle","topright",

"bottomleft","bottommiddle","bottomright"];

console.log(grid[2+(1*3)]);

//→bottomright

Посколькудоступбудетзавёрнутвметодахобъектасетки,внешнемукодувсёравно,какойподходбудетвыбран.Явыбралвторой,потомучтоснимпрощесоздаватьмассив.ПривызовеконструктораArrayсоднимчисломвкачествеаргументаонсоздаётновыйпустоймассивзаданнойдлины.

СледующийкодобъявляетобъектGrid(сетка)сосновнымиметодами:

functionGrid(width,height){

this.space=newArray(width*height);

this.width=width;

this.height=height;

}

Grid.prototype.isInside=function(vector){

returnvector.x>=0&amp;&amp;vector.x<this.width&amp;&amp;

vector.y>=0&amp;&amp;vector.y<this.height;

};

Grid.prototype.get=function(vector){

returnthis.space[vector.x+this.width*vector.y];

};

Grid.prototype.set=function(vector,value){

this.space[vector.x+this.width*vector.y]=value;

};

Элементарныйтест:

vargrid=newGrid(5,5);

console.log(grid.get(newVector(1,1)));

//→undefined

ВыразительныйJavascript

130Проект:электроннаяжизнь

Page 131: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

grid.set(newVector(1,1),"X");

console.log(grid.get(newVector(1,1)));

//→X

Передтем,какзанятьсяконструктороммираWorld,намнадоопределитьсясобъектамисуществ,населяющихего.Яупомянул,чтомирбудетспрашиватьсуществ,какиеонихотятпроизвестидействия.Работатьэтобудеттак:укаждогообъектасуществаестьметодact,которыйпривызовевозвращаетдействиеaction.Action–объекттипаproperty,которыйназываеттипдействия,котороехочетсовершитьсущество,кпримеру“move”.Actionможетсодержатьдополнительнуюинформацию—такую,какнаправлениедвижения.

Существаужасноблизорукиивидяттольконепосредственноприлегающиекнимклетки.Ноиэтоможетпригодитьсяпривыборедействий.Привызовеметодаactемудаётсяобъектview,которыйпозволяетсуществуизучитьприлегающуюместность.Мыназываемвосемьсоседнихклетокихнаправлениямипокомпасу:“n”насевер,“ne”насеверо-восток,ит.п.Воткакойобъектбудетиспользоватьсядляпреобразованияизназванийнаправленийвсмещенияпокоординатам:

vardirections={

"n":newVector(0,-1),

"ne":newVector(1,-1),

"e":newVector(1,0),

"se":newVector(1,1),

"s":newVector(0,1),

"sw":newVector(-1,1),

"w":newVector(-1,0),

"nw":newVector(-1,-1)

};

Уобъектаviewестьметодlook,которыйпринимаетнаправлениеивозвращаетсимвол,кпримеру"#",еслитамстена,илипробел,еслитамничегонет.ОбъекттакжепредоставляетудобныеметодыfindиfindAll.Обапринимаютодинизсимволов,представляющихвещинакарте,какаргумент.Первыйвозвращаетнаправление,вкоторомэтотпредметможнонайти

Программныйинтерфейссуществ

ВыразительныйJavascript

131Проект:электроннаяжизнь

Page 132: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

рядомссуществом,илижеnull,еслитакогопредметарядомнет.Второйвозвращаетмассивсовсемивозможныминаправлениями,гденайдентакойпредмет.Например,существослеваотстены(назападе)получит[«ne»,«e»,«se»]привызовеfindAllсаргументом“#”.

Вотпростоетупоесущество,котороепростоидёт,поканеврезаетсявпрепятствие,азатемотскакиваетвслучайномнаправлении.

functionrandomElement(array){

returnarray[Math.floor(Math.random()*array.length)];

}

functionBouncingCritter(){

this.direction=randomElement(Object.keys(directions));

};

BouncingCritter.prototype.act=function(view){

if(view.look(this.direction)!="")

this.direction=view.find("")||"s";

return{type:"move",direction:this.direction};

};

ВспомогательнаяфункцияrandomElementпростовыбираетслучайныйэлементмассива,используяMath.randomинемногоарифметики,чтобыполучитьслучайныйиндекс.Мыидальшебудемиспользоватьслучайность,таккакона–полезнаяштукавсимуляциях.

КонструкторBouncingCritterвызываетObject.keys.Мывиделиэтуфункциювпредыдущейглаве–онавозвращаетмассивсовсемиименамисвойствобъекта.Тутонаполучаетвсеименанаправленийизобъектаdirections,заданногоранее.

Конструкция“||«s»”вметодеactнужна,чтобыthis.directionнеполучилnull,вслучаееслисуществозабилосьвуголбезсвободногопространствавокруг–например,окруженодругимисуществами.

ТеперьможноприступатькмировомуобъектуWorld.Конструкторпринимаетплан(массивстрок,представляющихсеткумира)иобъектlegend.Это

Мировойобъект

ВыразительныйJavascript

132Проект:электроннаяжизнь

Page 133: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

объект,сообщающий,чтоозначаеткаждыйизсимволовкарты.Внёместьконструктордлякаждогосимвола–кромепробела,которыйссылаетсянаnull(представляющийпустоепространство).

functionelementFromChar(legend,ch){

if(ch=="")

returnnull;

varelement=newlegend[ch]();

element.originChar=ch;

returnelement;

}

functionWorld(map,legend){

vargrid=newGrid(map[0].length,map.length);

this.grid=grid;

this.legend=legend;

map.forEach(function(line,y){

for(varx=0;x<line.length;x++)

grid.set(newVector(x,y),

elementFromChar(legend,line[x]));

});

}

ВelementFromCharмысначаласоздаёмэкземплярнужноготипа,находяконструкторсимволаиприменяякнемуnew.ПотомдобавляемсвойствоoriginChar,чтобыбылопростовыяснить,изкакогосимволаэлементбылсозданизначально.

НампонадобитсяэтосвойствоoriginCharприизготовлениимировогометодаtoString.Методстроиткартуввидестрокиизтекущегосостояниямира,проходядвумернымцикломпоклеткамсетки.

functioncharFromElement(element){

if(element==null)

return"";

else

returnelement.originChar;

}

World.prototype.toString=function(){

varoutput="";

for(vary=0;y<this.grid.height;y++){

for(varx=0;x<this.grid.width;x++){

varelement=this.grid.get(newVector(x,y));

ВыразительныйJavascript

133Проект:электроннаяжизнь

Page 134: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

output+=charFromElement(element);

}

output+="\n";

}

returnoutput;

};

Стенаwall–простойобъект.Используетсядлязанятияместаинеимеетметодаact.

functionWall(){}

ПроверяяобъектWorld,создавэкземплярсиспользованиемплана,заданноговначалеглавы,изатемвызвавегометодtoString,мыполучимоченьпохожуюнаэтотпланстроку.

varworld=newWorld(plan,{"#":Wall,"o":BouncingCritter});

console.log(world.toString());

//→############################

//###o##

//##

//#######

//#######

//#######

//######

//######

//###o#

//#o#o####

//###

//############################

thisиегообластьвидимости

ВконструктореWorldестьвызовforEach.Хочуотметить,чтовнутрифункции,передаваемойвforEach,мыужененаходимсянепосредственновобластивидимостиконструктора.Каждыйвызовфункцииполучаетсвоёпространствоимён,поэтомуthisвнутринёуженессылаетсянасоздаваемыйобъект,накоторыйссылаетсяthisснаружифункции.Ивообще,еслифункциявызываетсянекакметод,thisбудетотноситьсякглобальномуобъекту.

Значит,мынеможемписатьthis.gridдлядоступаксеткеизнутрицикла.Вместоэтоговнешняяфункциясоздаётлокальнуюпеременнуюgrid,через

ВыразительныйJavascript

134Проект:электроннаяжизнь

Page 135: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

которуювнутренняяфункцияполучаетдоступксетке.

ЭтопромахвдизайнеJavaScript.Ксчастью,вследующейверсииестьрешениеэтойпроблемы.Апокаестьпутиобхода.Обычнопишут

varself=this

ипослеэтогоработаютспеременнойself.

Другоерешение–использоватьметодbind,которыйпозволяетпривязатьсякконкретномуобъектуthis.

vartest={

prop:10,

addPropTo:function(array){

returnarray.map(function(elt){

returnthis.prop+elt;

}.bind(this));

}

};

console.log(test.addPropTo([5]));

//→[15]

Функция,передаваемаявmap–результатпривязкивызова,ипосемуеёthisпривязанкпервомуаргументу,переданномувbind,тоестьпеременнойthisвнешнейфункции(вкоторойсодержитсяобъектtest).

Большинствостандартныхметодоввысшегопорядкаумассивов,такихкакforEachиmap,принимаютнеобязательныйвторойаргумент,которыйтожеможноиспользоватьдляпередачиthisпривызовахитерационнойфункции.Вымоглибынаписатьпредыдущийпримерчутьпроще:

vartest={

prop:10,

addPropTo:function(array){

returnarray.map(function(elt){

returnthis.prop+elt;

},this);//←безbind

}

};

console.log(test.addPropTo([5]));

//→[15]

ВыразительныйJavascript

135Проект:электроннаяжизнь

Page 136: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Этоработаеттолькостемифункциямивысшегопорядка,укоторыхестьтакойконтекстныйпараметр.Еслинет–приходитсяиспользоватьдругиеупомянутыеподходы.

Внашейсобственнойфункциивысшегопорядкамыможемвключитьподдержкуконтекстногопараметра,используяметодcallдлявызовафункции,переданнойвкачествеаргумента.Кпримеру,вотвамметодforEachдлянашеготипаGrid,вызывающийзаданнуюфункциюдлякаждогоэлементарешётки,которыйнеравенnullилиundefined:

Grid.prototype.forEach=function(f,context){

for(vary=0;y<this.height;y++){

for(varx=0;x<this.width;x++){

varvalue=this.space[x+y*this.width];

if(value!=null)

f.call(context,value,newVector(x,y));

}

}

};

Следующийшаг–созданиеметодаturn(шаг)длямировогообъекта,дающегосуществамвозможностьдействовать.ОнбудетобходитьсеткуметодомforEach,иискатьобъекты,укоторыхестьметодact.Найдяобъект,turnвызываетэтотметод,получаяобъектactionипроизводитэтодействие,еслионодопустимо.Покамыпонимаемтолькодействие“move”.

Естьоднавозможнаяпроблема.Можетеувидеть,какая?Еслимыпозволимсуществамдвигатьсяпомеретого,какмыихперебираем,онимогутперейтинаклетку,которуюмыещёнеобработали,итогдамыпозволимимсдвинутьсяещёраз,когдаочередьдойдётдоэтойклетки.Такимобразом,намнадохранитьмассивсуществ,которыеужесделалисвойшаг,иигнорироватьихприповторномпроходе.

World.prototype.turn=function(){

varacted=[];

this.grid.forEach(function(critter,vector){

if(critter.act&amp;&amp;acted.indexOf(critter)==-1){

Оживляеммир

ВыразительныйJavascript

136Проект:электроннаяжизнь

Page 137: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

acted.push(critter);

this.letAct(critter,vector);

}

},this);

};

ВторойпараметрметодаforEachиспользуетсядлядоступакправильнойпеременнойthisвовнутреннейфункции.МетодletActсодержитлогику,котораяпозволяетсуществамдвигаться.

World.prototype.letAct=function(critter,vector){

varaction=critter.act(newView(this,vector));

if(action&amp;&amp;action.type=="move"){

vardest=this.checkDestination(action,vector);

if(dest&amp;&amp;this.grid.get(dest)==null){

this.grid.set(vector,null);

this.grid.set(dest,critter);

}

}

};

World.prototype.checkDestination=function(action,vector){

if(directions.hasOwnProperty(action.direction)){

vardest=vector.plus(directions[action.direction]);

if(this.grid.isInside(dest))

returndest;

}

};

Сначаламыпростопросимсуществодействовать,передаваяемуобъектview,которыйзнаетпромиритекущееположениесуществавмире(мыскорозададимView).Методactвозвращаеткакое-либодействие.

Еслитипдействияне“move”,оноигнорируется.Если“move”,иеслиунегоестьсвойствоdirection,ссылающеесянадопустимоенаправление,иесликлеткавэтомнаправлениипустует(null),мыназначаемклетке,гдетолькочтобылосущество,null,исохраняемсуществовклеткеназначения.

Заметьте,чтоletActзаботитсяобигнорированиинеправильныхвходныхданных.Оннепредполагаетпоумолчанию,чтонаправлениедопустимо,или,чтосвойствотипаимеетсмысл.Такогородазащитноепрограммированиевнекоторыхситуацияхимеетсмысл.Восновномэтоделаетсядляпроверкивходныхданных,приходящихотисточников,которыевынеконтролируете

ВыразительныйJavascript

137Проект:электроннаяжизнь

Page 138: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

(вводпользователяиличтениефайла),ноонотакжеполезнодляизолированияподсистемдруготдруга.Внашемслучаеегоцель–учесть,чтосуществамогутбытьзапрограммированынеаккуратно.Имненадопроверять,имеютлиихнамерениясмысл.Онипростозапрашиваютвозможностьдействия,амирсамрешает,разрешатьлиего.

Этидваметоданепринадлежатквнешнемуинтерфейсумировогообъекта.Ониявляютсядеталямивнутреннейреализации.Некоторыеязыкипредусматриваютспособыобъявлятьопределённыеметодыисвойства«приватными»,ивыдаватьошибкуприпопыткеихиспользованияснаружиобъекта.JavaScriptнепредусматриваеттакого,такчтовампридётсяполагатьсянадругиеспособысообщитьотом,чтоявляетсячастьюинтерфейсаобъекта.Иногдапомогаетиспользованиесхемыименованиясвойствдляразличениявнутреннихивнешних,например,сособымиприставкамикименамвнутренних,типаподчёркивания(_).Этооблегчитвыявлениеслучайногоиспользованиясвойств,неявляющихсячастьюинтерфейса.

Апропущеннаячасть,типView,выглядитследующимобразом:

functionView(world,vector){

this.world=world;

this.vector=vector;

}

View.prototype.look=function(dir){

vartarget=this.vector.plus(directions[dir]);

if(this.world.grid.isInside(target))

returncharFromElement(this.world.grid.get(target));

else

return"#";

};

View.prototype.findAll=function(ch){

varfound=[];

for(vardirindirections)

if(this.look(dir)==ch)

found.push(dir);

returnfound;

};

View.prototype.find=function(ch){

varfound=this.findAll(ch);

if(found.length==0)returnnull;

returnrandomElement(found);

};

ВыразительныйJavascript

138Проект:электроннаяжизнь

Page 139: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Методlookвычисляеткоординаты,накоторыемыпытаемсяпосмотреть.Еслионинаходятсявнутрисетки,тополучаетсимвол,соответствующийэлементу,находящемусятам.Длякоординатснаружисеткиlookпростопритворяется,чтотамстена–есливызададитемирбезокружающихстен,существанесмогутсойтискрая.

Мысоздалиэкземплярмировогообъекта.Теперь,когдавсенеобходимыеметодыготовы,унасдолжнополучитьсязаставитьегодвигаться.

for(vari=0;i<5;i++){

world.turn();

console.log(world.toString());

}

//→…пятьходов

Простовыводитьпятькопийкарты–неоченьудобныйспособнаблюдениязамиром.Поэтомувпесочницедлякниги(иливфайлахдляскачивания)естьволшебнаяфункцияanimateWorld,котораяпоказываетмиркаканимациюнаэкране,делаяпотришагавсекунду,покавыненажмётестоп.

animateWorld(world);

//→…заработало!

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

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

Япридумалсущество,двигающеесяпостенке.Онодержитсвоюлевуюруку

Онодвигается

Большеформжизни

ВыразительныйJavascript

139Проект:электроннаяжизнь

Page 140: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

(лапу,щупальце,чтоугодно)настенеидвигаетсявдольнеё.Это,какоказалось,нетак-топростозапрограммировать.

Намнужнобудетвычислять,используянаправлениявпространстве.Таккакнаправлениязаданынаборомстрок,намнадозадатьсвоюоперациюdirPlusдляподсчётаотносительныхнаправлений.dirPlus(«n»,1)означаетповоротпочасовойна45градусовнасевер,чтоприводитк“ne”.dirPlus(«s»,-2)означаетповоротпротивчасовойсюга,тоестьнавосток.

vardirectionNames=Object.keys(directions);

functiondirPlus(dir,n){

varindex=directionNames.indexOf(dir);

returndirectionNames[(index+n+8)%8];

}

functionWallFollower(){

this.dir="s";

}

WallFollower.prototype.act=function(view){

varstart=this.dir;

if(view.look(dirPlus(this.dir,-3))!="")

start=this.dir=dirPlus(this.dir,-2);

while(view.look(this.dir)!=""){

this.dir=dirPlus(this.dir,1);

if(this.dir==start)break;

}

return{type:"move",direction:this.dir};

};

Методactтолькосканируетокружениесущества,начинаяслевойстороныидальшепочасовой,поканенаходитпустуюклетку.Затемондвигаетсявнаправленииэтойклетки.

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

Такчтоестьещёоднапроверкачерезif,чтосканированиенужноначинать,еслисуществотолькочтопрошломимокакого-либопрепятствия.Тоесть,еслипространствосзадиислеванепустое.Впротивномслучаесканировать

ВыразительныйJavascript

140Проект:электроннаяжизнь

Page 141: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

начинаемвпереди,поэтомувпустомпространствеонбудетидтипрямо.

Инаконец,естьпроверканасовпадениеthis.dirиstartнакаждомпроходецикла,чтобыоннезациклился,когдасуществунекудаидтииз-застенилидругихсуществ,иононеможетнайтипустуюклетку.

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

animateWorld(newWorld(

["############",

"###",

"#~~#",

"####",

"###o####",

"##",

"############"],

{"#":Wall,

"~":WallFollower,

"o":BouncingCritter}

));

Чтобысделатьжизньвнашеммиркеболееинтересной,добавимпонятияедыиразмножения.Укаждогоживогосуществапоявляетсяновоесвойство,energy(энергия),котораяуменьшаетсяприсовершениидействий,иувеличиваетсяприпоеданииеды.Когдаусуществадостаточноэнергии,онможетразмножаться,создаваяновоесуществотогожетипа.Дляупрощениярасчётовнашисуществаразмножаютсясамипосебе.

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

Чтобыэтозаработало,намнуженмирсдругимметодомletAct.МымоглибыпростозаменитьметодпрототипаWorld,нояпривыккнашейсимуляцииходящихпостенамсуществинехотелбыеёразрушать.

Болеежизненнаяситуация

ВыразительныйJavascript

141Проект:электроннаяжизнь

Page 142: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Одноизрешений–использоватьнаследование.Мысоздаёмновыйконструктор,LifelikeWorld,чейпрототипоснованнапрототипеWorld,нопереопределяетметодletAct.НовыйletActпередаётработупосовершениюдействийвразныефункции,хранящиесявобъектеactionTypes.

functionLifelikeWorld(map,legend){

World.call(this,map,legend);

}

LifelikeWorld.prototype=Object.create(World.prototype);

varactionTypes=Object.create(null);

LifelikeWorld.prototype.letAct=function(critter,vector){

varaction=critter.act(newView(this,vector));

varhandled=action&amp;&amp;

action.typeinactionTypes&amp;&amp;

actionTypes[action.type].call(this,critter,

vector,action);

if(!handled){

critter.energy-=0.2;

if(critter.energy<=0)

this.grid.set(vector,null);

}

};

НовыйметодletActпроверяет,былолипереданохотькакое-тодействие,затем–естьлифункция,обрабатывающаяего,ивконце–возвращаетлиэтафункцияtrue,показывая,чтодействиевыполненоуспешно.Обратитевниманиенаиспользованиеcall,чтобыдатьфункциидоступкмировомуобъектучерезthis.

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

Самоепростоедействие–рост,егоиспользуютрастения.Когдавозвращаетсяобъектactionтипа{type:«grow»},будетвызванследующийметод-обработчик:

Обработчикидействий

ВыразительныйJavascript

142Проект:электроннаяжизнь

Page 143: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

actionTypes.grow=function(critter){

critter.energy+=0.5;

returntrue;

};

Роствсегдауспешенидобавляетполовинуединицыкэнергетическомууровнюрастения.

Движениеполучаетсяболеесложным.

actionTypes.move=function(critter,vector,action){

vardest=this.checkDestination(action,vector);

if(dest==null||

critter.energy<=1||

this.grid.get(dest)!=null)

returnfalse;

critter.energy-=1;

this.grid.set(vector,null);

this.grid.set(dest,critter);

returntrue;

};

Этодействиевначалепроверяет,используяметодcheckDestination,объявленныйранее,предоставляетлидействиедопустимоенаправление.Еслинет,илижевтомнаправлениинепустойучасток,илижеусуществанедостаётэнергии–moveвозвращаетfalse,показывая,чтодействиенесостоялось.Виномслучаеондвигаетсуществоивычитаетэнергию.

Кромедвижения,существамогутесть.

actionTypes.eat=function(critter,vector,action){

vardest=this.checkDestination(action,vector);

varatDest=dest!=null&amp;&amp;this.grid.get(dest);

if(!atDest||atDest.energy==null)

returnfalse;

critter.energy+=atDest.energy;

this.grid.set(dest,null);

returntrue;

};

Поеданиедругогосуществатакжетребуетпредоставлениядопустимойклеткинаправления.Вэтомслучаеклеткадолжнасодержатьчто-либос

ВыразительныйJavascript

143Проект:электроннаяжизнь

Page 144: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

энергией,напримерсущество(нонестену,ихестьнельзя).Еслиэтоподтверждается,энергиясъеденногопереходиткедоку,ажертваудаляетсяссетки.

Инаконец,мыпозволяемсуществамразмножаться.

actionTypes.reproduce=function(critter,vector,action){

varbaby=elementFromChar(this.legend,

critter.originChar);

vardest=this.checkDestination(action,vector);

if(dest==null||

critter.energy<=2*baby.energy||

this.grid.get(dest)!=null)

returnfalse;

critter.energy-=2*baby.energy;

this.grid.set(dest,baby);

returntrue;

};

Размножениеотнимаетвдваразабольшеэнергии,чеместьуноворожденного.Поэтомумысоздаёмгипотетическогоотпрыска,используяelementFromCharнаоригинальномсуществе.Кактолькоунасестьотпрыск,мыможемвыяснитьегоэнергетическийуровеньипроверить,естьлиуродителядостаточноэнергии,чтобыродитьего.Такженампотребуетсядопустимаяклетканаправления.

Есливсёвпорядке,отпрыскпомещаетсянасетку(иперестаётбытьгипотетическим),аэнергиятратится.

Теперьунасестьосновадлясимуляциисуществ,большепохожихнанастоящие.Мымоглибыпоместитьвновыймирсуществизстарого,ноонибыпростоумерли,таккакунихнетсвойстваenergy.Давайтесделаемновых.Сначаланапишемрастение,которое,посути,довольнопростаяформажизни.

functionPlant(){

this.energy=3+Math.random()*4;

}

Населяеммир

ВыразительныйJavascript

144Проект:электроннаяжизнь

Page 145: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Plant.prototype.act=function(context){

if(this.energy>15){

varspace=context.find("");

if(space)

return{type:"reproduce",direction:space};

}

if(this.energy<20)

return{type:"grow"};

};

Растенияначинаютсослучайногоуровняэнергииот3до7,чтобыонинеразмножалисьвсеводинход.Когдарастениедостигаетэнергии15,арядоместьпустаяклетка–оноразмножаетсявнеё.Еслиононеможетразмножится,топросторастёт,поканедостигнетэнергии20.

Теперьопределимпоедателярастений.

functionPlantEater(){

this.energy=20;

}

PlantEater.prototype.act=function(context){

varspace=context.find("");

if(this.energy>60&amp;&amp;space)

return{type:"reproduce",direction:space};

varplant=context.find("*");

if(plant)

return{type:"eat",direction:plant};

if(space)

return{type:"move",direction:space};

};

Длярастенийбудемиспользоватьсимвол*—то,чтобудетискатьсуществовпоискахеды.

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

varvalley=newLifelikeWorld(

["############################",

Вдохнёмжизнь

ВыразительныйJavascript

145Проект:электроннаяжизнь

Page 146: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

"###########",

"##*****##",

"#*##****O*##",

"#***O##***#",

"#O##***#",

"###**#",

"#O#*#",

"#*#**O#",

"#***##**O**#",

"##****###****###",

"############################"],

{"#":Wall,

"O":PlantEater,

"*":Plant}

);

Большуючастьвременирастенияразмножаютсяиразрастаются,нозатемизобилиеедыприводитквзрывномуроступопуляциитравоядных,которыесъедаютпочтивсюрастительность,чтоприводиткмассовомувымираниюотголода.Иногдаэкосистемавосстанавливаетсяиначинаетсяновыйцикл.Вдругихслучаяхкакой-тоизвидоввымирает.Еслитравоядные,тогдавсёпространствозаполняетсярастениями.Еслирастения–оставшиесясуществаумираютотголода,идолинапревращаетсявнеобитаемуюпустошь.О,жестокостьприроды…

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

Унашихтравоядныхестьнесколькоочевидныхпроблем.Во-первых,онижадные—поедаюткаждоерастение,котороенаходят,покаполностьюнеуничтожатвсюрастительность.Во-вторых,ихслучайноедвижение(вспомните,чтометодview.findвозвращаетслучайноенаправление)заставляетихболтатьсянеэффективноипомиратьсголоду,еслирядомнеокажетсярастений.Инаконец,онислишкомбыстроразмножаются,чтоделаетциклыотизобилиякголодуслишкомбыстрыми.

Упражнения

Искусственныйидиот

ВыразительныйJavascript

146Проект:электроннаяжизнь

Page 147: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Напишитеновыйтипсущества,которыйстараетсясправитсясоднимилинесколькимипроблемамиизаменитеимстарыйтипPlantEaterвмиредолины.Последитезаними.Выполнитенеобходимыеподстройки.

//Вашкод

functionSmartPlantEater(){}

animateWorld(newLifelikeWorld(

["############################",

"###########",

"##*****##",

"#*##****O*##",

"#***O##***#",

"#O##***#",

"###**#",

"#O#*#",

"#*#**O#",

"#***##**O**#",

"##****###****###",

"############################"],

{"#":Wall,

"O":SmartPlantEater,

"*":Plant}

));

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

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

//Вашкодтут

functionTiger(){}

animateWorld(newLifelikeWorld(

["####################################################",

Хищники

ВыразительныйJavascript

147Проект:электроннаяжизнь

Page 148: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

"#####****###",

"#*@##########OO##",

"#*##OO*****#",

"###*##########*#",

"###**********#",

"#***#****#########**#",

"#***#*#***#",

"####O#***######",

"#*@##*O##",

"#*#######**#",

"###*********#",

"#O@O#",

"#*###########*#",

"#**#*#####O#",

"##**OO##******###**#",

"####*********#",

"####################################################"],

{"#":Wall,

"@":Tiger,

"O":SmartPlantEater,//изпредыдущегоупражнения

"*":Plant}

));

ВыразительныйJavascript

148Проект:электроннаяжизнь

Page 149: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Отладкаизначальновдвоесложнеенаписаниякода.Поэтому,есливыпишетекоднастолькозаумный,насколькоможете,топоопределениювынеспособныотлаживатьего.БрайанКерниганиП.Ж.Плауэр,«Основыпрограммногостиля»

Юан-Манаписалнебольшуюпрограмму,использующуюмногоглобальныхпеременныхиужасныххаков.Ученик,читаяпрограмму,спросилего:«Выпредупреждалинасоподобныхтехниках,ноприэтомянахожуихввашейжепрограмме.Какэтовозможно?»Мастерответил:«Ненужнобежатьзаполивальнымшлангом,еслидомнегорит».

МастерЮан-Ма,«Книгапрограммирования».

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

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

Частопроблемавозникаетвтехситуациях,возникновениекоторыхпрограммистизначальнонепредвидел.Иногдаэтихситуацийнельзяизбежать.Когдапользователяпросятввестиеговозраст,аонвводит«апельсин»,этоставитпрограммувнепростуюситуацию.Этиситуациинеобходимопредвидетьикак-тообрабатывать.

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

Поискиобработкаошибок

Ошибкипрограммистов

ВыразительныйJavascript

149Поискиобработкаошибок

Page 150: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Разныеязыкипо-разномумогутпомогатьвамвпоискеошибок.Ксожалению,JavaScriptнаходитсянаконцеэтойшкалы,обозначенномкак«вообщепочтинепомогает».Некоторымязыкамнадоточнознатьтипывсехпеременныхивыраженийещёдозапускапрограммы,ионисразусообщатвам,еслитипыиспользованынекорректно.JavaScriptрассматриваеттипытолькововремяисполненияпрограмм,идажетогдаонразрешаетделатьнеоченьосмысленныевещибезвсякихжалоб,например

x=true*"обезьяна"

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

НочастовашибессмысленныевычисленияпростопородятNaN(notanumber)илиundefined.Программарадостнопродолжит,будучиувереннойвтом,чтоонаделаетчто-тоосмысленное.Ошибкапроявитсебяпозже,когдатакоефиктивноезначениеужепройдётчерезнесколькофункций.Онаможетвообщеневызватьсообщениеобошибке,апростопривестикнеправильномурезультатувыполнения.Поискисточникатакихпроблем–сложнаязадача.

Процесспоискаошибок(bugs)впрограммахназываетсяотладкой(debugging).

JavaScriptможнозаставитьбытьпостроже,переведяеговстрогийрежим.Дляэтогонаверхуфайлаилителафункциипишется«usestrict».Пример:

functioncanYouSpotTheProblem(){

"usestrict";

Строгийрежим(strictmode)

ВыразительныйJavascript

150Поискиобработкаошибок

Page 151: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

for(counter=0;counter<10;counter++)

console.log("Всёбудетофигенно");

}

canYouSpotTheProblem();

//→ReferenceError:counterisnotdefined

Обычно,когдатызабываешьнаписатьvarпередпеременной,каквпримерепередcounter,JavaScriptпо-тихомусоздаётглобальнуюпеременнуюииспользуетеё.Встрогомрежимевыдаётсяошибка.Этооченьудобно.Однако,ошибканевыдаётся,когдаглобальнаяпеременнаяужесуществует–толькотогда,когдаприсваиваниесоздаётновуюпеременную.

Ещёодноизменение–привязкаthisсодержитundefinedвтехфункциях,которыевызывалинекакметоды.Когдамывызываемфункциюневстрогомрежиме,thisссылаетсянаобъектглобальнойобластивидимости.Поэтомуесливыслучайнонеправильновызоветеметодвстрогомрежиме,JavaScriptвыдастошибку,еслипопытаетсяпрочестьчто-тоизthis,анебудетрадостноработатьсглобальнымобъектом.

Кпримеру,рассмотримкод,вызывающийконструкторбезключевогословаnew,вслучаечегоthisнебудетссылатьсянасоздаваемыйобъект.

functionPerson(name){this.name=name;}

varferdinand=Person("Евлампий");//ой-вэй

console.log(name);

//→Евлампий

НекорректныйвызовPersonуспешнопроисходит,новозвращаетсякакundefinedисоздаётглобальнуюпеременнуюname.Встрогомрежимевсёпо-другому:

"usestrict";

functionPerson(name){this.name=name;}

//Опаньки,мыжзабыли'new'

varferdinand=Person("Евлампий");

//→TypeError:Cannotsetproperty'name'ofundefined

Намсразусообщаютобошибке.Оченьудобно.

Строгийрежимумеетещёкое-что.Онзапрещаетвызыватьфункциюс

ВыразительныйJavascript

151Поискиобработкаошибок

Page 152: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Корочеговоря,надпись«usestrict»передтекстомпрограммыредкопричиняетпроблемы,затопомогаетвамвидетьих.

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

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

ДляпримеравновьобратимсяктипуVector.

functionVector(x,y){

this.x=x;

this.y=y;

}

Vector.prototype.plus=function(other){

returnnewVector(this.x+other.x,this.y+other.y);

};

Мынапишемпрограмму,котораяпроверит,чтонашареализацияVectorработает,какнужно.Затемпослекаждогоизмененияреализациимыбудемзапускатьпроверочнуюпрограмму,чтобыубедиться,чтомыничегонесломали.Когдамыдобавимфункциональности(кпримеру,новыйметод)ктипуVector,мыдобавимпроверокэтойновойфункциональности.

functiontestVector(){

varp1=newVector(10,20);

varp2=newVector(-10,5);

varp3=p1.plus(p2);

if(p1.x!==10)return"облом:xproperty";

if(p1.y!==20)return"облом:yproperty";

Тестирование

ВыразительныйJavascript

152Поискиобработкаошибок

Page 153: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

if(p2.x!==-10)return"облом:negativexproperty";

if(p3.x!==0)return"облом:xfromplus";

if(p3.y!==25)return"облом:yfromplus";

return"всёпучком";

}

console.log(testVector());

//→всёпучком

Написаниетакихпроверокприводиткпоявлениюповторяющегосякода.Ксчастью,естьпрограммныепродукты,помогающиеписатьнаборыпроверокприпомощиспециальногоязыка,приспособленногоименнодлянаписанияпроверок.Ихназываютtestingframeworks.

Когдавызаметилипроблемувпрограмме,–онаведётсебянеправильноивыдаётошибки,-самоевремявыяснить,вчёмпроблема.

Иногдаэтоочевидно.Сообщениеобошибкенаводитваснаконкретнуюстрокупрограммы,иесливыпрочтётеописаниеошибкииэтустроку,вычастосможетенайтипроблему.

Ноневсегда.Иногдастрочка,приводящаякошибке,простооказываетсяпервымместом,гденекорректноезначение,полученноегде-тоещё,используетсянеправильно.Иногдавообщенетсообщенияобошибке–естьпростоневерныйрезультат.Есливыделалиупражненияизпредыдущихглав,вынавернякапопадаливтакиеситуации.

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

functionnumberToString(n,base){

varresult="",sign="";

if(n<0){

sign="-";

n=-n;

}

do{

result=String(n%base)+result;

Отладка(debugging)

ВыразительныйJavascript

153Поискиобработкаошибок

Page 154: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

n/=base;

}while(n>0);

returnsign+result;

}

console.log(numberToString(13,10));

//→1.5e-3231.3e-3221.3e-3211.3e-3201.3e-3191.3e-3181.3…

Дажеесливынашлипроблему–притворитесь,чтоещёненашли.Мызнаем,чтопрограммасбоит,инамнужноузнать,почему.

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

Размещениенесколькихвызововconsole.logвстратегическихместах–хорошийспособполучитьдополнительнуюинформациюотом,чтопрограммаделает.Внашемслучаенамнужно,чтобыnпринималазначения13,1,затем0.Давайтевыведемзначениявначалецикла:

13

1.3

0.13

0.013

1.5e-323

Н-да.Деление13на10выдаётнецелоечисло.Вместоn/=baseнамнужноn=Math.floor(n/base),тогдачислобудеткорректно«сдвинуто»вправо.

Кромеconsole.logможновоспользоватьсяотладчикомвбраузере.Современныебраузерыумеютставитьточкуостановкинавыбраннойстрочкекода.Этоприведёткприостановкевыполненияпрограммыкаждыйраз,когдабудетдостигнутавыбраннаястрочка,итогдавысможетепросмотретьсодержимоепеременных.Небудуподробнорасписыватьпроцесс,посколькууразныхбраузеровонорганизованпо-разному–поищитеввашембраузере“developertools”,инструментыразработчика.Ещёодинспособустановитьточкуостановки–включитьвкодинструкциюдляотладчика,состоящуюизключевогословаdebugger.Еслиинструменты

ВыразительныйJavascript

154Поискиобработкаошибок

Page 155: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

Простыепрограммы,илипрограммы,работающиеподвашимнадзором,могутпросто«сдаваться»втакоймомент.Выможетеизучитьпроблемуипопробоватьснова.«Настоящие»приложениянедолжныпросто«падать».Иногдаприходитсяприниматьнеправильныевходныеданныеикак-тоснимиработать.Вдругихслучаях,нужносообщитьпользователю,чточто-топошлонетак–ипотомужесдаваться.Влюбомслучаепрограммадолжначто-тосделатьвответнавозникновениепроблемы.

Допустим,увасестьфункцияpromptInteger,котораязапрашиваетцелоечислоивозвращаетего.Чтоонадолжнасделать,еслипользовательвведёт«апельсин»?

Одинизвариантов–вернутьособоезначение.Обычнодляэтихцелейиспользуютnullиundefined.

functionpromptNumber(question){

varresult=Number(prompt(question,""));

if(isNaN(result))returnnull;

elsereturnresult;

}

console.log(promptNumber("Сколькопальцеввидите?"));

Этонадёжнаястратегия.Теперьлюбойкод,вызывающийpromptNumber,долженпроверять,былоливозвращеночисло,иеслинет,как-товыйтиизситуации–спроситьснова,илизадатьзначениепо-умолчанию.Иливернутьспециальноезначениеужетому,ктоеговызвал,сообщаяонеудаче.

Вомногихтакихслучаях,когдаошибкивозникаютчастоивызывающий

Распространениеошибок

ВыразительныйJavascript

155Поискиобработкаошибок

Page 156: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

функциюкоддолженприниматьихвовнимание,совершеннодопустимовозвращатьспециальноезначениекакиндикаторошибки.Ноестьиминусы.Во-первых,что,еслифункцияитакможетвернутьлюбойтипзначения?Длянеёсложнонайтиспециальноезначение,котороебудетотличатьсяотдопустимогорезультата.

Втораяпроблема–работасоспециальнымизначениямиможетзамусоритькод.ЕслифункцияpromptNumberвызывается10раз,тонадо10разпроверить,невернулалионаnull.Еслиреакциянаnullзаключаетсяввозвратеnullнауровеньвыше,тогдатам,гдевызывалсяэтоткод,тоженужновстраиватьпроверкунаnull,итакдалее.

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

Код,встретившийпроблемувмоментвыполнения,можетподнять(иливыкинуть)исключение(raiseexception,throwexception),котороепредставляетизсебянекоезначение.Возвратисключениянапоминаетнекий«прокачанный»возвратизфункции–онвыпрыгиваетнетолькоизсамойфункции,ноиизвсехвызывавшихеёфункций,дотогоместа,скоторогоначалосьвыполнение.Этоназываетсяразвёртываниемстека(unwindingthestack).Можетбыть,выпомнитестекфункцийизглавы3…Исключениебыстропроматываетстеквниз,выкидываявсеконтекстывызовов,которыевстречает.

Еслибыисключениясразудоходилидосамогонизастека,пользыотнихбылобынемного.Онибыпростопредоставлялиинтересныйспособвзорватьпрограмму.Ихсилавтом,чтонаихпутивстекеможнопоставить«препятствия»,которыебудутловитьисключения,мчащиесяпостеку.Итогдасэтимможносделатьчто-тополезное,послечегопрограммапродолжаетвыполнятьсястойточки,гдебылопойманоисключение.

Пример:

Исключения

ВыразительныйJavascript

156Поискиобработкаошибок

Page 157: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functionpromptDirection(question){

varresult=prompt(question,"");

if(result.toLowerCase()=="left")return"L";

if(result.toLowerCase()=="right")return"R";

thrownewError("Недопустимоенаправление:"+result);

}

functionlook(){

if(promptDirection("Куда?")=="L")

return"дом";

else

return"двоихразъярённыхмедведей";

}

try{

console.log("Вывидите",look());

}catch(error){

console.log("Что-тонетак:"+error);

}

Ключевоесловоthrowиспользуетсядлявыбрасыванияисключения.Ловлейзанимаетсякусоккода,обёрнутыйвблокtry,закоторымследуетcatch.Когдакодвблокеtryвыкидываетисключение,выполняетсяблокcatch.Переменная,указаннаявскобках,будетпривязанакзначениюисключения.Послезавершениявыполненияблокаcatch,илижееслиблокtryвыполняетсябезпроблем,выполнениепереходитккоду,лежащемупослеинструкцииtry/catch.

ВданномслучаедлясозданияисключениямыиспользоваликонструкторError.Этостандартныйконструктор,создающийобъектсосвойствомmessage.ВсовременныхокруженияхJavaScriptэкземплярыэтогоконструкторатакжесобираютинформациюостекевызовов,которыйбылнакопленвмоментвыкидыванияисключения–такназываемоеотслеживаниестека(stacktrace).Этаинформациясохраняетсявсвойствеstack,иможетпомочьприразборепроблемы–онасообщает,вкакойфункциислучиласьпроблемаикакиедругиефункциипривеликданномувызову.

Обратитевнимание,чтофункцияlookполностьюигнорируетвозможностьвозникновенияпроблемвpromptDirection.Этопреимуществоисключений–код,обрабатывающийошибки,нужентольковтомместе,гдепроисходит

ВыразительныйJavascript

157Поискиобработкаошибок

Page 158: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ошибка,итам,гдеонаобрабатывается.Промежуточныефункциипростонеобращаютнаэтовнимания.

Ну,почти.

Представьтеследующуюситуацию:функцияwithContextжелаетудостовериться,чтововремяеёвыполненияпеременнаяверхнегоуровняcontextполучитспециальноезначениеконтекста.Послезавершениявыполнения,онавосстанавливаетеёстароезначение.

varcontext=null;

functionwithContext(newContext,body){

varoldContext=context;

context=newContext;

varresult=body();

context=oldContext;

returnresult;

}

Что,еслифункцияbodyвыброситисключение?ВтакомслучаевызовwithContextбудетвыброшенисключениемизстека,ипеременнойcontextникогданебудетвозвращенопервоначальноезначение.

Ноуинструкцииtryестьещёоднаособенность.Занейможетследоватьблокfinally,либовместоcatch,либовместесcatch.Блокfinallyозначает«выполнитькодвлюбомслучаепослевыполненияблокаtry”.Еслифункциинадочто-топодчистить,топодчищающийкоднужновключатьвблокfinally.

functionwithContext(newContext,body){

varoldContext=context;

context=newContext;

try{

returnbody();

}finally{

context=oldContext;

}

}

Подчищаемзаисключениями

ВыразительныйJavascript

158Поискиобработкаошибок

Page 159: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Заметьте,чтонамбольшененужносохранятьрезультатвызоваbodyвотдельнойпеременной,чтобывернутьего.Дажееслимывозвращаемсяизблокаtry,блокfinallyвсёравнобудетвыполнен.Теперьмыможембезопасносделатьтак:

try{

withContext(5,function(){

if(context<10)

thrownewError("Контекстслишкоммал!");

});

}catch(e){

console.log("Игнорируем:"+e);

}

//→Игнорируем:Error:Контекстслишкоммал!

console.log(context);

//→null

Несмотрянато,чтовызываемаяизwithContextфункция«сломалась»,сампосебеwithContextпо-прежнемуподчищаетзначениепеременнойcontext.

Когдаисключениедоходитдонизастекаиегониктонепоймал—егообрабатываетокружение.Какименно–зависитотконкретногоокружения.Вбраузерахописаниеошибкивыдаётсявконсоль(онаобычнодоступнавменю«Инструменты»или«Разработка»).

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

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

Недопустимоеиспользованиеязыка–ссылкинанесуществующую

Выборочныйотловисключений

ВыразительныйJavascript

159Поискиобработкаошибок

Page 160: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Привходевблокcatchмызнаемтолько,чточто-товнутриблокаtryпривелокисключению.Мынезнаем,чтоименно,икакоеисключениепроизошло.

JavaScript(чтоявляетсявопиющимупущением)непредоставляетнепосредственнойподдержкивыборочногоотловаисключений:либоловимвсе,либоникакие.Из-заэтоголюдичастопредполагают,чтослучившеесяисключение–именното,радикоторогоиписалсяблокcatch.

Номожетбытьипо-другому.Нарушениепроизошлогде-тоещё,иливпрограммувкраласьошибка.Вотпример,гдемыпробуемвызыватьpromptDirectionдотехпор,поканеполучимдопустимыйответ:

for(;;){

try{

vardir=promtDirection("Куда?");//←опечатка!

console.log("Вашвыбор",dir);

break;

}catch(e){

console.log("Недопустимоенаправление.Попробуйтеещёраз.");

}

}

Конструкцияfor(;;)–способустроитьбесконечныйцикл.Мывываливаемсяизнего,толькокогдаполучаемдопустимоенаправление.НомынеправильнонаписалиназваниеpromptDirection,чтоприводиткошибке“undefinedvariable”.Атаккакблокcatchигнорируетзначениеисключенияe,предполагая,чтоонразбираетсясдругойпроблемой,онсчитает,чтовыброшенноеисключениеявляетсярезультатомнеправильныхвходныхданных.Этоприводиткбесконечномуциклуискрываетполезноесообщениеобошибкенасчётнеправильногоименипеременной.

Какправило,нестоиттакловитьисключения,еслитолькоуваснетцелиперенаправитьихкуда-либо–кпримеру,посети,чтобысообщитьдругойсистемеопадениинашейпрограммы.Идажетогдавнимательносмотрите,небудетлискрытаважнаяинформация.

ВыразительныйJavascript

160Поискиобработкаошибок

Page 161: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Значит,намнадопойматьопределённоеисключение.Мыможемвблокеcatchпроверять,являетсялислучившеесяисключениеинтересующимнасисключением,авпротивномслучаезанововыбрасыватьего.Нокакнамраспознатьисключение?

Конечно,мымоглибысравнитьсвойствоmessageссообщениемобошибке,которуюмыждём.Ноэтоненадёжныйспособписатькод–использоватьинформацию,предназначающуюсядлячеловека(сообщение),чтобыпринятьпрограммноерешение.Кактолькокто-нибудьпоменяетилипереведётэтосообщение,кодперестанетработать.

Давайтелучшеопределимновыйтипошибкиииспользуемinstanceofдляегораспознавания.

functionInputError(message){

this.message=message;

this.stack=(newError()).stack;

}

InputError.prototype=Object.create(Error.prototype);

InputError.prototype.name="InputError";

ПрототипнаследуетсяотError.prototype,поэтомуinstanceofErrorтожебудетвыполнятьсядляобъектовтипаInputError.Иемуназначеносвойствоname,какидругимстандартнымтипамошибок(Error,SyntaxError,ReferenceError,ит.п.)

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

ТеперьpromptDirectionможетсотворитьтакуюошибку.

functionpromptDirection(question){

varresult=prompt(question,"");

if(result.toLowerCase()=="left")return"L";

if(result.toLowerCase()=="right")return"R";

thrownewInputError("Invaliddirection:"+result);

}

Авциклееёбудетловитьсподручнее.

for(;;){

ВыразительныйJavascript

161Поискиобработкаошибок

Page 162: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

try{

vardir=promptDirection("Куда?");

console.log("Вашвыбор",dir);

break;

}catch(e){

if(einstanceofInputError)

console.log("Недопустимоенаправление.Попробуйтеещёраз.");

else

throwe;

}

}

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

Утверждения–инструментдляпростойпроверкиошибок.Рассмотримвспомогательнуюфункциюassert:

functionAssertionFailed(message){

this.message=message;

}

AssertionFailed.prototype=Object.create(Error.prototype);

functionassert(test,message){

if(!test)

thrownewAssertionFailed(message);

}

functionlastElement(array){

assert(array.length>0,"пустоймассиввlastElement");

returnarray[array.length-1];

}

Это–компактныйспособужесточениятребованийкзначениям,которыйвыбрасываетисключениевслучае,когдазаданноеусловиеневыполняется.Кпримеру,функцияlastElement,добывающаяпоследнийэлементмассива,вернулабыundefinedдляпустыхмассивов,еслибмынеиспользовалиassertion.Извлечениепоследнегоэлементапустогомассиванеимеетсмысла,иэтоявнобылобыошибкойпрограммиста.

Утверждения(Assertions)

ВыразительныйJavascript

162Поискиобработкаошибок

Page 163: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Утверждения–способубедитьсявтом,чтоошибкипровоцируютпрерываниепрограммывтомместе,гдеонисовершены,анепростовыдаютстранныевеличины,которыепередаютсяпосистемеивызываютпроблемывкаких-тодругих,несвязанныхсэтим,местах.

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

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

Выбросисключенияприводиткразматываниюстекадотехпор,поканебудетвстреченблокtry/catchилипокамынедойдёмдоднастека.Значениеисключениябудетпередановблокcatch,которыйсможетудостоверитьсявтом,чтоэтоисключениедействительното,котороеонждёт,иобработатьего.Дляработыснепредсказуемымисобытиямивпотокепрограммыможноиспользоватьблокиfinally,чтобыопределённыечастикодабыливыполненывлюбомслучае.

Допустим,увасестьфункцияprimitiveMultiply,котораяв50%случаевперемножает2числа,авостальныхслучаяхвыбрасываетисключениетипаMultiplicatorUnitFailure.Напишитефункцию,обёртывающуюэту,ипростовызывающуюеёдотехпор,поканебудетполученуспешныйрезультат.

Убедитесь,чтовыобрабатываететольконужныевамисключения.

Итог

Упражнения

Повтор

ВыразительныйJavascript

163Поискиобработкаошибок

Page 164: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functionMultiplicatorUnitFailure(){}

functionprimitiveMultiply(a,b){

if(Math.random()<0.5)

returna*b;

else

thrownewMultiplicatorUnitFailure();

}

functionreliableMultiply(a,b){

//Вашкод

}

console.log(reliableMultiply(8,8));

//→64

Рассмотримтакой,достаточнонадуманный,объект:

varbox={

locked:true,

unlock:function(){this.locked=false;},

lock:function(){this.locked=true;},

_content:[],

getcontent(){

if(this.locked)thrownewError("Заперто!");

returnthis._content;

}

};

Этокоробочкасзамком.Внутрилежитмассив,нодонегоможнодобратьсятолько,когдакоробканезаперта.Напрямуюобращатьсяксвойству_contentнельзя.

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

functionwithBoxUnlocked(body){

//Вашкод

Запертаякоробка

ВыразительныйJavascript

164Поискиобработкаошибок

Page 165: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

}

withBoxUnlocked(function(){

box.content.push("золотишко");

});

try{

withBoxUnlocked(function(){

thrownewError("Пиратынагоризонте!Отмена!");

});

}catch(e){

console.log("Произошлаошибка:",e);

}

console.log(box.locked);

//→true

Вкачествепризовойигрыубедитесь,чтопривызовеwithBoxUnlocked,когдакоробканезаперта,коробкаостаётсянезапертой.

ВыразительныйJavascript

165Поискиобработкаошибок

Page 166: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Некоторыелюди,столкнувшисьспроблемой,думают:«О,аиспользую-каярегулярныевыражения».Теперьунихестьдвепроблемы.ДжеймиЗавински

Юан-Масказал:«Требуетсябольшаясила,чтобырезатьдеревопоперёкструктурыдревесины.Требуетсямногокода,чтобыпрограммироватьпоперёкструктурыпроблемы.МастерЮан-Ма,«Книгапрограммирования»

Инструментыитехникипрограммированиявыживаютираспространяютсяхаотично-эволюционнымспособом.Иногдавыживаютнекрасивыеигениальные,апростотакие,которыедостаточнохорошоработаютвсвоейобласти–кпримеру,еслиихинтегрируютвдругуюуспешнуютехнологию.

Вэтойглавемыобсудимтакойинструмент–регулярныевыражения.Этоспособописыватьшаблонывстроковыхданных.Онисоздаютнебольшойотдельныйязык,которыйвходитвJavaScriptивомножестводругихязыковиинструментов.

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

Регулярка–типобъекта.Еёможносоздать,вызвавконструкторRegExp,илинаписавнужныйшаблон,окружённыйслешами.

varre1=newRegExp("abc");

varre2=/abc/;

Обаэтихрегулярныхвыраженияпредставляютодиншаблон:символ“a”,закоторымследуетсимвол“b”,закоторымследуетсимвол“c”.

Регулярныевыражения

Создаёмрегулярноевыражение

ВыразительныйJavascript

166Регулярныевыражения

Page 167: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Втораязапись,гдешаблоннаходитсямеждуслешами,обрабатываетобратныеслешипо-другому.Во-первых,таккакшаблонзаканчиваетсяпрямымслешем,тонужноставитьобратныйслешпередпрямымслешем,которыймыхотимвключитьвнашшаблон.Крометого,обратныеслеши,неявляющиесячастьюспециальныхсимволовтипа\n,будутсохранены(анепроигнорированы,каквстроках),иизменятсмыслшаблона.Унекоторыхсимволов,таких,какзнаквопросаилиплюс,естьособоезначениеврегулярках,иесливамнужнонайтитакойсимвол,еготакженадопредварятьобратнымслешем.

vareighteenPlus=/eighteen\+/;

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

Урегулярокестьнесколькометодов.Простейший–test.Еслипередатьемустроку,онвернётбулевскоезначение,сообщая,содержитлистрокавхождениезаданногошаблона.

console.log(/abc/.test("abcde"));

//→true

console.log(/abc/.test("abxde"));

//→false

Регулярка,состоящаятолькоизнеспециальныхсимволов,простопредставляетсобойпоследовательностьэтихсимволов.Еслиabcестьгде-товстроке,которуюмыпроверяем(нетольковначале),testвернётtrue.

Проверяемнасовпадения

ВыразительныйJavascript

167Регулярныевыражения

Page 168: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

Обавыражениянаходятсявстрочках,содержащихцифру.

console.log(/[0123456789]/.test("in1992"));

//→true

console.log(/[0-9]/.test("in1992"));

//→true

Вквадратныхскобкахтиремеждудвумясимволамииспользуетсядлязаданиядиапазонасимволов,гдепоследовательностьзадаётсякодировкойUnicode.Символыот0до9находятсятампростоподряд(кодыс48до57),поэтому[0-9]захватываетихвсеисовпадаетслюбойцифрой.

Унесколькихгруппсимволовестьсвоивстроенныесокращения.

\dЛюбаяцифра\wАлфавитно-цифровойсимвол\sПробельныйсимвол(пробел,табуляция,переводстроки,ит.п.)\Dнецифра\Wнеалфавитно-цифровойсимвол\Sнепробельныйсимвол.любойсимвол,кромепереводастроки

Такимобразомможнозадатьформатдатыивременивроде30-01-200315:20следующимвыражением:

vardateTime=/\d\d-\d\d-\d\d\d\d\d\d:\d\d/;

console.log(dateTime.test("30-01-200315:20"));

//→true

console.log(dateTime.test("30-jan-200315:20"));

//→false

Выглядитужасно,нетакли?Слишкоммногообратныхслешей,которыезатрудняютпониманиешаблона.Позжемыслегкаулучшимего.

Ищемнаборсимволов

ВыразительныйJavascript

168Регулярныевыражения

Page 169: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Обратныеслешиможноиспользоватьивквадратныхскобках.Например,[\d.]означаетлюбуюцифруилиточку.Заметьте,чтоточкавнутриквадратныхскобоктеряетсвоёособоезначениеипревращаетсяпростовточку.Тожекасаетсяидругихспециальныхсимволов,типа+.

Инвертироватьнаборсимволов–тоесть,сказать,чтовамнадонайтилюбойсимвол,крометех,чтоестьвнаборе–можно,поставивзнак^сразупослеоткрывающейквадратнойскобки.

varnotBinary=/[^01]/;

console.log(notBinary.test("1100100010100110"));

//→false

console.log(notBinary.test("1100100010200110"));

//→true

Мызнаем,какнайтиоднуцифру.Аеслинамнадонайтичислоцеликом–последовательностьизоднойилиболеецифр?

Еслипоставитьпослечего-либоврегуляркезнак+,этобудетозначать,чтоэтотэлементможетбытьповторёнболееодногораза./\d+/означаетоднуилинесколькоцифр.

console.log(/'\d+'/.test("'123'"));

//→true

console.log(/'\d+'/.test("''"));

//→false

console.log(/'\d*'/.test("'123'"));

//→true

console.log(/'\d*'/.test("''"));

//→true

Узвёздочки*значениепочтитакоеже,ноонаразрешаетшаблонуприсутствоватьнольраз.Еслипослечего-тостоитзвёздочка,тоононикогданепрепятствуетнахождениюшаблонавстроке–онопростонаходитсятамнольраз.

Знаквопросаделаетчастьшаблонанеобязательной,тоестьонаможет

Повторяемчастишаблона

ВыразительныйJavascript

169Регулярныевыражения

Page 170: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

varneighbor=/neighbou?r/;

console.log(neighbor.test("neighbour"));

//→true

console.log(neighbor.test("neighbor"));

//→true

Чтобызадатьточноеколичествораз,котороешаблондолженвстретиться,используютсяфигурныескобки.{4}послеэлементаозначает,чтоондолженвстретитьсявстроке4раза.Такжеможнозадатьпромежуток:{2,4}означает,чтоэлементдолженвстретитьсянеменее2инеболее4раз.

Ещёоднаверсияформатадатыивремени,гдеразрешеныдни,месяцыичасыизоднойилидвухцифр.Иещёоначутьболеечитаема.

vardateTime=/\d{1,2}-\d{1,2}-\d{4}\d{1,2}:\d{2}/;

console.log(dateTime.test("30-1-20038:45"));

//→true

Можноиспользоватьпромежуткисоткрытымконцом,опускаяодноизчисел.{,5}означает,чтошаблонможетвстретитьсяотнулядопятираз,а{5,}–отпятииболее.

Чтобыиспользоватьоператоры*или+нанесколькихэлементахсразу,можноиспользоватькруглыескобки.Частьрегулярки,заключённаявскобки,считаетсяоднимэлементомсточкизренияоператоров.

varcartoonCrying=/boo+(hoo+)+/i;

console.log(cartoonCrying.test("Boohoooohoohooo"));

//→true

Первыйивторойплюсыотносятсятолькоковторымбуквамовсловахbooиhoo.Третий+относитсякцелойгруппе(hoo+),находяоднуилинесколькотакихпоследовательностей.

Группировкаподвыражений

ВыразительныйJavascript

170Регулярныевыражения

Page 171: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Букваiвконцевыраженияделаетрегуляркунечувствительнойкрегиструсимолов–так,чтоBсовпадаетсb.

Методtest–самыйпростойметодпроверкирегулярок.Онтолькосообщает,былолинайденосовпадение,илинет.Урегулярокестьещёметодexec,которыйвернётnull,еслиничегонебылонайдено,авпротивномслучаевернётобъектсинформациейосовпадении.

varmatch=/\d+/.exec("onetwo100");

console.log(match);

//→["100"]

console.log(match.index);

//→8

Увозвращаемогоexecобъектаестьсвойствоindex,гдесодержитсяномерсимвола,скоторогослучилосьсовпадение.Авообщеобъектвыглядиткакмассивстрок,гдепервыйэлемент–строка,которуюпроверялинасовпадение.Внашемпримереэтобудетпоследовательностьцифр,которуюмыискали.

Устрокестьметодmatch,работающийпримернотакже.

console.log("onetwo100".match(/\d+/));

//→["100"]

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

varquotedText=/'([^']*)'/;

console.log(quotedText.exec("shesaid'hello'"));

//→["'hello'","hello"]

Когдагруппаненайденавообще(например,еслизанейстоитзнаквопроса),

Совпаденияигруппы

ВыразительныйJavascript

171Регулярныевыражения

Page 172: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

console.log(/bad(ly)?/.exec("bad"));

//→["bad",undefined]

console.log(/(\d)+/.exec("123"));

//→["123","3"]

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

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

ВJavaScriptестьстандартныйтипобъектадлядат–аточнее,моментоввовремени.ОнназываетсяDate.Еслипростосоздатьобъектдатычерезnew,выполучитетекущиедатуиремя.

console.log(newDate());

//→SunNov09201400:07:57GMT+0300(CET)

Такжеможносоздатьобъект,содержащийзаданноевремя

console.log(newDate(2015,9,21));

//→WedOct21201500:00:00GMT+0300(CET)

console.log(newDate(2009,11,9,12,59,59,999));

//→WedDec09200912:59:59GMT+0300(CET)

JavaScriptиспользуетсоглашение,вкоторомномерамесяцевначинаютсяснуля,аномерадней–сединицы.Этоглупоинелепо.Поберегитесь.

Последниечетыреаргумента(часы,минуты,секундыимиллисекунды)необязательны,ивслучаеотсутствияприравниваютсякнулю.

Меткивременихранятсякакколичествомиллисекунд,прошедшихсначала

Типдаты

ВыразительныйJavascript

172Регулярныевыражения

Page 173: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

1970года.Длявременидо1970годаиспользуютсяотрицательныечисла(этосвязаноссоглашениемпоUnixtime,котороебылосозданопримерновтовремя).МетодgetTimeобъектадатывозвращаетэточисло.Оно,естественно,большое.

console.log(newDate(2013,11,19).getTime());

//→1387407600000

console.log(newDate(1387407600000));

//→ThuDec19201300:00:00GMT+0100(CET)

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

УобъектаDateдляизвлеченияегокомпонентовестьметодыgetFullYear,getMonth,getDate,getHours,getMinutes,иgetSeconds.ЕстьтакжеметодgetYear,возвращающийдовольнобесполезныйдвузначныйкод,типа93или14.

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

functionfindDate(string){

vardateTime=/(\d{1,2})-(\d{1,2})-(\d{4})/;

varmatch=dateTime.exec(string);

returnnewDate(Number(match[3]),

Number(match[2])-1,

Number(match[1]));

}

console.log(findDate("30-1-2003"));

//→ThuJan30200300:00:00GMT+0100(CET)

Ксожалению,findDateтакжерадостноизвлечётбессмысленнуюдату00-1-3000изстроки«100-1-30000».Совпадениеможетслучитьсявлюбомместестроки,такчтовданномслучаеонпростоначнётсовторогосимволаизакончитнапредпоследнем.

Границысловаистроки

ВыразительныйJavascript

173Регулярныевыражения

Page 174: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Еслинамнадопринудитьсовпадениевзятьвсюстрокуцеликом,мыиспользуемметки^и$.^совпадаетсначаломстроки,а$сконцом.Поэтому/^\d+$/совпадаетсострокой,состоящейтолькоизоднойилинесколькихцифр,/^!/совпадаетсосторокой,начинающейсясвосклицательногознака,а/x^/несовпадаетнискакойстрочкой(передначаломстрокинеможетбытьx).

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

console.log(/cat/.test("concatenate"));

//→true

console.log(/\bcat\b/.test("concatenate"));

//→false

Отметим,чтометкаграницынепредставляетизсебясимвол.Этопростоограничение,обозначающее,чтосовпадениепроисходиттолькоесливыполняетсяопределённоеусловие.

Допустим,надовыяснить,содержитлитекстнепростономер,аномер,закоторымследуетpig,cow,илиchickenвединственномилимножественномчисле.

Можнобылобынаписатьтрирегуляркиипроверитьихпоочереди,ноестьспособлучше.Символ|обозначаетвыбормеждушаблонамислеваисправаотнего.Иможносказатьследующее:

varanimalCount=/\b\d+(pig|cow|chicken)s?\b/;

console.log(animalCount.test("15pigs"));

//→true

console.log(animalCount.test("15pigchickens"));

//→false

Шаблонысвыбором

ВыразительныйJavascript

174Регулярныевыражения

Page 175: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

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

Значит,проверкасовпадениянашейрегуляркивстроке«the3pigs»припрохождениипоблок-схемевыглядиттак:

—напозиции4естьграницаслова,ипроходимпервыйпрямоугольник—начинаяс4позициинаходимцифру,ипроходимвторойпрямоугольник—напозиции5одинпутьзамыкаетсяназадпередвторымпрямоугольником,авторойпроходитдалеекпрямоугольникуспробелом.Унаспробел,анецифра,имывыбираемвторойпуть.—теперьмынапозиции6,начало“pigs”,инатройномразветвлениипутей.Встрокенет“cow”или“chicken”,затоесть“pig”,поэтомумывыбираемэтотпуть.—напозиции9послетройногоразветвления,одинпутьобходит“s”инаправляетсякпоследнемупрямоугольникусграницейслова,авторойпроходитчерез“s”.Унасесть“s”,поэтомумыидёмтуда.—напозиции10мывконцестроки,исовпастьможеттолькограницаслова.Конецстрокисчитаетсяграницей,имыпроходимчерезпоследнийпрямоугольник.Ивотмыуспешнонашлинашшаблон.

Механизмпоиска

ВыразительныйJavascript

175Регулярныевыражения

Page 176: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Регулярка/\b([01]+b|\d+|[\da-f]h)\b/совпадаетлибосдвоичнымчислом,закоторымследуетb,либосдесятичнымчисломбезсуффикса,либошестнадцатеричным(цифрыот0до9илисимволыотaдоh),закоторымидётh.Соответствующаядиаграмма:

Впоискахсовпаденияможетслучиться,чтоалгоритмпошёлповерхнемупути(двоичноечисло),дажеесливстрокенеттакогочисла.Еслитаместьстрока“103”,кпримеру,понятно,чтотолькодостигнувцифры3алгоритмпоймёт,чтооннанеправильномпути.Вообщестрокасовпадаетсрегуляркой,простоневэтойветке.

Откаты

ВыразительныйJavascript

176Регулярныевыражения

Page 177: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Тогдаалгоритмсовершаетоткат.Наразвилкеонзапоминаеттекущееположение(внашемслучае,этоначалостроки,сразупослеграницыслова),чтобыможнобыловернутьсяназадипопробоватьдругойпуть,есливыбранныйнесрабатывает.Длястроки“103”послевстречистройкойонвернётсяипопытаетсяпройтипутьдлядесятичныхчисел.Этосработает,поэтомусовпадениебудетнайдено.

Алгоритмостанавливается,кактольконайдётполноесовпадение.Этозначит,чтодажееслинескольковариантовмогутподойти,используетсятолькоодинизних(втомпорядке,вкакомонипоявляютсяврегулярке).

Откатыслучаютсяприиспользованииоператоровповторения,таких,как+и.Есливыищете/^.x/встроке«abcxe»,частьрегулярки.*попробуетпоглотитьвсюстрочку.Алгоритмзатемсообразит,чтоемунуженещёи“x”.Таккакникакого“x”послеконцастрокинет,алгоритмпопробуетпоискатьсовпадение,откатившисьнаодинсимвол.Послеabcxтоженетx,тогдаонсноваоткатывается,ужекподстрокеabc.Ипослестрочкионнаходитxидокладываетобуспешномсовпадении,напозицияхс0по4.

Можнонаписатьрегулярку,котораяприведёткомножественнымоткатам.Такаяпроблемавозникает,когдашаблонможетсовпастьсвходнымиданнымимножествомразныхспособов.Например,еслимыошибёмсяпринаписаниирегуляркидлядвоичныхчисел,мыможемслучайнонаписатьчто-товроде/([01]+)+b/.

Еслиалгоритмбудетискатьтакойшаблонвдлиннойстрокеизнолейиединиц,несодержащейвконце“b”,онсначалапройдётповнутреннейпетле,покаунегонекончатсяцифры.Тогдаонзаметит,чтовконценет“b”,сделаетоткатнаоднупозицию,пройдётповнешнейпетле,опятьсдастся,попытаетсяоткатитьсянаещёоднупозициюповнутреннейпетле…Ибудетдальшеискатьтакимобразом,задействуяобепетли.Тоесть,количествоработыскаждымсимволомстрокибудетудваиваться.Дажедлянесколькихдесятковсимволовпоисксовпадениязаймёточеньдолгоевремя.

Методreplace

ВыразительныйJavascript

177Регулярныевыражения

Page 178: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Устрокестьметодreplace,которыйможетзаменятьчастьстрокидругойстрокой.

console.log("папа".replace("п","м"));

//→мапа

Первыйаргументможетбытьирегулярной,вкакомслучаезаменяетсяпервоевхождениерегуляркивстроке.Когдакрегуляркедобавляетсяопция“g”(global,всеобщий),заменяютсявсевхождения,анетолькопервое

console.log("Borobudur".replace(/[ou]/,"a"));

//→Barobudur

console.log("Borobudur".replace(/[ou]/g,"a"));

//→Barabadar

Имелобысмыслпередаватьопцию«заменитьвсе»черезотдельныйаргумент,иличерезотдельныйметодтипаreplaceAll.Ноксожалению,опцияпередаётсячерезсамурегулярку.

Всясиларегулярокраскрывается,когдамыиспользуемссылкинанайденныевстрокегруппы,заданныеврегулярке.Например,унасестьстрока,содержащаяименалюдей,одноимянастрочку,вформате«Фамилия,Имя».Еслинамнадопоменятьихместамииубратьзапятую,чтобыполучилось«ИмяФамилия»,мыпишемследующее:

console.log(

"Hopper,Grace\nMcCarthy,John\nRitchie,Dennis"

.replace(/([\w]+),([\w]+)/g,"$2$1"));

//→GraceHopper

//JohnMcCarthy

//DennisRitchie

$1и$2встрочкеназаменуссылаютсянагруппысимволов,заключённыевскобки.$1заменяетсятекстом,которыйсовпалспервойгруппой,$2–совторойгруппой,итакдалее,до$9.Всёсовпадениецеликомсодержитсявпеременной$&.

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

ВыразительныйJavascript

178Регулярныевыражения

Page 179: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Простойпример:

vars="theciaandfbi";

console.log(s.replace(/\b(fbi|cia)\b/g,function(str){

returnstr.toUpperCase();

}));

//→theCIAandFBI

Авотболееинтересный:

varstock="1lemon,2cabbages,and101eggs";

functionminusOne(match,amount,unit){

amount=Number(amount)-1;

if(amount==1)//осталсятолькоодин,удаляем's'вконце

unit=unit.slice(0,unit.length-1);

elseif(amount==0)

amount="no";

returnamount+""+unit;

}

console.log(stock.replace(/(\d+)(\w+)/g,minusOne));

//→nolemon,1cabbage,and100eggs

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

Группа(\d+)попадаетваргументamount,а(\w+)–вunit.Функцияпреобразовываетamountвчисло–иэтовсегдасрабатывает,потомучтонашшаблонкакраз\d+.Изатемвноситизменениявслово,наслучайеслиосталсявсего1предмет.

Несложноприпомощиreplaceнаписатьфункцию,убирающуювсекомментарииизкодаJavaScript.Вотперваяпопытка:

functionstripComments(code){

returncode.replace(/\/\/.*|\/\*[^]*\*\//g,"");

}

Жадность

ВыразительныйJavascript

179Регулярныевыражения

Page 180: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

console.log(stripComments("1+/*2*/3"));

//→1+3

console.log(stripComments("x=10;//ten!"));

//→x=10;

console.log(stripComments("1/*a*/+/*b*/1"));

//→11

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

Новыводпредыдущегопримеранеправильный.Почему?

Частьсначалапопытаетсязахватитьстолькосимволов,сколькоможет.Еслииз-заэтогоследующаячастьрегуляркиненайдётсебесовпадения,произойдётоткатнаодинсимволипопробуетснова.Впримере,алгоритмпытаетсязахватитьвсюстроку,изатемоткатывается.Откатившисьна4символаназад,оннайдётвстрочке/—аэтонето,чегомыдобивались.Мы-тохотелизахватитьтолькоодинкомментарий,анепройтидоконцастрокиинайтипоследнийкомментарий.

Из-заэтогомыговорим,чтооператорыповторения(+,,?,and{})жадные,тоестьонисначалазахватывают,сколькомогут,апотомидутназад.Есливыпоместитевопроспослетакогооператора(+?,?,??,{}?),онипревратятсявнежадных,иначнутнаходитьсамыемаленькиеизвозможныхвхождений.

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

functionstripComments(code){

returncode.replace(/\/\/.*|\/\*[^]*?\*\//g,"");

}

console.log(stripComments("1/*a*/+/*b*/1"));

//→1+1

ВыразительныйJavascript

180Регулярныевыражения

Page 181: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Множествоошибоквозникаетприиспользованиижадныхоператороввместонежадных.Прииспользованииоператораповторасначалавсегдарассматривайтевариантнежадногооператора.

Внекоторыхслучаяхточныйшаблоннеизвестенвовремянаписаниякода.Например,вамнадобудетискатьимяпользователявтексте,изаключатьеговподчёркивания.Таккаквыузнаетеимятолькопослезапускапрограммы,вынеможетеиспользоватьзаписьсослешами.

НовыможетепостроитьстрокуииспользоватьконструкторRegExp.Вотпример:

varname="гарри";

vartext="АуГарриналбушрам.";

varregexp=newRegExp("\\b("+name+")\\b","gi");

console.log(text.replace(regexp,"_$1_"));

//→Ау_Гарри_налбушрам.

Присозданииграницсловаприходитсяиспользоватьдвойныеслеши,потомучтомыпишемихвнормальнойстроке,аневрегуляркеспрямымислешами.ВторойаргументдляRegExpсодержитопциидлярегулярок–внашемслучае“gi”,т.е.глобальныйирегистро-независимый.

Ночто,еслиимябудет«dea+hl[]rd»(еслинашпользователь–кульхацкер)?Врезультатемыполучимбессмысленнуюрегулярку,котораяненайдётвстрокесовпадений.

Мыможемдобавитьобратныхслешейпередлюбымсимволом,которыйнамненравится.Мынеможемдобавлятьобратныеслешипередбуквами,потомучто\bили\n–этоспецсимволы.Нодобавлятьслешипередлюбыминеалфавитно-цифровымисимволамиможнобезпроблем.

varname="dea+hl[]rd";

vartext="Этотdea+hl[]rdвсехдостал.";

varescaped=name.replace(/[^\w\s]/g,"\\$&amp;");

ДинамическоесозданиеобъектовRegExp

ВыразительныйJavascript

181Регулярныевыражения

Page 182: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

varregexp=newRegExp("\\b("+escaped+")\\b","gi");

console.log(text.replace(regexp,"_$1_"));

//→Этот_dea+hl[]rd_всехдостал.

МетодindexOfнельзяиспользоватьсрегулярками.Затоестьметодsearch,которыйкакразожидаетрегулярку.КакиindexOf,онвозвращаетиндекспервоговхождения,или-1,еслиегонеслучилось.

console.log("word".search(/\S/));

//→2

console.log("".search(/\S/));

//→-1

Ксожалению,никакнельзязадать,чтобыметодискалсовпадение,начинаясконкретногосмещения(какэтоможносделатьсindexOf).Этобылобыполезно.

Методexecтоженедаётудобногоспособаначатьпоисксзаданнойпозициивстроке.Нонеудобныйспособдаёт.

Уобъектарегулярокестьсвойства.Одноизних–source,содержащеестроку.Ещёодно–lastIndex,контролирующее,внекоторыхусловиях,гденачнётсяследующийпоисквхождений.

Этиусловиявключаютнеобходимостьприсутствияглобальнойопцииg,ито,чтопоискдолженидтисприменениемметодаexec.Болееразумнымрешениембылобыпростодопуститьдополнительныйаргументдляпередачивexec,норазумность–неосновополагающаячертавинтерфейсерегулярокJavaScript.

varpattern=/y/g;

pattern.lastIndex=3;

varmatch=pattern.exec("xyzzy");

console.log(match.index);

Методsearch

СвойствоlastIndex

ВыразительныйJavascript

182Регулярныевыражения

Page 183: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

//→4

console.log(pattern.lastIndex);

//→5

Еслипоискбылуспешным,вызовexecобновляетсвойствоlastIndex,чтобоноуказывалонапозициюпосленайденноговхождения.Еслиуспеханебыло,lastIndexустанавливаетсявноль–какиlastIndexутолькочтосозданногообъекта.

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

vardigit=/\d/g;

console.log(digit.exec("hereitis:1"));

//→["1"]

console.log(digit.exec("andnow:1"));

//→null

Ещёодининтересныйэффектопцииgвтом,чтоонаменяетработуметодаmatch.Когдаонвызываетсясэтойопцией,вместовозвратамассива,похожегонарезультатработыexec,оннаходитвсевхожденияшаблонавстрокеивозвращаетмассивизнайденныхподстрок.

console.log("Банан".match(/ан/g));

//→["ан","ан"]

Такчтопоосторожнеесглобальнымипеременными-регулярками.Вслучаях,когдаонинеобходимы–вызовыreplaceилиместа,гдевыспециальноиспользуетеlastIndex–пожалуйивсеслучаи,вкоторыхихследуетприменять.

Типичнаязадача–пройтиповсемвхождениямшаблонавстрокутак,чтобыиметьдоступкобъектуmatchвтелецикла,используяlastIndexиexec.

Циклыповхождениям

ВыразительныйJavascript

183Регулярныевыражения

Page 184: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

varinput="Строчкас3числамивней...42и88.";

varnumber=/\b(\d+)\b/g;

varmatch;

while(match=number.exec(input))

console.log("Нашёл",match[1],"на",match.index);

//→Нашёл3на14

//Нашёл42на33

//Нашёл88на40

Используетсятотфакт,чтозначениемприсвоенияявляетсяприсваиваемоезначение.Используяконструкциюmatch=re.exec(input)вкачествеусловиявциклеwhile,мыпроизводимпоисквначалекаждойитерации,сохраняемрезультатвпеременной,изаканчиваемцикл,когдавсесовпадениянайдены.

Взаключениеглавырассмотримзадачусиспользованиемрегулярок.Представьте,чтомыпишемпрограмму,собирающуюсведенияонашихврагахчерезинтернетвавтоматическомрежиме.(Всюпрограммуписатьнебудем,толькотучасть,котораячитаетфайлснастройками.Извините.)Файлвыглядиттак:

searchengine=http://www.google.com/search?q=$1

spitefulness=9.7

;передкомментариямиставитсяточкасзапятой

;каждаясекцияотноситсякотдельномуврагу

[larry]

fullname=LarryDoe

type=бычараиздетсада

website=http://www.geocities.com/CapeCanaveral/11451

[gargamel]

fullname=Gargamel

type=злойволшебник

outputdir=/home/marijn/enemies/gargamel

Точныйформатфайла(которыйдовольноширокоиспользуется,иобычноназываетсяINI),следующий:

—пустыестрокиистроки,начинающиесясточкисзапятой,игнорируются—

РазборINIфайлы

ВыразительныйJavascript

184Регулярныевыражения

Page 185: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

строки,заключённыевквадратныескобки,начинаютновуюсекцию—строки,содержащиеалфавитно-цифровойидентификатор,закоторымследует=,добавляютнастройкувданнойсекции

Всёостальное–неверныеданные.

Нашазадача–преобразоватьтакуюстрокувмассивобъектов,каждыйсосвойствомnameимассивомнастроек.Длякаждойсекциинуженодинобъект,иещёодин–дляглобальныхнастроексверхуфайла.

Таккакфайлнадоразбиратьпострочно,неплохоначатьсразбиенияфайланастроки.Дляэтоговглаве6мыиспользовалиstring.split("\n").Некоторыеоперационкииспользуютдляпереводастрокинеодинсимвол\n,адва—\r\n.Таккакметодsplitпринимаетрегуляркивкачествеаргумента,мыможемделитьлинииприпомощивыражения/\r?\n/,разрешающегоиодиночные\nи\r\nмеждустроками.

functionparseINI(string){

//Начнёмсобъекта,содержащегонастройкиверхнегоуровня

varcurrentSection={name:null,fields:[]};

varcategories=[currentSection];

string.split(/\r?\n/).forEach(function(line){

varmatch;

if(/^\s*(;.*)?$/.test(line)){

return;

}elseif(match=line.match(/^\[(.*)\]$/)){

currentSection={name:match[1],fields:[]};

categories.push(currentSection);

}elseif(match=line.match(/^(\w+)=(.*)$/)){

currentSection.fields.push({name:match[1],

value:match[2]});

}else{

thrownewError("Строчка'"+line+"'содержитневерныеданные.");

}

});

returncategories;

}

Кодпроходитвсестроки,обновляяобъекттекущейсекции“currentsection”.Сначалаонпроверяет,можнолиигнорироватьстрочку,припомощирегулярки/^\s(;.)?$/.Соображаете,какэтоработает?Частьмеждускобок

ВыразительныйJavascript

185Регулярныевыражения

Page 186: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

совпадаетскомментариями,а?делаеттак,чторегуляркасовпадётисострочками,состоящимиизоднихпробелов.

Еслистрока–некомментарий,кодпроверяет,начинаетлионановуюсекцию.Еслида,онсоздаётновыйобъектдлятекущейсекции,ккоторомудобавляютсяпоследующиенастройки.

Последняяосмысленнаявозможность–строкаявляетсяобычнойнастройкой,ивэтомслучаеонадобавляетсяктекущемуобъекту.

Еслиниодинвариантнесработал,функциявыдаётошибку.

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

Конструкцияif(match=string.match(...))похожанатрюк,использующийприсвоениекакусловиевциклеwhile.Частовынезнаете,чтовызовmatchбудетуспешным,поэтомувыможетеполучитьдоступкрезультирующемуобъектутольковнутриблокаif,которыйэтопроверяет.Чтобнеразбиватькрасивуюцепочкупроверокif,мыприсваиваемрезультатпоискапеременной,исразуиспользуемэтоприсвоениекакпроверку.

Из-заизначальнопростойреализацииязыка,ипоследующейфиксациитакойреализации«вграните»,регуляркиJavaScriptтупятссимволами,невстречающимисяванглийскомязыке.Кпримеру,символ«буквы»сточкизрениярегулярокJavaScript,можетбытьоднимиз26букванглийскогоалфавита,ипочему-тоещёподчёркиванием.Буквытипаéилиβ,однозначноявляющиесябуквами,несовпадаютс\w(исовпадутс\W,тоестьсне-буквой).

Постранномустечениюобстоятельств,исторически\s(пробел)совпадаетсовсемисимволами,которыевUnicodeсчитаютсяпробельными,включаятакиештуки,какнеразрывныйпробелилимонгольскийразделительгласных.

Международныесимволы

ВыразительныйJavascript

186Регулярныевыражения

Page 187: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

УнекоторыхреализацийрегуляроквдругихязыкахестьособыйсинтаксисдляпоискаспециальныхкатегорийсимволовUnicode,типа«всепрописныебуквы»,«всезнакипрепинания»или«управляющиесимволы».ЕстьпланыподобавлениютакихкатегорийивJavaScript,ноони,видимо,будутреализованынескоро.

Регулярки–этообъекты,представляющиешаблоныпоискавстроках.Онииспользуютсвойсинтаксисдлявыраженияэтихшаблонов.

/abc/Последовательностьсимволов/[abc]/Любойсимволизсписка/abc/Любойсимвол,кромесимволовизсписка/[0-9]/Любойсимволизпромежутка/x+/Одноилиболеевхожденийшаблонаx/x+?/Одноилиболеевхождений,нежадное/x*/Нольилиболеевхождений/x?/Нольилиодновхождение/x{2,4}/Отдвухдочетырёхвхождений/(abc)/Группа/a|b|c/Любойизнесколькихшаблонов/\d/Любаяцифра/\w/Любойалфавитно-цифровойсимвол(«буква»)/\s/Любойпробельныйсимвол/./Любойсимвол,кромепереводовстроки/\b/Границаслова/^/Началостроки/$/Конецстроки

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

Устрокестьметодmatchдляпоискашаблонов,иметодsearch,возвращающийтольконачальнуюпозициювхождения.Методreplaceможетзаменятьвхожденияшаблонанадругуюстроку.Кромеэтого,выможетепередатьвreplaceфункцию,котораябудетстроитьстрочкуназамену,основываясьнашаблонеинайденныхгруппах.

Урегулярокестьнастройки,которыепишутпослезакрывающегослеша.Опцияiделаетрегуляркурегистронезависимой,аопцияgделаетеёглобальной,что,кромепрочего,заставляетметодreplaceзаменятьвсенайденныевхождения,анетолькопервое.

КонструкторRegExpможноиспользоватьдлясозданиярегулярокизстрок.

Регулярки–острыйинструментснеудобнойручкой.Онисильноупрощают

Итог

ВыразительныйJavascript

187Регулярныевыражения

Page 188: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Неизбежноприрешениизадачувасвозникнутнепонятныеслучаи,ивыможетеиногдаотчаиваться,видянепредсказуемоеповедениенекоторыхрегулярок.Иногдапомогаетизучитьповедениерегуляркичерезонлайн-сервистипаdebuggex.com,гдеможнопосмотретьеёвизуализациюисравнитьсжелаемымэффектом.

«Гольфом»вкоденазываютигру,гденужновыразитьзаданнуюпрограммуминимальнымколичествомсимволов.Регулярныйгольф–практическоеупражнениепонаписаниюнаименьшихвозможныхрегулярокдляпоисказаданногошаблона,итолькоего.

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

—carиcat—popиprop—ferret,ferry,иferrari—Любоеслово,заканчивающеесянаious—Пробел,закоторымидётточка,запятая,двоеточиеилиточкасзапятой.—Словодлинеешестибукв—Словобезбуквe

//Впишитесвоирегулярки

verify(/.../,

["mycar","badcats"],

["camper","highart"]);

verify(/.../,

["popculture","madprops"],

["plop"]);

Упражнения

Регулярныйгольф

ВыразительныйJavascript

188Регулярныевыражения

Page 189: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

verify(/.../,

["ferret","ferry","ferrari"],

["ferrum","transferA"]);

verify(/.../,

["howdelicious","spaciousroom"],

["ruinous","consciousness"]);

verify(/.../,

["badpunctuation."],

["escapethedot"]);

verify(/.../,

["hottentottententen"],

["no","hottentottententen"]);

verify(/.../,

["redplatypus","wobblingnest"],

["earthbed","learningape"]);

functionverify(regexp,yes,no){

//Ignoreunfinishedexercises

if(regexp.source=="...")return;

yes.forEach(function(s){

if(!regexp.test(s))

console.log("Ненашлось'"+s+"'");

});

no.forEach(function(s){

if(regexp.test(s))

console.log("Неожиданноевхождение'"+s+"'");

});

}

Допустим,вынаписалирассказ,ивездедляобозначениядиалоговиспользовалиодинарныекавычки.Теперьвыхотитезаменитькавычкидиалоговнадвойные,иоставитьодинарныевсокращенияхсловтипаaren’t.

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

Кавычкивтексте

Сновачисла

ВыразительныйJavascript

189Регулярныевыражения

Page 190: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Последовательностицифрможнонайтипростойрегуляркой/\d+/.

Напишитевыражение,находящеетолькочисла,записанныевстилеJavaScript.Онодолжноподдерживатьвозможныйминусилиплюспередчислом,десятичнуюточку,иэкспоненциальнуюзапись5e-3или1E10–опять-такисвозможнымиплюсомилиминусом.Такжезаметьте,чтодоилипослеточкинеобязательномогутстоятьцифры,ноприэтомчислонеможетсостоятьизоднойточки.Тоесть,.5или5.–допустимыечисла,аоднаточкасамапосебе–нет.

//Впишитесюдарегулярку.

varnumber=/^...$/;

//Tests:

["1","-1","+15","1.55",".5","5.","1.3e2","1E-4",

"1e+12"].forEach(function(s){

if(!number.test(s))

console.log("Ненашла'"+s+"'");

});

["1a","+-1","1.2.3","1+1","1e4.5",".5.","1f5",

"."].forEach(function(s){

if(number.test(s))

console.log("Неправильнопринято'"+s+"'");

});

ВыразительныйJavascript

190Регулярныевыражения

Page 191: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

Мастер-программистзнает,когданужнаструктура,акогданужнооставитьвещивпростомвиде.Егопрограммысловноглина–твёрдые,ноподатливые.

МастерЮан-Ма,Книгапрограммирования

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

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

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

Модули

Зачемнужнымодули

ВыразительныйJavascript

191Модули

Page 192: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Некоторыепрограммыорганизованыпомоделиобычноготекста,гдепорядокследованиячёткоопределён,игдечитателюпредлагаетсяпоследовательноеизучениепрограммыимножествопрозы(комментариев)дляописаниякода.Этоделаетчтениекодаменеепугающим(ачтениечужогокодаобычнопугает),нотребуетбольшихусилийприсозданиикода.Такжетакуюпрограммусложнееменять,потомучточастипрозысвязанымеждусобойсильнее,чемчастикода.Этотстильназываетсялитературнымпрограммированием.Теглавыкниги,вкоторыхобсуждаютсяпроекты,можносчитатьлитературнымкодом.

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

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

Загрязнениепространстваимён(ПИ),когданесвязанныедругсдругомчастикодаделятодиннаборпеременных,упоминаласьвглаве4.ТамобъектMathбылприведёнвкачествепримераобъекта,которыйгруппируетфункциональность,связаннуюсматематикой,ввидемодуля.

ХотяJavaScriptнепредлагаетнепосредственноконструкциидлясозданиямодуля,мыможемиспользоватьобъектыдлясозданияподпространствимён,доступныхотовсюду.Афункцииможноиспользоватьдлясоздания

Пространствоимён

ВыразительныйJavascript

192Модули

Page 193: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Впроекте,неразбитомнамодули,непонятно,какиечастикоданеобходимыдляконкретнойфункции.Вмоейпрограмме,шпионящейзаврагами(глава9),янаписалфункциючтенияфайловснастройками.Еслимнепонадобитсяиспользоватьеёвдругомпроекте,ядолженбудускопироватьчастистаройпрограммы,которыевродебысвязанысэтойфункцией,вмойновыйпроект.Аеслиянайдутамошибку,яеёисправлютольковтомпроекте,надкоторымработаювданныймомент,изабудуисправитьеёвовсехостальных.

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

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

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

Итакойсервисесть!ОнназываетсяNPM(npmjs.org).NPM–онлайн-базамодулейиинструментдляскачиванияиапгрейдамодулей,откоторыхзависитвашапрограмма.онвыросизNode.js,окруженияJavaScript,нетребующегобраузера,котороемыобсудимвглаве20,нотакжеможетиспользоватьсяивбраузерныхпрограммах.

Повторноеиспользование

ВыразительныйJavascript

193Модули

Page 194: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Ещёодназадачамодулей–изолироватьнесвязанныемеждусобойчастикодатак,какэтоделаютинтерфейсыобъектов.Хорошопродуманныймодульпредоставляетинтерфейсдляегоиспользованиявовне.Когдамодульобновляютилиисправляют,существующийинтерфейсостаётсянеизменным,чтобыдругиемодулимоглииспользоватьновую,обновлённуюверсиюбезизмененийвнихсамих.

Стабильныйинтерфейснеозначает,чтовнегонедобавляютновыефункции,методыилипеременные.Главное,чтосуществующаяфункциональностьнеудаляетсяиеёсмыслнеменяется.Хорошийинтерфейспозволяетмодулюрасти,неломаястарыйинтерфейс.Аэтозначит–выставлятьнаружукакможноменьшевнутреннейкухнимодуля,приэтомязыкинтерфейсадолженбытьдостаточногибкимимощнымдляприменениявразличныхситуациях.

Интерфейсы,выполняющиепростуюзадачу,вродечтениянастроекизфайла,выходяттакимиестественнымобразом.Длядругих–кпримеру,дляредакторатекстов,укоторогоестьмножестворазныхаспектов,требующихдоступаизвне(содержимое,стили,действияпользователяит.п.)интерфейснеобходимоскрупулёзнопродумывать.

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

Обратитевниманиенапростейшиймодуль,связывающийименасномерамиднейнедели–какделаетметодgetDayобъектаDate.

varnames=["Понедельник","Вторник","Среда","Четверг","Пятница","Суббота","Воскресенье"];

functiondayName(number){

returnnames[number];

}

Устранениесвязей(Decoupling)

Использованиефункцийвкачествепространствимён

ВыразительныйJavascript

194Модули

Page 195: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

console.log(dayName(1));

//→Вторник

ФункцияdayName–частьинтерфейсамодуля,апеременнаяnames–нет.Нохотелосьбынезагрязнятьглобальноепространствоимён.

Можносделатьтак:

vardayName=function(){

varnames=["Понедельник","Вторник","Среда","Четверг","Пятница","Суббота","Воскресенье"];

returnfunction(number){

returnnames[number];

};

}();

console.log(dayName(3));

//→Четверг

Теперьnames–локальнаяпеременнаябезымяннойфункции.Функциясоздаётсяисразувызывается,аеёвозвращаемоезначение(уженужнаянамфункцияdayName)хранитсявпеременной.Мыможемнаписатьмногостраницкодавфункции,объявитьтамсотнюпеременных,ивсеонибудутвнутреннимидлянашегомодуля,анедлявнешнегокода.

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

(function(){

functionsquare(x){returnx*x;}

varhundred=100;

console.log(square(hundred));

})();

//→10000

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

ВыразительныйJavascript

195Модули

Page 196: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

глобальнойОВиспользуемымиимпеременными.

Азачеммызаключилифункциювкруглыескобки?ЭтосвязаносглюкомсинтаксисаJavaScript.Есливыражениеначинаетсясключевогословаfunction,этофункциональноевыражение.Аеслиинструкцияначинаетсясfunction,этообъявлениефункции,котороетребуетназвания,и,таккакэтоневыражение,неможетбытьвызваноприпомощискобок()посленеё.Можнопредставлятьсебезаключениевскобкикактрюк,чтобыфункцияпринудительноинтерпретироваласькаквыражение.

Представьте,чтонамнадодобавитьещёоднуфункциювнашмодуль«деньнедели».Мыуженеможемвозвращатьфункцию,адолжнызавернутьдвефункциивобъект.

varweekDay=function(){

varnames=["Понедельник","Вторник","Среда","Четверг","Пятница","Суббота","Воскресенье"];

return{

name:function(number){returnnames[number];},

number:function(name){returnnames.indexOf(name);}

};

}();

console.log(weekDay.name(weekDay.number("Sunday")));

//→Sunday

Когдамодульбольшой,собиратьвсевозвращаемыезначениявобъектвконцефункциинеудобно,потомучтомногиевозвращаемыефункциибудутбольшими,ивамбылобыудобнееихзаписыватьгде-товдругомместе,рядомсосвязаннымснимикодом.Удобнообъявитьобъект(обычноназываемыйexports)идобавлятькнемусвойствакаждыйраз,когданамнадочто-тоэкспортировать.Вследующемпримерефункциямодуляпринимаетобъектинтерфейсакакаргумент,позволяякодуснаружифункциисоздатьегоисохранитьвпеременной.Снаружифункцииthisссылаетсянаобъектглобальнойобластивидимости.

(function(exports){

Объектывкачествеинтерфейсов

ВыразительныйJavascript

196Модули

Page 197: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

varnames=["Понедельник","Вторник","Среда","Четверг","Пятница","Суббота","Воскресенье"];

exports.name=function(number){

returnnames[number];

};

exports.number=function(name){

returnnames.indexOf(name);

};

})(this.weekDay={});

console.log(weekDay.name(weekDay.number("Saturday")));

//→Saturday

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

Подкрутивкое-что,мыможемсделатьсистему,разрешающуюодномумодулюобращатьсякинтерфейсномуобъектудругого,безвыходавглобальнуюОВ.Нашацель–функцияrequire,которая,получаяимямодуля,загрузитегофайл(сдискаилиизсети,взависимостиотплатформы)ивернётсоответствующеезначениесинтерфейсом.

Этотподходрешаетпроблемы,упомянутыеранее,иунегоестьещёоднопреимущество–зависимостивашейпрограммыстановятсяявными,ипоэтомусложнееслучайновызватьненужныйваммодульбезчёткогоегообъявления.

Нампонадобятсядвевещи.Во-первых,функцияreadFile,возвращающаясодержимоефайлаввидестроки.ВстандартномJavaScriptтакойфункциинет,норазныеокружения,такиекакбраузерилиNode.js,предоставляютсвоиспособыдоступакфайлам.Покапритворимся,чтоунасестьтакаяфункция.Во-вторых,намнужнавозможностьвыполнитьсодержимоеэтой

Отсоединяемсяотглобальнойобластивидимости

ВыразительныйJavascript

197Модули

Page 198: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

строкикаккод.

Естьнесколькоспособовполучитьданные(строкукода)ивыполнитьихкакчастьтекущейпрограммы.

Самыйочевидный–операторeval,которыйвыполняетстрокукодавтекущемокружении.Этоплохаяидея–оннарушаетнекоторыесвойстваокружения,которыеобычноунегоесть,напримеризоляцияотвнешнегомира.

functionevalAndReturnX(code){

eval(code);

returnx;

}

console.log(evalAndReturnX("varx=2"));

//→2

Способлучше–использоватьконструкторFunction.Онпринимаетдвааргумента–строку,содержащуюсписокимёнаргументовчереззапятую,истроку,содержащуютелофункции.

varplusOne=newFunction("n","returnn+1;");

console.log(plusOne(4));

//→5

Этото,чтонамнадо.Мыобернёмкодмодулявфункцию,иеёобластьвидимостистанетобластьювидимостинашегомодуля.

Вотминимальнаяверсияфункцииrequire:

functionrequire(name){

varcode=newFunction("exports",readFile(name));

varexports={};

Выполняемданныекаккод

Require

ВыразительныйJavascript

198Модули

Page 199: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

code(exports);

returnexports;

}

console.log(require("weekDay").name(1));

//→Вторник

ТаккакконструкторnewFunctionоборачиваеткодмодулявфункцию,намненадописатьфункцию,оборачивающуюпространствоимён,внутрисамогомодуля.Атаккакexportsявляетсяаргументомфункциимодуля,модулюненужноегообъявлять.Этоубираетмногомусораизнашегомодуля-примера.

varnames=["Понедельник","Вторник","Среда","Четверг","Пятница","Суббота","Воскресенье"];

exports.name=function(number){

returnnames[number];

};

exports.number=function(name){

returnnames.indexOf(name);

};

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

varweekDay=require("weekDay");

vartoday=require("today");

console.log(weekDay.name(today.dayNumber()));

Утакогопростоговариантаrequireестьнедостатки.Во-первых,онзагрузитивыполнитмодулькаждыйраз,когдаегогрузятчерезrequire–еслиунесколькихмодулейестьодинаковыезависимости,иливызовrequireнаходитсявнутрифункции,котораявызываетсямногократно,будетпотеряновремяиэнергия.

Этоможнорешить,храняужезагруженныемодуливобъекте,ивозвращаясуществующеезначение,когдаонгрузитсянесколькораз.

Втораяпроблема–модульнеможетэкспортироватьпеременнуюнапрямую,толькочерезобъектexport.Кпримеру,модулюможетпотребоваться

ВыразительныйJavascript

199Модули

Page 200: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

экспортироватьтолькоконструкторобъекта,объявленноговнём.Сейчасэтоневозможно,посколькуrequireвсегдаиспользуетобъектexportsвкачествевозвращаемогозначения.

Традиционноерешение–предоставитьмодулисдругойпеременной,module,котораяявляетсяобъектомсосвойствомexports.Оноизначальноуказываетнапустойобъект,созданныйrequire,номожетбытьперезаписанодругимзначением,чтобыэкспортироватьчто-либоещё.

functionrequire(name){

if(nameinrequire.cache)

returnrequire.cache[name];

varcode=newFunction("exports,module",readFile(name));

varexports={},module={exports:exports};

code(exports,module);

require.cache[name]=module.exports;

returnmodule.exports;

}

require.cache=Object.create(null);

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

ТакойстильсистемымодулейназываетсяCommonJS,поименипсевдостандарта,которыйпервымегоописал.ОнвстроенвсистемуNode.js.Настоящиереализацииделаютгораздобольшеописанногомною.Главное,чтоунихестьболееумныйспособпереходаотименимодулякегокоду,которыйразрешаетзагружатьмодулипоотносительномупутикфайлу,илижепоименимодуля,указывающемуналокальноустановленныемодули.

ХотяивозможноиспользоватьстильCommonJSдлябраузера,нооннеоченьподходитдляэтого.ЗагрузкафайлаизСетипроисходитмедленнее,чемсжёсткогодиска.Покаскриптвбраузереработает,насайтеничегодругогонепроисходит(попричинам,которыестанутяснык14главе).

Медленнаязагрузкамодулей

ВыразительныйJavascript

200Модули

Page 201: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Можнообойтиэто,запускаяпрограммутипаBrowserifyсвашимкодомпередвыкладываниемеёввеб.Онапросмотритвсевызовыrequire,обработаетвсезависимостиисоберётнужныйкодводинбольшойфайл.Веб-сайтпростогрузитэтотфайлиполучаетвсенеобходимыемодули.

Второйвариант–оборачиватькодмодулявфункцию,чтобызагрузчикмодулейсначалагрузилзависимостивфоне,апотомвызывалфункцию,инициализирующуюмодуль,послезагрузкизависимостей.ЭтимзанимаетсясистемаAMD(асинхронноеопределениемодулей).

НашапростаяпрограммасзависимостивыгляделабывAMDтак:

define(["weekDay","today"],function(weekDay,today){

console.log(weekDay.name(today.dayNumber()));

});

Функцияdefineздесьсамаяважная.Онапринимаетмассивимёнмодулей,азатемфункцию,принимающуюодинаргументдлякаждойиззависимостей.Оназагрузитзависимости(еслиониуженезагружены)вфоне,позволяястраницеработать,покафайлокачается.Когдавсёзагружено,defineвызываетданнуюемуфункцию,синтерфейсамиэтихзависимостейвкачествеаргументов.

Загруженныетакимобразоммодулидолжнысодержатьвызовыdefine.Вкачествеихинтерфейсаиспользуетсято,чтобыловозвращенофункцией,переданнойвdefine.ВотмодульweekDay:

define([],function(){

varnames=["Понедельник","Вторник","Среда","Четверг","Пятница","Суббота","Воскресенье"];

return{

name:function(number){returnnames[number];},

number:function(name){returnnames.indexOf(name);}

};

});

Чтобыпоказатьминимальнуюреализациюdefine,притворимся,чтоунас

ВыразительныйJavascript

201Модули

Page 202: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

естьфункцияbackgroundReadFile,котораяпринимаетимяфайлаифункцию,ивызываетэтуфункциюссодержимымэтогофайла,кактолькоонбудетзагружен.(Вглаве17будетобъяснено,какнаписатьтакуюфункцию).

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

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

vardefineCache=Object.create(null);

varcurrentMod=null;

functiongetModule(name){

if(nameindefineCache)

returndefineCache[name];

varmodule={exports:null,

loaded:false,

onLoad:[]};

defineCache[name]=module;

backgroundReadFile(name,function(code){

currentMod=module;

newFunction("",code)();

});

returnmodule;

}

Мыпредполагаем,чтозагружаемыйфайлтожесодержитвызовdefine.ПеременнаяcurrentModиспользуется,чтобысообщитьэтомувызовуозагружаемомобъектемодуля,чтобытотсмогобновитьэтотобъектпослезагрузки.Мыещёвернёмсякэтомумеханизму.

ФункцияdefineсамаиспользуетgetModuleдлязагрузкиилисозданияобъектовмодулейдлязависимостейтекущегомодуля.Еёзадача–запланироватьзапускфункцииmoduleFunction(содержащейсамкодмодуля)послезагрузкизависимостей.ДляэтогоонаопределяетфункциюwhenDepsLoaded,добавляемуювмассивonLoad,содержащийвсепокаещёнезагруженныезависимости.Этафункциясразупрекращаетработу,еслиестьещёнезагруженныезависимости,такчтоонавыполнитсвоюработу

ВыразительныйJavascript

202Модули

Page 203: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

functiondefine(depNames,moduleFunction){

varmyMod=currentMod;

vardeps=depNames.map(getModule);

deps.forEach(function(mod){

if(!mod.loaded)

mod.onLoad.push(whenDepsLoaded);

});

functionwhenDepsLoaded(){

if(!deps.every(function(m){returnm.loaded;}))

return;

varargs=deps.map(function(m){returnm.exports;});

varexports=moduleFunction.apply(null,args);

if(myMod){

myMod.exports=exports;

myMod.loaded=true;

myMod.onLoad.every(function(f){f();});

}

}

whenDepsLoaded();

}

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

Первое,чтоделаетdefine,этосохраняетзначениеcurrentMod,котороебылоунегопривызове,впеременнойmyMod.Вспомните,чтоgetModuleпрямопередисполнениемкодамодулясохранилсоответствующийобъектмодулявcurrentMod.ЭтопозволяетwhenDepsLoadedхранитьвозвращаемоезначениефункциимодулявсвойствеexportsэтогомодуля,установитьсвойствоloadedмодулявtrue,ивызватьвсефункции,ждавшиезагрузкимодуля.

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

ВыразительныйJavascript

203Модули

Page 204: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

НастоящаяреализацияAMDгораздоумнееподходиткпревращениюимёнмодулейвURLиболеенадёжна,чемпоказановпримере.ПроектRequireJSпредоставляетпопулярнуюреализациютакогостилязагрузчикамодулей.

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

Лучшийспособпознатьзначимостьхорошегоинтерфейса–использоватьмногоинтерфейсов.Некоторыебудутплохие,некоторыехорошие.Опытпокажетвам,чтоработает,ачто–нет.Никогданепринимайтекакдолжноеплохойинтерфейс.Исправьтеего,илизаключитевдругойинтерфейс,которыйлучшевамподходит.

Еслипрограммистможетпредсказать,какработаетвашинтерфейс,емунепридётсячастоотвлекатьсяисмотретьподсказкупоегоиспользованию.Постарайтесьследоватьобщепринятымсоглашениям.ЕслиестьмодульиличастьязыкаJavaScript,котораяделаетчто-топохожеенато,чтовыпытаетесьреализовать–будетнеплохо,есливашинтерфейсбудетнапоминатьсуществующий.Такимобразом,онбудетпривычендлялюдей,знакомыхссуществующиминтерфейсом.

Вповедениивашегокодапредсказуемостьтакжеважна.Васможетпостичьискушениесделатьинтерфейсслишкомзаумнымякобыпотому,чтоегоудобнееиспользовать.Кпримеру,выможетеприниматьлюбыевидытиповикомбинацийаргументовипроделыватьсними«то,чтонадо».Илипредоставлятьдесяткиспециализированныхфункций,которыепредлагаютнезначительноотличающуюсяфункциональность.Этоможетсделатькод,опирающийсянавашинтерфейс,немногокороче,затозатруднитьлюдям,работающимсним,строитьчёткуюмысленнуюмодельработывашегомодуля.

Разработкаинтерфейса

Предсказуемость

ВыразительныйJavascript

204Модули

Page 205: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Старайтесьиспользоватьвинтерфейсахнастолькопростыеструктурыданных,насколькоэтовозможно.Делайтетак,чтобыфункциивыполнялипростыеипонятныевещи.Еслиэтоприменимо,делайтефункциичистыми(см.Главу3).

Кпримеру,частенькомодулипредлагаютсвоюверсиюмассивоподобныхколлекцийобъектовсосвоиминтерфейсомдляподсчётаиизвлеченияэлементов.УтакихобъектовнетметодовmapилиforEach,иникакаяфункция,ожидающаянастоящиймассив,несможетснимиработать.Этопримерплохойкомпонуемости–модульнельзялегкоскомпоноватьсдругимкодом.

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

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

Компонуемость

Многослойныеинтерфейсы

ВыразительныйJavascript

205Модули

Page 206: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Частоприходитсяпредлагатьдваинтерфейса:детализированныйнизкоуровневыйдлясложныхситуаций,ипростойвысокоуровневыйдляобычногоиспользования.Второйможнопостроитьнаосновепервого.Вмодуледляотправкиемейловвысокоуровневыйинтерфейсможетбытьпростофункцией,котораяпринимаетсообщение,адресполучателяиотправителя,иотправляетписьмо.Низкоуровневыйдолжендаватьдоступкзаголовкам,приложеннымфайлам,HTMLписьмамит.д.

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

ХотяJavaScriptсовершеннонепомогаетделатьмодули,егогибкиефункциииобъектыпозволяютсделатьдостаточнонеплохуюсистемумодулей.Областьвидимостифункцийиспользуетсякаквнутреннеепространствоимёнмодуля,аобъектыиспользуютсядляхранениянаборовпеременных.

Естьдвапопулярныхподходакиспользованиюмодулей.Один–CommonJS,построенныйнафункцииrequire,котораявызываетмодулипоимениивозвращаетихинтерфейс.Другой–AMD,использующийфункциюdefine,принимающуюмассивимёнмодулейи,послеихзагрузки,исполняющуюфункцию,аргументамикоторойявляютсяихинтерфейсы.

Напишитепростоймодультипаweekday,преобразующийномерамесяцев(начинаяснуля)вназванияиобратно.Выделитеемусобственноепространствоимён,т.к.емупотребуетсявнутренниймассивсназваниямимесяцев,ииспользуйтечистыйJavaScript,безсистемызагрузкимодулей.

Итог

Упражнения

Названиямесяцев

ВыразительныйJavascript

206Модули

Page 207: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

//Вашкод

console.log(month.name(2));

//→March

console.log(month.number("November"));

//→10

Надеюсь,чтоглава7ещёнестёрласьизвашейпамяти.Вернитеськразработаннойтамсистемеипредложитеспособразделениякоданамодули.Чтобыосвежитьвампамять–вотсписокфункцийитипов,попорядкупоявления:

Vector

Grid

directions

directionNames

randomElement

BouncingCritter

elementFromChar

World

charFromElement

Wall

View

WallFollower

dirPlus

LifelikeWorld

Plant

PlantEater

SmartPlantEater

Tiger

Ненадосоздаватьслишкоммногомодулей.Книга,вкоторойнакаждойстраницебылабыноваяглава,действовалабывамнанервы(хотябыпотому,чтовсёместосъедалибызаголовки).Ненужноделатьдесятьфайловдляодногомелкогопроекта.Рассчитывайтена3-5модулей.

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

Вернёмсякэлектроннойжизни

ВыразительныйJavascript

207Модули

Page 208: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Запутаннаятемавуправлениизависимостями–круговыезависимости,когдамодульАзависитотБ,аБзависитотА.Многиесистемымодулейэтопростозапрещают.МодулиCommonJSдопускаютограниченныйвариант:этоработает,покамодулинезаменяютобъектexports,существующийпо-умолчанию,другимзначением,иначинаютиспользоватьинтерфейсыдругдругатолькопослеокончаниязагрузки.

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

Круговыезависимости

ВыразительныйJavascript

208Модули

Page 209: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

ХэлАбельсониЖеральдСасман,«Структураиинтерпретациякомпьютерныхпрограмм».

КогдаучениеспросилучителяоприродециклаДанныхиКонтроля,Юань-Маответил:«Подумайокомпиляторе,компилирующемсамогосебя».

МастерЮань-Ма,«Книгапрограммирования»

Создатьсвойязыкпрограммированияудивительнолегко(покавынеставитезапредельныхцелей)идовольнопоучительно.

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

МыпостроимязыкпрограммированияEgg(Яйцо).Онбудетнебольшим,простым,нодостаточномощнымдлявыражениялюбыхрасчётов.Онтакжебудетосуществлятьпростыеабстракции,основанныенафункциях.

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

Унашегоязыкабудетпростойиоднородныйсинтаксис.ВEggвсёбудетявлятьсявыражением.Выражениеможетбытьпеременной,число,строка

Проект:языкпрограммирования

Разбор(parsing)

ВыразительныйJavascript

209Проект:языкпрограммирования

Page 210: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

илиприложение.Приложенияиспользуютсядлявызовафункцийиконструкцийтипаifилиwhile.

ДляупрощенияпарсингастрокивEggнебудутподдерживатьобратныхслешейиподобныхвещей.Строка–простопоследовательностьсимволов,неявляющихсядвойнымикавычками,заключённаявдвойныекавычки.Число–последовательностьцифр.Именапеременныхмогутсостоятьизлюбыхсимволов,неявляющихсяпробеламиинеимеющихспециальногозначениявсинтаксисе.

Приложениязаписываютсятакже,каквJS—припомощискобокпослевыраженияислюбымколичествомаргументоввскобках,разделённыхзапятыми.

do(define(x,10),

if(>(x,5)),

print("много"),

print("мало"))

Однородностьязыкаозначает,чтото,чтовJSявляетсяоператорами,применяетсятакже,какиостальныефункции.Таккаквсинтаксисенетконцепцииблоков,намнужнаконструкцияdoдляобозначениянесколькихвещей,выполняемыхпоследовательно.

Структураданных,описывающаяпрограмму,будетсостоятьизобъектоввыражений,укаждогоизкоторыхбудетсвойствоtype,отражающеетипэтоговыраженияидругиесвойства,описывающиесодержимое.

Выражениятипа“value”представляютстрокииличисла.Ихсвойствоvalueсодержитстрокуиличисло,котороеонипредставляют.Выражениятипа“word”используютсядляидентификаторов(имён).Утакихобъектовестьсвойствоname,содержащееимяидентификатораввидестроки.Инаконец,выражения“apply”представляютприложения.Унихестьсвойство“operator”,ссылающеесянаприменяемоевыражение,исвойство“args”смассивомаргументов.

Часть>(x,5)будетпредставленатак:

{

ВыразительныйJavascript

210Проект:языкпрограммирования

Page 211: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

type:"apply",

operator:{type:"word",name:">"},

args:[

{type:"word",name:"x"},

{type:"value",value:5}

]

}

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

Сравнитеэтоспарсером,написаннымнамидляфайланастроеквглаве9,укоторогобылапростаяструктура:онделилвводнастрокииобрабатывалиходнузадругой.Тамбыловсегонесколькоформ,которыеразрешеноприниматьстроке.

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

МыопределяемфункциюparseExpression,принимающуюстрокунавходивозвращающуюобъект,содержащийструктуруданныхдлявыражениясначаластроки,вместесчастьюстроки,оставшейсяпослепарсинга.Приразбореподвыражений(таких,какаргументприложения),этафункциясновавызывается,возвращаявыражениеаргументавместесоставшимсятекстом.Тоттекстможет,всвоюочередь,содержатьещёаргументы,илижебытьзакрывающейскобкой,завершающейсписокаргументов.

Перваячастьпарсера:

functionparseExpression(program){

program=skipSpace(program);

varmatch,expr;

Структурасинтаксическогодерева

ВыразительныйJavascript

211Проект:языкпрограммирования

Page 212: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

if(match=/^"([^"]*)"/.exec(program))

expr={type:"value",value:match[1]};

elseif(match=/^\d+\b/.exec(program))

expr={type:"value",value:Number(match[0])};

elseif(match=/^[^\s(),"]+/.exec(program))

expr={type:"word",name:match[0]};

else

thrownewSyntaxError("Неожиданныйсинтаксис:"+program);

returnparseApply(expr,program.slice(match[0].length));

}

functionskipSpace(string){

varfirst=string.search(/\S/);

if(first==-1)return"";

returnstring.slice(first);

}

ПосколькуEggразрешаетлюбоеколичествопробеловвэлементах,намнадопостоянновырезатьпробелысначаластроки.СэтимсправляетсяskipSpace.

Пропустивначальныепробелы,parseExpressionиспользуеттрирегуляркидляраспознаваниятрёхпростых(атомарных)элементов,поддерживаемыхязыком:строк,чиселислов.Парсерсоздаётразныеструктурыдляразныхтипов.Есливводнеподходитнипододнуизформ,этонеявляетсядопустимымвыражением,ионвыбрасываетошибку.SyntaxError–стандартныйобъектдляошибок,которыйсоздаётсяприпопыткезапусканекорректнойпрограммыJavaScript.

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

functionparseApply(expr,program){

program=skipSpace(program);

if(program[0]!="(")

return{expr:expr,rest:program};

program=skipSpace(program.slice(1));

expr={type:"apply",operator:expr,args:[]};

while(program[0]!=")"){

vararg=parseExpression(program);

ВыразительныйJavascript

212Проект:языкпрограммирования

Page 213: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

expr.args.push(arg.expr);

program=skipSpace(arg.rest);

if(program[0]==",")

program=skipSpace(program.slice(1));

elseif(program[0]!=")")

thrownewSyntaxError("Ожидается','or')'");

}

returnparseApply(expr,program.slice(1));

}

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

Виномслучае,онапропускаетоткрывающуюскобкуисоздаётобъектсинтаксическогодеревадляэтоговыражения.ЗатемонарекурсивновызываетparseExpressionдляразборакаждогоаргумента,поканевстретитзакрывающуюскобку.Рекурсиянепрямая,parseApplyиparseExpressionвызываютдругдруга.

Посколькуприложениесамопосебеможетбытьвыражением(multiplier(2)(1)),parseApplyдолжна,послеразбораприложения,вызватьсебяснова,проверив,неидётлидалеедругаяпараскобок.

Вотивсё,чтонамнужнодляразбораEgg.Мыобернёмэтовудобнуюфункциюparse,проверяющую,чтоонадошладоконцастрокипослеразборавыражения(программаEgg–этоодновыражение),иэтодастнамструктуруданныхпрограммы.

functionparse(program){

varresult=parseExpression(program);

if(skipSpace(result.rest).length>0)

thrownewSyntaxError("Неожиданныйтекстпослепрограммы");

returnresult.expr;

}

console.log(parse("+(a,10)"));

//→{type:"apply",

//operator:{type:"word",name:"+"},

//args:[{type:"word",name:"a"},

//{type:"value",value:10}]}

Работает!Онаневыдаётполезнойинформацииприошибке,инехранитномерастрокиистолбца,скоторыхначинаетсякаждоевыражение,что

ВыразительныйJavascript

213Проект:языкпрограммирования

Page 214: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

моглобыпригодитьсяприразбореошибок–нодлянасиэтогохватит.

Ачтонамделатьссинтаксическимдеревомпрограммы?Запускатьеё!Этимзанимаетсяинтерпретатор.Выдаётеемусинтаксическоедеревоиобъектокружения,которыйсвязываетименасозначениями,аонинтерпретируетвыражение,представляемоедеревом,ивозвращаетрезультат.

functionevaluate(expr,env){

switch(expr.type){

case"value":

returnexpr.value;

case"word":

if(expr.nameinenv)

returnenv[expr.name];

else

thrownewReferenceError("Неопределённаяпеременная:"+

expr.name);

case"apply":

if(expr.operator.type=="word"&&

expr.operator.nameinspecialForms)

returnspecialForms[expr.operator.name](expr.args,

env);

varop=evaluate(expr.operator,env);

if(typeofop!="function")

thrownewTypeError("Приложениенеявляетсяфункцией.");

returnop.apply(null,expr.args.map(function(arg){

returnevaluate(arg,env);

}));

}

}

varspecialForms=Object.create(null);

Уинтерпретатораестькоддлякаждогоизтиповвыражений.Длялитераловонвозвращаетихзначение.Например,выражение100интерпретируетсявчисло100.Упеременноймыдолжныпроверить,определеналионавокружении,иеслида–запроситьеёзначение.

Сприложениямисложнее.Еслиэтоособаяформатипаif,мыничегонеинтерпретируем,апростопередаёмаргументывместесокружениемв

Интерпретатор

ВыразительныйJavascript

214Проект:языкпрограммирования

Page 215: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

ДляпредставлениязначенийфункцийEggмыбудемиспользоватьпростыезначенияфункцийJavaScript.Мывернёмсякэтомупозже,когдаопределимспециальнуюформуfun.

Рекурсивнаяструктураинтерпретаторанапоминаетпарсер.Обаотражаютструктуруязыка.Можнобылобыинтегрироватьпарсервинтерпретаториинтерпретироватьвовремяразбора,ноихразделениеделаетпрограммуболеечитаемой.

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

ОбъектspecialFormsиспользуетсядляопределенияособогосинтаксисаEgg.Онсопоставляетсловасфункциями,интерпретирующимиэтиспециальныеформы.Покаонпуст.Давайтедобавимнесколькоформ.

specialForms["if"]=function(args,env){

if(args.length!=3)

thrownewSyntaxError("Неправильноеколичествоаргументовдляif");

if(evaluate(args[0],env)!==false)

returnevaluate(args[1],env);

else

returnevaluate(args[2],env);

};

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

Специальныеформы

ВыразительныйJavascript

215Проект:языкпрограммирования

Page 216: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

EggотличаетсяотJavaScriptтем,каконобрабатываетусловиеif.Оннебудетсчитатьнольилипустуюстрокузаfalse.

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

Формадляwhileсхожая.

specialForms["while"]=function(args,env){

if(args.length!=2)

thrownewSyntaxError("Неправильноеколичествоаргументовдляwhile");

while(evaluate(args[0],env)!==false)

evaluate(args[1],env);

//ПосколькуundefinedнезадановEgg,

//заотсутствиемосмысленногорезультатавозвращаемfalse

returnfalse;

};

Ещёоднаосновнаячастьязыка–do,выполняющийвсеаргументысверхувниз.Егозначение–этозначение,выдаваемоепоследнимаргументом.

specialForms["do"]=function(args,env){

varvalue=false;

args.forEach(function(arg){

value=evaluate(arg,env);

});

returnvalue;

};

Чтобысоздаватьпеременныеидаватьимзначения,мысоздаёмформуdefine.Онаожидаетwordвкачествепервогоаргумента,ивыражение,производящеезначение,котороенадоприсвоитьэтомусловувкачествевторого.define,какивсё,являетсявыражением,поэтомуонодолжновозвращатьзначение.Пустьоновозвращаетприсвоенноезначение(прямкакоператор=вJavaScript).

specialForms["define"]=function(args,env){

if(args.length!=2||args[0].type!="word")

ВыразительныйJavascript

216Проект:языкпрограммирования

Page 217: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

thrownewSyntaxError("Baduseofdefine");

varvalue=evaluate(args[1],env);

env[args[0].name]=value;

returnvalue;

};

Окружение,принимаемоеинтерпретатором—этообъектсосвойствами,чьиименасоответствуютименампеременных,азначения–значениямэтихпеременных.Давайтеопределимобъектокружения,представляющийглобальнуюобластьвидимости.

Дляиспользованияконструкцииifмыдолжнысоздатьбулевскиезначения.Таккакихвсегодва,особыйсинтаксисдлянихненужен.Мыпростоделаемдвепеременныесозначениямиtrueиfalse.

vartopEnv=Object.create(null);

topEnv["true"]=true;

topEnv["false"]=false;

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

varprog=parse("if(true,false,true)");console.log(evaluate(prog,topEnv));//→false

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

["+","-","*","/","==","<",">"].forEach(function(op){

topEnv[op]=newFunction("a,b","returna"+op+"b;");

});

Такжепригодитсяспособвыводазначений,такчтомыобернёмconsole.logв

Окружение

ВыразительныйJavascript

217Проект:языкпрограммирования

Page 218: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

функциюиназовёмеёprint.

topEnv["print"]=function(value){

console.log(value);

returnvalue;

};

Этодаётнамдостаточноэлементарныхинструментовдлянаписанияпростыхпрограмм.Следующаяфункцияrunдаётудобныйспособзаписиизапуска.Онасоздаётсвежееокружение,парситиразбираетстрочки,которыемыейпередаём,так,какбудтоониявляютсяоднойпрограммой.

functionrun(){

varenv=Object.create(topEnv);

varprogram=Array.prototype.slice

.call(arguments,0).join("\n");

returnevaluate(parse(program),env);

}

ИспользованиеArray.prototype.slice.call–уловкадляпревращенияобъекта,похожегонамассив,такогокакаргументы,внастоящиймассив,чтобымымоглиприменитькнемуjoin.Онапринимаетвсеаргументы,переданныевrun,исчитает,чтовсеони–строчкипрограммы.

run("do(define(total,0),",

"define(count,1),",

"while(<(count,11),",

"do(define(total,+(total,count)),",

"define(count,+(count,1)))),",

"print(total))");

//→55

Этупрограммумывиделиуженесколькораз–онаподсчитываетсуммучиселот1до10наязыкеEgg.ОнауродливееэквивалентнойпрограммынаJavaScript,нонетакужиплохадляязыка,заданногоменеечем150строчкамикода.

Функции

ВыразительныйJavascript

218Проект:языкпрограммирования

Page 219: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Языкпрограммированиябезфункций–плохойязык.

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

specialForms["fun"]=function(args,env){

if(!args.length)

thrownewSyntaxError("Функциинужнотело");

functionname(expr){

if(expr.type!="word")

thrownewSyntaxError("Именааргументовдолжныбытьтипаword");

returnexpr.name;

}

varargNames=args.slice(0,args.length-1).map(name);

varbody=args[args.length-1];

returnfunction(){

if(arguments.length!=argNames.length)

thrownewTypeError("Неверноеколичествоаргументов");

varlocalEnv=Object.create(env);

for(vari=0;i<arguments.length;i++)

localEnv[argNames[i]]=arguments[i];

returnevaluate(body,localEnv);

};

};

УфункцийвEggсвоёлокальноеокружение,какивJavaScript.МыиспользуемObject.createдлясозданияновогообъекта,имеющегодоступкпеременнымвовнешнемокружении(своегопрототипа),ноонтакжеможетсодержатьновыепеременные,неменяявнешнейобластивидимости.

Функция,созданнаяформойfun,создаётсвоёлокальноеокружениеидобавляеткнемупеременные-аргументы.Затемонаинтерпретируеттеловэтомокруженииивозвращаетрезультат.

run("do(define(plusOne,fun(a,+(a,1))),",

"print(plusOne(10)))");

//→11

run("do(define(pow,fun(base,exp,",

"if(==(exp,0),",

"1,",

"*(base,pow(base,-(exp,1)))))),",

ВыразительныйJavascript

219Проект:языкпрограммирования

Page 220: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

"print(pow(2,10)))");

//→1024

Мысвамипостроилиинтерпретатор.Вовремяинтерпретациионработаетспредставлениемпрограммы,созданнымпарсером.

Компиляция–добавлениеещёодногошагамеждупарсеромизапускомпрограммы,котораяпревращаетвпрограммувнечто,чтоможновыполнятьболееэффективно,путёмпроделываниябольшинстваработызаранее.Кпримеру,вхорошоорганизованныхязыкахприкаждомиспользованиипеременнойочевидно,ккакойпеременнойобращаются,дажебеззапускапрограммы.Этоможноиспользовать,чтобынеискатьпеременнуюпоименикаждыйраз,когдакнейобращаются,анапрямуювызыватьеёизкакой-тозаранееопределённойобластипамяти.

Потрадициикомпиляциятакжепревращаетпрограммувмашинныйкод–сыройформат,пригодныйдляисполненияпроцессором.Нокаждыйпроцесспревращенияпрограммывдругойвид,посути,являетсякомпиляцией.

МожнобылобысоздатьдругойинтерпретаторEgg,которыйсначалапревращаетпрограммувпрограммунаязыкеJavaScript,используетnewFunctionдлявызовакомпилятораJavaScriptивозвращаетрезультат.ПриправильнойреализацииEggвыполнялсябыоченьбыстроприотносительнопростойреализации.

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

Когдамыопределялиifиwhile,вымоглизаметить,чтоонипредставлялисобойпростыеобёрткивокругifиwhileвJavaScript.ЗначениявEgg–такжеобычныезначенияJavaScript.

СравниваяреализациюEgg,построеннуюнаJavaScript,собъёмомработы,

Компиляция

Мошенничество

ВыразительныйJavascript

220Проект:языкпрограммирования

Page 221: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

необходимойдлясозданияязыкапрограммированиянепосредственнонамашинномязыке,торазницастановитсяогромной.Темнеменее,этотпример,надеюсь,даётвампредставлениеоработеязыковпрограммирования.

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

ТакойязыкнеобязаннапоминатьобыныйЯП.ЕслибыJavaScriptнесодержалрегулярныхвыражений,вымоглибынаписатьсвоипарсериинтерпретатордлятакогосуб-языка.

Илипредставьте,чтовыстроитегигантскогоробота-динозавраивамнужнозапрограммироватьегоповедение.JavaScript–несамыйэффективныйспособсделатьэто.Можновместоэтоговыбратьязыкпримернотакогосвойства:

behaviorwalk

performwhen

destinationahead

actions

moveleft-foot

moveright-foot

behaviorattack

performwhen

Godzillain-view

actions

firelaser-eyes

launcharm-rockets

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

Упражнения

ВыразительныйJavascript

221Проект:языкпрограммирования

Page 222: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ДобавьтеподдержкумассивоввEgg.Дляэтогодобавьтетрифункциивосновнуюобластьвидимости:array(...)длясозданиямассива,содержащегозначенияаргументов,length(array)длявозвратадлинымассиваиelement(array,n)длявозвратаn-ногоэлемента.

//Добавьтекода

topEnv["array"]="...";

topEnv["length"]="...";

topEnv["element"]="...";

run("do(define(sum,fun(array,",

"do(define(i,0),",

"define(sum,0),",

"while(<(i,length(array)),",

"do(define(sum,+(sum,element(array,i))),",

"define(i,+(i,1)))),",

"sum))),",

"print(sum(array(1,2,3))))");

//→6

СпособопределенияfunпозволяетфункциямвEggзамыкатьсявокругокружения,ииспользоватьлокальныепеременныевтелефункции,которыевиднывовремяопределения,точнокаквфункцияхJavaScript.

Следующаяпрограммаиллюстрируетэто:функцияfвозвращаетфункцию,добавляющуюеёаргументкаргументуf,тоесть,ейнужендоступклокальнойобластивидимостивнутриfдляиспользованияпеременнойa.

run("do(define(f,fun(a,fun(b,+(a,b)))),",

"print(f(4)(5)))");

//→9

Объясните,используяопределениеформыfun,какоймеханизмпозволяетэтойконструкцииработать.

Массивы

Замыкания

Комментарии

ВыразительныйJavascript

222Проект:языкпрограммирования

Page 223: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ХорошобылобыиметькомментариивEgg.Кпримеру,мымоглибыигнорироватьоставшуюсячастьстроки,встречаясимвол“#”–так,какэтопроисходитс“//”вJS.

Большиеизменениявпарсеределатьнепридётся.МыпростопоменяемskipSpace,чтобыонапропускалакомментарии,будтоониявляютсяпробелами–ивовсехместах,гдевызываетсяskipSpace,комментариитожебудутпропущены.Внеситеэтоизменение.

//Поменяйтестаруюфункцию

functionskipSpace(string){

varfirst=string.search(/\S/);

if(first==-1)return"";

returnstring.slice(first);

}

console.log(parse("#hello\nx"));

//→{type:"word",name:"x"}

console.log(parse("a#one\n#two\n()"));

//→{type:"apply",

//operator:{type:"word",name:"a"},

//args:[]}

Сейчасмыможемприсвоитьпеременнойзначениетолькочерезdefine.Этаконструкцияработаеткакприприсвоениистарымпеременным,такиприсозданииновых.

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

Добавьтеформуset,схожуюсdefine,котораяприсваиваетпеременнойновоезначение,обновляяпеременнуювовнешнейобластивидимости,еслионанезаданавлокальной.Еслипеременнаявообщенезадана,швыряйтеReferenceError(ещёодинстандартныйтипошибки).

Техникапредставленияобластейвидимостиввидепростыхобъектов,до

Чинимобластьвидимости

ВыразительныйJavascript

223Проект:языкпрограммирования

Page 224: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

сегомоментабывшаяудобной,теперьбудетваммешать.ВамможетпонадобитьсяфункцияObject.getPrototypeOf,возвращающаяпрототипобъекта.Такжепомните,чтообластьвидимостиненаследуетсяотObject.prototype,поэтомуесливамнадовызватьнанихhasOwnProperty,придётсяиспользоватьтакуюнеуклюжуюконструкцию:

Object.prototype.hasOwnProperty.call(scope,name);

ЭтовызываетметодhasOwnPropertyпрототипаObjectизатемвызываетегонаобъектеscope.

specialForms["set"]=function(args,env){

//Вашкод

};

run("do(define(x,4),",

"define(setx,fun(val,set(x,val))),",

"setx(50),",

"print(x))");

//→50

run("set(quux,true)");

//→ОшибкавидаReferenceError

ВыразительныйJavascript

224Проект:языкпрограммирования

Page 225: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Браузер–крайневраждебнаяпрограммнаясреда

ДугласКрокфорд,«ЯзыкпрограммированияJavaScript»(видеолекция)

Следующаячастькнигирасскажетовеб-браузерах.БезнихнебылобыJavaScript.Аеслибыибыл,никтобынеобратилнанеговнимания.

Технологиивебассамогоначалабылидецентрализованными–нетолькотехнически,ноисточкизренияихэволюции.Различныеразработчикибраузеровдобавлялиновуюфункциональность«послучаю»,непродуманно,ичастоэтафункциональностьобреталаподдержкувдругихбраузерахистановиласьстандартом.

Этоиблагословлениеипроклятие.Соднойстороны,здоровонеиметьконтролирующегоцентра,чтобытехнологияразвиваласьразличнымисторонами,иногдасотрудничающими,иногдаконкурирующими.Сдругой–бессистемноеразвитиеязыкапривелоктому,чторезультатнеявляетсяяркимпримеромвнутреннейсогласованности.Некоторыечастипривносятпутаницуибеспорядок.

Компьютерныесетипоявилисьв1950-х.Есливыпроложитекабельмеждудвумяилинесколькимикомпьютерамииразрешитеимпередаватьданные,выможетделатьмногоудивительныхвещей.Аеслисвязьдвухмашинводномзданиипозволяетделатьмногоразного,тосвязькомпьютеровповсейпланетедолжнапозволятьещёбольше.Технология,позволяющаяэтосделать,быласозданав1980-х,иполучившаясясетьзовётсяинтернетом.Ионаоправдалаожидания.

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

JavaScriptибраузер

Сетииинтернет

ВыразительныйJavascript

225JavaScriptибраузер

Page 226: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

кодированияиспользуется.

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

Кпримеру,простойпротоколчатаможетсостоятьизодногокомпьютера,отправляющегобиты,представляющиетекст«ЧАТ?»надругой,авторогоотвечающеготекстом«ОК!»,дляподтверждениятого,чтоонпонялпротокол.Дальшеонимогутперейтикотправкедругдругутекстов,чтенияполученныхтекстовивыводаихнаэкран.

Большинствопротоколовпостроенонаосноведругихпротоколов.Нашпротоколчатаизпримерарассматриваетсетькакпотоковоеустройство,вкотороеможновводитьбитыизаказыватьихприходнаконкретныйадресвправильномпорядке.Аобеспечениеэтогопроцесса–самопосебеявляетсясложнойзадачей.TransmissionControlProtocol(TCP)–протокол,решающийэтузадачу.Всеустройства,подключённыекинтернету,говорятнанём,ибольшинствообщениявинтернетепостроенонаегооснове.

СоединениепоTCPработаеттак:одинкомпьютерждёт,или«слушает»,покадругиененачнутснимговорить.Чтобыможнобылослушатьразныевидыобщенияводноитожевремя,длякаждогоизнихназначаетсяномер(называемыйпортом).Большинствопротоколовустанавливаютпорт,используемыйпоумолчанию.Кпримеру,еслимыотправляеме-мейлпопротоколуSMTP,компьютер,черезкоторыймыегошлём,долженслушатьпорт25.

Тогдадругойкомпьютерможетустановитьсоединение,связавшисьскомпьютеромназначенияпоправильномупорту.Еслимашинаназначениядоступна,ионаслушаетэтотпорт,соединениеустанавливается.Слушающийкомпьютерзовётсясервером,асоединяющийся–клиентом.

Такоесоединениеработаеткакдвусторонняятруба,покоторойтекутбиты–обемашинымогутпомещатьвнеёданные.Когдабитыпереданы,другаямашинаможетихпрочесть.Этоудобнаямодель.Можносказать,чтоTCPобеспечиваетабстракциюсети.

ВыразительныйJavascript

226JavaScriptибраузер

Page 227: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

ЧтобыдобавитьвВебсодержимое,вамнужносоединитьмашинусинтернетомизаставитьеёслушать80порт,используяпротоколпередачигипертекста,HypertextTransferProtocol(HTTP).Онпозволяетдругимкомпьютерамзапрашиватьдокументыпосети.

Каждыйдокументимеетимяввидеуниверсальноголокатораресурсов,UniversalResourceLocator(URL),которыйвыглядитпримернотак:

http://eloquentjavascript.net/12_browser.html

||||

протоколсерверпуть

Перваячастьговоритнам,чтоURLиспользуетпротоколHTTP(вотличиеот,скажем,зашифрованногоHTTP,которыйзаписываетсякакhttps://).Затемидётчасть,определяющая,скакогосерверамызапрашиваемдокумент.Последняя–строкапути,определяющаяконкретныйдокументилиресурс.

Укаждоймашины,присоединённойкинтернету,естьсвойадресIP,которыйвыглядиткак37.187.37.82.ЕгоиногдаможноиспользоватьвместоименисерверавURL.Ноцифрысложнеезапоминатьипечатать,чемимена–поэтомуобычновырегистрируетедоменноеимя,котороеуказываетнаконкретнуюмашину(илинабормашин).Язарегистрировалeloquentjavascript.net,указывающийнаIP-адресмашины,которуюяконтролирую,поэтомуможноиспользоватьэтотадресдляпредоставлениявеб-страниц.

ЕсливывведётеуказанныйURLвадреснуюстрокубраузера,онпопробуетзапроситьипоказатьдокумент,находящийсяпоэтомуURL.Во-первых,браузерунадовыяснить,кудассылаетсядоменeloquentjavascript.net.Затем,используяпротоколHTTP,онсоединяетсяссерверомпоэтомуадресу,и

Веб

ВыразительныйJavascript

227JavaScriptибраузер

Page 228: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

спрашиваетегоресурспоимени/12_browser.html

Вглаве17мыподробнеерассмотримпротоколHTTP.

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

ПростойHTMLдокументможетвыглядетьтак:

<!doctypehtml>

<html>

<head>

<title>Моядомашняястраничка</title>

</head>

<body>

<h1>Моядомашняястраничка</h1>

<p>Привет,яМарийниэтомоядомашняястраничка.</p>

<p>Аещёякнижкунаписал!Читайтееё

<ahref="http://eloquentjavascript.net">здесь</a>.</p>

</body>

</html>

Теги,окружённыеугловымискобками<и>,описываютинформациюоструктуредокумента.Всёостальное–простотекст.

Документначинаетсяс<!doctypehtml>,иэтоговоритбраузеру,чтоегонадоинтерпретироватькаксовременныйHTML,вотличиеотразныхдиалектовпрошлого.

УHTMLдокументовестьзаголовокитело.Заголовоксодержитинформациюодокументе,атело–самдокумент.Внашемслучаемыобъявили,чтоназваниестраницыбудет«Моядомашняястраничка»,затемописалидокумент,содержащийзаголовок(

HTML

,тоестьheading1,заголовок1.

ВыразительныйJavascript

228JavaScriptибраузер

Page 229: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Естьещё

–,заголовкиразныхразмеров)идвапараграфа.

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

изаканчиваетсязакрывающим

.Некоторыеоткрывающиетеги,типассылки,содержатдополнительнуюинформациюввидеимя=”значение”.Онаназывается«атрибутами».Внашемслучаеадресссылкизаданкакhref="http://eloquentjavascript.net",гдеhrefозначает«гипертекстоваяссылка»,“hypertextreference”.

Некоторыетегиничегонеокружают,иихненадозакрывать.Пример–тегкартинки

<imgsrc="http://example.com/image.jpg">

Чтобывключатьвтекстдокументаугловыескобки,нужнопользоватьсяспециальнойзаписью,таккаквHTMLониимеютособоезначение.Открывающаяскобка(онажезнак«меньше»)записываетсякак<(«lessthan»,«меньше,чем»),закрывающая—>(“greaterthat”,«больше,чем»).ВHTMLамперсанд&,закоторымидётсловоиточкасзапятой,зовётсясущностьюизаменяетсясимволом,которыйкодируетсяэтойпоследовательностью.

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

HTMLразбираетсяпарсеромдовольнолиберальнопоотношениюквозможнымошибкам.Есликакие-тотегипропущены,браузерихвоссоздаёт.Какименноэтопроисходит,записановстандартах,поэтомуможноожидать,чтовсесовременныебраузерыбудутделатьэтоодинаково.

ВыразительныйJavascript

229JavaScriptибраузер

Page 230: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ВконтекстенашейкнигисамыйглавныйтегHTML—

Следующийдокументбудетобработантакже,какипредыдущий.

<!doctypehtml>

<title>Моядомашняястраничка</title>

<h1>Моядомашняястраничка</h1>

<p>Привет,яМарийниэтомоядомашняястраничка.

<p>Аещёякнижкунаписал!Читайтееё

<ahref=http://eloquentjavascript.net>here</a>.

Отсутствуюттеги,и.Браузерзнает,что

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

Такжеобычноябудуопускатьdoctype.Янесоветуюделатьэтовам–браузерыиногдамогуттворитьстранныевещи,когдавыихопускаете.Считайте,чтоониприсутствуютвпримерахпоумолчанию.

HTMLиJavaScript

ВыразительныйJavascript

230JavaScriptибраузер

Page 231: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Когдавыоткрываетевеб-страницувбраузере,онполучаетисходныйтекстHTMLиразбирает(парсит)егопримернотак,какнашпарсеризглавы11разбиралпрограмму.Браузерстроитмодельструктурыдокументаииспользуетеё,чтобынарисоватьстраницунаэкране.

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

Структурадокумента

МожнопредставитьHTMLкакнаборвложенныхкоробок.Тегивроде<body>и</body>включаютвсебядругиетеги,которыевсвоюочередьвключаюттеги,илитекст.Вотвампримердокументаизпредыдущейглавы:

<!doctypehtml>

<html>

<head>

<title>Моядомашняястраничка</title>

</head>

<body>

<h1>Моядомашняястраничка</h1>

<p>Привет,яМарийниэтомоядомашняястраничка.</p>

<p>Аещёякнижкунаписал!Читайтееё

<ahref="http://eloquentjavascript.net">здесь</a>.</p>

</body>

</html>

Уэтойстраницыследующаяструктура:

DocumentObjectModel

ВыразительныйJavascript

231DocumentObjectModel

Page 232: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Структураданных,использующаясябраузеромдляпредставлениядокумента,отражаетегоформу.Длякаждойкоробкиестьобъект,скоторыммыможемвзаимодействоватьиузнаватьпронегоразныеданные–какойтегонпредставляет,какиекоробкиитекстсодержит.ЭтопредставлениеназываетсяDocumentObjectModel(объектнаямодельдокумента),илисокращённоDOM.

Мыможемполучитьдоступкэтимобъектамчерезглобальнуюпеременнуюdocument.ЕёсвойствоdocumentElementссылаетсянаобъект,представляющийтег.Онтакжепредоставляетсвойстваheadиbody,вкоторыхсодержатсяобъектыдлясоответствующихэлементов.

Вспомнитесинтаксическиедеревьяизглавы11.Ихструктураудивительнопохожанаструктурудокументабраузера.Каждыйузелможетссылатьсянадругиеузлы,укаждогоизответвленийможетбытьсвоёответвление.Этаструктура–типичныйпримервложенныхструктур,гдеэлементысодержатподэлементы,похожиенанихсамих.

Деревья

ВыразительныйJavascript

232DocumentObjectModel

Page 233: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Мызовёмструктуруданныхдеревом,когдаонаразветвляется,неимеетциклов(узелнеможетсодержатьсамсебя),иимеетединственныйярковыраженный«корень».ВслучаеDOMвкачествекорнявыступаетdocument.documentElement.

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

Утипичногодереваестьразныеузлы.УсинтаксическогодереваязыкаEggбылипеременные,значенияиприложения.Уприложенийвсегдабылидочерниеветви,апеременныеизначениябыли«листьями»,тоестьузламибездочернихответвлений.

ТожеиуDOM.Узлыдляобычныхэлементов,представляющихтегиHTML,определяютструктурудокумента.Унихмогутбытьдочерниеузлы.Примертакогоузла—document.body.Некоторыеизэтихдочернихузловмогутоказатьсялистьями–например,текстиликомментарии(вHTMLкомментариизаписываютсямеждусимволами<!--и-->).

УкаждогоузловогообъектаDOMестьсвойствоnodeType,содержащеецифровойкод,определяющийтипузла.Уобычныхэлементовонравен1,чтотакжеопределеноввидесвойства-константыdocument.ELEMENT_NODE.Утекстовыхузлов,представляющихотрывкитекста,онравен3(document.TEXT_NODE).Укомментариев—8(document.COMMENT_NODE).

Тоесть,вотещёодинспособграфическипредставитьдереводокумента:

ВыразительныйJavascript

233DocumentObjectModel

Page 234: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Листья–текстовыеузлы,астрелкипоказываютвзаимоотношенияотец-ребёнокмеждуузлами.

Использоватьзагадочныецифрыдляпредставлениятипаузла–этоподходневстилеJavaScript.ПозжемывстретимсясдругимичастямиинтерфейсаDOM,которыетожекажутсячуждымиинескладными.Причинавтом,чтоDOMразрабатывалсянетолькодляJavaScript.Онпытаетсяопределитьинтерфейс,независящийотязыка,которыйможноиспользоватьивдругихсистемах–нетольковHTML,ноивXML,которыйпредставляетизсебяформатданныхобщегоназначенияссинтаксисом,напоминающимHTML.

Получаетсянеудобно.Хотястандарты–ивесьмаполезнаяштука,внашемслучаепреимуществонезависимостиотязыканетакоеужиполезное.Лучшеиметьинтерфейс,хорошоприспособленныйкязыку,которыйвыиспользуете,чеминтерфейс,которыйбудетзнакомприиспользованииразныхязыков.

Чтобыпоказатьнеудобнуюинтеграциюсязыком,рассмотримсвойствоchildNodes,котороеестьуузловDOM.Внёмсодержитсяобъект,похожийнамассив,сосвойствомlength,ипронумерованныесвойствадлядоступакдочернимузлам.Ноэто–экземпляртипаNodeList,ненастоящиймассив,поэтомуунегонетметодоввродеforEach.

Естьтакжепроблемы,связанныесплохойпродуманностьюсистемы.К

Стандарт

ВыразительныйJavascript

234DocumentObjectModel

Page 235: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

УзлыDOMсодержатмногоссылокнасоседние.Этопоказанонадиаграмме:

Хотятутпоказанотолькопооднойссылкекаждоготипа,укаждогоузлаестьсвойствоparentNode,указывающегонаегородительскийузел.Такжеукаждогоузла-элемента(тип1)естьсвойствоchildNodes,указывающеенамассивоподобныйобъект,содержащийегодочерниеузлы.

Втеорииможнопройтивлюбуючастьдерева,используятолькоэтиссылки.НоJavaScriptпредоставляетнаммногодополнительныхвспомогательныхссылок.СвойстваfirstChildиlastChildпоказываютнапервыйипоследнийдочернийэлементы,илисодержатnullутехузлов,укоторыхнетдочерних.

Обходдерева

ВыразительныйJavascript

235DocumentObjectModel

Page 236: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

previousSiblingиnextSiblingуказываютнасоседниеузлы–узлытогожеродителя,чтоитекущегоузла,нонаходящиесявспискесразудоилипослетекущей.УпервогоузласвойствоpreviousSiblingбудетnull,аупоследнегоnextSiblingбудетnull.

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

functiontalksAbout(node,string){

if(node.nodeType==document.ELEMENT_NODE){

for(vari=0;i<node.childNodes.length;i++){

if(talksAbout(node.childNodes[i],string))

returntrue;

}

returnfalse;

}elseif(node.nodeType==document.TEXT_NODE){

returnnode.nodeValue.indexOf(string)>-1;

}

}

console.log(talksAbout(document.body,"книг"));

//→true

СвойстватекстовогоузлаnodeValueсодержитстрочкутекста.

Частобываетполезнымориентироватьсяпоэтимссылкаммеждуродителями,детьмииродственнымиузламиипроходитьповсемудокументу.Однакоеслинамнуженконкретныйузелвдокументе,оченьнеудобноидтипонему,начинаясdocument.bodyитупоперебираяжёсткозаданныйвкодепуть.Поступаятак,мывносимвпрограммудопущенияоточнойструктуредокумента–аеёмыпозжеможемзахотетьпоменять.Другойусложняющийфактор–текстовыеузлысоздаютсядажедляпробеловмеждуузлами.Вдокументеизпримераутегаbodyнетридочерних(h1идваp),ацелыхсемь:этитриплюспробелыдо,послеимеждуними.

Такчтоеслинамнуженатрибутhrefизссылки,мынедолжныписатьв

Поискэлементов

ВыразительныйJavascript

236DocumentObjectModel

Page 237: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

программечто-товроде:«второйребёнокшестогоребёнкаdocument.body».Лучшебы,еслибмымоглисказать:«перваяссылкавдокументе».Итакможносделать:

varlink=document.body.getElementsByTagName("a")[0];

console.log(link.href);

Увсехузлов-элементовестьметодgetElementsByTagName,собирающийвсеэлементысданнымтэгом,которыепроисходят(прямыеилинепрямыепотомки)отэтогоузла,ивозвращаетеговвидемассивоподобногообъекта.

Чтобынайтиконкретныйузел,можнозадатьемуатрибутidииспользоватьметодdocument.getElementById.

<p>МойстраусГертруда:</p>

<p><imgid="gertrude"src="img/ostrich.png"></p>

<script>

varostrich=document.getElementById("gertrude");

console.log(ostrich.src);

</script>

Третийметод–getElementsByClassName,который,какиgetElementsByTagName,ищетвсодержимомузла-элементаивозвращаетвсеэлементы,содержащиевсвоёмклассезаданнуюстрочку.

ПочтивсёвструктуреDOMможноменять.Уузлов-элементовестьнаборметодов,которыеиспользуютсядляихизменения.МетодremoveChildудаляетзаданнуюдочернийузел.ДлядобавленияузламожноиспользоватьappendChild,которыйдобавляетузелвконецсписка,либоinsertBefore,добавляющийузел,переданнуюпервымаргументом,передузлом,переданнымвторымаргументом.

<p>Один</p>

<p>Два</p>

<p>Три</p>

Меняемдокумент

ВыразительныйJavascript

237DocumentObjectModel

Page 238: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

<script>

varparagraphs=document.body.getElementsByTagName("p");

document.body.insertBefore(paragraphs[2],paragraphs[0]);

</script>

Узелможетсуществоватьвдокументетольководномместе.Поэтомувставляяпараграф«Три»передпараграфом«Один»мыфактическиудаляемегоизконцаспискаивставляемвначало,иполучаем«Три/Один/Два».Всеоперацииповставкеузлаприведуткегоисчезновениюстекущейпозиции(еслиунеготаковаябыла).

МетодreplaceChildиспользуетсядлязаменыодногодочернегоузладругим.Онпринимаетдваузла:новый,итот,которыйнадозаменить.Заменяемыйузелдолженбытьдочернимузломтогоэлемента,чейметодмывызываем.КакreplaceChild,такиinsertBeforeвкачествепервогоаргументаожидаютполучитьновыйузел.

Вследующемпримеренамнадосделатьскрипт,заменяющийвсекартинки(тег<img>)вдокументетекстом,содержащимсявихатрибуте“alt”,которыйзадаётальтернативноетекстовоепредставлениекартинки.

Дляэтогонадонетолькоудалитькартинки,ноидобавитьновыетекстовыеузлыимназамену.Дляэтогомыиспользуемметодdocument.createTextNode.

<p>Это<imgsrc="img/cat.png"alt="Кошка">в

<imgsrc="img/hat.png"alt="сапожках">.</p>

<p><buttononclick="replaceImages()">Заменить</button></p>

<script>

functionreplaceImages(){

varimages=document.body.getElementsByTagName("img");

for(vari=images.length-1;i>=0;i--){

varimage=images[i];

if(image.alt){

vartext=document.createTextNode(image.alt);

image.parentNode.replaceChild(text,image);

}

}

}

Созданиеузлов

ВыразительныйJavascript

238DocumentObjectModel

Page 239: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

</script>

Получаястроку,createTextNodeдаётнамтип3узлаDOM(текстовый),которыймыможемвставитьвдокумент,чтобыонбылпоказаннаэкране.

Циклпокартинкамначинаетсявконцеспискаузлов.Этосделанопотому,чтосписокузлов,возвращаемыйметодомgetElementsByTagName(илисвойствомchildNodes)постояннообновляетсяприизмененияхдокумента.Еслибмыначалисначала,удалениепервойкартинкипривелобыкпотереспискомпервогоэлемента,ивовремявторогопроходацикла,когдаiравно1,онбыостановился,потомучтодлинаспискасталабытакжеравняться1.

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

vararrayish={0:"один",1:"два",length:2};

varreal=Array.prototype.slice.call(arrayish,0);

real.forEach(function(elt){console.log(elt);});

//→один

//два

Длясозданияузлов-элементов(тип1)можноиспользоватьdocument.createElement.Методпринимаетимятегаивозвращаетновыйпустойузелзаданноготипа.Следующийпримеропределяетинструментelt,создающийузел-элементииспользующийостальныеаргументывкачествеегодетей.Этафункцияпотомиспользуетсядлядобавлениядополнительнойинформациикцитате.

<blockquoteid="quote">

Никакаякниганеможетбытьзакончена.Вовремяработынаднеймыузнаёмдостаточнодлятого,чтобынайтиеёнезрелойсразужепослетого,какмыотвлеклисьотнеё.

</blockquote>

<script>

functionelt(type){

varnode=document.createElement(type);

for(vari=1;i<arguments.length;i++){

varchild=arguments[i];

if(typeofchild=="string")

child=document.createTextNode(child);

node.appendChild(child);

}

returnnode;

ВыразительныйJavascript

239DocumentObjectModel

Page 240: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

}

document.getElementById("quote").appendChild(

elt("footer","—",

elt("strong","КарлПоппер"),

",предисловиековторомуизданию",

elt("em","Открытоеобществоиеговраги"),

",1950"));

</script>

Кнекоторымэлементаматрибутов,типаhrefуссылок,можнополучитьдоступчерезодноимённоесвойствообъекта.Этовозможнодляограниченногочислачастоиспользуемыхстандартныхатрибутов.

НоHTMLпозволяетназначатьузламлюбыеатрибуты.Этополезно,т.к.позволяетвамхранитьдополнительнуюинформациювдокументе.Есливыпридумаетесвоиназванияатрибутов,ихнебудетсредисвойствузла-элемента.ВместоэтоговамнадобудетиспользоватьметодыgetAttributeиsetAttributeдляработысними.

<pdata-classified="secret">Кодзапуска00000000.</p>

<pdata-classified="unclassified">Укошкичетыреноги.</p>

<script>

varparas=document.body.getElementsByTagName("p");

Array.prototype.forEach.call(paras,function(para){

if(para.getAttribute("data-classified")=="secret")

para.parentNode.removeChild(para);

});

</script>

Рекомендуюпередименамипридуманныхатрибутовставить“data-“,чтобыбытьуверенным,чтоонинеконфликтуютслюбымидругими.Вкачествепростогопримерамынапишемподсветкусинтаксиса,которыйищеттеги<pre>(“preformatted”,предварительноотформатированный–используетсядлякодаипростоготекста)сатрибутомdata-language(язык)идовольногрубопытаетсяподсветитьключевыесловавязыке.

Атрибуты

ВыразительныйJavascript

240DocumentObjectModel

Page 241: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functionhighlightCode(node,keywords){

vartext=node.textContent;

node.textContent="";//Очистимузел

varmatch,pos=0;

while(match=keywords.exec(text)){

varbefore=text.slice(pos,match.index);

node.appendChild(document.createTextNode(before));

varstrong=document.createElement("strong");

strong.appendChild(document.createTextNode(match[0]));

node.appendChild(strong);

pos=keywords.lastIndex;

}

varafter=text.slice(pos);

node.appendChild(document.createTextNode(after));

}

ФункцияhighlightCodeпринимаетузел

ирегулярку(свключённойнастройкойglobal),совпадающуюсключевымсловомязыкапрограммирования,котороесодержитэлемент.

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

Мыможемавтоматическиподсветитьвеськодстраницы,перебираявциклевсеэлементы<pre>,укоторыхестьатрибутdata-language,ивызываянакаждомhighlightCodeсправильнойрегуляркой.

<sourcelang="javascript">

varlanguages={

javascript:/\b(function|return|var)\b/g/*…etc*/

};

functionhighlightAllCode(){

varpres=document.body.getElementsByTagName("pre");

for(vari=0;i<pres.length;i++){

varpre=pres[i];

varlang=pre.getAttribute("data-language");

if(languages.hasOwnProperty(lang))

highlightCode(pre,languages[lang]);

ВыразительныйJavascript

241DocumentObjectModel

Page 242: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

}

}

</source>

Вотпример:

<sourcelang="html">

<p>Авотиона,функцияидентификации:</p>

<predata-language="javascript">

functionid(x){returnx;}

Естьодинчастоиспользуемыйатрибут,class,имякоторогоявляетсяключевымсловомвJavaScript.Поисторическимпричинам,когдастарыереализацииJavaScriptнеумелиобращатьсясименамисвойств,совпадавшимисключевымисловами,этотатрибутдоступенчерезсвойствоподназваниемclassName.Вытакжеможетеполучитькнемудоступпоегонастоящемуимени“class”черезметодыgetAttributeиsetAttribute.

Вымоглизаметить,чторазныетипыэлементоврасполагаютсяпо-разному.Некоторые,типапараграфов<p>изаголовков<h1>растягиваютсянавсюширинудокументаипоявляютсянаотдельныхстроках.Такиеэлементыназываютблочными.Другие,какссылки<a>илижирныйтекст<strong>появляютсянаоднойстрочкесокружающимихтекстом.Ониназываютсявстроенными(inline).

Длялюбогодокументабраузерымогутпостроитьрасположениеэлементов,расклад,вкоторомукаждогобудетразмериположениенаосновееготипаисодержимого.Затемэтотраскладиспользуетсядлясозданиявнешнеговидадокумента.

РазмериположениеэлементаможноузнатьчерезJavaScript.СвойстваoffsetWidthиoffsetHeightвыдаютразмервпикселях,занимаемыйэлементом.Пиксель–основнаяединицаизмеренийвбраузерах,иобычносоответствуетразмеруминимальнойточкиэкрана.Сходнымобразом,clientWidthиclientHeightдаютразмервнутреннейчастиэлемента,несчитаябордюра(или,какговорятнекоторые,поребрика).

Расположениеэлементов(layout)

ВыразительныйJavascript

242DocumentObjectModel

Page 243: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

<pstyle="border:3pxsolidred">

Явкоробочке

</p>

<script>

varpara=document.body.getElementsByTagName("p")[0];

console.log("clientHeight:",para.clientHeight);

console.log("offsetHeight:",para.offsetHeight);

</script>

Самыйэффективныйспособузнатьточноерасположениеэлементанаэкране–методgetBoundingClientRect.Онвозвращаетобъектсосвойствамиtop,bottom,left,иright(сверху,снизу,слеваисправа),которыесодержатположениеэлементаотносительнолевоговерхнегоуглаэкранавпикселях.Есливамнадополучитьэтиданныеотносительновсегодокумента,вамнадоприбавитьтекущуюпозициюпрокрутки,котораясодержитсявглобальныхпеременныхpageXOffsetиpageYOffset.

Разбордокумента–задачасложная.Вцеляхбыстродействиябраузерныедвижкинеперестраиваютдокументкаждыйразпослеегоизменения,аждуттакдолго.какэтовозможно.КогдапрограммаJavaScript,изменившаядокумент,заканчиваетработу,браузерунадобудетпросчитатьновуюраскладкустраницы,чтобывывестиизменённыйдокументнаэкран.Когдапрограммазапрашиваетпозициюилиразмерчего-либо,читаясвойстватипаoffsetHeightиливызываяgetBoundingClientRect,дляпредоставлениякорректнойинформациитоженеобходиморассчитыватьраскладку.

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

<p><spanid="one"></span></p>

<p><spanid="two"></span></p>

<script>

functiontime(name,action){

varstart=Date.now();//Текущеевремявмиллисекундах

action();

console.log(name,"заняло",Date.now()-start,"ms");

ВыразительныйJavascript

243DocumentObjectModel

Page 244: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

}

time("тупо",function(){

vartarget=document.getElementById("one");

while(target.offsetWidth<2000)

target.appendChild(document.createTextNode("X"));

});

//→тупозаняло32ms

time("умно",function(){

vartarget=document.getElementById("two");

target.appendChild(document.createTextNode("XXXXX"));

vartotal=Math.ceil(2000/(target.offsetWidth/5));

for(vari=5;i<total;i++)

target.appendChild(document.createTextNode("X"));

});

//→умнозаняло1ms

</script>

Мывидели,чторазныеэлементыHTMLведутсебяпо-разному.Некоторыепоказываютсяввидеблоков,другиевстроенные.Некоторыедобавляютвизуальныйстиль–например,<strong>делаетжирнымтексти<a>делаеттекстподчёркнутымисиним.

Внешнийвидкартинкивтеге<img>илито,чтоссылкавтеге<a>прикликеоткрываетновуюстраницу,связаностипомэлемента.Ноосновныестили,связанныесэлементом,вродецветатекстаилиподчёркивания,могутбытьнамиизменены.Вотпримериспользованиясвойстваstyle(стиль):

<p><ahref=".">Обычнаяссылка</a></p>

<p><ahref="."style="color:green">Зелёнаяссылка</a></p>

Атрибутstyleможетсодержатьодноилинесколькообъявленийсвойств(color),закоторымследуетдвоеточиеизначение.Вслучаенесколькихобъявленийониразделяютсяточкойсзапятой:“color:red;border:none”.

Многовсякогоможноизменитьприпомощистилей.Например,свойствоdisplayконтролирует,показываетсялиэлементвблочномиливстроенномвиде.

Стили

ВыразительныйJavascript

244DocumentObjectModel

Page 245: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Текстпоказан<strong>встроенным</strong>,

<strongstyle="display:block">ввидеблока</strong>,и

<strongstyle="display:none">вообщеневиден</strong>.

Блочныйэлементвыводитсяотдельнымблоком,апоследнийвообщеневиден–display:noneотключаетпоказэлементов.Такимобразомможнопрятатьэлементы.Обычноэтопредпочтительнополномуудалениюихиздокумента,потомучтоихлегчепотомпринеобходимостисновапоказать.

КодJavaScriptможетнапрямуюдействоватьнастильэлементачерезсвойствоузлаstyle.Внёмсодержитсяобъект,имеющийсвойствадлявсехсвойствстилей.Ихзначения–строки,вкоторыемыможемписатьдлясменыкакого-тоаспектастиляэлемента.

<pid="para"style="color:purple">

Красотень

</p>

<script>

varpara=document.getElementById("para");

console.log(para.style.color);

para.style.color="magenta";

</script>

Некоторыеименасвойствстилейсодержатдефисы,напримерfont-family.ТаккаксниминеудобнобылобыработатьвJavaScript(пришлосьбыписатьstyle[«font-family»]),названиясвойстввобъектестилейпишутсябездефиса,авместоэтоговнихпоявляютсяпрописныебуквы:style.fontFamily

СистемастилейвHTMLназываетсяCSS(CascadingStyleSheets,каскадныетаблицыстилей).Таблицастилей–наборстилейвдокументе.Егоможнописатьвнутритега<style>:

<style>

strong{

font-style:italic;

color:gray;

Каскадныестили

ВыразительныйJavascript

245DocumentObjectModel

Page 246: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

}

</style>

<p>Теперь<strong>тексттегаstrong</strong>наклонныйисерый.</p>

«Каскадные»означает,чтонесколькоправилкомбинируютсядляполученияокончательногостилядокумента.Впримеренастильпоумолчаниюдля<strong>,которыйделаеттекстжирным,накладываетсяправилоизтега<style>,покоторомудобавляетсяfont-styleицвет.

Когдазначениесвойстваопределяетсянесколькимиправилами,приоритетостаётсяуболеепоздних.Еслибыстильтекстав<style>включалправилоfont-weight:normal,конфликтующеесостилемпоумолчанию,тотекстбылбыобычный,анежирный.Стили,которыеприменяютсякузлучерезатрибутstyle,имеютнаивысшийприоритет.

ВCSSвозможнозадаватьнетольконазваниетегов.Правилодля.abcприменяетсяковсемэлементам,укоторыхуказанкласс“abc”.Правилодля#xyzприменяетсякэлементусатрибутомidравным“xyz”(атрибутыidнеобходимоделатьуникальнымидлядокумента).

.subtle{

color:gray;

font-size:80%;

}

#header{

background:blue;

color:white;

}

/*Элементыp,укоторыхуказаныклассыaиb,аidзаданкакmain*/

p.a.b#main{

margin-bottom:20px;

}

Приоритетсамыхпозднихправилработает,когдауправилодинаковаядетализация.Этомератого,насколькоточнооноописываетподходящиеэлементы,определяемаячисломивидомнеобходимыхаспектовэлементов.Кпримеру,правилодляp.aболеедетально,чемправиладляpилипросто.a,ибудетиметьприоритет.

Записьp>a{…}применимаковсемтегам<a>,находящимсявнутритега<p>иявляющимсяегопрямымипотомками.pa{…}применимотакжеко

ВыразительныйJavascript

246DocumentObjectModel

Page 247: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

всемтегам<a>внутри<p>,приэтомневажно,являетсяли<a>прямымпотомкомилинет.

Вэтойкнигемынебудемчастоиспользоватьтаблицыстилей.Пониманиеихработыкритичнодляпрограммированиявбраузере,ноподробноеразъяснениевсехихсвойствзанялобы2-3книги.Главнаяпричиназнакомстваснимииссинтаксисомселекторов(записей,определяющих,ккакимэлементамотносятсяправила)–мыможемиспользоватьтотжеэффективныймини-языкдляпоискаэлементовDOM.

МетодquerySelectorAll,существующийиуобъектаdocument,иуэлементов-узлов,принимаетстрокуселектораивозвращаетмассивоподобныйобъект,содержащийвсеэлементы,подходящиеподнего.

<p>Люблюгрозувначале

<span>мая</span></p>

<p>Когдавесеннийпервыйгром</p>

<p>Какбы<span>резвяся

<span>ииграя</span></span></p>

<p>Грохочетвнебеголубом.</p>

<script>

functioncount(selector){

returndocument.querySelectorAll(selector).length;

}

console.log(count("p"));//Всеэлементы<p>

//→4

console.log(count(".animal"));//Классanimal

//→2

console.log(count("p.animal"));//Классanimalвнутри<p>

//→2

console.log(count("p>.animal"));//Прямойпотомок<p>

//→1

</script>

ВотличиеотметодоввродеgetElementsByTagName,возвращаемыйquerySelectorAllобъектнеинтерактивный.Оннеизменится,есливыизменитедокумент.

МетодquerySelector(безAll)работаетсходнымобразом.Оннужен,есливам

Селекторызапросов

ВыразительныйJavascript

247DocumentObjectModel

Page 248: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

необходимодинконкретныйэлемент.Онвернёттолькопервоесовпадение,илиnull,еслисовпаденийнет.

Свойствостилейpositionсильновлияетнарасположениеэлементов.Поумолчаниюоноравноstatic,чтоозначает,чтоэлементнаходитсянасвоёмобычномместевдокументе.Когдаоноравноrelative,элементвсёещёзанимаетместо,нотеперьсвойстваtopиleftможноиспользоватьдлясдвигаотносительноегообычногорасположения.Когдаоноравноabsolute,элементудаляетсяизнормального«потока»документа–тоесть,оннезанимаетместоиможетнакладыватьсянадругие.Крометого,егосвойстваleftиtopможноиспользоватьдляабсолютногопозиционированияотносительнолевоговерхнегоуглаближайшеговключающегоегоэлемента,укоторогоpositionнеравноstatic.Аеслитакогоэлементанет,тогдаонпозиционируетсяотносительнодокумента.

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

<pstyle="text-align:center">

<imgsrc="img/cat.png"style="position:relative">

</p>

<script>

varcat=document.querySelector("img");

varangle=0,lastTime=null;

functionanimate(time){

if(lastTime!=null)

angle+=(time-lastTime)*0.001;

lastTime=time;

cat.style.top=(Math.sin(angle)*20)+"px";

cat.style.left=(Math.cos(angle)*200)+"px";

requestAnimationFrame(animate);

}

requestAnimationFrame(animate);

</script>

Картинкаотцентрировананастраницеиейзаданаposition:relative.Мыпостояннообновляемсвойстваtopиleftкартинки,чтобыонадвигалась.

Расположениеианимация

ВыразительныйJavascript

248DocumentObjectModel

Page 249: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

СкриптиспользуетrequestAnimationFrameдлявызовафункцииanimateкаждыйраз,когдабраузерготовперерисовыватьэкран.ФункцияanimateсамаопятьвызываетrequestAnimationFrame,чтобызапланироватьследующееобновление.Когдаокнобраузера(илизакладка)активна,этоприведёткобновлениямсоскоростьпримерно60развсекунду,чтопозволяетдобитьсяхорошовыглядящейанимации.

ЕслибымыпростообновлялиDOMвцикле,страницабызавислаиничегонебылобывидно.БраузерынеобновляютстраницувовремяработыJavaScript,инедопускаютвэтовремяработысостраницей.ПоэтомунамнужнаrequestAnimationFrame–онасообщаетбраузеру,чтомыпоказакончили,ионможетзаниматьсясвоимибраузернымивещами,напримеробновлятьэкраниотвечатьназапросыпользователя.

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

ДвижениепокругуосуществляетсясприменениемтригонометрическихфункцийMath.cosиMath.sin.Якраткоопишуихдлятех,ктосниминезнаком,таккаконипонадобятсянамвдальнейшем.

Math.cosиMath.sinполезнытогда,когданадонайтиточкинакругесцентромвточке(0,0)ирадиусомвединицу.Обефункцииинтерпретируютсвойаргументкакпозициюнакруге,где0обозначаетточкусправогокраякруга,затемнужнопротивчасовойстрелки,покапутьдинойв2π(около6.28)непроведётнаспокругу.Math.cosсчитаеткоординатупоосиxтойточки,котораяявляетсянашейтекущейпозициейнакруге,аMath.sinвыдаёткоординатуy.Позиции(илиуглы)больше,чем2πилименьшечем0,тожедопустимы–поворотыповторяютсятак,чтоa+2πозначаеттотжесамыйугол,чтоиa.

ВыразительныйJavascript

249DocumentObjectModel

Page 250: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Анимациякотахранитсчётчикangleдлятекущегоуглаповоротаанимации,иувеличиваетегопропорциональнопрошедшемувременикаждыйразпривызовефункцииanimation.Этотуголиспользуетсядляподсчётатекущейпозицииэлементаimage.СтильtopподсчитываетсячерезMath.sinиумножаетсяна20–этовертикальныйрадиуснашегоэллипса.СтильleftсчитаетсячерезMath.cosиумножаетсяна200,такчтоширинаэллипсасильнобольшевысоты.

Стилямобычнотребуютсяединицыизмерения.Внашемслучаеприходитсядобавлятьpxкчислу,чтобыобъяснитьбраузеру,чтомысчитаемвпикселях(аневсантиметрах,emsилидругихединицах).Этолегкозабыть.Использованиечиселбезединицизмеренияприведёткигнорированиюстиля–еслитолькочислонеравно0,чтонезависитотединицизмерения.

ПрограммыJavaScriptмогутизучатьиизменятьтекущийотображаемыйбраузеромдокументчерезструктуруподназваниемDOM.Этаструктураданныхпредставляетмодельдокументабраузера,апрограммаJavaScriptможетизменятьеёдляизменениявидимогодокумента.DOMорганизованввидедерева,вкоторомэлементырасположеныиерархическивсоответствиисоструктуройдокумента.УобъектовэлементовестьсвойстватипаparentNodeиchildNodes,которыиспользуютсядляориентированиянадереве.

Использованиесинусаикосинусадлявычислениякоординат

Итог

ВыразительныйJavascript

250DocumentObjectModel

Page 251: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Внешнийвиддокументаможноизменятьчерезстили,либодобавляястиликузламнапрямую,либоопределяяправиладлякаких-либоузлов.Устилейестьоченьмногосвойств,таких,какcolorилиdisplay.JavaScriptможетвлиятьнастильэлементанапрямуючерезегосвойствоstyle.

Мыстроилитаблицыизпростоготекставглаве6.HTMLупрощаетпостроениетаблиц.ТаблицавHTMLстроитсяприпомощиследующихтегов:

<table>

<tr>

<th>name</th>

<th>height</th>

<th>country</th>

</tr>

<tr>

<td>Kilimanjaro</td>

<td>5895</td>

<td>Tanzania</td>

</tr>

</table>

Длякаждойстрокивтеге<table>содержитсятег<tr>.Внутринегомыможемразмещатьячейки:либоячейкизаголовков<th>,либообычныеячейки<td>.

Тежеданные,чтомыиспользоваливглаве6,сновадоступнывпеременнойMOUNTAINS.

НапишитефункциюbuildTable,которая,принимаямассивобъектовсодинаковымисвойствами,строитструктуруDOM,представляющуютаблицу.Утаблицыдолжнабытьстрокасзаголовками,гдеименасвойствобёрнутывэлементы<th>,идолжнобытьпооднойстрочкенаобъектизмассива,гдеегосвойстваобёрнутывэлементы<td>.ЗдесьпригодитсяфункцияObject.keys,возвращающаямассив,содержащийименасвойствобъекта.

Упражнения

Строимтаблицу

ВыразительныйJavascript

251DocumentObjectModel

Page 252: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Когдавыразберётесьсосновами,выровняйтеячейкисчисламипоправомукраю,изменивихсвойствоstyle.textAlignна«right».

<style>

/*Определяетстилидлякрасивыхтаблиц*/

table{border-collapse:collapse;}

td,th{border:1pxsolidblack;padding:3px8px;}

th{text-align:left;}

</style>

<script>

functionbuildTable(data){

//Вашкод

}

document.body.appendChild(buildTable(MOUNTAINS));

</script>

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

Чтобывыяснитьимятегаэлемента,используйтесвойствоtagName.Заметьте,чтооновозвратитимятегавверхнемрегистре.ИспользуйтеметодыстрокtoLowerCaseилиtoUpperCase.

<h1>Заголовоксэлементом<span>span</span>внутри.</h1>

<p>Параграфс<span>раз</span>,<span>два</span>элементамиspans.</p>

<script>

functionbyTagName(node,tagName){

//Вашкод

}

console.log(byTagName(document.body,"h1").length);

//→1

console.log(byTagName(document.body,"span").length);

//→3

varpara=document.querySelector("p");

console.log(byTagName(para,"span").length);

//→2

Элементыпоименитегов

ВыразительныйJavascript

252DocumentObjectModel

Page 253: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

</script>

Расширьтеанимациюкота,чтобыикотиегошляпа<imgsrc="img/hat.png">леталипопротивоположнымсторонамэллипса.

Илипустьшляпалетаетвокругкота.Илиещёчто-нибудьинтересноепридумайте.

Чтобыупроститьрасположениемножестваобъектов,неплохобудетпереключитьсянаабсолютноепозиционирование.Тогдаtopиleftбудутсчитатьсяотносительнолевоговерхнегоугладокумента.Чтобынеиспользоватьотрицательныекоординаты,выможетедобавитьзаданноечислопикселейкзначениямposition.

<imgsrc="img/cat.png"id="cat"style="position:absolute">

<imgsrc="img/hat.png"id="hat"style="position:absolute">

<script>

varcat=document.querySelector("#cat");

varhat=document.querySelector("#hat");

//Yourcodehere.

</script>

Шляпакота

ВыразительныйJavascript

253DocumentObjectModel

Page 254: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Вывластнынадсвоимразумом,ноненадвнешнимисобытиями.Когдавыпоймётеэто,выобретётесилу.МаркАврелий,«Медитации».

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

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

Такимобразомвводобрабатывалсянапримитивныхустройствах.Шагомвперёдбылобы,еслижелезоилиоперационказамечалибынажатиекнопкиипередавалибыеговочередь.Затемпрограммапериодическимоглабыпроверятьочередьнановыесобытияиреагироватьнато,чтонаходитсявочереди.

Разумеется,онадолжнапомнитьопроверке,иделатьэтодостаточночасто,потомучтоналичиедлительногопромежуткавременимеждунажатиемкнопкиитем,когдапрограммазамечаетиреагируетнаэто,ведётквосприятиюэтойпрограммыкакмедленноработающей.Такойподходиспользуетсядостаточноредко.

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

Обработкасобытий

Обработчикисобытий

ВыразительныйJavascript

254Обработкасобытий

Page 255: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

<p>Щёлкнитеподокументудлязапускаобработчика.</p>

<script>

addEventListener("click",function(){

console.log("Щёлк!");

});

</script>

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

Каждыйобработчиксобытийбраузеразарегистрированвконтексте.КогдавывызываетеaddEventListener,вывызываетееёкакметодцелогоокна,потомучтовбраузереглобальнаяобластьвидимости–этообъектwindow.УкаждогоэлементаDOMестьсвойметодaddEventListener,позволяющийслушатьсобытияотэтогоэлемента.

<button>Нажмименянежно.</button>

<p>Аздесьнетобработчиков.</p>

<script>

varbutton=document.querySelector("button");

button.addEventListener("click",function(){

console.log("Кнопканажата.");

});

</script>

ПримерназначаетобработчикнаDOM-узелкнопки.Нажатиянакнопкузапускаютобработчик,анажатиянадругиечастидокумента–незапускают.

Присвоениеузлуатрибутаonclickработаетпохоже.Ноуузлаестьтолькоодинатрибутonclick,значиттакимспособомвыможетезарегистрироватьтолькоодинобработчик.МетодaddEventListenerпозволяетдобавлятьлюбоеколичествообработчиков,такчтовынезаменитеслучайноуженазначенныйранееобработчик.

МетодremoveEventListener,вызванныйстакимижеаргументами,какaddEventListener,удаляетобработчик.

СобытияиузлыDOM

ВыразительныйJavascript

255Обработкасобытий

Page 256: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

<button>Act-oncebutton</button>

<script>

varbutton=document.querySelector("button");

functiononce(){

console.log("Done.");

button.removeEventListener("click",once);

}

button.addEventListener("click",once);

</script>

Чтобыэтопровернуть,мыдаёмфункцииимя(вданномслучае,once),чтобыеёможнобылопередатьивaddEventListener,ивremoveEventListener.

Впримерахмыпроигнорировалитотфакт,чтофункциям-обработчикампередаётсяаргумент–объектсобытия.Внёмхранитсядополнительнаяинформацияособытии.Кпримеру,еслинадоузнать,какаякнопкамышибыланажата,мыможемобратитьсяксвойствуwhichэтогообъекта.

<button>Жмименя,чемхочешь!</button>

<script>

varbutton=document.querySelector("button");

button.addEventListener("mousedown",function(event){

if(event.which==1)

console.log("Левая");

elseif(event.which==2)

console.log("Средняя");

elseif(event.which==3)

console.log("Правая");

});

</script>

Хранящаясявобъектеинформация–разнаядлякаждоготипасобытий.Мыобсудимэтитипыпозже.Свойствообъектаtypeвсегдасодержитстроку,описывающуюсобытие(например,«click»или«mousedown»).

События,зарегистрированныенаузлах,имеющихдочерниеузлы,получати

Объектысобытий

Распространение(propagation)

ВыразительныйJavascript

256Обработкасобытий

Page 257: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

некоторыесобытия,случившиесясихдетьми.Есликликнутьнакнопкувнутрипараграфа,обработчикисобытийпараграфаполучатсобытиеclick.

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

ВлюбоймоментобработчикможетвызватьметодstopPropagationобъектасобытия,чтобы«высшие»узлынеполучилиего.Этоможетбытьполезным,когдаувасестькнопкавнутридругогокликабельногоэлемента,ивынехотите,чтобыкликипокнопкеактивировалиповедениевнешнегоэлемента.

Следующийпримеррегистрируетобработчики«mousedown»какнакнопке,такинаокружающемпараграфе.ПрищелчкеправойкнопкойобработчиккнопкивызываетstopPropagation,которыйпредотвращаетзапускобработчикапараграфа.Прикликедругойкнопкойзапускаютсяобаобработчика.

<p>Параграфс<button>кнопкой</button>.</p>

<script>

varpara=document.querySelector("p");

varbutton=document.querySelector("button");

para.addEventListener("mousedown",function(){

console.log("Обработчикпараграфа.");

});

button.addEventListener("mousedown",function(event){

console.log("Обработчиккнопки.");

if(event.which==3)

event.stopPropagation();

});

</script>

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

Такжевозможноиспользоватьсвойствоtarget,чтобыраспространитьобработкуконкретноготипасобытия.Кпримеру,еслиувасестьузел,

ВыразительныйJavascript

257Обработкасобытий

Page 258: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

<button>A</button>

<button>B</button>

<button>C</button>

<script>

document.body.addEventListener("click",function(event){

if(event.target.nodeName=="BUTTON")

console.log("Clicked",event.target.textContent);

});

</script>

Умногихсобытийестьдействияпоумолчанию.Прикликенассылкувыперейдётепоней.Принажатиинастрелкувнизбраузерпрокрутитстраницувниз.Поправомукликумышивыувидитеконтекстноеменю.Итакдалее.

Длябольшинстватиповсобытийобработчикисобытийвызываютсядотого,каксработаетдействиепоумолчанию.Еслиобработчикнехочет,чтобыэтодействиепроисходило(частопотому,чтоонужеобработалего),онможетвызватьметодpreventDefaultобъектасобытия.

Этоможноиспользоватьдлясозданиясвоихгорячихклавишиликонтекстногоменю.Такжеэтоможноиспользоватьдлясломапривычногопользователюинтерфейса.Кпримеру,вотссылка,покоторойнельзяпройти.

<ahref="https://developer.mozilla.org/">MDN</a>

<script>

varlink=document.querySelector("a");

link.addEventListener("click",function(event){

console.log("Фигушки.");

event.preventDefault();

});

</script>

Неделайтетак–еслиуваснеточеньсерьёзнойпричины!Пользователям

Действияпоумолчанию

ВыразительныйJavascript

258Обработкасобытий

Page 259: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

вашейстраницыбудеточеньнеудобно,когдаонистолкнутсяснеожиданнымирезультатамисвоихдействий.Взависимостиотбраузера,некоторыесобытияперехватитьнельзя.ВChromeнельзяобрабатыватьгорячиеклавишизакрытиятекущейзакладки(Ctrl-WorCommand-W).

Принажатиикнопкинаклавиатуребраузерзапускаетсобытие«keydown».Когдаонаотпускается,происходитсобытие«keyup».

<p>СтраницапонажатиюVофиолетивает.</p>

<script>

addEventListener("keydown",function(event){

if(event.keyCode==86)

document.body.style.background="violet";

});

addEventListener("keyup",function(event){

if(event.keyCode==86)

document.body.style.background="";

});

</script>

Несмотрянаназвание,«keydown»происходитнетолькотогда,когданакнопкунажимают.Еслинажатьиудерживатькнопку,событиебудетпроисходитькаждыйразпоприходуповторногосигналаотклавиши(keyrepeat).Есливам,кпримеру,надоувеличиватьскоростьигровогоперсонажа,когданажатакнопкасострелкой,иуменьшатьеё,когдаонаотпущена–надобытьосторожным,чтобынеувеличитьскоростькаждыйразприповторесигналаоткнопки,иначескоростьвозрастёточеньсильно.

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

ДляцифрибуквкодбудеткодомсимволаUnicode,связанногоспрописнымсимволом,изображённымнакнопке.МетодстрокиcharCodeAtдаётнамэтоткод.

console.log("Violet".charCodeAt(0));

Событияоткнопокклавиатуры

ВыразительныйJavascript

259Обработкасобытий

Page 260: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

//→86

console.log("1".charCodeAt(0));

//→49

Удругихкнопоккодыменеепредсказуемы.Лучшийспособихвыяснить–экспериментальный.Зарегистрироватьобработчик,которыйзаписываеткодыклавиш,инажатьнужнуюкнопку.

Кнопки-модификаторытипаShift,Ctrl,Alt,иMeta(CommandнаMac)создаютсобытия,какинормальныекнопки.Ноприразборекомбинацийклавишможновыяснить,былилинажатымодификаторы,черезсвойстваshiftKey,ctrlKey,altKey,иmetaKeyсобытийклавиатурыимыши.

<p>НажмитеCtrl-Spaceдляпродолжения.</p>

<script>

addEventListener("keydown",function(event){

if(event.keyCode==32&amp;&amp;event.ctrlKey)

console.log("Продолжаем!");

});

</script>

События«keydown»и«keyup»даютинформациюофизическомнажатиикнопок.Аесливамнужноузнать,какойтекствводитпользователь?Создаватьегоизнажатийкнопок–неудобно.Дляэтогосуществуетсобытие«keypress»,происходящеесразупосле«keydown»(иповторяющеесявместес«keydown»,есликлавишупродолжаютудерживать),нотолькодлятехкнопок,которыевыдаютсимволы.СвойствообъектасобытияcharCodeсодержиткод,которыйможноинтерпретироватькаккодUnicode.МыможемиспользоватьфункциюString.fromCharCodeдляпревращениякодавстрокуизодногосимвола.

<p>Переведитефокуснастраницуипечатайте.</p>

<script>

addEventListener("keypress",function(event){

console.log(String.fromCharCode(event.charCode));

});

</script>

Источникомсобытиянажатияклавишиузелстановитсявзависимостиоттого,гденаходилсяфокусвводавовремянажатия.Обычныеузлынемогут

ВыразительныйJavascript

260Обработкасобытий

Page 261: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

получитьфокусввода(еслитольковынезадалииматрибутtabindex),атакие,какссылки,кнопкииполяформ–могут.Мывернёсякполямвводавглаве18.Когданиучегонетфокуса,вкачествецелевогоузласобытийработаетdocument.body

Нажатиекнопкимышитожезапускаетнесколькособытий.События«mousedown»и«mouseup»похожина«keydown»и«keyup»,изапускаются,когдакнопканажатаикогдаотпущена.СобытияпроисходятутехузловDOM,надкоторыминаходилсякурсормыши.

Послесобытия«mouseup»наузле,накоторыйпришлисьинажатие,иотпусканиекнопки,запускаетсясобытие“click”.Например,еслиянажалкнопкунадоднимпараграфом,потомпередвинулмышьнадругойпараграфиотпустилкнопку,событие“click”случитсяуэлемента,которыйсодержалвсебеобаэтипараграфа.

Еслидващелкапроисходятдостаточнобыстродругзадругом,запускаетсясобытие«dblclick»(double-click),сразупослевторогозапуска“click”.

Дляполученияточныхкоординатместа,гдепроизошлособытиемыши,обратитеськсвойствамpageXиpageY–онисодержаткоординатывпикселяхотносительноверхнеголевогоугла.

Впримересозданапримитивнаяпрограммадлярисования.Каждыйразпокликунадокументеондобавляетточкуподвашимкурсором.Вглаве19будетпредставленаменеепримитивнаяпрограммадлярисования.

<style>

body{

height:200px;

background:beige;

}

.dot{

height:8px;width:8px;

border-radius:4px;/*скруглённыеуглы*/

background:blue;

position:absolute;

}

Кнопкимыши

ВыразительныйJavascript

261Обработкасобытий

Page 262: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

</style>

<script>

addEventListener("click",function(event){

vardot=document.createElement("div");

dot.className="dot";

dot.style.left=(event.pageX-4)+"px";

dot.style.top=(event.pageY-4)+"px";

document.body.appendChild(dot);

});

</script>

СвойстваclientXиclientYпохожинаpageXиpageY,нодаюткоординатыотносительночастидокумента,котораявиднасейчас(еслидокументбылпрокручен).Этоудобноприсравнениикоординатмышискоординатами,которыевозвращаетgetBoundingClientRect–еговозвраттожесвязансотносительнымикоординатамивидимойчастидокумента.

Каждыйразприсдвигекурсорамышизапускаетсясобытие«mousemove».Егоможноиспользоватьдляотслеживанияпозициимыши.Обычноэтонужноприсозданиинекоейфункциональности,связаннойсперетаскиваниемобъектовмышью.

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

<p>Переместитемышьдляувеличенияширины:</p>

<divstyle="background:orange;width:60px;height:20px">

</div>

<script>

varlastX;//Последняяпозициямыши

varrect=document.querySelector("div");

rect.addEventListener("mousedown",function(event){

if(event.which==1){

lastX=event.pageX;

addEventListener("mousemove",moved);

event.preventDefault();//Запретимвыделение

}

});

functionmoved(event){

Движениемыши

ВыразительныйJavascript

262Обработкасобытий

Page 263: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

if(event.which!=1){

removeEventListener("mousemove",moved);

}else{

vardist=event.pageX-lastX;

varnewWidth=Math.max(10,rect.offsetWidth+dist);

rect.style.width=newWidth+"px";

lastX=event.pageX;

}

}

</script>

Обратитевнимание–обработчик«mousemove»зарегистрированувсегоокна.Дажееслимышьуходитзапределыполоски,намнадообновлятьеёразмерипрекращатьэто,когдакнопкуотпускают.

Когдакурсорпопадаетнаузелиуходитснего,происходятсобытия«mouseover»or«mouseout».Ихможноиспользовать,кромепрочего,длясозданияэффектовпроведениямыши,показываяилименяястильчего-либо,когдакурсорнаходитсянадэтимэлементом.

Ксожалению,созданиетакогоэффектанеограничиваетсязапускомегоприсобытии«mouseover»изавершениемприсобытии«mouseout».Придвижениимышиотузлакегодочернимузламнародительскомузлепроисходитсобытие«mouseout»,хотямышь,вообщеговоря,егоинепокидала.Чтоещёхуже,этисобытияраспространяютсякакивседругие,поэтомувывсёравнополучаете«mouseout»приуходекурсорасодногоихдочернихузловтогоузла,гдевызарегистрировалиобработчик.

ДляобходапроблемыможноиспользоватьсвойствоrelatedTargetобъектасобытий.Онсообщает,накакомузлебыладоэтогомышьпривозникновениисобытия«mouseover»,инакакойэлементонапереходитприсобытии«mouseout».Намнадоменятьэффект,толькокогдаrelatedTargetнаходитсявненашегоцелевогоузла.Тольковэтомслучаесобытиенасамомделепредставляетсобойпереходнанашузел(илиуходсузла).

<p>Наведитемышьнаэтот<strong>параграф</strong>.</p>

<script>

varpara=document.querySelector("p");

functionisInside(node,target){

for(;node!=null;node=node.parentNode)

if(node==target)returntrue;

}

ВыразительныйJavascript

263Обработкасобытий

Page 264: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

para.addEventListener("mouseover",function(event){

if(!isInside(event.relatedTarget,para))

para.style.color="red";

});

para.addEventListener("mouseout",function(event){

if(!isInside(event.relatedTarget,para))

para.style.color="";

});

</script>

ФункцияisInsideперебираетвсехпредковузла,поканедоходитдоверхадокумента(итогдаузелравенnull),илижененаходитзаданногоейродителя.

Должендобавить,чтотакойэффектдостижимгораздопрощечерезпсевдоселекторCSSподназванием:hover,какпоказанониже.Нокогдапринаведениивамнадоделатьчто-тоболеесложное,чемизменениестиляузла,придётсяиспользоватьтрюкссобытиями«mouseover»и«mouseout».

<style>

p:hover{color:red}

</style>

<p>Наведитемышьнаэтот<strong>параграф</strong>.</p>

Когдаэлементпрокручивается,запускаетсясобытие«scroll».Этоиспользуетсявомногихслучаях,напримерчтобыузнать,начтосейчаспользовательсмотрит(чтобыостанавливатьанимацию,непопавшуюнаэкран,илиотправлятьсекретныешпионскиедонесенияввашзлодейскийштаб),иливизуальнодемонстрироватьпрогресс(подсвечиваячастьсодержанияилипоказываяномерстраницы).

Впримеревправомверхнемуглудокументасоздаётсяиндикаторпроцесса,которыйзаполняетсяпомерепрокруткиэлементавниз.

<style>

.progress{

border:1pxsolidblue;

width:100px;

Событияпрокрутки

ВыразительныйJavascript

264Обработкасобытий

Page 265: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

position:fixed;

top:10px;right:10px;

}

.progress>div{

height:12px;

background:blue;

width:0%;

}

body{

height:2000px;

}

</style>

<div><div></div></div>

<p>Scrollme...</p>

<script>

varbar=document.querySelector(".progressdiv");

addEventListener("scroll",function(){

varmax=document.body.scrollHeight-innerHeight;

varpercent=(pageYOffset/max)*100;

bar.style.width=percent+"%";

});

</script>

Позицияэлементаfixedозначаетпочтитоже,чтоabsolute,ноещёипредотвращаетпрокручиваниеэлементавместесостальнымдокументом.Смыслвтом,чтобыоставитьнашиндикаторвуглу.Внутринегонаходитсядругойэлемент,которыйизменяетразмер,отражаятекущийпрогресс.Мыиспользуемпроцентывместоpxдлязаданияширины,чтобыразмерэлементаизменялсяотносительноразмеравсегоиндикатора.

ГлобальнаяпеременнаяinnerHeightдаётвысотуокна,которуюнадовычестьизполнойвысотыпрокручиваемогоэлемента–придостиженииконцаэлементапрокрутказаканчивается.(ТакжевдополнениекinnerHeightестьпеременнаяinnerWidth).ПоделивтекущуюпозициюпрокруткиpageYOffsetнамаксимальнуюпозициюпрокрутки,иумноживна100,мыполучилипроцентдляиндикатора.

ВызовpreventDefaultнепредотвращаетпрокрутку.Обработчиксобытиявызываетсяужепослетого,какпрокруткаслучилась.

События,связанныесфокусом

ВыразительныйJavascript

265Обработкасобытий

Page 266: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Приполученииэлементомфокусабраузерзапускаетсобытие“focus”.Когдаонтеряетфокус,запускаетсясобытие“blur”.

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

Следующийпримердемонстрируеттекстподсказкидлятоготекстовогополя,укотороговданныймоментфокус.

<p>Имя:<inputtype="text"data-help="Вашеполноеимя"></p>

<p>Возраст:<inputtype="text"data-help="Возраствгодах"></p>

<pid="help"></p>

<script>

varhelp=document.querySelector("#help");

varfields=document.querySelectorAll("input");

for(vari=0;i<fields.length;i++){

fields[i].addEventListener("focus",function(event){

vartext=event.target.getAttribute("data-help");

help.textContent=text;

});

fields[i].addEventListener("blur",function(event){

help.textContent="";

});

}

</script>

Объектwindowполучаетсобытияfocusиblur,когдапользовательвыделяетилиубираетфокуссзакладкибраузераилиокнабраузера,вкоторомпоказандокумент.

Когдазаканчиваетсязагрузкастраницы,наобъектахwindowиbodyзапускаетсясобытие“load”.Эточастоиспользуетсядляпланированияинициализирующихдействий,которымнеобходимполностьюпостроенныйдокумент.Вспомните,чтосодержимоетегов<script>запускаетсясразу,кактолькотегвстречается.Иногдаэтослишкомрано–например,когдаскриптунужночто-тосделатьстемичастямидокумента,которыенаходятсяпослетега<script>.

Событиезагрузки

ВыразительныйJavascript

266Обработкасобытий

Page 267: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Уэлементовтипакартинокилитеговскрипта,которыезагружаютвнешнийфайл,тожеестьсобытие“load”,котороепоказывает,чтофайлзагружен.Какисобытияфокуса,событиязагрузкинераспространяются.

Когдастраницазакрываетсяилиснеёуходят(например,поссылке),запускаетсясобытие«beforeunload».Основнаяцель–защититьпользователяотслучайнойпотериданныхпризакрытиидокумента.Предотвращениезакрытиястраницынепроизводится,каквымоглиподумать,припомощиpreventDefault.Вместоэтогоиспользуетсявозвратстрокиизобработчика.Строкабудетиспользованавдиалоге,которыйспрашиваетпользователя,хочетлионостатьсянастраницеилипокинутьеё.Этотмеханизмгарантирует,чтопользовательможетпокинутьстраницу,дажееслинанейработаетзловредныйскрипт,которыйбыхотелнеотпускатьпользователя,авместоэтогопоказывалбыемумошенническуюрекламупоснижениювеса.

Нескольковещеймогутпривестикстартускрипта.Чтениетега<script>—однаизних.Запусксобытия–ещёодна.Вглаве13обсуждаетсяфункцияrequestAnimationFrame,котораяпланируетзапускфункциипередследующейперерисовкойстраницы.Этоещёодинспособзапуститьскрипт.

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

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

Графиквыполненияскрипта

ВыразительныйJavascript

267Обработкасобытий

Page 268: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

То,чтопрограммыJavaScriptделаютпооднойвещизараз,облегчаетнашужизнь.Есливамоченьнадосделатьвфонечто-тотяжёлое,неподвешиваяприэтомстраницу,браузерыпредоставляютштукуподназванием«сетевыерабочие»(webworker)–изолированноеокружениеJavaScript,работающеевместесглавнойпрограммойнаддокументом,котороеможетобщатьсяснейтолькопосредствомсообщений.

Предположим,унасестьследующийкодвфайлеcode/squareworker.js:

addEventListener("message",function(event){

postMessage(event.data*event.data);

});

Представьте,чтовозведениевквадрат–оченьтяжёлое,долгоработающеевычисление,котороенамнадозапуститьфоновымпотоком.Такойкодпорождает«рабочего»,отправляетемунесколькосообщений,ивыводитрезультаты.

varsquareWorker=newWorker("code/squareworker.js");

squareWorker.addEventListener("message",function(event){

console.log("Theworkerresponded:",event.data);

});

squareWorker.postMessage(10);

squareWorker.postMessage(24);

ФункцияpostMessageотправляетсообщение,котороезапускаетсобытие“message”упринимающейстороны.Скрипт,создавшийрабочего,отправляетиполучаетсообщениячерезобъектWorker,тогдакакрабочийобщаетсясоскриптом,создавшимего,отправляяиполучаясообщениячерезегособственноеглобальноеокружение–котороеявляетсяотдельнымокружением,несвязаннымсоригинальнымскриптом.

ФункцияsetTimeoutсхожасrequestAnimationFrame.Онапланируетзапускдругойфункциивбудущем.Новместовызовафункцииприследующейперерисовкестраницы,онаждётзаданноевмиллисекундахвремя.Этастраницачерездвесекундыпревращаетсяизсинейвжёлтую:

Установкатаймеров

ВыразительныйJavascript

268Обработкасобытий

Page 269: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

<script>

document.body.style.background="blue";

setTimeout(function(){

document.body.style.background="yellow";

},2000);

</script>

Иногдавамнадоотменитьзапланированнуюфункцию.Этоможносделать,сохранивзначение,возвращаемоеsetTimeout,изатемвызвавснимclearTimeout.

varbombTimer=setTimeout(function(){

console.log("BOOM!");

},500);

if(Math.random()<0.5){//50%chance

console.log("Defused.");

clearTimeout(bombTimer);

}

ФункцияcancelAnimationFrameработаеттакже,какclearTimeout–вызовеёсозначением,возвращённымrequestAnimationFrame,отменитэтоткадр(еслионуженебылвызван).

Похожийнаборфункций,setIntervalиclearIntervalиспользуетсядляустановкитаймеров,которыебудутповторятьсякаждыеXмиллисекунд.

varticks=0;

varclock=setInterval(function(){

console.log("tick",ticks++);

if(ticks==10){

clearInterval(clock);

console.log("stop.");

}

},200);

Унекоторыхсобытийестьвозможностьвыполнятьсябыстроимногоразподряд(например,«mousemove»и«scroll»).Приобработкетакихсобытий

Устранениепомех(debouncing)

ВыразительныйJavascript

269Обработкасобытий

Page 270: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Есливтакомобработчикенадосделатьчто-тонетривиальное,можноиспользоватьsetTimeout,чтобыгарантировать,чтовыделаетеэтонеслишкомчасто.Этообычноназывают«устранениемпомех»всобытии.Кэтомусуществуетнесколькослегкаразличающихсяподходов.

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

<textarea>Напишитетутчто-нибудь...</textarea>

<script>

vartextarea=document.querySelector("textarea");

vartimeout;

textarea.addEventListener("keydown",function(){

clearTimeout(timeout);

timeout=setTimeout(function(){

console.log("Выостановились.");

},500);

});

</script>

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

Можноиспользоватьнемногодругойподход,еслинамнадоразделитьответыминимальнымипромежуткамивремени,ноприэтомзапускатьихвтовремя,когдапроисходятсобытия,анепосле.Кпримеру,надореагироватьнасобытия«mousemove»,показываятекущиекоординатымыши,нотолькокаждые250миллисекунд.

ВыразительныйJavascript

270Обработкасобытий

Page 271: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

<script>

functiondisplayCoords(event){

document.body.textContent=

"Мышьна"+event.pageX+","+event.pageY;

}

varscheduled=false,lastEvent;

addEventListener("mousemove",function(event){

lastEvent=event;

if(!scheduled){

scheduled=true;

setTimeout(function(){

scheduled=false;

displayCoords(lastEvent);

},250);

}

});

</script>

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

Усобытийестьопределяющийихтип(«keydown»,«focus»,итакдалее).БольшинствособытийвызываютсяконкретнымиузламиDOM,изатемраспространяютсянаихпредков,позволяясвязаннымиснимиобработчикамобрабатыватьих.

Привызовеобработчикаемупередаётсяобъектсобытиясдополнительнойинформациейособытии.Уобъектатакжеестьметоды,позволяющиеостановитьдальнейшеераспространение(stopPropagation)ипредотвратитьобработкусобытиябраузеромпоумолчанию(preventDefault).

Нажатиянаклавишизапускаютсобытия«keydown»,«keypress»и«keyup».Нажатиянакнопкимышизапускаютсобытия«mousedown»,«mouseup»и«click».Движениямышизапускаютсобытия«mousemove»,ивозможно«mouseenter»и«mouseout».

Прокруткуможнообнаружитьчерезсобытие“scroll”,аизмененияфокуса

Итог

ВыразительныйJavascript

271Обработкасобытий

Page 272: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

черезсобытия«focus»и«blur».Когдазаканчиваетсязагрузкадокумента,уобъектаwindowзапускаетсясобытие“load”.

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

Впромежуткес1928по2013годтурецкиезаконызапрещалииспользованиебуквQ,WиXвофициальныхдокументах.Этоявлялосьчастьюобщейинициативыподавлениякурдскойкультуры–этибуквыиспользуютсявязыкекурдов,нонеутурков.

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

<inputtype="text">

<script>

varfield=document.querySelector("input");

//Yourcodehere.

</script>

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

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

Упражнения

Цензураклавиатуры

Следмыши

ВыразительныйJavascript

272Обработкасобытий

Page 273: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

курсором.

Кэтомуможноподойтимногимиспособами.Можносделатьоченьпростоеилиоченьсложноерешение,какугодно.Простое–хранитьфиксированноеколичествоэлементовипроходитьпонимвцикле,двигаякаждыйследующийнатекущееместокурсора,каждыйразкогдаслучаетсясобытие«mousemove».

<style>

.trail{/*classNameдляэлементов,летящихзакурсором*/

position:absolute;

height:6px;width:6px;

border-radius:3px;

background:teal;

}

body{

height:300px;

}

</style>

<script>

//Вашкод.

</script>

Интерфейсзакладоквстречаетсячасто.Онпозволяетвамвыбиратьпанельинтерфейса,выбираяоднуизнесколькихторчащихзакладокнадэлементом.

Вупражнениивамнужносделатьпростойинтерфейсзакладок.НапишитефункциюasTabs,котораяпринимаетузелDOM,исоздаётзакладочныйинтерфейс,показываядочерниеэлементыэтогоузла.Ейнужновставлятьсписокэлементов<button>вверхуузла,поодномунакаждыйдочернийэлемент,содержащихтекст,полученныйизатрибутаdata-tabname.Все,кромеодногоиздочернихэлементов,должныбытьспрятаны(припомощиdisplaystylenone),атекущийвидимыйузелможновыбиратьнажатиемкнопки.

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

Закладки

ВыразительныйJavascript

273Обработкасобытий

Page 274: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

<divid="wrapper">

<divdata-tabname="one">Закладкаодин</div>

<divdata-tabname="two">Закладкадва</div>

<divdata-tabname="three">Закладкатри</div>

</div>

<script>

functionasTabs(node){

//Вашкод.

}

asTabs(document.querySelector("#wrapper"));

</script>

ВыразительныйJavascript

274Обработкасобытий

Page 275: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Всянашажизнь–игра.ИйенБэнкс,«Игрок»

Впервыеяувлёксякомпьютерами,какибольшинстводетей,черезкомпьютерныеигры.Менязатянулововселеннуюсимулированныхмиров,которымиможнобылоуправлять,ивкоторыхрассказывалисьистории–мнекажетсябольшепотому,чтовнихбылданпростормоемувоображению,чемиз-зареальныхвозможностей,которыеонипредоставляли.

Никомубынепожелалкарьеруигровогопрограммиста.Какивмузыкальнойиндустрии,несоответствиемеждуколичествоммолодыхлюдей,желающихпопастьтудаиреальнымспросомнаних,создаётнездоровуюсреду.Нонаписаниеигрдляразвлечения–этооченьздорово.

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

НашаиграбудетпримернобазироватьсянаигреDarkBlueотТомасаПалефа.Явыбралеё,потомучтоонакакразвлекательная,такиминималистичная,иеёможносделатьминимумомкода.Выглядитонатак:

Чёрныйпрямоугольникпредставляетигрока,чьязадача–собиратьжёлтыеквадраты(монеты),избегаякрасныхучастков(лава?).Уровеньзаканчивается,когдаигроксобралвсемонеты.

Игрокможетходитьклавишамивлевоивправо,ипрыгатьклавишейвверх.Прыжки–этоспециальностьнашегоперсонажа.Онможетпрыгатьвнесколькоразвышесвоегоростаименятьнаправлениедвиженияввоздухе.Этонеочень-тореалистично,нопомогаетигрокупочувствоватьполныйконтрольнадегоэкраннымаватаром.

Проект:игра-платформер

Игра

ВыразительныйJavascript

275Проект:игра-платформер

Page 276: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Уигрыфиксированныйфонввидерешётки,гдедвижущиесяэлементынакладываютсянафон.Каждаяячейкарешёткилибопустая,либоявляетсястеной,либолавой.Движущиесяэлементы–игрок,монеты,инекоторыекусочкилавы.Вотличиеотсимуляциижизниизглавы7,позицииэтихэлементовнепривязаныкрешётке.Ихкоординатымогутбытьдробными,обеспечиваяплавноедвижение.

МыиспользуемDOMбраузерадляграфики,ичитаемвводпользователя,обслуживаясобытияклавиатуры.

Код,относящийсякэкрануиклавиатуре–небольшаячастьработы,которуюнамнадпроделатьдлясозданияигры.Таккаквсёсостоитизцветныхквадратиков,рисоватьэтопросто:мысоздаёмэлементыDOMииспользуемстили,чтобызадатьимцветфона,размерирасположение.

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

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

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

Вглаве7мыиспользовалимассивыстрокдляописаниядвумернойрешётки.

Технология

Уровни

ВыразительныйJavascript

276Проект:игра-платформер

Page 277: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Мыможемсделатьтожеиздесь.Этопозволитнамразрабатыватьуровнибезтого,чтобысначалаписатьредакторуровней.

Простойуровеньможетвыглядетьтак:

varsimpleLevelPlan=[

"",

"",

"x=x",

"xoox",

"x@xxxxxx",

"xxxxxx",

"x!!!!!!!!!!!!x",

"xxxxxxxxxxxxxx",

""

];

Фиксированнаярешёткаидвижущиесяэлементывключены.Символыxобозначаютстены,пробелы–пустоеместо,авосклицательныезнаки–фиксированнаялава.

@отмечаетместо,гдеигрокначинает.o–монетки,знакравенства=означаетблокдвижущейсялавы,которыйдвигаетсяпогоризонталитудаисюда.Заметьте,чторешётканаэтихпозицияхбудетсодержатьпустоепространство,идляотслеживанияпозицииэтихподвижныхэлементовиспользуетсяещёоднаструктураданных.

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

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

Чтениеуровня

Следующийконструкторсоздаётобъектуровня.Аргументомдолженбытьмассивстрок,задающихуровень.

ВыразительныйJavascript

277Проект:игра-платформер

Page 278: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functionLevel(plan){

this.width=plan[0].length;

this.height=plan.length;

this.grid=[];

this.actors=[];

for(vary=0;y<this.height;y++){

varline=plan[y],gridLine=[];

for(varx=0;x<this.width;x++){

varch=line[x],fieldType=null;

varActor=actorChars[ch];

if(Actor)

this.actors.push(newActor(newVector(x,y),ch));

elseif(ch=="x")

fieldType="wall";

elseif(ch=="!")

fieldType="lava";

gridLine.push(fieldType);

}

this.grid.push(gridLine);

}

this.player=this.actors.filter(function(actor){

returnactor.type=="player";

})[0];

this.status=this.finishDelay=null;

}

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

Уровеньсохраняетсвоиширинуивысотуиещёдвамассива–одиндлярешётки,иодиндлядвижущихсячастей.Решёткупредставляетмассивмассивов,гдекаждыйвложенныймассивпредставляетгоризонтальнуюлинию,акаждыйквадратсодержитлибоnullдляпустыхквадратов,либостроку,отражающуютипквадрата–“wall”или“lava”.

Массивactorsсодержитобъекты,отслеживающиеположенияисостояниядинамическихэлементов.Укаждогоизнихдолжнобытьсвойствоpos,содержащеепозицию(координатыверхнеголевогоугла),свойствоsizeсразмером,исвойствоtypeсострочкой,описывающейеготип(«lava»,«coin»или«player»).

ВыразительныйJavascript

278Проект:игра-платформер

Page 279: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Послепостроениярешёткимыиспользуемметодfilter,чтобынайтиобъектигрока,хранящийсявсвойствеуровня.Свойствоstatusотслеживает,выигралигрокилипроиграл.Когдаэтослучается,используетсяfinishDelay,котороедержитуровеньактивнымнекотороевремядляпоказапростойанимации.(Простосразувосстанавливатьсостояниеуровняилиначинатьследующий–этовыглядитнекрасиво).Этотметодможноиспользовать,чтобыузнать,законченлиуровень:

Level.prototype.isFinished=function(){

returnthis.status!=null&amp;&amp;this.finishDelay<0;

};

ДляхраненияпозициииразмеранашихактёровмывернёмсякнашемуверномутипуVector,которыйгруппируеткоординатыxиyвобъект.

functionVector(x,y){

this.x=x;this.y=y;

}

Vector.prototype.plus=function(other){

returnnewVector(this.x+other.x,this.y+other.y);

};

Vector.prototype.times=function(factor){

returnnewVector(this.x*factor,this.y*factor);

};

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

ВпредыдущейсекцииконструкторомLevelбылиспользованобъектactorChars,чтобысвязатьсимволысфункциямиконструктора.Объектвыглядиттак:

varactorChars={

"@":Player,

"o":Coin,

"=":Lava,"|":Lava,"v":Lava

Действующиелица(актёры)

ВыразительныйJavascript

279Проект:игра-платформер

Page 280: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

};

ТрисимволассылаютсянаLava.КонструкторLevelпередаётисходныйсимволактёравкачествевторогоаргументаконструктора,иконструкторLavaиспользуетегодлякорректировкисвоегоповедения(прыгатьпогоризонтали,прыгатьповертикали,капать).

Типplayerпостроенследующимконструктором.Унегоестьсвойствоspeed,хранящеееготекущуюскорость,чтопоможетнамсимулироватьимпульсигравитацию.

functionPlayer(pos){

this.pos=pos.plus(newVector(0,-0.5));

this.size=newVector(0.8,1.5);

this.speed=newVector(0,0);

}

Player.prototype.type="player";

Посколькувысотойигроквполтораквадратика,егоначальнаяпозициязадаётсянаполквадратавышепозиции,гдерасположенсимвол“@”.Такимобразомегонизсовпадаетснизомквадрата,вкоторомонпоявляется.

ПрисозданиидинамическогообъектаLava,намнадопроинициализироватьобъектвзависимостиотсимвола.Динамическаялавадвигаетсясзаданнойскоростью,поканевстретитпрепятствие.Затем,еслиунеёестьсвойствоrepeatPos,онаотпрыгнетназаднастартовуюпозицию(капающая).Еслинет,онаинвертируетскоростьипродолжаетдвигатьсявобратномнаправлении(отскакивает).Конструкторзадаёттольконеобходимыесвойства.Позжемынапишемметод,которыйзанимаетсясамимдвижением.

functionLava(pos,ch){

this.pos=pos;

this.size=newVector(1,1);

if(ch=="="){

this.speed=newVector(2,0);

}elseif(ch=="|"){

this.speed=newVector(0,2);

}elseif(ch=="v"){

this.speed=newVector(0,3);

this.repeatPos=pos;

}

ВыразительныйJavascript

280Проект:игра-платформер

Page 281: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

}

Lava.prototype.type="lava";

Монетыпростывреализации.Онипростосидятнаместе.Нодляоживленияигрыонибудутподрагивать,слегкадвигаясьповертикалитуда-сюда.Дляотслеживанияэтого,объектcoinхранитосновнуюпозициювместесосвойствомwobble,котороеотслеживаетфазудвижения.Вместеониопределяютположениемонеты(хранящеесявсвойствеpos).

functionCoin(pos){

this.basePos=this.pos=pos.plus(newVector(0.2,0.1));

this.size=newVector(0.6,0.6);

this.wobble=Math.random()*Math.PI*2;

}

Coin.prototype.type="coin";

Вглаве13мывидели,чтоMath.sinдаёткоординатуyточкинакруге.Онадвижетсятудаиобратноввидеплавнойволны,покамыдвижемсяпокругу,чтоделаетфункциюсинусапригоднойдлямоделированияволновогодвижения.

Чтобыизбежатьслучая,когдавсемонеткидвигаютсясинхронно,начальнаяфазакаждойбудетслучайной.ФазаволныMath.sinиширинаволны—2π.Мыумножаемзначение,возвращаемоеMath.random,наэтотномер,чтобызадатьмонетеслучайноеначальноеположениевволне.

Теперьмынаписаливсё,чтонеобходимодляпредставлениясостоянияуровня.

varsimpleLevel=newLevel(simpleLevelPlan);

console.log(simpleLevel.width,"by",simpleLevel.height);

//→22by9

Нампредстоитвыводитьэтиуровнинаэкранимоделироватьвремяидвижениевнутриних.

Вбольшинствеслучаевкодданнойглавынезаботитсяобинкапсуляции.Во-

Бремяинкапсуляции

ВыразительныйJavascript

281Проект:игра-платформер

Page 282: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

первых,инкапсуляциятребуетдополнительныхусилий.Программыстановятсябольше,требуютбольшеконцепцийиинтерфейсов.Атаккакотслишкомбольшогообъёмакодаглазачитателястекленеют,япостаралсясохранитьпрограммунебольшой.

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

Некоторыечастисистемыхорошоподдаютсяразделениюнакусочкисострогопрописаннымиинтерфейсами,адругие–нет.Впопыткахинкапсулироватьнечто,неимеющеечёткихграниц,выгарантированнопотратитемногосил.Совершивтакуюошибку,выувидите,чтоинтерфейсыстановятсячересчурбольшимиидетальными,ичтоихнадочастоменятьвпроцессеэволюциипрограммы.

Однувещьмывсё-такиинкапсулируем–подсистемурисования.Этосделаноспециальнодлятого,чтобывследующейглавемымогливыводитьнаэкрантужеигрудругимспособом.Спрятаврисованиезаинтерфейс,мыможемпростозагрузитьтужепрограммуиподключитькнейновыймодульвыводанаэкран.

Инкапсулироватькоддлярисованиямыбудем,введяобъектdisplay,которыйвыводитуровеньнаэкран.Типэкрана,которыймыопределяем,зовётсяDOMDisplay,потомучтоониспользуетэлементыDOMдляпоказауровня.

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

Рисование

ВыразительныйJavascript

282Проект:игра-платформер

Page 283: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Следующаявспомогательнаяфункциядаётпростойспособсозданияэлементасназначениемкласса.

functionelt(name,className){

varelt=document.createElement(name);

if(className)elt.className=className;

returnelt;

}

Экрансоздаём,передаваяемуродительскийэлемент,ккоторомунеобходимоподсоединиться,иобъектуровня.

functionDOMDisplay(parent,level){

this.wrap=parent.appendChild(elt("div","game"));

this.level=level;

this.wrap.appendChild(this.drawBackground());

this.actorLayer=null;

this.drawFrame();

}

Используятотфакт,чтоappendChildвозвращаетдобавленныйэлемент,мысоздаёмокружающийэлементwrapperисохраняемеговсвойствеwrap.

Неизменныйфонуровнярисуетсяединожды.Актёрыперерисовываютсякаждыйразприобновленииэкрана.СвойствоactorLayerиспользуетсявdrawFrameдляотслеживанияэлемента,содержащегоактёра–чтобыихбылолегкоудалятьизаменять.

Координатыиразмерыизмеряютсявединицах,относительныхкразмерурешёткитак,чтодистанциявединицуозначаетодинэлементрешётки.Когдамызадаёмразмерывпикселях,намнужнобудетмасштабироватькоординаты–играбылабыоченьмелкой,еслибодинквадратикзадавалсяоднимпикселем.Переменнаяscaleдаётколичествопикселей,котороезанимаетодинэлементрешётки.

varscale=20;

DOMDisplay.prototype.drawBackground=function(){

vartable=elt("table","background");

table.style.width=this.level.width*scale+"px";

ВыразительныйJavascript

283Проект:игра-платформер

Page 284: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

this.level.grid.forEach(function(row){

varrowElt=table.appendChild(elt("tr"));

rowElt.style.height=scale+"px";

row.forEach(function(type){

rowElt.appendChild(elt("td",type));

});

});

returntable;

};

Какмыужеупоминали,фонрисуетсячерезэлемент<table>.Этоудобносоответствуеттомуфакту,чтоуровеньзаданввидерешётки–каждыйрядрешёткипревращаетсяврядтаблицы(элемент<tr>).Строкирешёткииспользуютсякакименаклассовячеектаблицы(<td>).СледующийCSSприводитфонкнеобходимомунамвнешнемувиду:

.background{background:rgb(52,166,251);

table-layout:fixed;

border-spacing:0;}

.backgroundtd{padding:0;}

.lava{background:rgb(255,100,100);}

.wall{background:white;}

Некоторыеизнастроек(table-layout,border-spacingиpadding)используютсядляподавлениянежелательногоповеденияпоумолчанию.Ненужно,чтобывидтаблицызависелотсодержимогоячеек,иненужныпробелымеждуячейкамиилиотступывнутриних.

Правилоbackgroundзадаётцветфона.CSSразрешаетзадаватьцветасловами(white)ивформатеrgb(R,G,B),гдекрасная,зелёнаяисиняякомпонентыразделенынатричислаот0до255.Тоесть,взаписиrgb(52,166,251)красныйкомпонентравен52,зелёный166исиний251.Посколькусинийкомпонентсамыйбольшой,результирующийцветбудетсиневатым.Выможетевидеть,чтосамыйбольшойкомпонентвправиле.lava–красный.

КаждыйактёррисуетсясозданиемэлементаDOMизаданиемпозициииразмера,основываясьнасвойстваактёра.Значениянадоумножатьнамасштабscale,чтобыпереходитьотединицигрыкпикселям.

DOMDisplay.prototype.drawActors=function(){

varwrap=elt("div");

ВыразительныйJavascript

284Проект:игра-платформер

Page 285: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

this.level.actors.forEach(function(actor){

varrect=wrap.appendChild(elt("div",

"actor"+actor.type));

rect.style.width=actor.size.x*scale+"px";

rect.style.height=actor.size.y*scale+"px";

rect.style.left=actor.pos.x*scale+"px";

rect.style.top=actor.pos.y*scale+"px";

});

returnwrap;

};

Чтобызадатьэлементубольшеодногокласса,мыразделяемихименапробелами.ВкодеCSSклассactorзадаётпозициюabsolute.Имятипаиспользуетсявдополнительномкласседлязаданияцвета.Намненадозановоопределятьклассlava,потомучтомыповторноиспользуемклассдлялавыизрешётки,которыймыопределилиранее.

.actor{position:absolute;}

.coin{background:rgb(241,229,89);}

.player{background:rgb(64,64,64);}

ПриобновленииэкранаметодdrawFrameудаляетстароеизображениеактёра,еслионобыло,изатемперерисовываетегонановойпозиции.НапрашиваетсяиспользованиеэлементовDOMвкачествеактёров,нодляэтогонампотребовалосьбыпередаватьслишкоммногодополнительнойинформациимеждукодомдисплеяикодомсимуляции.НадобылобысвязатьактёровсэлементамиDOM,икодрисованиядолженбылбыудалятьэлементыприисчезновенииактёров.Таккакобычновигреактёровсовсемнемного,ихперерисовкаотнимаетнемногоресурсов.

DOMDisplay.prototype.drawFrame=function(){

if(this.actorLayer)

this.wrap.removeChild(this.actorLayer);

this.actorLayer=this.wrap.appendChild(this.drawActors());

this.wrap.className="game"+(this.level.status||"");

this.scrollPlayerIntoView();

};

Добавиввобёрткуwrapperтекущийстатусуровняввидекласса,мыможемстилизоватьперсонажапо-разномувзависимостиоттого,выигранаиграилипроиграна.МыдобавимправилоCSS,котороеработает,толькокогдау

ВыразительныйJavascript

285Проект:игра-платформер

Page 286: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

игрокаестьпотомоксзаданнымклассом.

.lost.player{

background:rgb(160,64,64);

}

.won.player{

box-shadow:-4px-7px8pxwhite,4px-7px8pxwhite;

}

Послеприкосновенияклавецветаигрокастановятсятёмно-красными,будтоонсгорел.Когдапоследняямонеткасобрана,мыиспользуемразмытыетенидлясозданияэффектагало.

Нельзяпредполагать,чтоуровнивсегдавмещаютсявокнопросмотра.ПоэтомунамнуженscrollPlayerIntoView–оннужендлягарантиитого,чтоеслиуровеньневлезаетвокно,онбудетпрокручен,чтобыигроквсегдабылблизкокцентру.СледующийCSSзадаётобёрткемаксимальныйразмер,игарантирует,чтовсёвылезающеезанегоневидно.Такжемызадаёмэлементупозициюrelative,чтобыактёрывнутринегорасполагалисьотносительноеголевоговерхнегоугла.

.game{

overflow:hidden;

max-width:600px;

max-height:450px;

position:relative;

}

ВметодеscrollPlayerIntoViewмынаходимположениеигрокаиобновляемпозициюпрокруткиобёртывающегоэлемента.Мыменяемпозицию,работаясосвойствамиscrollLeftиscrollTop,когдаигрокподходитблизкоккраю.

DOMDisplay.prototype.scrollPlayerIntoView=function(){

varwidth=this.wrap.clientWidth;

varheight=this.wrap.clientHeight;

varmargin=width/3;

//Theviewport

varleft=this.wrap.scrollLeft,right=left+width;

vartop=this.wrap.scrollTop,bottom=top+height;

varplayer=this.level.player;

ВыразительныйJavascript

286Проект:игра-платформер

Page 287: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

varcenter=player.pos.plus(player.size.times(0.5))

.times(scale);

if(center.x<left+margin)

this.wrap.scrollLeft=center.x-margin;

elseif(center.x>right-margin)

this.wrap.scrollLeft=center.x+margin-width;

if(center.y<top+margin)

this.wrap.scrollTop=center.y-margin;

elseif(center.y>bottom-margin)

this.wrap.scrollTop=center.y+margin-height;

};

Методнахожденияцентраигрокапоказывает,какметодынашихтиповVectorпозволяютзаписыватьрасчёты,производимыесобъектами,наглядно.Чтобынайтицентрактёра,мыдобавляемегопозицию(еголевыйверхнийугол)иполовинувысоты.Этоцентрвкоординатахуровня,нонамоннуженвкоординатахпикселей,поэтомумыумножаемрезультирующийвекторнанашмасштаб.

Затемсерияпроверокподтверждает,чтоигрокненаходитсявнедоступногопространства.Иногдаврезультатебудутзаданынеправильныекоординатыпрокрутки,ниженуляилибольше,чемразмерпрокручиваемогоэлемента.Ноэтонестрашно–DOMавтоматическиограничитихдопустимымизначениями.ЕслиназначитьscrollLeftзначение-10,онбудетравен0.

Былобынемногопрощепробоватьпрокручиватьпозициюигрокавцентрокнапросмотра–ноэтосоздаётнеприятныйдрожащийэффект.Вовремяпрыжковвидбудетпостояннодвигатьсявверхивниз.Гораздоприятнееиметь«нейтральную»зонувсерединеэкрана,гдеможнодвигаться,невызываяпрокрутки.

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

DOMDisplay.prototype.clear=function(){

this.wrap.parentNode.removeChild(this.wrap);

};

Теперьмыможемпоказатьнашуровень.

ВыразительныйJavascript

287Проект:игра-платформер

Page 288: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

<linkrel="stylesheet"href="css/game.css">

<script>

varsimpleLevel=newLevel(simpleLevelPlan);

vardisplay=newDOMDisplay(document.body,simpleLevel);

</script>

Тэг<link>прииспользованиисrel="stylesheet"позволяетзагружатьфайлсCSS.Файлgame.cssсодержитнеобходимыедляигрыстили.

Теперьнамнадодобавитьобработкудвижений–самоеинтересноевигре.Простойподход,которыйиспользуюбольшинствоигр–разделитьвремянанебольшиеотрезки,инакаждомшагесдвигатьактёровнадистанцию,соответствующуюихскорости(расстояниевсекунду),умноженноенадлительностьвременногоотрезка(всекундах).

Этопросто.Сложностьвтом,чтонадообрабатыватьвзаимодействиепредметов.Когдаигроккасаетсяполаилистены,оннедолженпроходитьнасквозь.Иградолжназамечать,когдадвижениеодногообъектаприводиткстолкновениюсдругимиреагироватьсоответственно.Стеныостанавливаютдвижение,монетысобираются,итакдалее.

Вобщемслучаеэтазадачанетакаяпростая.Можнонайтибиблиотеки,обычноназывающиеся«физическимидвижками»,симулирующиевзаимодействиямеждуфизическимиобъектамивдвухилитрёхизмерениях.Вэтойглавемыпоступимпроще,таккакнамнужнообрабатыватьстолкновениятолькопрямоугольныхобъектов.

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

Подходтребуетиспользованиянебольшихотрезковвремени,чтобыобъектыостанавливалисьдосоприкосновения.Есливзятьслишкомбольшиеотрезки,игрокбудетзависатьнадземлёй.Можнобылобыиспользоватьболее

Движениеистолкновение

ВыразительныйJavascript

288Проект:игра-платформер

Page 289: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

сложныйвариант–вычислитьместонепосредственногосоприкосновенияиподвинутьактёратуда.Мыпоступимпроще,искроемегопроблемы,выбравнебольшиевременныеотрезки.

Методсообщает,непересекаетсялипрямоугольник(заданныйпозициейиразмером)скаким-либонепустымпространствомфоновойрешётки.

Level.prototype.obstacleAt=function(pos,size){

varxStart=Math.floor(pos.x);

varxEnd=Math.ceil(pos.x+size.x);

varyStart=Math.floor(pos.y);

varyEnd=Math.ceil(pos.y+size.y);

if(xStart<0||xEnd>this.width||yStart<0)

return"wall";

if(yEnd>this.height)

return"lava";

for(vary=yStart;y<yEnd;y++){

for(varx=xStart;x<xEnd;x++){

varfieldType=this.grid[y][x];

if(fieldType)returnfieldType;

}

}

};

Методвычисляетзанимаемыетеломячейкирешётки,применяяMath.floorиMath.ceilнакоординатахтела.Помните,чторазмерыячеек–1х1единиц.Округляяграницытелавверхивниз,мыполучаемпромежутокизячеекфона,которыхкасаетсятело.

Еслителовысовываетсяизуровня,мывсегдавозвращаем“wall”длядвухсторониверхаи“lava”дляниза.Этообеспечитгибельигрокапривыходезапределыуровня.Когдателовнутрирешётки,мывциклепроходимблокквадратоврешётки,найденныйокруглениемкоординат,ивозвращаемсодержимоепервогонепустогоквадратика.

Столкновенияигрокасдругимиактёрами(монеты,движущаясялава)обрабатываютсяпослесдвигаигрока.Когдадвижениеприводитегокдругомуактёру,срабатываетсоответствующийэффект(сбормонетили

Поискстолкновенийнарешётке

ВыразительныйJavascript

289Проект:игра-платформер

Page 290: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

гибель).

Этотметодсканируетмассивактёров,впоискахтого,которыйнакладываетсяназаданныйаргумент:

Level.prototype.actorAt=function(actor){

for(vari=0;i<this.actors.length;i++){

varother=this.actors[i];

if(other!=actor&amp;&amp;

actor.pos.x+actor.size.x>other.pos.x&amp;&amp;

actor.pos.x<other.pos.x+other.size.x&amp;&amp;

actor.pos.y+actor.size.y>other.pos.y&amp;&amp;

actor.pos.y<other.pos.y+other.size.y)

returnother;

}

};

МетодanimateтипаLevelдаётвозможностьвсемактёрамуровнясдвинуться.Аргументstepзадаётвременнойпромежуток.Объектkeysсодержитинформациюпрострелкиклавиатуры,нажатыеигроком.

varmaxStep=0.05;

Level.prototype.animate=function(step,keys){

if(this.status!=null)

this.finishDelay-=step;

while(step>0){

varthisStep=Math.min(step,maxStep);

this.actors.forEach(function(actor){

actor.act(thisStep,this,keys);

},this);

step-=thisStep;

}

};

Когдаусвойствауровняstatusестьзначение,отличноеотnull(аэтобывает,когдаигроквыигралилипроиграл),мыуменьшитьдонулясчётчикfinishDelay,считающийвремямеждумоментом,когдапроизошёлвыигрышилипроигрышимоментом,когданадозаканчиватьпоказуровня.

Актёрыидействия

ВыразительныйJavascript

290Проект:игра-платформер

Page 291: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Циклwhileделитвременнойинтервалнаудобныемелкиекуски.Онследит,чтобыпромежуткибылинебольшеmaxStep.Кпримеру,шагв0.12секундыбудетнарезаннадвашагапо0.05иостатокв0.02

Уобъектовактёровестьметодact,которыйпринимаетвременнойшаг,объектlevelиобъектkeys.ВотондлятипаLava,которыйигнорируетобъектkey:

Lava.prototype.act=function(step,level){

varnewPos=this.pos.plus(this.speed.times(step));

if(!level.obstacleAt(newPos,this.size))

this.pos=newPos;

elseif(this.repeatPos)

this.pos=this.repeatPos;

else

this.speed=this.speed.times(-1);

};

Онсчитаетновуюпозицию,добавляярезультатумножениявременногопромежуткаитекущейскоростикстаройпозиции.Еслиновуюпозициюнезанимаетпрепятствие,происходитперемещение.Еслипрепятствиесуществует,поведениезависитоттипаблокалавы.УкапающейлавыестьсвойствоrepeatPos,ионапривстречеспрепятствиемотражаетсявобратнуюсторону.Прыгающаялавапростоинвертируетскорость(умножаетна-1),чтобыпродолжитьдвижениевобратномнаправлении.

Монетыиспользуютметодact,чтобыдрожать.Столкновенияониигнорируют,посколькуонипростоподрагиваютвнутрисвоегоквадрата,астолкновениясигрокомбудутобрабатыватьсяметодомactигрока.

varwobbleSpeed=8,wobbleDist=0.07;

Coin.prototype.act=function(step){

this.wobble+=step*wobbleSpeed;

varwobblePos=Math.sin(this.wobble)*wobbleDist;

this.pos=this.basePos.plus(newVector(0,wobblePos));

};

Свойствоwobbleобновляется,чтобыследитьзавременем,ипотомиспользуетсякакаргументMath.sinдлясозданияволны,котораяиспользуетсядляподсчётановойпозиции.

ВыразительныйJavascript

291Проект:игра-платформер

Page 292: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Остаётсяигрок.Движениеигрокаобрабатываетсяпоразнымосямотдельно,потомучтовстречасполомнедолжнамешатьгоризонтальномуперемещению,австречасостеной–падениюилипрыжку.Этотметодработаетсгоризонтальнымперемещением.

varplayerXSpeed=7;

Player.prototype.moveX=function(step,level,keys){

this.speed.x=0;

if(keys.left)this.speed.x-=playerXSpeed;

if(keys.right)this.speed.x+=playerXSpeed;

varmotion=newVector(this.speed.x*step,0);

varnewPos=this.pos.plus(motion);

varobstacle=level.obstacleAt(newPos,this.size);

if(obstacle)

level.playerTouched(obstacle);

else

this.pos=newPos;

};

Перемещениеподсчитываетсянаосновесостоянияклавиш«направо»и«налево».Когдаперемещениеприводитквстречеспрепятствием,вызываетсяметодуровняplayerTouched,которыйобрабатываетгибельвлавеисбормонеток.Виномслучаеобъектобновляетсвоюпозицию.

Движениеповертикалиработаетсходнымобразом,носимулируетпрыжкиигравитацию.

vargravity=30;

varjumpSpeed=17;

Player.prototype.moveY=function(step,level,keys){

this.speed.y+=step*gravity;

varmotion=newVector(0,this.speed.y*step);

varnewPos=this.pos.plus(motion);

varobstacle=level.obstacleAt(newPos,this.size);

if(obstacle){

level.playerTouched(obstacle);

if(keys.up&amp;&amp;this.speed.y>0)

this.speed.y=-jumpSpeed;

else

this.speed.y=0;

}else{

this.pos=newPos;

ВыразительныйJavascript

292Проект:игра-платформер

Page 293: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

}

};

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

Затеммысновапроверяемпрепятствия.Еслимыеговстретили,возможныдваварианта.Когданажатаклавиша«вверх»,имыдвигаемсявниз(тоесть,мывстретилисьсчем-то,чтонаходитсяподнами),скоростиприсваиваетсядовольнобольшоеотрицательноезначение.Врезультатеигрокпрыгает.Виномслучае,мыпростовочто-товрезалисьискоростьобнуляется.

Самметодactследующий:

Player.prototype.act=function(step,level,keys){

this.moveX(step,level,keys);

this.moveY(step,level,keys);

varotherActor=level.actorAt(this);

if(otherActor)

level.playerTouched(otherActor.type,otherActor);

//Losinganimation

if(level.status=="lost"){

this.pos.y+=step;

this.size.y-=step;

}

};

Последвиженияметодпроверяетдругихактёров,скоторымиигроксталкивается,иопятьвызываетplayerTouched,еслитаковойнашёлся.Вэтотразонпередаётвторымаргументомобъектactor,таккакеслидругимактёромбыламонетка,методplayerTouchedдолжензнать,какуюименномонеткумысобрали.

Вфинале,когдаигрокпогибает(дотронувшисьдолавы),мыделаемнебольшуюанимацию,из-закоторойперсонажсжимается(илитонет),уменьшаявысотуобъектаplayer.

Вотметод,обрабатывающийстолкновениямеждуигрокомидругими

ВыразительныйJavascript

293Проект:игра-платформер

Page 294: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

объектами:

Level.prototype.playerTouched=function(type,actor){

if(type=="lava"&amp;&amp;this.status==null){

this.status="lost";

this.finishDelay=1;

}elseif(type=="coin"){

this.actors=this.actors.filter(function(other){

returnother!=actor;

});

if(!this.actors.some(function(actor){

returnactor.type=="coin";

})){

this.status="won";

this.finishDelay=1;

}

}

};

Когдамытронулилаву,статусигрыустанавливаетсяв“lost”.Когдасобранамонетка,онаудаляетсяизмассиваактёров,аеслиэтобылапоследняя–статусигрыменяетсяна“won”.Всёэтодаётнамуровень,пригодныйдляанимации.Нехватаеттолькокода,еёобрабатывающего.

Длятакойигрынамненужныклавиши,эффекткоторыхработаетоднократнопослеkeypress.Намнуженэффект,продолжающийсявсёвремя,покаклавишанажата(движущаясяфигурка)

Намнадосделатьобработчикклавиш,хранящийтекущеесостояниекнопоквлево,вправовверхивниз.ТакженамнадовызыватьдлянихpreventDefault,чтобыонинепрокручивалистраницу.

Следующаяфункция,когдаейдаютобъектскодамиклавишввидеимёнсвойствиназваниямиклавишввидезначений,возвращаетдругойобъект,которыйотслеживаеттекущеесостояниекнопок.Онрегистрируетобработчикисобытийдлясобытий«keydown»и«keyup»,икогдакодклавишисобытиясовпадаетсотслеживаемымкодом,обновляетобъект.

vararrowCodes={37:"left",38:"up",39:"right"};

Отслеживаниеклавиш

ВыразительныйJavascript

294Проект:игра-платформер

Page 295: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functiontrackKeys(codes){

varpressed=Object.create(null);

functionhandler(event){

if(codes.hasOwnProperty(event.keyCode)){

vardown=event.type=="keydown";

pressed[codes[event.keyCode]]=down;

event.preventDefault();

}

}

addEventListener("keydown",handler);

addEventListener("keyup",handler);

returnpressed;

}

Обратитевнимание,какоднафункцияобработчикаиспользуетсядлясобытийобоихтипов.Онапроверяетсвойствоtypeобъектасобытия,определяя,надолиобновлятьсостояниекнопкинаtrue(«keydown»)илиfalse(«keyup»).

ФункцияrequestAnimationFrame,которуюмывиделивглаве13,предоставляетхорошийспособанимироватьигру.Ноинтерфейсеёпримитивен–егоиспользованиезаставляетнасотслеживатьмоментвремени,вкоторыйонабылавызванавпрошлыйраз,ивызыватьrequestAnimationFrameкаждыйразпослекаждогокадра.

Давайтеопределимвспомогательнуюфункцию,оборачивающуюэтискучныеоперациивудобныйинтерфейс,ипозволяющуюнампростовызватьrunAnimation,задаваяейфункцию,котораяпринимаетразницувовремениирисуетодинкадр.Когдафункцияframeвозвращаетfalse,анимацияостанавливается.

functionrunAnimation(frameFunc){

varlastTime=null;

functionframe(time){

varstop=false;

if(lastTime!=null){

vartimeStep=Math.min(time-lastTime,100)/1000;

stop=frameFunc(timeStep)===false;

}

Запускигры

ВыразительныйJavascript

295Проект:игра-платформер

Page 296: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

lastTime=time;

if(!stop)

requestAnimationFrame(frame);

}

requestAnimationFrame(frame);

}

Яназначилмаксимальноевремядлякадрав100миллисекунд(1/10секунды).Когдазакладкаилиокнобраузераспрятано,вызовыrequestAnimationFrameпрекратятся,показакладкаилиокнонестанутсноваактивны.Вэтомслучае,разницамеждуlastTimeитекущимвременембудетравнатомувремени,втечениекоторогостраницабыласпрятана.Продвигатьигрунавсёэтовремябылобыглупоизатратно(вспомнитеразделениевременивметодеanimate).

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

ФункцияrunLevelпринимаетобъектLevel,конструктордляdisplay,и,необязательнымпараметром–функцию.Онавыводитуровеньвdocument.bodyипозволяетпользователюигратьнанём.Когдауровеньзакончен(победаилипоражение),runLevelочищаетэкран,останавливаетанимацию,аеслизаданафункцияandThen,вызываетеёсостатусомуровня.

vararrows=trackKeys(arrowCodes);

functionrunLevel(level,Display,andThen){

vardisplay=newDisplay(document.body,level);

runAnimation(function(step){

level.animate(step,arrows);

display.drawFrame(step);

if(level.isFinished()){

display.clear();

if(andThen)

andThen(level.status);

returnfalse;

}

});

}

Игра–этопоследовательностьуровней.Когдаигрокпогибает,уровень

ВыразительныйJavascript

296Проект:игра-платформер

Page 297: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

начинаетсязаново.Когдауровеньзакончен,мыпереходимнаследующий.Этоможновыразитьследующейфункцией,принимающеймассивплановуровней(массивстрок)иконструкторdisplay

functionrunGame(plans,Display){

functionstartLevel(n){

runLevel(newLevel(plans[n]),Display,function(status){

if(status=="lost")

startLevel(n);

elseif(n<plans.length-1)

startLevel(n+1);

else

console.log("Youwin!");

});

}

startLevel(0);

}

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

Такойстильпрограммированияобычноназываютасинхронным.Обработкасобытий–тожепримертакогостиля,имыснимвстретимсяещёнераз,когдабудемработатьсзадачами,которыемогутзанятьпроизвольныепромежуткивремени–например,сетевыезапросывглаве17,иливводивыводобщегоназначениявглаве20.

ВпеременнойGAME_LEVELSхранитсянаборплановуровней.ТакаястраницаскармливаетихвrunGame,котораязапускаетсамуигру.

<linkrel="stylesheet"href="css/game.css">

<body>

<script>

runGame(GAME_LEVELS,DOMDisplay);

</script>

</body>

ВыразительныйJavascript

297Проект:игра-платформер

Page 298: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Попробуйтевыиграть.Яздоровоповеселился,сочиняяих.

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

ПодредактируйтеrunGame,чтобыонаподдерживалажизни.Пустьигрокначинаетстрёх.

<linkrel="stylesheet"href="css/game.css">

<body>

<script>

//СтараяфункцияrunGame–поменяйтееё...

functionrunGame(plans,Display){

functionstartLevel(n){

runLevel(newLevel(plans[n]),Display,function(status){

if(status=="lost")

startLevel(n);

elseif(n<plans.length-1)

startLevel(n+1);

else

console.log("Youwin!");

});

}

startLevel(0);

}

runGame(GAME_LEVELS,DOMDisplay);

</script>

</body>

СделайтевозможнымставитьисниматьигруспаузыпонажатиюклавишиEsc.

Этогоможнодостичь,поменявфункциюrunLevel,чтобыонаиспользовала

Упражнения

Конецигры

Пауза

ВыразительныйJavascript

298Проект:игра-платформер

Page 299: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Напервыйвзглядможетпоказаться,чтоинтерфейсrunAnimationнепредназначендляэтого–ноесливыпоменяетееговызовизrunLevel,всёполучится.

Когдаполучится,можетепопробоватьещёкое-что.Мырегистрируемсобытиясклавиатурынесамымлучшимспособом.Объектarrows–глобальнаяпеременная,иегообработчикисобытийнаходятсявпамяти,дажееслиигранезапущена.Можносказать,ониутекаютизсистемы.РасширьтеtrackKeys,чтобможнобылоразрегистрироватьобработчикиизатемпоменяйтеrunLevel,чтобонарегистрировалаихнастарте,иразрегистрироваланафинише.

<linkrel="stylesheet"href="css/game.css">

<body>

<script>

//СтараяфункцияrunLevel–поменяйтееё...

functionrunLevel(level,Display,andThen){

vardisplay=newDisplay(document.body,level);

runAnimation(function(step){

level.animate(step,arrows);

display.drawFrame(step);

if(level.isFinished()){

display.clear();

if(andThen)

andThen(level.status);

returnfalse;

}

});

}

runGame(GAME_LEVELS,DOMDisplay);

</script>

</body>

ВыразительныйJavascript

299Проект:игра-платформер

Page 300: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Рисование—этообман.М.К.Эшер

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

НотакоеиспользованиеDOM–нето,длячегоонсоздавался.Некоторыезадачи,типарисованиялиниимеждудвумяпроизвольнымиточками,крайненеудобновыполнятьприпомощиобычныхэлементовHTML.

Естьдвеальтернативы.Первая–SVG,масштабируемаявекторнаяграфика,такжеоснованнаянаDOM,нобезучастияHTML.SVG–диалектдляописаниядокументов,которыйконцентрируетсянаформах,анетексте.SVGможновстроитьвHTML,иливключитьчерезтег<img>.

Втораяальтернатива–холст(canvas).Холст–этоодинэлементDOM,вкоторомнаходитсякартинка.ОнпредоставляетAPIдлярисованияформнатомместе,котороезанимаетэлемент.РазницамеждухолстомиSVGвтом,чтовSVGхранитсяначальноеописаниеформ–ихможновлюбоймоментсдвигатьилименятьразмер.Холстжепреобразовываетформывпиксели(цветныеточкирастра),кактольконарисуетих,инезапоминает,чтоэтипикселиизсебяпредставляют.Единственнымспособомсдвинутьформанахолстеявляетсяочиститьхолст(илитучасть,котораяокружаетформу)иперерисоватьеёнадругомместе.

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

Рисованиенахолсте

SVG

ВыразительныйJavascript

300Рисованиенахолсте

Page 301: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

применения.

ВотдокументHTML,содержащийпростуюSVG-картинку:

<p>NormalHTMLhere.</p>

<svgxmlns="http://www.w3.org/2000/svg">

<circler="50"cx="50"cy="50"fill="red"/>

<rectx="120"y="5"width="90"height="90"

stroke="blue"fill="none"/>

</svg>

Атрибутxmlnsменяетпространствоимёнэлементапоумолчанию.ЭтопространствозадаётсячерезURLиобозначаетдиалект,накотороммысейчасговорим.Тэгии,несуществующиевHTML,имеютсмыслвSVG–онирисуютформы,используястильипозицию,заданныеихатрибутами.

ОнисоздаютэлементыDOMтакже,кактэгиHTML.Кпримеру,такойкодменяетцветэлементанаcyan:

varcircle=document.querySelector(«circle»);circle.setAttribute(«fill»,«cyan»);

Графикухолстаможнорисоватьнаэлементе<canvas>.Емуможнозадатьширинуивысоту,такимобразомопределяяегоразмервпикселях.

Новыйхолстпуст,тоестьонполностьюпрозраченипоказываетнампустоепространстводокумента.

Тэг<canvas>поддерживаетразныестилирисования.Чтобыполучитьдоступкинтерфейсурисования,сначаланужносоздатьcontext–объект,чьиметодыпредоставляютэтотинтерфейс.Сейчасестьдваширокораспространённыхстилярисования:“2d”длядвумернойграфикии“webgl”длятрёхмернойграфикиприпомощиинтерфейсаOpenGL.

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

Элементхолстаcanvas

ВыразительныйJavascript

301Рисованиенахолсте

Page 302: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

эффективнуюграфикупрямоизJavaScript.

ContextсоздаётсяметодомgetContextэлемента<canvas>.

<p>Beforecanvas.</p>

<canvaswidth="120"height="60"></canvas>

<p>Aftercanvas.</p>

<script>

varcanvas=document.querySelector("canvas");

varcontext=canvas.getContext("2d");

context.fillStyle="red";

context.fillRect(10,10,100,50);

</script>

Послесозданияобъектаcontextпримеррисуетпрямоугольникширинойв100пикселейивысотойв50,скоординатамилевоговерхнегоугла(10,10).

ТочнокаквHTML(иSVG),используемаяхолстомсистемакоординатпомещаетточку(0,0)влевыйверхнийугол,иположительнаячастьосиYидётоттудавниз.Тоесть,точка(10,10)на10пикселейнижеиправееверхнеголевогоугла.

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

МетодfillRectзаливаетпрямоугольник.Онпринимаеткоординатылевоговерхнегоуглаx,y,затемширинуивысоту.СхожийметодstrokeRectрисуетпериметрпрямоугольника.

Большеуметодовпараметровнет.Цветзаливки,толщинаобводкиидругиепараметрыопределяютсянеаргументамиметода(какможнобылобыожидать),асвойствамиобъектаcontext.

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

Заливкаиобводка

ВыразительныйJavascript

302Рисованиенахолсте

Page 303: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

СвойствоstrokeStyleработаеттакже,ноопределяетцвет,которымбудетнарисованаобводка.ТолщиналинииопределяетсясвойствомlineWidth,котороеможетсодержатьлюбоеположительноечисло.

<canvas></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

cx.strokeStyle="blue";

cx.strokeRect(5,5,50,50);

cx.lineWidth=5;

cx.strokeRect(135,5,50,50);

</script>

Когданезаданыатрибутыwidthилиheight,имназначаютсязначенияпоумолчанию–300дляшириныи150длявысоты.

Путь–последовательностьлиний.Двумерныйхолстимеетстранныйподходкописаниюпутей.Всёделаетсячерезпобочныеэффекты.Пути–незначения,которыеможнохранитьилипередавать.Вместоэтого,есливамчто-тонадосделатьспутём,высоздаётепоследовательностьвызововметодадляописанияегоформы.

<canvas></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

cx.beginPath();

for(vary=10;y<100;y+=10){

cx.moveTo(10,y);

cx.lineTo(90,y);

}

cx.stroke();

</script>

Примерсоздаётпутьизнесколькихгоризонтальныхотрезков,изатемобводитихметодомstroke.Каждыйсегмент,созданныйчерезlineTo,начинаетсястекущейпозициипути.Этапозиция–обычноконецпредыдущегосегмента,еслитольконебыловызоваmoveTo.Впоследнемслучаеследующийсегментначнётсяспозиции,заданнойвmoveTo.

Пути

ВыразительныйJavascript

303Рисованиенахолсте

Page 304: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Призаливкепутикаждаяизформзаливаетсяотдельно.Путьможетсодержатьнесколькоформ–каждоедвижениеmoveToначинаетновую.Нопутьдолженбытьзакрытым(началоиконецнаходятсянаодномместе),преждечемегоможнобудетзакрасить.Еслипутьнезакрыт,отегоконцадоначаладобавляетсялиния,изаливаетсяформа,очерченнаязакрытымпутём.

<canvas></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

cx.beginPath();

cx.moveTo(50,10);

cx.lineTo(10,70);

cx.lineTo(90,70);

cx.fill();

</script>

Примеррисуетзакрашенныйтреугольник.Заметьте,чтонепосредственнобылинарисованытолькодвестороны.Третья,отправогонижнегоуглаобратноквершине,подразумевается–онанебудетзакрашенавызовомstroke.

ТакжеможноиспользоватьметодclosePath,чтобыпринудительнозакрытьпуть,добавивреальныйсегментдоначалапути.Этотсегментбудетзакрашенвызовомstroke.

Путьможетсостоятьизкривых.Ихрисоватьпосложнее,нежелипрямые.

МетодquadraticCurveToрисуеткривуюдонужнойточки.Дляопределениякривизныметодудаётсяконтрольнаяточкавместесточкойназначения.Представьте,чтоконтрольнаяточкакакбыпритягиваетлинию,задаваякривойкривизну.Линиянепроходитчерезконтрольнуюточку.Вместоэтогонаправлениялиниивеёначальнойиконечнойточкахбудутстремитьсякконтрольнойточке.Следующийпримериллюстрируетэто:

<canvas></canvas>

<script>

Кривые

ВыразительныйJavascript

304Рисованиенахолсте

Page 305: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

varcx=document.querySelector("canvas").getContext("2d");

cx.beginPath();

cx.moveTo(10,90);

//control=(60,10)goal=(90,90)

cx.quadraticCurveTo(60,10,90,90);

cx.lineTo(60,10);

cx.closePath();

cx.stroke();

</script>

Рисуемслеванаправоквадратичнуюкривую,укоторойконтрольнаяточказаданакак(60,10),азатемрисуемдвасегмента,проходящиеобратночерезконтрольнуюточкуиначалолинии.РезультатнапоминаетэмблемуЗвёздногопути.Можноувидетьдействиеконтрольнойточки:линия,выходящаяизначальнойиконечнойточек,начинаетсяпонаправлениюкконтрольнойточке,азатемзагибается.

МетодbezierCurveрисуетсхожуюкривую.Вместооднойконтрольнойточкиунеёестьдве–пооднойнакаждыйизконцовкривой.Вотпохожийрисунокдляиллюстрацииповедениятакойкривой:

<canvas></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

cx.beginPath();

cx.moveTo(10,90);

//control1=(10,10)control2=(90,10)goal=(50,90)

cx.bezierCurveTo(10,10,90,10,50,90);

cx.lineTo(90,10);

cx.lineTo(10,10);

cx.closePath();

cx.stroke();

</script>

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

Сэтимикривымисложноватоработать–невсегдапонятно,какискатьконтрольныеточки,которыеприведуткнужнойвамформе.Иногдаихможновычислить,иногдаприходитсяподбиратьметодомпробиошибок.

Дуги,фрагментыкругов,легчевобращении.МетодarcToпринимаетцелых

ВыразительныйJavascript

305Рисованиенахолсте

Page 306: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

пятьаргументов.Первыечетыре–похожинааргументыquadraticCurveTo.Перваяпаразадаётчто-товродеконтрольнойточки,вторая–местоназначениякривой.Пятыйзадаётрадиусдуги.Методсоздаётскруглённыйугол–линию,идущуюкконтрольнойточке,азатемкточкеназначения–искругляетуголзаданнымрадиусом.МетодarcToрисуеткруглуючасть,атакжелиниюотточкистартадоначалазакруглённойчасти.

<canvas></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

cx.beginPath();

cx.moveTo(10,10);

//control=(90,10)goal=(90,90)radius=20

cx.arcTo(90,10,90,90,20);

cx.moveTo(10,10);

//control=(90,10)goal=(90,90)radius=80

cx.arcTo(90,10,90,90,80);

cx.stroke();

</script>

arcToнерисуетлиниюотконцазакруглённойчастидоточкиназначения,несмотрянасвоёназвание.ЕёможнозакончитьчерезlineToстакимижекоординатами.

Чтобынарисоватькруг,можносделатьчетыревызоваarcTo,гдекаждыйповёрнутотносительнодругогона90градусов.Нометодarcпредоставляетспособпроще.Онпринимаетпарукоординатцентраарки,радиусиначальныйиконечныйуглы.

Двапоследнихпараметрамогутпомочьврисованиичастикруга.Углыизмеряютсяврадианах,анеградусах.Этозначит,чтополныйкругимеетуголв2π,или2*Math.PI,чтопримерноравно6.28.Уголначинаетотсчётотточкисправаотцентра,иидётпротивчасовойстрелки.Чтобынарисоватьполныйкруг,можнозадатьначалов0,аконецбольше2π(кпримеру,7).

<canvas></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

cx.beginPath();

//center=(50,50)radius=40angle=0to7

cx.arc(50,50,40,0,7);

//center=(150,50)radius=40angle=0to½π

ВыразительныйJavascript

306Рисованиенахолсте

Page 307: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

cx.arc(150,50,40,0,0.5*Math.PI);

cx.stroke();

</script>

Накартинкеврезультатебудетлинияслеваоткруга(первыйвызовarc),долевойчастичетвертикруга(второйвызов).Какидругиеметодырисованияпутей,линиядугисоединенаспредыдущимсегментомпути.ДляначаларисованияновогопутинадовызватьmoveTo.

Рисуемкруговуюдиаграмму

Представьте,чтовыполучилиработувООО«Экономикадлявсех»,ивашимпервымзаданиембудетнарисоватькруговуюдиаграммуудовлетворённостиклиентовсогласнорезультатамопроса.

Переменнаяresultсодержитмассивобъектов,представляющихрезультаты.

varresults=[

{name:"Удовлетворён",count:1043,color:"lightblue"},

{name:"Нейтральное",count:563,color:"lightgreen"},

{name:"Неудовлетворён",count:510,color:"pink"},

{name:"Безкомментариев",count:175,color:"silver"}

];

Чтобынарисоватьдиаграмму,мырисуемнесколькосекторов,каждыйизкоторыхделаетсяизаркиипарылинийотцентра.Уголмывычисляем,деляполныйкруг(2π)наобщееколичествоотзывов,иумножаянаколичестволюдей,выбравшихданныйвариантответа.

<canvaswidth="200"height="200"></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

vartotal=results.reduce(function(sum,choice){

returnsum+choice.count;

},0);

//Startatthetop

varcurrentAngle=-0.5*Math.PI;

results.forEach(function(result){

varsliceAngle=(result.count/total)*2*Math.PI;

cx.beginPath();

//center=100,100,radius=100

//fromcurrentangle,clockwisebyslice'sangle

cx.arc(100,100,100,

ВыразительныйJavascript

307Рисованиенахолсте

Page 308: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

currentAngle,currentAngle+sliceAngle);

currentAngle+=sliceAngle;

cx.lineTo(100,100);

cx.fillStyle=result.color;

cx.fill();

});

</script>

Нодиаграмманерасшифровываетзначениясекторов–этонеудобно.Намнадокак-тонарисоватьнахолстетекст.

УконтекстадвумерногохолстаестьметодыfillTextиstrokeText.Последнийможноиспользоватьдляобведённыхбукв,нообычноиспользуетсяfillText.ОнзаполняетзаданныйтекстцветомfillColor.

<canvas></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

cx.font="28pxGeorgia";

cx.fillStyle="fuchsia";

cx.fillText("Яитекстмогурисовать!",10,50);

</script>

Можнозадатьразмер,стильишрифттекстачерезсвойствоfont.Впримерезадаётсятолькоразмеришрифт.Можнодобавитьнаклонижирностьвначалестроки.

ДвапоследнихаргументаfillText(иstrokeText)задаютпозицию,скоторойначинаетсятекст.Поумолчаниюэтоначалолинии,накоторой«стоят»буквы–несчитаясвисающихчастейбуквтипариу.Можноменятьпозициюпогоризонтали,задаваясвойствуtextAlignзначения«end»или«center»,аповертикали–задаваяtextBaseline«top»,«middle»,или«bottom».

Вконцеглавымывернёмсякнашейдиаграмме.

Текст

Изображения

ВыразительныйJavascript

308Рисованиенахолсте

Page 309: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

МетодdrawImageпозволяетвыводитьнахолстпиксельныеданные.Онимогутбытьвзятыизэлемента<img>илисдругогохолста,которыенеобязательновиднывсамомдокументе.Следующийпримерсоздаётэлемент<img>изагружаетвнегофайлизображения.Нооннеможетсразуначатьрисоватьприпомощиэтойкартинки,потомучтобраузермогнеуспетьеёподгрузить.Дляэтогомырегистрируемобработчиксобытия“load”ирисуемпослезагрузки.

<canvas></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

varimg=document.createElement("img");

img.src="img/hat.png";

img.addEventListener("load",function(){

for(varx=10;x<200;x+=30)

cx.drawImage(img,x,10);

});

</script>

ПоумолчаниюdrawImageнарисуеткартинкуоригинальногоразмера.Емуможнозадатьдвадополнительныхпараметрадляизмененияшириныивысоты.

КогдаdrawImageзаданодевятьаргументов,еёможноиспользоватьдлярисованиячастиизображения.Совторогопопятыйаргументыобозначаютпрямоугольник(x,y,ширинаивысота)висходнойкартинке,которыйнадоскопировать.Сшестогоподевятый–прямоугольникнахолсте,кудаегонадоскопировать.

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

ВыразительныйJavascript

309Рисованиенахолсте

Page 310: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Перебираяпозы,мыможемвывестианимациюидущегоперсонажа.

ДляанимациинахолстепригодитсяметодclearRect.ОннапоминаетfillRect,новместоокраскипрямоугольникаонделаетегопрозрачным,удаляяпредыдущиепиксели.

Мызнаем,чтокаждыйспрайтшириной24ивысотой30пикселей.Следующийкодзагружаеткартинкуизадаётинтервалдлярисованияследующихкадров:

<canvas></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

varimg=document.createElement("img");

img.src="img/player.png";

varspriteW=24,spriteH=30;

img.addEventListener("load",function(){

varcycle=0;

setInterval(function(){

cx.clearRect(0,0,spriteW,spriteH);

cx.drawImage(img,

//sourcerectangle

cycle*spriteW,0,spriteW,spriteH,

//destinationrectangle

0,0,spriteW,spriteH);

cycle=(cycle+1)%8;

},120);

});

</script>

Переменнаяcycleотслеживаетпозициюванимации.Каждыйкадронаувеличиваетсяиподостижению7начинаетсначала,используяоператорделениясостатком.Онаиспользуетсядляподсчётакоординатыx,накоторойвизображениинаходитсяспрайтснужнойпозой.

Ачто,еслинамнадо,чтобыперсонажшёлвлево,аневправо?Мымоглибыдобавитьещёодиннаборспрайтов.Номытакжеможемсказатьхолсту,чтоб

Преобразования

ВыразительныйJavascript

310Рисованиенахолсте

Page 311: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

онрисовалкартинкузеркально.

Вызовметодаscaleприведётктому,чтовсепоследующиерисункибудутмасштабированы.Онпринимаетдвапараметра–масштабпогоризонталииповертикали.

<canvas></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

cx.scale(3,.5);

cx.beginPath();

cx.arc(50,50,40,0,7);

cx.lineWidth=3;

cx.stroke();

</script>

Масштабированиерастягиваетилисжимаетвсепараметрыкартинки,включаяширинулиниипозаданнымпараметрам.Масштабированиесотрицательнымпараметромпереворачиваеткартинкузеркально.Переворотпроисходитвокругточки(0,0),чтоозначает,чтонаправлениесистемыкоординаттожепоменяется.Приприменениигоризонтальногомасштаба-1,форма,нарисованнаянапозицииx=100,будетнарисованатам,гдераньшебылапозиция-100.

Значит,дляотзеркаливаниякартинкимынеможемпростодобавитьcx.scale(-1,1)передвызовомdrawImage–нашакартинкауедетсхолстаинебудетвидна.Можнобылобыподправитькоординаты,передаваемыевdrawImage,чтобыкомпенсироватьэтотсдвиг.Другойвариантдействий,когдакодрисованияничегонезнаетпромасштабирование,заключаетсявизменениинаправленияоси.

Естьнесколькодругихметодовкромемасштабирования,влияющихнакоординатнуюсистемухолста.Нарисованныеформыможноповорачиватьметодомrotateисдвигатьметодомtranslate.Интересно,чтовсетрансформациинакапливаются,тоестькаждаяпоследующаяпроисходитотносительнопредыдущих.

Значит,еслимыдваждысдвинемизображениена10пикселейпогоризонтали,товсёбудетнарисованона20пикселейправее.Еслимысначаласдвинемначалоотсчётана(50,50),азатемповернёмвсёна20

ВыразительныйJavascript

311Рисованиенахолсте

Page 312: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

градусов(0.1πрадиан),поворотпроизойдётвокругточки(50,50).

Аеслимысначалаповернёмвсёна20градусов,аужезатемсдвинемна(50,50),топреобразованиеслучитсявповёрнутойсистемекоординат,чтоприведёткиномурезультату.Порядокпреобразованийимеетзначение.

Чтобыотзеркалитькартинкуотносительновертикалиназаданнойпозицииx,мыделаемследующее:

functionflipHorizontally(context,around){

context.translate(around,0);

context.scale(-1,1);

context.translate(-around,0);

}

МысдвигаемосьYтуда,гденамнужнорасположитьнашезеркало,проводимотзеркаливание,исдвигаемосьYобратнонаполагающеесяместовзеркальнойвселенной.Следующийрисунокобъясняет,какэтоработает:

Тутпоказанысистемыкоординатдоипослеотзеркаливанияотносительноцентральнойлинии.ЕслимынарисуемтреугольниквположительнойполуплоскостиотносительноY,онбудетнаходитьсянаместетреугольника1.ВызовflipHorizontallyсначаласдвигаетеговправо,наместотреугольника2.

ВыразительныйJavascript

312Рисованиенахолсте

Page 313: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Затемпроисходитмасштабирование,итреугольникоказываетсянаместе3.Ондолженбытьнетам,еслинамнадоотзеркалитьегоотносительнозаданнойлинии.Второйвызовtranslateисправляетэто–он«отменяет»изначальныйсдвигипомещаеттреугольникнапозицию4.

Теперьможнонарисоватьотзеркаленногоперсонажанапозиции(100,0),перевернувмиротносительновертикалиизображенияперсонажа.

<canvas></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

varimg=document.createElement("img");

img.src="img/player.png";

varspriteW=24,spriteH=30;

img.addEventListener("load",function(){

flipHorizontally(cx,100+spriteW/2);

cx.drawImage(img,0,0,spriteW,spriteH,

100,0,spriteW,spriteH);

});

</script>

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

Возможносохранитьтекущеепреобразование,порисоватьчто-то,азатемвернутьстароесостояние.Такдолжнапоступатьфункция,делающаявременноепреобразованиесистемыкоординат.Сначаламысохраняемтопреобразование,котороеиспользовалкод,вызвавшийэтуфункцию.Затемфункцияотрабатываетнаосновепреобразований,проведённыхнаэтотмомент,и,возможно,добавляетновые.Ивконцемывозвращаемпреобразованиякначалу.

Этимзанимаютсяметодыsaveиrestoreдвумерногохолста.Посути,онихранятстексостоянийпреобразований.Привызовеsaveвстекдобавляетсятекущеесостояние,априrestoreберётсясостояниесверхустекаиприменяетсявкачестветекущегоконтекставсехпреобразований.

Хранениеиочисткапреобразований

ВыразительныйJavascript

313Рисованиенахолсте

Page 314: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Функциярисуетдревовиднуюструктуру,рисуялинию,потомпередвигаяцентркоординатнаконецлинии,ивызываясебязатемдважды–сначала,повернуввлево,азатемвправо.Каждыйвызовуменьшаетдлинуветви,ирекурсияостанавливается,когдадлинападаетменьше8.

<canvaswidth="600"height="300"></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

functionbranch(length,angle,scale){

cx.fillRect(0,0,1,length);

if(length<8)return;

cx.save();

cx.translate(0,length);

cx.rotate(-angle);

branch(length*scale,angle,scale);

cx.rotate(2*angle);

branch(length*scale,angle,scale);

cx.restore();

}

cx.translate(300,0);

branch(60,0.5,0.8);

</script>

Еслибынебыловызововsaveиrestore,второйрекурсивныйвызовbranchначиналбыспозициииповорота,созданныхпервым.Онбылбысоединённестекущейветкой,авнутреннейправойветкой,нарисованнойпервымвызовом.Врезультатеполучаетсятожеинтереснаяформа,ноуженедревовидная.

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

Назадкигре

ВыразительныйJavascript

314Рисованиенахолсте

Page 315: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

МыопределимтипобъектаCanvasDisplay,которыйбудетподдерживатьтотжеинтерфейс,чтоиDOMDisplayизглавы15,аименно,методыdrawFrameandclear.

Объектхранитбольшеинформации,чемDOMDisplay.ВместоиспользованияпозициипрокруткиэлементаDOM,онотслеживаетокнопросмотра,котороесообщает,какуючастьуровнямысейчасвидим.Такжеонотслеживаетвремяииспользуетэто,чтобырешить,какойкадранимациипоказывать.ИещёонхранитсвойствоflipPlayer,чтобыдажекогдаигрокстоялнаместе,онбылповёрнутвтусторону,вкоторуюшёлвпоследнийраз.

functionCanvasDisplay(parent,level){

this.canvas=document.createElement("canvas");

this.canvas.width=Math.min(600,level.width*scale);

this.canvas.height=Math.min(450,level.height*scale);

parent.appendChild(this.canvas);

this.cx=this.canvas.getContext("2d");

this.level=level;

this.animationTime=0;

this.flipPlayer=false;

this.viewport={

left:0,

top:0,

width:this.canvas.width/scale,

height:this.canvas.height/scale

};

this.drawFrame(0);

}

CanvasDisplay.prototype.clear=function(){

this.canvas.parentNode.removeChild(this.canvas);

};

В15главемыпередавалиразмершагавdrawFrameиз-засчётчикаanimationTime,несмотрянато,чтоDOMDisplayегонеиспользовал.НашановаяфункцияdrawFrameиспользуетегодляотсчётавремени,чтобыпереключатьсямеждукадрамианимациивзависимостиоттекущеговремени.

CanvasDisplay.prototype.drawFrame=function(step){

ВыразительныйJavascript

315Рисованиенахолсте

Page 316: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

this.animationTime+=step;

this.updateViewport();

this.clearDisplay();

this.drawBackground();

this.drawActors();

};

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

Таккакформынахолсте–всеголишьпиксели,послеихотрисовкиихнельзясдвинуть(илиубрать).Единственнымспособомобновитьхолстбудеточиститьегоиперерисоватьсцену.

МетодupdateViewportпохожнаметодscrollPlayerIntoViewизDOMDisplay.Онпроверяет,ненаходитсялиигрокслишкомблизкоккраюэкранаидвигаетокнопросмотра,еслиэтослучается.

CanvasDisplay.prototype.updateViewport=function(){

varview=this.viewport,margin=view.width/3;

varplayer=this.level.player;

varcenter=player.pos.plus(player.size.times(0.5));

if(center.x<view.left+margin)

view.left=Math.max(center.x-margin,0);

elseif(center.x>view.left+view.width-margin)

view.left=Math.min(center.x+margin-view.width,

this.level.width-view.width);

if(center.y<view.top+margin)

view.top=Math.max(center.y-margin,0);

elseif(center.y>view.top+view.height-margin)

view.top=Math.min(center.y+margin-view.height,

this.level.height-view.height);

};

ВызовыMath.maxиMath.minгарантируют,чтоокнопросмотранебудетпоказыватьпространствозапределамиуровня.Math.max(x,0)гарантирует,чтоитоговоечислонеменьшенуля.СходнымобразомMath.minгарантирует,чтозначениенепревыситзаданнуюграницу.

ВыразительныйJavascript

316Рисованиенахолсте

Page 317: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

CanvasDisplay.prototype.clearDisplay=function(){

if(this.level.status=="won")

this.cx.fillStyle="rgb(68,191,255)";

elseif(this.level.status=="lost")

this.cx.fillStyle="rgb(44,136,214)";

else

this.cx.fillStyle="rgb(52,166,251)";

this.cx.fillRect(0,0,

this.canvas.width,this.canvas.height);

};

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

varotherSprites=document.createElement("img");

otherSprites.src="img/sprites.png";

CanvasDisplay.prototype.drawBackground=function(){

varview=this.viewport;

varxStart=Math.floor(view.left);

varxEnd=Math.ceil(view.left+view.width);

varyStart=Math.floor(view.top);

varyEnd=Math.ceil(view.top+view.height);

for(vary=yStart;y<yEnd;y++){

for(varx=xStart;x<xEnd;x++){

vartile=this.level.grid[y][x];

if(tile==null)continue;

varscreenX=(x-view.left)*scale;

varscreenY=(y-view.top)*scale;

vartileX=tile=="lava"?scale:0;

this.cx.drawImage(otherSprites,

tileX,0,scale,scale,

screenX,screenY,scale,scale);

}

}

};

Непустыеклетки(null)рисуютсячерезdrawImage.ИзображениеotherSpritesсодержиткартинкидляэлементов,неотносящихсякигроку.Слеванаправо—этостена,лаваимонетка.

ВыразительныйJavascript

317Рисованиенахолсте

Page 318: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Клеткифона20х20пикселей,таккакмыиспользуемтужешкалу,чтобылавDOMDisplay.Значит,сдвигклетоклавы20(значениепеременнойscale),асдвигстен0.

Мынеждёмзагрузкиспрайта.ВызовdrawImageснезагруженнойпокакартинкойничегонесделает.Поэтому,нанесколькихпервыхкадрахиграможетбытьотрисовананеверно,ноэтонетакужкритично.Таккакмыобновляемэкран,правильнаясценапоявитсясразупослеокончаниязагрузки.

Нашперсонажбудетиспользованвкачествеигрока.Кодегоотрисовкидолженвыбиратьправильныйспрайтинаправление,зависящееоттекущегодвиженияигрока.Первыевосемьспрайтовсодержатанимациюходьбы.Когдаигрокпередвигаетсяпополу,мыперебираемихвзависимостиотсвойстваanimationTimeобъектаdisplay.Оноизмеряетсявсекундах,анамнадоменятькадры12развсекунду,поэтомумыумножаемвремяна12.Когдаигрокстоит,мырисуемдевятыйспрайт.Впрыжках,которыемыраспознаёмпотому,чтовертикальнаяскоростьотличнаотнуля,мырисуемдесятый,самыйправыйспрайт.

Посколькуспрайтычутьширешириныобъектаигрока–24пикселявместо16,чтобыбыломестодлярукиног,методдолженподправлятькоординатуxиширинуназаданноечисло(playerXOverlap).

varplayerSprites=document.createElement("img");

playerSprites.src="img/player.png";

varplayerXOverlap=4;

CanvasDisplay.prototype.drawPlayer=function(x,y,width,

height){

varsprite=8,player=this.level.player;

width+=playerXOverlap*2;

x-=playerXOverlap;

if(player.speed.x!=0)

this.flipPlayer=player.speed.x<0;

Спрайтыдлянашейигры

ВыразительныйJavascript

318Рисованиенахолсте

Page 319: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

if(player.speed.y!=0)

sprite=9;

elseif(player.speed.x!=0)

sprite=Math.floor(this.animationTime*12)%8;

this.cx.save();

if(this.flipPlayer)

flipHorizontally(this.cx,x+width/2);

this.cx.drawImage(playerSprites,

sprite*width,0,width,height,

x,y,width,height);

this.cx.restore();

};

МетодdrawPlayerвызываетсячерезdrawActors,которыйрисуетвсехактёроввигре.

CanvasDisplay.prototype.drawActors=function(){

this.level.actors.forEach(function(actor){

varwidth=actor.size.x*scale;

varheight=actor.size.y*scale;

varx=(actor.pos.x-this.viewport.left)*scale;

vary=(actor.pos.y-this.viewport.top)*scale;

if(actor.type=="player"){

this.drawPlayer(x,y,width,height);

}else{

vartileX=(actor.type=="coin"?2:1)*scale;

this.cx.drawImage(otherSprites,

tileX,0,width,height,

x,y,width,height);

}

},this);

};

Приотрисовкечего-либокромеигрокамысмотримнаеготип,чтобынайтисмещениедлянужногоспрайта.Лаванаходитсяпосмещению20,монета–40.

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

ВыразительныйJavascript

319Рисованиенахолсте

Page 320: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

СледующиймаленькийдокументподключаетновыйdisplayвrunGame:

<body>

<script>

runGame(GAME_LEVELS,CanvasDisplay);

</script>

</body>

Когдавамнужносоздаватьграфикувбраузере,увасестьвыбор–HTML,SVGихолст.Несуществуетидеальногоподходадлявсехситуаций.Укаждоговариантаестьплюсыиминусы.

ЧистыйHTMLпрост.Онхорошосочетаетсястекстом.SVGихолстпозволяютрисоватьтекст,нонепомогаютвегорасположенииинеделаютпереносов,когдаонзанимаетболееоднойлинии.ВHTMLпростовключатьблокитекста.

SVGможноиспользоватьдлясозданиячёткойграфики,котораявыглядитхорошоприлюбомувеличении.ОнсложнееобычногоHTML,ноигораздомощнее.

SVGиHTMLстроятструктуруданных(DOM),котораяпредставляеткартинку.Этопозволяетизменятьэлементыпослетого,каконинарисованы.Есливамнадопериодическименятьнебольшуючастьбольшойкартинкивответнадействияпользователяиливкачествеанимации,нахолстеэтобудетделатьоченьзатратно.DOMпозволяетрегистрироватьобработчикисобытиймышиналюбомэлементекартинки(даженаформах,нарисованныхчерезSVG).Схолстомэтонепройдёт.

Нопиксельныйподходхолстаимеетпреимуществопририсованиибольшогоколичестванебольшихэлементов.Оннестроитструктуруданных,апросторисуетнатойжесамойповерхностипиксели,чтоснижаетзатратностьвпересчётенаформы.

Естьещёфакторы,типасозданиясценыпопиксельно(например,прииспользованиитрассировкилучей)илипостобработкакартинкивJavaScript

Выборграфическогоинтерфейса

ВыразительныйJavascript

320Рисованиенахолсте

Page 321: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

(размытиеилиискажение),которыеможносделатьтолькоприпомощипопиксельногорисования.

Внекоторыхслучаяхможнокомбинироватьэтитехники.Например,можнонарисоватьграфчерезSVGилихолст,атекстовуюинформациюпоказывать,позиционируяэлементыHTMLповерхкартинки.

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

Вэтойглавемыобсудилитехникирисованияграфикивбраузере,сконцентрировавшисьнаэлементе<canvas>.

Узелхолстапредставляетобластьдокумента,гдепрограммаможетрисовать.Этоделаетсячерезобъектcontext,создаваемыйметодомgetContext.Интерфейсдвумерногорисованияпозволяетзакрашиватьиобводитьразныеформы.СвойствоfillStyleзадаётзаливкуформ.СвойстваstrokeStyleиlineWidthуправляюттем,какрисуютсялинии.

Прямоугольникиикускитекстаможнорисоватьоднимвызовомметода.МетодыfillRectиstrokeRectрисуютпрямоугольники,аfillTextиstrokeTextвыводяттекст.Длясозданияпроизвольныхформнамнужностроитьпути.

ВызовbeginPathначинаетпуть.Несколькометодовдобавляютлинииикривыектекущемупути.Например,lineToдобавляетпрямую.Когдапутьзакончен,егоможнозаполитьметодомfillилиобвестиметодомstroke.

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

Итог

ВыразительныйJavascript

321Рисованиенахолсте

Page 322: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Перемещенияпозволяютрисоватьформу,ориентированнуюпо-разному.Двумерныйконтекстхраниттекущеепреобразование,котороеможноменятьчерезметодыtranslate,scaleиrotate.Этоповлияетнавсеостальныеоперациирисования.Текущеесостояниепреобразованийможносохранитьметодомsaveивосстановитьметодомrestore.

ПририсованиианимацийнахолстеможноиспользоватьметодclearRectдляочисткичастейхолстапередперерисовкой.

Напишитепрограмму,рисующуюследующиефигуры:

1. трапецию2. красныйромб3. зигзаг4. спиральиз100отрезков5. жёлтуюзвезду

Рисуядвепоследних,консультируйтесьсописаниямифункцийMath.cosиMath.sinизглавы13,котораяописываетполучениекоординатнакругесихиспользованием.

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

<canvaswidth="600"height="200"></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

//Вашкод

Упражнения

Формы

ВыразительныйJavascript

322Рисованиенахолсте

Page 323: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

</script>

Ранеемывиделипримерпрограммыдлярисованиякруговойдиаграммы.Поменяйтееё,чтобыимякаждойкатегориибылопоказанорядомскуском,которыйеёпредставляет.Попробуйтеотыскатьсимпатичныйвариантавтоматическогопозиционированиятекста,которыйбыработалинадругихнаборахданных.Можнопредположить,чтонеткатегориименьше5%(чтобытекстнегромоздилсядругнадруга).

ВамсновамогутпонадобитьсяMath.sinиMath.cos.

<canvaswidth="600"height="300"></canvas>

<script>

varcx=document.querySelector("canvas").getContext("2d");

vartotal=results.reduce(function(sum,choice){

returnsum+choice.count;

},0);

varcurrentAngle=-0.5*Math.PI;

varcenterX=300,centerY=150;

//Добавьтекоддлявыводаметок

results.forEach(function(result){

varsliceAngle=(result.count/total)*2*Math.PI;

cx.beginPath();

cx.arc(centerX,centerY,100,

currentAngle,currentAngle+sliceAngle);

currentAngle+=sliceAngle;

cx.lineTo(centerX,centerY);

cx.fillStyle=result.color;

cx.fill();

});

</script>

ИспользуйтетехникуrequestAnimationFrameизглав13и15длярисованияпрямоугольникаспрыгающимвнутримячом.Мячдвигаетсяспостояннойскоростьюиотскакиваетотсторонпрямоугольникаприсоударении.

<canvaswidth="400"height="400"></canvas>

Круговаядиаграмма

Прыгающиймячик

ВыразительныйJavascript

323Рисованиенахолсте

Page 324: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

<script>

varcx=document.querySelector("canvas").getContext("2d");

varlastTime=null;

functionframe(time){

if(lastTime!=null)

updateAnimation(Math.min(100,time-lastTime)/1000);

lastTime=time;

requestAnimationFrame(frame);

}

requestAnimationFrame(frame);

functionupdateAnimation(step){

//Вашкод

}

</script>

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

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

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

Предварительнорассчитанноеотзеркаливание

ВыразительныйJavascript

324Рисованиенахолсте

Page 325: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

ТимБернес-Ли,Всемирнаяпаутина:Оченькороткаяличнаяистория

Есливадреснойстрокебраузеранабратьeloquentjavascript.net/17_http.html,браузерсначалараспознаетадрессервера,связанныйсименемeloquentjavascript.netипопробуетоткрытьTCPсоединениепо80порту–портдляHTTPпоумолчанию.Еслисерверсуществуетипринимаетсоединение,браузеротправляетчто-товроде:

GET/17_http.htmlHTTP/1.1Host:eloquentjavascript.netUser-Agent:Названиебраузера

Серверотвечаетпотомужесоединению:

HTTP/1.1200OKContent-Length:65585Content-Type:text/htmlLast-Modified:Wed,09Apr201410:48:09GMT

<!doctypehtml>…остатокдокумента

Браузерберёттучасть,чтоидётзаответомпослепустойстрокиипоказываетеёввидеHTML-документа.

Информация,отправленнаяклиентом,называетсязапросом.Онначинаетсясостроки:

GET/17_http.htmlHTTP/1.1

Первоеслово–методзапроса.GETозначает,чтонамнужнополучить

HTTP

Протокол

ВыразительныйJavascript

325HTTP

Page 326: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Частьпосленазванияметода–путькресурсу,ккоторомуотправлензапрос.Впростейшемслучае,ресурс–простофайлнасервере,нопротоколнеограничиваетсяэтойвозможностью.Ресурсможетбытьчемугодно,чтоможнопередатьвкачествефайла.Многиесерверысоздаютответыналету.Кпримеру,есливыоткроетеtwitter.com/marijnjh,серверпосмотритвбазеданныхпользователяmarijnjh,иеслинайдёт–создастстраницупрофиляэтогопользователя.

ПослепутикресурсуперваястроказапросаупоминаетHTTP/1.1,чтобысообщитьоверсииHTTP–протокола,которуюонаиспользует.

Ответсервератакженачинаетсясверсиипротокола,закоторойидётстатусответа–сначалакодизтрёхцифр,затемстрочка.

HTTP/1.1200OK

Кодыстатуса,начинающиесяс2,обозначаютуспешныезапросы.Коды,начинающиесяс4,означают,чточто-топошлонетак.404–самыйзнаменитыйстатусHTTP,обозначающий,чтозапрошенныйресурсненайден.Коды,начинающиесяс5,обозначают,чтонасерверепроизошлаошибка,нонеповинезапроса.

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

Content-Length:65585Content-Type:text/htmlLast-Modified:Wed,09Apr201410:48:09GMT

Тутопределяетсяразмеритипдокумента,полученноговответ.ВданномслучаеэтоHTML-документразмером65’585байт.Такжетутуказано,когдадокументбылизменёнпоследнийраз.

ВыразительныйJavascript

326HTTP

Page 327: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Побольшейчастиклиентилисерверопределяют,какиезаголовкинеобходимовключатьвзапросилиответ,хотянекоторыезаголовкиобязательны.Кпримеру,Host,обозначающийимяхоста,долженбытьвключёнвзапрос,потомучтоодинсерверможетобслуживатьмногоимёнхостовнаодномip-адресе,ибезэтогозаголовкасервернеузнает,скакимхостомклиентпытаетсяобщаться.

Послезаголовков,какзапрос,такиответмогутуказатьпустуюстроку,закоторойследуеттело,содержащеепередаваемыеданные.ЗапросыGETиDELETEнепересылаютдополнительныхданных,аPUTиPOSTпересылают.Некоторыеответы,например,сообщенияобошибке,нетребуютналичиятела.

Какмывиделивпримере,браузеротправляетзапрос,когдамывводимURLвадреснуюстроку.КогдавполученномHTMLдокументесодержатсяупоминаниядругихфайлов,такие,каккартинкиилифайлыJavaScript,онитожезапрашиваютсяссервера.

Веб-сайтсреднейрукилегкоможетсодержатьот10до200ресурсов.Чтобыиметьвозможностьзапроситьихпобыстрее,браузерыделаютнесколькозапросоводновременно,анеждутокончаниязапросоводногозадругим.ТакиедокументывсегдазапрашиваютсячереззапросыGET.

НастраницахHTMLмогутбытьформы,которыепозволяютпользователямвписыватьинформациюиотправлятьеёнасервер.Вотпримерформы:

<formmethod="GET"action="example/message.html">

<p>Имя:<inputtype="text"name="name"></p>

<p>Сообщение:<br><textareaname="message"></textarea></p>

<p><buttontype="submit">Отправить</button></p>

</form>

Кодописываетформусдвумяполями:маленькоезапрашиваетимя,абольшое–сообщение.Принажатиикнопки«Отправить»информацияизэтихполейбудетзакодированавстрокузапроса(querystring).КогдаатрибутmethodэлементаравенGET,иликогдаонвообщенеуказан,строказапроса

БраузериHTTP

ВыразительныйJavascript

327HTTP

Page 328: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

помещаетсявURLизполяaction,ибраузерделаетGET-запроссэтимURL.

GET/example/message.html?name=Jean&message=Yes%3FHTTP/1.1

Началострокизапросаобозначенознакомвопроса.Послеэтогоидутпарыимёнизначений,соответствующиеатрибутуnameполейформыисодержимомуэтихполей.Амперсанд(&)используетсядляихразделения.

Сообщение,отправляемоевпримере,содержитстроку“Yes?”,хотязнаквопросаизаменёнкаким-тостраннымкодом.Некоторыесимволывстрокезапросанужноэкранировать(escape).Знаквопросавтомчисле,ионпредставляетсякодом%3F.Естькакое-тонеписаноеправило,покоторомуукаждогоформатадолженбытьспособэкранироватьсимволы.ЭтоправилоподназваниемкодированиеURLиспользуетпроцент,закоторымидутдвешестнадцатеричныецифры,которыепредставляюткодсимвола.3Fвдесятичнойсистемебудет63,иэтокодзнакавопроса.УJavaScriptестьфункцииencodeURIComponentиdecodeURIComponentдлякодированияираскодирования.

console.log(encodeURIComponent("Hello&amp;goodbye"));

//→Hello%20%26%20goodbye

console.log(decodeURIComponent("Hello%20%26%20goodbye"));

//→Hello&amp;goodbye

ЕслимыпоменяематрибутmethodвформевпредыдущемпримеренаPOST,запросHTTPсотправкойформыпройдётприпомощиметодаPOST,которыйотправитстрокузапросавтелезапроса,вместодобавленияеёкURL.

POST/example/message.htmlHTTP/1.1Content-length:24Content-type:application/x-www-form-urlencoded

name=Jean&message=Yes%3F

ПосоглашениюметодGETиспользуетсядлязапросов,неимеющихпобочныхэффектов,таких,какпоиск.Запросы,которыечто-томеняютнасервере–создаютновыйаккаунтилиразмещаютсообщение,должныотправлятьсяметодомPOST.Клиентскиепрограммытипабраузеразнают,чтопростотакделатьзапросыформатаPOSTненужно,ииногданезаметно

ВыразительныйJavascript

328HTTP

Page 329: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

Интерфейс,черезкоторыйJavaScriptвбраузереможетделатьHTTP-запросы,называетсяXMLHttpRequest(заметьте,какпрыгаетразмербукв).ОнбылразработанвMicrosoftдлябраузераInternetExplorerвконце1990-х.ВэтовремяформатXMLбылоченьпопулярнымвмиребизнес-программ–авэтоммиреMicrosoftвсегдачувствовалсебя,какдома.Онбылнастолькопопулярным,чтоаббревиатураXMLбылапришпиленапередназваниеминтерфейсадляработысHTTP,хотяпоследнийсXMLвообщенесвязан.

Ивсёжеимянеполностьюбессмысленное.Интерфейспозволяетразбиратьвамответы,какеслибыэтобылидокументыXML.Смешиватьдверазныевещи(запросиразборответа)водну–это,конечно,отвратительныйдизайн,ночтоподелаешь.

КогдаинтерфейсXMLHttpRequestбылдобавленвInternetExplorer,сталоможноделатьвещи,которыераньшебылоделатьоченьсложно.Кпримеру,сайтысталипоказыватьспискиизподсказок,покапользовательвводитчто-либовтекстовоеполе.СкриптотправляеттекстнасерверчерезHTTPодновременноснаборомтекстапользователем.Сервер,укоторогоестьбазаданныхдлявозможныхвариантовввода,ищетсредизаписейподходящиеивозвращаетихназаддляпоказа.Этовыгляделооченькруто–людидоэтогопривыклиждатьперезагрузкивсейстраницыпослекаждоговзаимодействияссайтом.

ДругойважныйбраузертоговремениMozilla(позжеFirefox),нехотелотставать.Чтобыразрешитьделатьсходныевещи,Mozillaскопировалинтерфейсвместесназванием.Следующеепоколениебраузеровпоследовалоэтомупримеру,исегодняXMLHttpRequestявляетсястандартомdefacto.

XMLHttpRequest

ВыразительныйJavascript

329HTTP

Page 330: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Чтобыотправитьпростойзапрос,мысоздаёмобъектзапросасконструкторомXMLHttpRequestивызываемметодыopenиsend.

varreq=newXMLHttpRequest();

req.open("GET","example/data.txt",false);

req.send(null);

console.log(req.responseText);

//→Thisisthecontentofdata.txt

Методopenнастраиваетзапрос.ВнашемслучаемырешилисделатьGETзапроснафайлexample/data.txt.URL,неначинающиесясназванияпротокола(например,http:)называютсяотносительными,тоестьониинтерпретируютсяотносительнотекущегодокумента.Когдаониначинаютсясослеша(/),онизаменяюттекущийпуть–частьпосленазваниясервера.ВиномслучаечастьтекущегопутивплотьдопоследнегослешапомещаетсяпередотносительнымURL.

Послеоткрытиязапросамыможемотправитьегометодомsend.Аргументомслужиттелозапроса.ДлязапросовGETиспользуетсяnull.Еслитретийаргументдляopenбылfalse,тоsendвернётсятолькопослетого,какбылполученответнанашзапрос.ДляполучениятелаответамыможемпрочестьсвойствоresponseTextобъектаrequest.

Можнополучитьизобъектаresponseидругуюинформацию.Кодстатусадоступенвсвойствеstatus,атекстстатуса–вstatusText.ЗаголовкиможнопрочестьизgetResponseHeader.

varreq=newXMLHttpRequest();

req.open("GET","example/data.txt",false);

req.send(null);

console.log(req.status,req.statusText);

//→200OK

console.log(req.getResponseHeader("content-type"));

//→text/plain

Названиязаголовковнечувствительныкрегистру.Ониобычнопишутсясзаглавнойбуквывначалекаждогослова,например“Content-Type”,но

Отправказапроса

ВыразительныйJavascript

330HTTP

Page 331: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

“content-type”или“cOnTeNt-TyPe”будутописыватьодинитотжезаголовок.

Браузерсамдобавитнекоторыезаголовки,такие,как“Host”идругие,которыенужнысерверу,чтобывычислитьразмертела.НовыможетедобавлятьсвоисобственныезаголовкиметодомsetRequestHeader.Этонужнодляособыхслучаевитребуетсодействиясервера,ккоторомувыобращаетесь–онволенигнорироватьзаголовки,которыеоннеумеетобрабатывать.

Впримерезапросбылокончен,когдазаканчиваетсявызовsend.Этоудобнопотому,чтосвойствавродеresponseTextстановятсядоступнымисразу.Ноэтозначит,чтопрограмманашабудетожидать,покабраузерисерверобщаютсямежсобой.Приплохойсвязи,слабомсервереилибольшомфайлеэтоможетзанятьдлительноевремя.Этоплохоещёипотому,чтоникакиеобработчикисобытийнесработают,покапрограмманаходитсяврежимеожидания–документперестанетреагироватьнадействияпользователя.

Еслитретьимаргументомopenмыпередадимtrue,запросбудетасинхронным.Этозначит,чтопривызовеsendзапросставитсявочередьнаотправку.Программапродолжаетработать,абраузерпозаботитьсяоботправкеиполученииданныхвфоне.

Нопоказапрособрабатывается,мынеполучимответ.Намнуженмеханизмоповещенияотом,чтоданныепоступилииготовы.Дляэтогонамнужнобудетслушатьсобытие“load”.

varreq=newXMLHttpRequest();

req.open("GET","example/data.txt",true);

req.addEventListener("load",function(){

console.log("Done:",req.status);

});

req.send(null);

Также,каквызовrequestAnimationFrameвглаве15,этоткодвынуждаетнасиспользоватьасинхронныйстильпрограммирования,оборачиваявфункцию

Асинхронныезапросы

ВыразительныйJavascript

331HTTP

Page 332: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

тоткод,которыйдолженбытьвыполненпослезапроса,иустраиваявызовэтойфункциивнужноевремя.Мывернёмсякэтомупозже.

Когдаресурс,возвращённыйобъектомXMLHttpRequest,являетсядокументомXML,свойствоresponseXMLбудетсодержатьразобранноепредставлениеодокументе.ОноработаетсхожимсDOMобразом,заисключениемтого,чтоунегонетприсущейHTMLфункциональностинавродесвойстваstyle.Объект,содержащийсявresponseXML,соответствуетобъектуdocument.ЕгосвойствоdocumentElementссылаетсянавнешнийтегдокументаXML.Вследующемдокументе(example/fruit.xml)такимтегомбудет:

<fruits>

<fruitname="banana"color="yellow"/>

<fruitname="lemon"color="yellow"/>

<fruitname="cherry"color="red"/>

</fruits>

Мыможемполучитьтакойфайлследующимобразом:

varreq=newXMLHttpRequest();

req.open("GET","example/fruit.xml",false);

req.send(null);

console.log(req.responseXML.querySelectorAll("fruit").length);

//→3

ДокументыXMLможноиспользоватьдляобменассерверомструктурированнойинформацией.Ихформа–вложенныетеги–хорошоподходитдляхранениябольшинстваданных,нуилипокрайнеймерелучше,чемтекстовыефайлы.ИнтерфейсDOMнеуклюжвпланеизвлеченияинформации,иXMLдокументыполучаютсядовольномногословными.ОбычнолучшеобщатьсяприпомощиданныхвформатеJSON,которыепрощечитатьиписать–какпрограммам,такилюдям.

varreq=newXMLHttpRequest();

req.open("GET","example/fruit.json",false);

ПолучениеданныхXML

ВыразительныйJavascript

332HTTP

Page 333: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

req.send(null);

console.log(JSON.parse(req.responseText));

//→{banana:"yellow",lemon:"yellow",cherry:"red"}

HTTP-запросыизвеб-страницывызываютвопросыкасаемобезопасности.Человек,контролирующийскрипт,можетиметьинтересыотличныеотинтересовпользователя,начьёмкомпьютереонзапущен.Конкретно,еслиязашёлнасайтthemafia.org,янехочу,чтобыихскриптымоглиделатьзапросыкmybank.com,используяинформациюмоегобраузеравкачествеидентификатора,идаваякомандуотправитьвсемоиденьгинакакой-нибудьсчётмафии.

Вебсайтымогутзащититьсебяотподобныхатак,нодляэтоготребуютсяопределённыеусилия,имногиесайтысэтимнесправляются.Из-заэтогобраузерызащищаютих,запрещаяскриптамделатьзапросыкдругимдоменам(именамвродеthemafia.orgиmybank.com).

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

Access-Control-Allow-Origin:*

Вглаве10внашейреализациимодульнойсистемыAMDмыиспользовалигипотетическуюфункциюbackgroundReadFile.Онапринималаимяфайлаифункцию,ивызывалаэтуфункциюпослепрочтениясодержимогофайла.Вотпростаяреализацияэтойфункции:

functionbackgroundReadFile(url,callback){

varreq=newXMLHttpRequest();

req.open("GET",url,true);

req.addEventListener("load",function(){

if(req.status<400)

ПесочницадляHTTP

Абстрагируемзапросы

ВыразительныйJavascript

333HTTP

Page 334: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

callback(req.responseText);

});

req.send(null);

}

ПростаяабстракцияупрощаетиспользованиеXMLHttpRequestдляпростыхGET-запросов.Есливыпишетепрограмму,котораяделаетHTTP-запросы,будетнеплохоиспользоватьвспомогательнуюфункцию,чтобывамнеприходилосьвсёвремяповторятьуродливыйшаблонXMLHttpRequest.

Аргументcallback(обратныйвызов)–термин,частоиспользующийсядляописанияподобныхфункций.Функцияобратноговызовапередаётсявдругойкод,чтобыонмогпозватьнасобратнопозже.

НесложнонаписатьсвоювспомогательнуюфункциюHTTP,специальноскроеннуюподвашупрограмму.ПредыдущаяделаеттолькоGET-запросы,инедаётнамконтролянадзаголовкамиилителомзапроса.МожнонаписатьещёодинвариантдлязапросаPOST,илиболееобщий,поддерживающийразныезапросы.МногиебиблиотекиJavaScriptпредлагаютобёрткидляXMLHttpRequest.

Основнаяпроблемасприведённойобёрткой–обработкаошибок.Когдазапросвозвращаеткодстатуса,обозначающийошибку(от400ивыше),онничегонеделает.Внекоторыхслучаяхэтонормально,нопредставьте,чтомыпоставилииндикаторзагрузкинастранице,показывающий,чтомыполучаеминформацию.Еслизапроснеудался,потомучтосерверупалилисоединениепрервано,страницабудетделатьвид,чтооначем-тозанята.Пользовательподождётнемного,потомемунадоестионрешит,чтосайткакой-тодурацкий.

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

Обработкаошибоквасинхронномкодеещёсложнее,чемвсинхронном.Посколькунамчастоприходитсяотделятьчастьработыиразмещатьеёвфункцииобратноговызова,областьвидимостиблокаtryтеряетсмысл.Вследующемкодеисключениенебудетпоймано,потомучтовызовbackgroundReadFileвозвращаетсясразуже.Затемуправлениеуходитиз

ВыразительныйJavascript

334HTTP

Page 335: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

блокаtry,ифункцияизнегонебудетвызвана.

try{

backgroundReadFile("example/data.txt",function(text){

if(text!="expected")

thrownewError("Thatwasunexpected");

});

}catch(e){

console.log("Hellofromthecatchblock");

}

Чтобыобрабатыватьнеудачныезапросы,придётсяпередаватьдополнительнуюфункциювнашуобёртку,ивызыватьеёвслучаепроблем.Другойвариант–использоватьсоглашение,чтоеслизапроснеудался,товфункциюобратноговызовапередаётсядополнительныйаргументсописаниемпроблемы.Пример:

functiongetURL(url,callback){

varreq=newXMLHttpRequest();

req.open("GET",url,true);

req.addEventListener("load",function(){

if(req.status<400)

callback(req.responseText);

else

callback(null,newError("Requestfailed:"+

req.statusText));

});

req.addEventListener("error",function(){

callback(null,newError("Networkerror"));

});

req.send(null);

}

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

Код,использующийgetURL,долженпроверятьневозвращеналиошибка,иобрабатыватьеё,еслионаесть.

getURL("data/nonsense.txt",function(content,error){

if(error!=null)

console.log("Failedtofetchnonsense.txt:"+error);

ВыразительныйJavascript

335HTTP

Page 336: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

else

console.log("nonsense.txt:"+content);

});

Сисключениямиэтонепомогает.Когдамысовершаемпоследовательнонесколькоасинхронныхдействий,исключениевлюбойточкецепочкивлюбомслучае(еслитольковынеобернуликаждыйобработчиквсвойблокtry/catch)вывалитсянаверхнемуровнеипрервётвсюцепочку.

Тяжелописатьасинхронныйкоддлясложныхпроектовввидепростыхобратныхвызовов.Оченьлегкозабытьпроверкунаошибкуилипозволитьнеожиданномуисключениюрезкопрерватьпрограмму.Крометого,организацияправильнойобработкиошибокипроходошибкичерезнесколькопоследовательныхобратныхвызововоченьутомительна.

Предпринималосьмножествопопытокрешитьэтупроблемудополнительнымиабстракциями.Однаизнаиболееудачныхпопытокназываетсяобещаниями(promises).Обещанияоборачиваютасинхронноедействиевобъект,которыйможетпередаватьсяикоторомунужносделатькакие-товещи,когдадействиезавершаетсяилинеудаётся.ТакойинтерфейсужесталчастьютекущейверсииJavaScript,адлястарыхверсийегоможноиспользоватьввидебиблиотеки.

Интерфейсобещанийнеособенноинтуитивнопонятный,номощный.Вэтойглавемылишьчастичноопишемего.Большеинформацииможнонайтинаwww.promisejs.org

ДлясозданияобъектаpromisesмывызываемконструкторPromise,задаваяемуфункциюинициализацииасинхронногодействия.Конструкторвызываетэтуфункциюипередаётейдвааргумента,которыесамитакжеявляютсяфункциями.Перваядолжнавызыватьсявудачномслучае,другая–внеудачном.

ИвотнашаобёрткадлязапросовGET,котораянаэтотразвозвращаетобещание.Теперьмыпростоназовёмегоget.

Обещания

ВыразительныйJavascript

336HTTP

Page 337: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functionget(url){

returnnewPromise(function(succeed,fail){

varreq=newXMLHttpRequest();

req.open("GET",url,true);

req.addEventListener("load",function(){

if(req.status<400)

succeed(req.responseText);

else

fail(newError("Requestfailed:"+req.statusText));

});

req.addEventListener("error",function(){

fail(newError("Networkerror"));

});

req.send(null);

});

}

Заметьте,чтоинтерфейсксамойфункцииупростился.МыпередаёмейURL,аонавозвращаетобещание.Оноработаеткакобработчикдлявыходныхданныхзапроса.Унегоестьметодthen,которыйвызываетсясдвумяфункциями:однойдляобработкиуспеха,другой–длянеудачи.

get("example/data.txt").then(function(text){

console.log("data.txt:"+text);

},function(error){

console.log("Failedtofetchdata.txt:"+error);

});

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

Вызовthenпроизводитновоеобещание,чейрезультат(значение,передающеесявобработчикиуспешныхрезультатов)зависитотзначения,возвращаемогопервойпереданнойнамивthenфункцией.Этафункцияможетвернутьещёоднообещание,обозначаячтопроводитсядополнительнаяасинхроннаяработа.Вэтомслучаеобещание,возвращаемоеthenсамопосебебудетждатьобещания,возвращённогофункцией-обработчиком,иуспехилинеудачапроизойдутстакимжезначением.Когдафункция-обработчиквозвращаетзначение,неявляющеесяобещанием,обещание,возвращаемоеthen,становитсяуспешным,в

ВыразительныйJavascript

337HTTP

Page 338: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

качестверезультатаиспользуяэтозначение.

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

functiongetJSON(url){

returnget(url).then(JSON.parse);

}

Последнийвызовthenнеобозначилобработчикнеудач.Этодопустимо.Ошибкабудетпереданавобещание,возвращаемоечерезthen,аведьэтонаминадо–getJSONнезнает,чтоделать,когдачто-тоидётнетак,ноестьнадежда,чтовызывающийеёкодэтознает.

Вкачествепримера,показывающегоиспользованиеобещаний,мынапишемпрограмму,получающуючислоJSON-файловссервера,ипоказывающуювовремяисполнениязапросаслово«загрузка».Файлысодержатинформациюолюдяхиссылкинадругиефайлысинформациейодругихлюдяхвсвойствахтипаотец,мать,супруг.

Намнужнополучитьимяматерисупругаизexample/bert.json.Вслучаепроблемнамнужноубратьтекст«загрузка»ипоказатьсообщениеобошибке.Воткакэтоможноделатьприпомощиобещаний:

<script>

functionshowMessage(msg){

varelt=document.createElement("div");

elt.textContent=msg;

returndocument.body.appendChild(elt);

}

varloading=showMessage("Загрузка...");

getJSON("example/bert.json").then(function(bert){

returngetJSON(bert.spouse);

}).then(function(spouse){

returngetJSON(spouse.mother);

}).then(function(mother){

showMessage("Имя-"+mother.name);

}).catch(function(error){

showMessage(String(error));

}).then(function(){

document.body.removeChild(loading);

ВыразительныйJavascript

338HTTP

Page 339: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

});

</script>

Итоговаяпрограммаотносительнокомпактнаичитаема.Методcatchсхожсthen,нооножидаеттолькообработчикнеудачногорезультатаивслучаеуспехапередаётдальшенеизменённыйрезультат.Исполнениепрограммыбудетпродолженообычнымпутёмпослеотловаисключения–также,каквслучаесtry/catch.Такимобразом,последнийthen,удаляющийсообщениеозагрузке,выполняетсявлюбомслучае,дажевслучаенеудачи.

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

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

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

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

Другойподход–построитьсвоюсистемуобщениянаконцепцииресурсовиметодовHTTP.ВместоудалённоговызовапроцедурыпоимениaddUserвыделаетезапросPUTк/users/larry.Вместокодированиясвойствпользователяваргументахфункциивыопределяетеформатдокументаилииспользуетесуществующийформат,которыйбудетпредставлятьпользователя.Тело

ЦенитеHTTP

ВыразительныйJavascript

339HTTP

Page 340: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

PUT-запроса,создающегоновыйресурс,будетпростодокументомэтогоформата.РесурсполучаетсячереззапросGETкегоURL(/user/larry),которыйвозвращаетпредставляющийэтотресурсдокумент.

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

Данныепутешествуютпоинтернетуподлинномуиопасномупути.Чтобыдобратьсядопунктаназначения,имнадопопрыгатьчерезвсякиеместа,начинаяотWi-Fiсетикофейнидосетей,контролируемыхразнымиорганизациямиигосударствами.Влюбойточкепутиихмогутпрочитатьилидажепоменять.

Еслинужнохранитьчто-либовсекрете,напримерпароликемейлу,илиданнымнеобходимоприйтивпунктназначениявнеизменномвиде—таким,например,какномербанковскогосчёта,накоторыйвыпереводитеденьги,-простогоHTTPнедостаточно.

БезопасныйпротоколHTTP,URLкоторогоначинаютсясhttps://,оборачиваетHTTP-трафиктак,чтобыегобылосложнеепрочитатьипоменять.Сначалаклиентподтверждает,чтосервер–тот,закогосебявыдаёт,требуяссерверапредставитькриптографическийсертификат,выданныйавторитетнойстороной,которуюпризнаётбраузер.Потом,вседанные,проходящиечерезсоединение,шифруютсятак,чтобыпредотвратитьпрослушкуиизменение.

Такимобразом,когдавсёработаетправильно,HTTPSпредотвращаеткакслучаи,когдакто-топритворяетсядругимвеб-сайтом,скоторымвыобщаетесь,такислучаипрослушкивашегообщения.Оннеидеален,иужебылислучаи,когдаHTTPSнесправлялсясработойиз-заподдельныхиликраденыхсертификатовилисломанныхпрограмм.Темнеменее,сHTTPоченьлегкосделатьчто-топлохое,авзломHTTPSтребуеттакихусилий,которыемогутприкладыватьтолькогосударственныеструктурыилиочень

БезопасностьиHTTPS

ВыразительныйJavascript

340HTTP

Page 341: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

серьёзныекриминальныеорганизации(амеждуэтимиорганизациямииногдасовсемнетразличий).

Вэтойглавемыувидели,чтоHTTP–этопротоколдоступакресурсамвинтернете.Клиентотправляетзапрос,содержащийметод(обычноGET),ипуть,которыйопределяетресурс.Серверрешает,чтоемуделатьсзапросомиотвечаетскодомстатусаителомответа.Запросыиответымогутсодержатьзаголовки,вкоторыхпередаётсядополнительнаяинформация.

БраузерыделаютGET-запросыдляполученияресурсов,необходимыхдляпоказастраницы.Страницаможетсодержатьформы,которыепозволяютинформации,введённойпользователем,бытьотправленнойвзапросе,которыйсоздаётсяпослеотправкиформы.Выузнаетеобэтомбольшевследующейглаве.

Интерфейс,черезкоторыйJavaScriptделаетHTTP-запросыизбраузера,называетсяXMLHttpRequest.Можноигнорироватьприставку“XML”(нописатьеёвсёравнонужно).Использоватьегоможнодвумяспособами:синхронным,которыйблокируетвсюработудоокончаниявыполнениязапроса,иасинхронным,которыйтребуетустановкиобработчикасобытий,отслеживающегоокончаниезапроса.Почтивовсехслучаяхпредпочтительнымявляетсяасинхронныйспособ.Созданиезапросавыглядиттак:

varreq=newXMLHttpRequest();

req.open("GET","example/data.txt",true);

req.addEventListener("load",function(){

console.log(req.statusCode);

});

req.send(null);

Асинхронноепрограммирование–непростаявещь.Обещания–интерфейс,которыйделаетеёпроще,помогаянаправлятьсообщенияобошибкахиисключениякнужномуобработчику,иабстрагируянекоторыеповторяющиесяэлементы,подверженныеошибкам.

Итог

ВыразительныйJavascript

341HTTP

Page 342: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Однаизвещей,которыеHTTPумеетделать,нокоторуюмынеобсуждали,называетсясогласованиемсодержания.ЗаголовокAcceptвзапросеможноиспользоватьдлясообщениясерверутого,какиетипыдокументовклиентжелаетполучить.Многиесерверыегоигнорируют,нокогдасерверзнаеторазныхспособахкодированияресурса,онможетвзглянутьназаголовокиотправитьтот,которыйпредпочитаетклиент.

URLeloquentjavascript.net/authorнастроеннаответкакпрямымтекстом,такиHTMLилиJSON,взависимостиотзапросаклиента.Этиформатыопределяютсястандартизированнымитипамисодержимогоtext/plain,text/html,иapplication/json.

Отправьтезапросдляполучениявсехтрёхформатовэтогоресурса.ИспользуйтеметодsetRequestHeaderобъектаXMLHttpRequestдляустановкизаголовкаAcceptводинизнужныхтиповсодержимого.Убедитесь,чтовыустанавливаетезаголовокпослеopen,нопередsend.

Наконец,попробуйтезапроситьсодержимоетипаapplication/rainbows+unicornsипосмотрите,чтопроизойдёт.

УконструктораPromiseестьметодall,который,получаямассивобещаний,возвращаетобещание,котороеждётзавершениявсехуказанныхвмассивеобещаний.Затемонвыдаётуспешныйрезультативозвращаетмассивсрезультатами.Есликакие-тоизобещанийвмассивезавершилисьнеудачно,общееобещаниетакжевозвращаетнеудачу(созначениемнеудавшегосяобещанияизмассива).

Попробуйтесделатьчто-либоподобное,написавфункциюall.

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

Упражнения

Согласованиесодержания(contentnegotiation)

Ожиданиенесколькихобещаний

ВыразительныйJavascript

342HTTP

Page 343: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ошибокввашемобещании.

functionall(promises){

returnnewPromise(function(success,fail){

//Вашкод.

});

}

//Проверочныйкод.

all([]).then(function(array){

console.log("Этодолженбыть[]:",array);

});

functionsoon(val){

returnnewPromise(function(success){

setTimeout(function(){success(val);},

Math.random()*500);

});

}

all([soon(1),soon(2),soon(3)]).then(function(array){

console.log("Этодолженбыть[1,2,3]:",array);

});

functionfail(){

returnnewPromise(function(success,fail){

fail(newError("бабах"));

});

}

all([soon(1),fail(),soon(3)]).then(function(array){

console.log("Сюдамыпопастьнедолжны");

},function(error){

if(error.message!="бабах")

console.log("Неожиданныйоблом:",error);

});

ВыразительныйJavascript

343HTTP

Page 344: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Мефистофель,в«Фаусте»Гёте

Формыбыликраткопредставленывпредыдущейглавевкачествеспособапередачиинформации,введённойпользователем,черезHTTP.ОнибылиразработаныввебедопоявленияJavaScript,стемрасчётом,чтовзаимодействиессерверомпроисходитприпереходенадругуюстраницу.

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

Веб-формасостоитизлюбогочислаполейввода,окружённыхтегом<form>.HTMLпредлагаетмногоразныхполей,отпростыхгалочексозначениямивкл/выклдовыпадающихсписковиполейдлявводатекста.Вэтойкнигенебудутподробнообсуждатьсявсевидыполей,номысделаемнебольшойихобзор.

Многотиповполейвводаиспользуюттег<input>.Егоатрибутtypeиспользуетсядлявыборастиляполя.Вотнесколькораспространённыхтипов:

textтекстовоеполенаоднустрокуpasswordтоже,чтотекст,нопрячетвводcheckboxпереключательвкл/выклradioчастьполясвозможностьювыбораизнесколькихвариантовfileпозволяетпользователювыбратьфайлнаегокомпьютере

Формыиполяформ

Поля

ВыразительныйJavascript

344Формыиполяформ

Page 345: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Поляформнеобязательнодолжныпоявлятьсявнутритега<form>.Ихможноразместитьвлюбомместестраницы.Информациюизтакихполейнельзяпередаватьнасервер(этовозможнотолькодлявсейформыцеликом),нокогдамыделаемполя,которыеобрабатываетJavaScript,намобычноиненужнопередаватьинформациюизполейчерезsubmit.

<p><inputtype="text"value="abc">(text)</p>

<p><inputtype="password"value="abc">(password)</p>

<p><inputtype="checkbox"checked>(checkbox)</p>

<p><inputtype="radio"value="A"name="choice">

<inputtype="radio"value="B"name="choice"checked>

<inputtype="radio"value="C"name="choice">(radio)</p>

<p><inputtype="file"checked>(file)</p>

ИнтерфейсJavaScriptдлятакихэлементовразнитсявзависимостиоттипа.Мырассмотримкаждыйизнихчутьпозже.

Утекстовыхполейнанесколькострокестьсвойтег<textarea>.Утегадолженбытьзакрывающийтег</textarea>,иониспользуеттекствнутриэтихтеговвместоиспользованияатрибутаvalue.

<textarea>

один

два

три

</textarea>

Атег<select>используетсядлясозданияполя,котороепозволяетпользователювыбратьодиниззаданныхвариантов.

<select>

<option>Блины</option>

<option>Запеканка</option>

<option>Мороженка</option>

</select>

Когдазначениеполяизменяется,запускаетсясобытие“change”.

Фокус

ВыразительныйJavascript

345Формыиполяформ

Page 346: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ВотличиеотбольшинстваэлементовдокументаHTML,поляформмогутполучатьфокусвводаклавиатуры.Прищелчкеиливыбореихдругимспособомонистановятсяактивными,т.е.главнымиприёмникамиклавиатурноговвода.

Есливдокументеестьтекстовоеполе,тонабираемыйтекстпоявитсявнём,толькоеслиполеимеетфокусввода.Другиеполяпо-разномуреагируютнаклавиатуру.Кпримеру,<select>пытаетсяперейтинавариант,содержащийтекст,которыйвводитпользователь,атакжеотвечаетнанажатиястрелок,передвигаявыборвариантавверхивниз.

УправлятьфокусомизJavaScriptможнометодамиfocusиblur.ПервыйперемещаетфокуснаэлементDOM,изкоторогоонвызван,авторойубираетфокус.Значениеdocument.activeElementсоответствуеттекущемуэлементу,получившемуфокус.

<inputtype="text">

<script>

document.querySelector("input").focus();

console.log(document.activeElement.tagName);

//→INPUT

document.querySelector("input").blur();

console.log(document.activeElement.tagName);

//→BODY

</script>

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

<inputtype="text"autofocus>

БраузерыпотрадициипозволяютпользователюперемещатьфокусклавишейTab.Мыможемвлиятьнапорядокперемещениячерезатрибутtabindex.Впримередокументбудетпереноситьфокусстекстовогополяна

ВыразительныйJavascript

346Формыиполяформ

Page 347: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

кнопкуOK,вместотого,чтобысначалапройтичерезссылкуhelp.

<inputtype="text"tabindex=1><ahref=".">(help)</a>

<buttononclick="console.log('ok')"tabindex=2>OK</button>

Поумолчанию,большинствотиповэлементовHTMLнеполучаютфокус.Нодобавивtabindexкэлементу,высделаетевозможнымполучениеимфокуса.

Всеполяможноотключитьатрибутомdisabled,которыйсуществуетиввидесвойстваэлементаобъектаDOM.

<button>Уменявсёхорошо</button>

<buttondisabled>Явотключке</button>

Отключённыеполянепринимаютфокусинеизменяются,ивотличиеотактивных,обычновыглядятсерымиивыцветшими.

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

Когдаполе,содержитсявэлементе<form>,уегоэлементаDOMбудетсвойствоform,котороебудетссылатьсянаформу.Элемент<form>,всвоюочередь,имеетсвойствоelements,содержащеемассивоподобнуюколлекциюполей.

Атрибутnameполязадаёт,какбудетопределенозначениеэтогополяприпередаченасервер.Еготакжеможноиспользоватькакимясвойствапридоступексвойствуформыelements,которыйработаетикакобъект,похожийнамассив(сдоступомпономерам),такиmap(сдоступомпоимени).

Отключённыеполя

Формавцелом

ВыразительныйJavascript

347Формыиполяформ

Page 348: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

<formaction="example/submit.html">

Имя:<inputtype="text"name="name"><br>

Пароль:<inputtype="password"name="password"><br>

<buttontype="submit">Войти</button>

</form>

<script>

varform=document.querySelector("form");

console.log(form.elements[1].type);

//→password

console.log(form.elements.password.type);

//→password

console.log(form.elements.name.form==form);

//→true

</script>

Кнопкасатрибутомtypeравнымsubmitпринажатииотправляетформу.НажатиеклавишиEnterвнутриполяформыимееттотжеэффект.

Отправкаформыобычноозначает,чтобраузерпереходитнастраницу,обозначеннуюватрибутеформыaction,используялибоGETлибоPOSTзапрос.Нопередэтимзапускаетсясвойство“submit”.ЕгоможнообработатьвJavaScript,иобработчикможетпредотвратитьповедениепоумолчанию,вызвавнаобъектеeventpreventDefault.

<formaction="example/submit.html">

Значение:<inputtype="text"name="value">

<buttontype="submit">Сохранить</button>

</form>

<script>

varform=document.querySelector("form");

form.addEventListener("submit",function(event){

console.log("Savingvalue",form.elements.value.value);

event.preventDefault();

});

</script>

Перехватсобытий“submit”полезенвнесколькихслучаях.Мыможемнаписатькод,проверяющийдопустимостьвведённыхзначенийисразужепоказатьошибкувместопередачиданныхформы.Илимыможемотключитьотправкуформыпоумолчаниюидатьпрограммевозможностьсамойобработатьввод,напримериспользуяXMLHttpRequestдляотправкиданныхнасервербезперезагрузкистраницы.

ВыразительныйJavascript

348Формыиполяформ

Page 349: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Полястегами<input>итипамиtextиpassword,атакжетеги,имеютобщийинтерфейс.УихэлементовDOMестьсвойствоvalue,вкоторомсодержитсяихтекущеесодержимоеввидестрокитекста.Присваиваниеэтомусвойствузначенияменяетсодержимоеполя.

СвойстватекстовыхполейselectionStartиselectionEndсодержатданныеоположениикурсораивыделениятекста.Когданичегоневыделено,ихзначениеодинаковое,иравноположениюкурсора.Например,0обозначаетначалотекста,10обозначает,чтокурсорнаходитсяна10-мсимволе.Когдавыделеначастьполя,свойстваимеютразныезначения,аименноначалоиконецвыделенноготекста.Вэтиполятакжеможнозаписыватьзначение.

Кпримеру,представьте,чтовыпишетестатьюпроKhasekhemwy,нозатрудняетесьписатьегоимяправильно.Следующийкодназначаеттегу<textarea>обработчиксобытий,которыйпринажатииF2вставляетстроку“Khasekhemwy”.

<textarea></textarea>

<script>

vartextarea=document.querySelector("textarea");

textarea.addEventListener("keydown",function(event){

//ThekeycodeforF2happenstobe113

if(event.keyCode==113){

replaceSelection(textarea,"Khasekhemwy");

event.preventDefault();

}

});

functionreplaceSelection(field,word){

varfrom=field.selectionStart,to=field.selectionEnd;

field.value=field.value.slice(0,from)+word+

field.value.slice(to);

//Putthecursoraftertheword

field.selectionStart=field.selectionEnd=

from+word.length;

};

</script>

ФункцияreplaceSelectionзаменяеттекущийвыделенныйтекстзаданнымсловом,иперемещаеткурсорнапозициюпослеэтогослова,чтобыможно

Текстовыеполя

ВыразительныйJavascript

349Формыиполяформ

Page 350: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

былопродолжатьпечатать.

Событие“change”длятекстовогополянесрабатываеткаждыйразпривводеодногосимвола.Оносрабатываетпослепотериполемфокуса,когдаегозначениебылоизменено.Чтобымгновеннореагироватьнаизменениетекстовогополянужнозарегистрироватьсобытие“input”,котороесрабатываеткаждыйразпривводесимвола,удалениитекстаилидругихманипуляцияхссодержимымполя.

Вследующемпримеремывидимтекстовоеполеисчётчик,показывающийтекущуюдлинувведённоготекста.

<inputtype="text">length:<spanid="length">0</span>

<script>

vartext=document.querySelector("input");

varoutput=document.querySelector("#length");

text.addEventListener("input",function(){

output.textContent=text.value.length;

});

</script>

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

<inputtype="checkbox"id="purple">

<labelfor="purple">Сделатьстраницуфиолетовой</label>

<script>

varcheckbox=document.querySelector("#purple");

checkbox.addEventListener("change",function(){

document.body.style.background=

checkbox.checked?"mediumpurple":"";

});

</script>

Тег<label>используетсядлясвязикускатекстасполемввода.Атрибутforдолженсовпадатьсidполя.Щелчокпометкеlabelвключаетполеввода,онополучаетфокусименяетзначение–еслиэтогалочкаилирадиокнопка.

Галочкиирадиокнопки

ВыразительныйJavascript

350Формыиполяформ

Page 351: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Цвет:

<inputtype="radio"name="color"value="mediumpurple">Фиолетовый

<inputtype="radio"name="color"value="lightgreen">Зелёныйы

<inputtype="radio"name="color"value="lightblue">Голубой

<script>

varbuttons=document.getElementsByName("color");

functionsetColor(event){

document.body.style.background=event.target.value;

}

for(vari=0;i<buttons.length;i++)

buttons[i].addEventListener("change",setColor);

</script>

Методdocument.getElementsByNameвыдаётвсеэлементысзаданныматрибутомname.Примерперебираетих(посредствомобычногоциклаfor,анеforEach,потомучтовозвращаемаяколлекция–ненастоящиймассив)ирегистрируетобработчиксобытийдлякаждогоэлемента.Помните,чтоуобъектовсобытийестьсвойствоtarget,относящеесякэлементу,которыйзапустилсобытие.Этополезнодлясозданияобработчиковсобытий–нашобработчикможетбытьвызванразнымиэлементами,иунегодолженбытьспособполучитьдоступктекущемуэлементу,которыйеговызвал.

Поляselectпохожинарадиокнопки–онитакжепозволяютвыбратьизнесколькихвариантов.Ноеслирадиокнопкипозволяютнамконтролироватьраскладкувариантов,товидполя<select>определяетбраузер.

Уполейselectестьвариант,большепохожийнасписокгалочек,чемнарадиокнопки.Приналичииатрибутаmultipleтег<select>позволитвыбиратьлюбоеколичествовариантов,анеодин.

<selectmultiple>

<option>Блины</option>

<option>Запеканка</option>

<option>Мороженка</option>

</select>

Поляselect

ВыразительныйJavascript

351Формыиполяформ

Page 352: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Атрибутsizeтега<select>используетсядлязаданияколичествавариантов,которыевидныодновременно–таквыможетевлиятьнавнешнийвидвыпадушки.Кпримеру,назначивsize3,выувидитетристрокиодновременно,безотносительнотого,присутствуетлиопцияmultiple.

Укаждоготега<option>естьзначение.Егоможноопределитьатрибутомvalue,ноеслионнезадан,тозначениетегаопределяеттекст,находящийсявнутритега<option>..</option>.Свойствоvalueэлементаотражаеттекущийвыбранныйвариант.Дляполясвозможностьювыборанесколькихвариантовэтосвойствонеособонужно,т.к.внёмбудетсодержатьсятолькоодинизнесколькихвыбранныхвариантов.

Ктегу<option>поля<select>можнополучитьдоступкаккмассивоподобномуобъектучерезсвойствоoptions.Укаждоговариантаестьсвойствоselected,показывающее,выбранлисейчасэтотвариант.Свойствотакжеможноменять,чтобывариантстановилсявыбраннымилиневыбранным.

Следующийпримеризвлекаетвыбранныезначенияизполяselectииспользуетихдлясозданиядвоичногочислаизбитов.НажмитеCtrl(илиCommandнаМаке),чтобывыбратьнесколькозначенийсразу.

<selectmultiple>

<optionvalue="1">0001</option>

<optionvalue="2">0010</option>

<optionvalue="4">0100</option>

<optionvalue="8">1000</option>

</select>=<spanid="output">0</span>

<script>

varselect=document.querySelector("select");

varoutput=document.querySelector("#output");

select.addEventListener("change",function(){

varnumber=0;

for(vari=0;i<select.options.length;i++){

varoption=select.options[i];

if(option.selected)

number+=Number(option.value);

}

ВыразительныйJavascript

352Формыиполяформ

Page 353: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

output.textContent=number;

});

</script>

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

Файловоеполеобычновыглядиткаккнопкаснадписьювроде«Выберитефайл»,синформациейпровыбранныйфайлрядомсней.

<inputtype="file">

<script>

varinput=document.querySelector("input");

input.addEventListener("change",function(){

if(input.files.length>0){

varfile=input.files[0];

console.log("Youchose",file.name);

if(file.type)

console.log("Ithastype",file.type);

}

});

</script>

Свойствоfilesэлемента–массивоподобныйобъект(ненастоящиймассив),содержащийсписоквыбранныхфайлов.Изначальноонпуст.Уэлементанетпростогосвойстваfile,потомучтопользовательможетвыбратьнесколькофайловзаразпривключённоматрибутеmultiple.

Уобъектоввсвойствеfilesестьсвойстваимя(имяфайла),размер(размерфайлавбайтах),итип(типфайлавсмыслеmediatype—text/plainилиimage/jpeg).

Чегоунегонет,такэтосвойства,содержащегосодержимоефайла.Чтобыполучитьсодержимое,приходитьсяпостараться.Таккакчтениефайлас

Файловоеполе

ВыразительныйJavascript

353Формыиполяформ

Page 354: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

дисказанимаетдлительноевремя,интерфейсдолженбытьасинхронным,чтобыдокументнезамирал.КонструкторFileReaderможнопредставлятьсебе,какконструкторXMLHttpRequest,толькодляфайлов.

<inputtype="file"multiple>

<script>

varinput=document.querySelector("input");

input.addEventListener("change",function(){

Array.prototype.forEach.call(input.files,function(file){

varreader=newFileReader();

reader.addEventListener("load",function(){

console.log("File",file.name,"startswith",

reader.result.slice(0,20));

});

reader.readAsText(file);

});

});

</script>

ЧтениефайлапроисходитприпомощисозданияобъектаFileReader,регистрациисобытия“load”длянего,ивызоваегометодаreadAsTextспередачейтомуфайла.Поокончаниюзагрузкивсвойствеresultсохраняетсясодержимоефайла.

ПримериспользуетArray.prototype.forEachдляпроходапомассиву,таккаквобычномциклебылобынеудобнополучатьнужныеобъектыfileиreaderотобработчикасобытий.Переменныебылибыобщимидлявсехитерацийцикла.

УFileReadersтакжеестьсобытие“error”,когдачтениефайланеполучается.Объектerrorбудетсохранёнвсвойствеerror.Есливынехотитезабиватьголовуещёоднойнеудобнойасинхроннойсхемой,выможетеобернутьеёвобещание(см.главу17):

functionreadFile(file){

returnnewPromise(function(succeed,fail){

varreader=newFileReader();

reader.addEventListener("load",function(){

succeed(reader.result);

});

reader.addEventListener("error",function(){

fail(reader.error);

});

ВыразительныйJavascript

354Формыиполяформ

Page 355: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

reader.readAsText(file);

});

}

Возможночитатьтолькочастьфайла,вызываяsliceипередаваярезультат(т.н.объектblob)объектуreader.

ПростыеHTML-страничкисдобавкойJavaScriptмогутвыступатьотличнойосновойдлямини-приложений–небольшихвспомогательныхпрограмм,автоматизирующихежедневныедела.Присоединивкполямформыобработчикисобытийвыможетеделатьвсё–отконвертациифаренгейтоввцельсиидогенерациипаролейизосновногопароляиименивеб-сайта.

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

Можнохранитьстроковыеданныетак,чтоонипереживутперезагрузкустраниц—дляэтогонадоположитьихвобъектlocalStorage.Онразрешаетхранитьстроковыеданныеподименами(которыетожеявляютсястроками),каквэтомпримере:

localStorage.setItem("username","marijn");

console.log(localStorage.getItem("username"));

//→marijn

localStorage.removeItem("username");

ПеременнаявlocalStorageхранится,покаеёнеперезапишут,удаляетсяприпомощиremoveItemилиочисткойлокальногохранилищапользователем.

Усайтовсразныхдоменов–разныеотделениявэтомхранилище.Тоесть,данные,сохранённыесвебсайтавlocalStorage,могутбытьпрочтеныили

Хранениеданныхнасторонеклиента

ВыразительныйJavascript

355Формыиполяформ

Page 356: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

перезаписанытолькоскриптамисэтогожесайта.

Такжебраузерыограничиваютобъёмхранимыхданных,обычновнесколькомегабайт.Этоограничение,вкупестемфактом,чтозабиваниежёсткихдисковулюдейнеприноситприбыли,предотвращаетотъеданиеместанадиске.

Следующийкодреализуетпростуюпрограммудляведениязаметок.Онахранитзаметкиввидеобъекта,ассоциируязаголовкиссодержимым.ОнкодируетсявJSONихранитсявlocalStorage.Пользовательможетвыбратьзапискучерезполе<select>ипоменятьеётекств<textarea>.Добавляетсязаписьпонажатиюнакнопку.

Заметки:<selectid="list"></select>

<buttononclick="addNote()">новая</button><br>

<textareaid="currentnote"style="width:100%;height:10em">

</textarea>

<script>

varlist=document.querySelector("#list");

functionaddToList(name){

varoption=document.createElement("option");

option.textContent=name;

list.appendChild(option);

}

//Берёмсписокизлокальногохранилища

varnotes=JSON.parse(localStorage.getItem("notes"))||

{"чтокупить":""};

for(varnameinnotes)

if(notes.hasOwnProperty(name))

addToList(name);

functionsaveToStorage(){

localStorage.setItem("notes",JSON.stringify(notes));

}

varcurrent=document.querySelector("#currentnote");

current.value=notes[list.value];

list.addEventListener("change",function(){

current.value=notes[list.value];

});

current.addEventListener("change",function(){

notes[list.value]=current.value;

saveToStorage();

ВыразительныйJavascript

356Формыиполяформ

Page 357: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

});

functionaddNote(){

varname=prompt("Имязаписи","");

if(!name)return;

if(!notes.hasOwnProperty(name)){

notes[name]="";

addToList(name);

saveToStorage();

}

list.value=name;

current.value=notes[name];

}

</script>

СкриптинициализируетпеременнуюnotesзначениемизlocalStorage,аеслиеготамнет–простымобъектомсоднойзаписью«чтокупить».ПопыткапрочестьотсутствующееполеизlocalStorageвернётnull.ПередавnullвJSON.parse,мыполучимnullобратно.Поэтомудлязначенияпоумолчаниюможноиспользоватьоператор||.

Когдаданныевnoteменяются(добавляетсяноваязаписьилименяетсятекущая),дляобновленияхранимогополявызываетсяфункцияsaveToStorage.Еслибмырассчитывали,чтоунасбудутхранитьсятысячизаписей,этобылобыслишкомнакладно,инампришлосьбыпридуматьболеесложнуюпроцедурудляхранения–например,своёполедлякаждойзаписи.

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

ЕстьещёодинпохожийнаlocalStorageобъектподназваниемsessionStorage.Разницамеждунимивтом,чтосодержимоеsessionStorageзабываетсяпоокончаниюсессии,чтодлябольшинствабраузеровозначаетмоментзакрытия.

Итог

ВыразительныйJavascript

357Формыиполяформ

Page 358: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

HTMLпредоставляетмножестворазличныхтиповполейформы–текстовые,галочки,множественноговыбора,выборафайла.

ИзJavaScriptможнополучатьзначениеиманипулироватьэтимиполями.Поизменениюонизапускаютсобытие“change”,повводусклавиатуры–“input”,иещёмногоразныхклавиатурныхсобытий.Онипомогаютнамотловитьмомент,когдапользовательвзаимодействуетсполемввода.Свойствавродеvalue(длятекстовыхполейиselect)илиchecked(длягалочекирадиокнопок)используютсядлячтенияизаписисодержимогополей.

Припередачеформыпроисходитсобытие“submit”.ОбработчикJavaScriptзатемможетвызватьpreventDefaultэтогособытия,чтобыостановитьпередачуданных.Элементыформынеобязаныбытьзаключенывтеги<form>.

Когдапользовательвыбралфайлсжёсткогодискачерезполевыборафайла,интерфейсFileReaderпозволитнамдобратьсядосодержимогофайлаизпрограммыJavaScript.

ОбъектыlocalStorageиsessionStorageможноиспользоватьдляхраненияинформациитакимспособом,которыйпереживётперезагрузкустраницы.Первыйсохраняетданныенавсегда(нуилипокапользовательспециальнонесотрётих),авторой–дозакрытиябраузера.

Сделайтеинтерфейс,позволяющийписатьиисполнятькусочкикодаJavaScript.

Сделайтекнопкурядомс<textarea>,понажатиюкоторойконструкторFunctionизглавы10будетобёртыватьвведённыйтекствфункциюивызыватьего.Преобразуйтезначение,возвращаемоефункцией,илилюбуюеёошибку,встроку,ивыведитееёпослетекстовогополя.

<textareaid="code">return"hi";</textarea>

<buttonid="button">Поехали</button>

Упражнения

ВерстакJavaScript

ВыразительныйJavascript

358Формыиполяформ

Page 359: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

<preid="output"></pre>

<script>

//Вашкод.

</script>

Дополнитетекстовоеполетак,чтопривводетекстаподнимпоявлялсябысписоквариантов.Увасестьмассиввозможныхвариантов,ипоказыватьнужнотеизних,которыеначинаютсясвводимоготекста.Когдапользовательщёлкаетпопредложенномуварианту,онменяетсодержимоеполянанего.

<inputtype="text"id="field">

<divid="suggestions"style="cursor:pointer"></div>

<script>

//Строитмассивизимёнглобальныхперменных,

//типа'alert','document',и'scrollTo'

varterms=[];

for(varnameinwindow)

terms.push(name);

//Вашкод.

</script>

Этопростаясимуляцияжизнинапрямоугольнойрешётке,каждыйэлементкоторойживойилинет.Каждоепоколение(шагигры)применяютсяследующиеправила:

—каждаяживаяклетка,количествососедейкоторойменьшедвухилибольшетрёх,погибает—каждаяживаяклетка,укоторойотдвухдотрёхсоседей,живётдоследующегохода—каждаямёртваяклетка,укоторойестьровнотрисоседа,оживает

Соседиклетки–этовсесоседниеснейклеткипогоризонтали,вертикалиидиагонали,всего8штук.

Обратитевнимание,чтоправилаприменяютсяковсейрешётке

Автодополнение

Игра«Жизнь»Конвея

ВыразительныйJavascript

359Формыиполяформ

Page 360: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

одновременно,анеккаждойизклетокпоочереди.Тоесть,подсчётколичествасоседейпроисходитводинмоментпередследующимшагом,иизменения,происходящиенасоседнихклетках,невлияютнановоесостояниеклетки.

Реализуйтеигру,используялюбыеподходящиеструктуры.ИспользуйтеMath.randomдлясозданияслучайныхначальныхпопуляций.Выводитеполекакрешёткуизгалочекскнопкой«перейтинаследующийшаг».Когдапользовательвключаетиливыключаетгалочки,этиизменениянужноучитыватьприподсчётеследующегопоколения.

<divid="grid"></div>

<buttonid="next">Следующеепоколение</button>

<script>

//Вашкод.

</script>

ВыразительныйJavascript

360Формыиполяформ

Page 361: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Ясмотрюнамногообразиецветов.Ясмотрюнапустойхолст.Затемяпытаюсьнанестицветакакслова,изкоторыхвозникаютпоэмы,какноты,изкоторыхвозникаетмузыка.

ЖоанМиро

Материалпредыдущихглавдаётвамвсёнеобходимоедлясозданияпростоговеб-приложения.Именноэтиммыизаймёмся.

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

Рисоватьнакомпьютереклёво.Ненадоволноватьсянасчётматериалов,умения,таланта.Простоберёшь,иначинаешькалякать.

Проект:Paint

Простаяпрограммарисования

ВыразительныйJavascript

361Проект:Paint

Page 362: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Интерфейспрограммывыводитвверхубольшойэлемент<canvas>,подкоторыместьнесколькополейввода.Пользовательрисуетнакартинке,выбираяинструментизполя<select>,азатемнажимаянахолстемышь.Естьинструментыдлярисованиялиний,стираниякусочковкартинки,добавлениятекстаит.п.

Щелчокнахолстепередаётсобытие«mousedown»текущемуинструменту,которыйобрабатываетего,каксчитаетнужным.Рисованиелиний,например,будетслушатьсобытия«mousemove»,покакнопкамышинебудетотпущена,инарисуетлиниюпопутимышитекущимцветомиразмеромкисти.

Цветиразмеркистивыбираютсявдополнительныхполяхввода.ОнивыполняютобновлениесвойствконтекстарисованиянахолстеfillStyle,strokeStyle,иlineWidthкаждыйразприихизменении.

Загрузитькартинкувпрограммуможнодвумяспособами.Первыйиспользуетполеfile,гдепользовательвыбираетфайлсосвоегодиска.ВтораязапрашиваетURLискачиваеткартинкуизинтернета.

Картинкихранятсянестандартнымспособом.Ссылкаsaveсправойстороныведётнатекущуюкартинку.Понейможнопроходить,делитьсяейилисохранятьфайлчерезнеё.Яскорообъясню,какэтоработает.

Интерфейспрограммысостоитизболеечем30элементовDOM.Нужноихкак-тособратьвместе.

ОчевиднымформатомдлясложныхструктурDOMявляетсяHTML.НоразделятьпрограммунаHTMLискриптнеудобно–дляэлементовDOMпонадобитсямножествообработчиковсобытийилидругихнеобходимыхвещей,которыенадобудеткак-тообрабатыватьизскрипта.ДляэтогопридётсяделатьмноговызововquerySelectorиимподобных,чтобынайтинужныйэлементDOMдляработы.

Реализация

СтроимDOM

ВыразительныйJavascript

362Проект:Paint

Page 363: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

БылобыудобноопределятьчастиDOMрядомстемичастямикодаJavaScript,которыеимиуправляют.ПоэтомуярешилсоздаватьвсюконструкциюDOMпрямовJavaScript.Какмывиделивглаве13,встроенныйинтерфейсдлясозданияструктурDOMужасномногословен.Посколькунампридётсясоздатьмногоконструкций,нампонадобитсявспомогательнаяфункция.

Этафункция–расширеннаяверсияфункцииeltизглавы13.Онасоздаётэлементсзаданнымименемиатрибутами,идобавляетвсеостальныеаргументы,которыеполучает,вкачестведочернихузлов,автоматическипреобразовываястрокивтекстовыеузлы.

functionelt(name,attributes){

varnode=document.createElement(name);

if(attributes){

for(varattrinattributes)

if(attributes.hasOwnProperty(attr))

node.setAttribute(attr,attributes[attr]);

}

for(vari=2;i<arguments.length;i++){

varchild=arguments[i];

if(typeofchild=="string")

child=document.createTextNode(child);

node.appendChild(child);

}

returnnode;

}

Такмылегкоипростосоздаёмэлементы,нераздуваякоддоразмеровлицензионногосоглашения.

Ядронашейпрограммы–функцияcreatePaint,добавляющаяинтерфейсрисованиякэлементуDOM,которыйпередаётсявкачествеаргумента.Таккакмысоздаёмпрограммупоследовательно,мыопределяемобъектcontrols,которыйбудетсодержатьфункциидляинициализацииразныхэлементовуправленияподкартинкой.

varcontrols=Object.create(null);

Основание

ВыразительныйJavascript

363Проект:Paint

Page 364: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functioncreatePaint(parent){

varcanvas=elt("canvas",{width:500,height:300});

varcx=canvas.getContext("2d");

vartoolbar=elt("div",{class:"toolbar"});

for(varnameincontrols)

toolbar.appendChild(controls[name](cx));

varpanel=elt("div",{class:"picturepanel"},canvas);

parent.appendChild(elt("div",null,panel,toolbar));

}

Укаждогоэлементауправленияестьдоступкконтекстурисованиянахолсте,ачерезнего–кэлементу<canvas>.Основноесостояниепрограммыхранитсявэтомхолсте–онсодержиттекущуюкартинку,выбранныйцвет(всвойствеfillStyle)иразмеркисти(всвойствеlineWidth).

Мыобернёмхолстиэлементыуправлениявэлементы<div>склассами,чтобыможнобылодобавитьимстили,напримерсеруюрамкувокругкартинки.

Первыйэлементуправления,которыймыдобавим–элемент<select>,позволяющийвыбиратьинструментрисования.Какивслучаесcontrols,мыбудемиспользоватьобъектдлясборанеобходимыхинструментов,чтобыненадобылоописыватьихработувкодепоотдельности,ичтобыможнобылолегкодобавлятьновые.Этотобъектсвязываетназванияинструментовсфункцией,котораявызываетсяприихвыбореиприкликенахолсте.

vartools=Object.create(null);

controls.tool=function(cx){

varselect=elt("select");

for(varnameintools)

select.appendChild(elt("option",null,name));

cx.canvas.addEventListener("mousedown",function(event){

if(event.which==1){

tools[select.value](event,cx);

event.preventDefault();

}

Выборинструмента

ВыразительныйJavascript

364Проект:Paint

Page 365: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

});

returnelt("span",null,"Tool:",select);

};

Вполеtoolестьэлементы<option>длявсехопределённыхнамиинструментов,аобработчиксобытия«mousedown»нахолстеберётнасебяобязанностьвызыватьфункциютекущегоинструмента,передаваяейобъектыeventиcontext.ТакжеонвызываетpreventDefault,чтобызажатиеиперетаскиваниемышиневызываловыделенияучастковстраницы.

Самыйпростойинструмент–линия,которыйрисуетлиниизамышью.Чтобырисоватьлинию,намнадосопоставитькоординатыкурсорамышискоординатамиточекнахолсте.Вскользьупомянутыйв13главеметодgetBoundingClientRectможетнамвэтомпомочь.Онговорит,гдепоказываетсяэлемент,относительнолевоговерхнегоуглаэкрана.СвойствасобытиямышиclientXиclientYтакжесодержаткоординатыотносительноэтогоугла,поэтомумыможемвычестьверхнийлевыйуголхолстаизнихиполучитьпозициюотносительноэтогоугла.

functionrelativePos(event,element){

varrect=element.getBoundingClientRect();

return{x:Math.floor(event.clientX-rect.left),

y:Math.floor(event.clientY-rect.top)};

}

Несколькоинструментоврисованиядолжныслушатьсобытие«mousemove»,покакнопкамышинажата.ФункцияtrackDragрегистрируетиубираетсобытиедляданныхситуаций.

functiontrackDrag(onMove,onEnd){

functionend(event){

removeEventListener("mousemove",onMove);

removeEventListener("mouseup",end);

if(onEnd)

onEnd(event);

}

addEventListener("mousemove",onMove);

addEventListener("mouseup",end);

}

ВыразительныйJavascript

365Проект:Paint

Page 366: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Унеёдвааргумента.Один–функция,котораявызываетсяприкаждомсобытии«mousemove»,адругая–функция,котораявызываетсяприотпусканиикнопки.Каждыйаргументможетбытьнезадан.

Инструментдлярисованиялинийиспользуетдвевспомогательныефункциидлясамогорисования.

tools.Line=function(event,cx,onEnd){

cx.lineCap="round";

varpos=relativePos(event,cx.canvas);

trackDrag(function(event){

cx.beginPath();

cx.moveTo(pos.x,pos.y);

pos=relativePos(event,cx.canvas);

cx.lineTo(pos.x,pos.y);

cx.stroke();

},onEnd);

};

ФункциясначалаустанавливаетсвойствоконтекстаlineCapв“round”,из-зачегоконцынарисованногопутистановятсязакруглёнными,анеквадратными,какэтопроисходитпоумолчанию.Этоттрюкобеспечиваетнепрерывностьлиний,когдаонинарисованывнесколькоприёмов.Еслирисоватьлиниибольшойширины,выувидитеразрывывуглахлиний,еслибудетеиспользоватьустановкуlineCapпоумолчанию.

Затем,покаждомусобытию«mousemove»,котороеслучается,покакнопканажата,рисуетсяпростаялиниямеждустаройиновойпозициямимыши,сиспользованиемтехзначенийпараметровstrokeStyleиlineWidth,которыезаданывданныймомент.

АргументonEndпростопередаётсядальше,вtrackDrag.Приобычномвызоветретийаргументпередаватьсянебудет,иприиспользованиифункциионбудетсодержатьundefined,поэтомувконцеперетаскиванияничегонепроизойдёт.Ноонпоможетнаморганизоватьещёодининструмент,ластикerase,используяоченьнебольшоедополнениеккоду.

tools.Erase=function(event,cx){

cx.globalCompositeOperation="destination-out";

tools.Line(event,cx,function(){

ВыразительныйJavascript

366Проект:Paint

Page 367: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

cx.globalCompositeOperation="source-over";

});

};

СвойствоglobalCompositeOperationвлияетнато,какоперациирисованиянахолстеменяютцветпикселей.Поумолчанию,значениесвойства«source-over»,чтоозначает,чтоцвет,которымрисуют,накладываетсяповерхсуществующего.Еслицветнепрозрачный,онпростозаменитсуществующий,ноеслиончастичнопрозрачный,онибудутсмешаны.

Инструмент“erase”устанавливаетglobalCompositeOperationв«destination-out»,чтоимеетэффектластика,иделаетпикселисновапрозрачными.

Вотунасужеестьдваинструментадлярисования.Мыможемрисоватьчёрныелинииводинпиксельшириной(этозаданозначениямисвойствхолстаstrokeStyleиlineWidthпоумолчанию),истиратьих.Работающий,хотяипримитивный,прототиппрограммы.

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

Вглаве18яобсуждалразныевариантыполейформы.Срединихнебылополейдлявыборацвета.Потрадицииубраузеровнетвстроенныхполейдлявыборацвета,нозапоследнеевремявстандартвключилинескольконовыхтиповполейформ.Одинизних—<inputtype="color">.Средидругих—«date»,«email»,«url»и«number».Покаещёихподдерживаютневсе.Длятега<input>типпоумолчанию–“text”,иприиспользованииновоготега,которыйещёнеподдерживаетсябраузером,браузерыбудутобрабатыватьегокактекстовоеполе.Значит,пользователямсбраузерами,которыенеподдерживаютинструментдлявыборацвета,необходимобудетвписыватьназваниецветавместотого,чтобывыбиратьегочерезудобныйэлементуправления.

controls.color=function(cx){

varinput=elt("input",{type:"color"});

Цветиразмеркисти

ВыразительныйJavascript

367Проект:Paint

Page 368: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

input.addEventListener("change",function(){

cx.fillStyle=input.value;

cx.strokeStyle=input.value;

});

returnelt("span",null,"Color:",input);

};

ПрисменезначенияполяcolorзначениясвойствконтекстахолстаfillStyleиstrokeStyleзаменяютсянановоезначение.

Настройкаразмеракистиработаетсходнымобразом.

controls.brushSize=function(cx){

varselect=elt("select");

varsizes=[1,2,3,5,8,12,25,35,50,75,100];

sizes.forEach(function(size){

select.appendChild(elt("option",{value:size},

size+"pixels"));

});

select.addEventListener("change",function(){

cx.lineWidth=select.value;

});

returnelt("span",null,"Brushsize:",select);

};

Кодсоздаётвариантыразмеровкистейизмассива,иубеждаетсявтом,чтосвойствохолстаlineWidthобновленопривыборекисти.

Чтобыобъяснить,какработаетссылканасохранение,сначаламненужнорассказатьпроURLсданными.Вотличиеотобычныхhttp:иhttps:,URLсданныминеуказываютнаресурс,асодержатвесьресурсвсебе.ЭтоURLсданными,содержащийпростойHTMLдокумент:

data:text/html,<h1style="color:red">Hello!</h1>

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

Сохранение

ВыразительныйJavascript

368Проект:Paint

Page 369: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

сперванакакой-либосервер.

УэлементахолстаестьудобныйметодtoDataURL,которыйвозвращаетURLсданными,содержащийкартинкунахолстеввидеграфическогофайла.Нонамнеследуетобновлятьссылкудлясохраненияприкаждомизменениикартинки.ВслучаебольшихкартинокперемещениеданныхвURLзанимаетмноговремени.Вместоэтогомыподключаемобновлениекссылке,чтобонаобновляласвойатрибутhrefкаждыйраз,когдаонаполучаетфокуссклавиатурыилинаднейпоявляетсякурсормыши.

controls.save=function(cx){

varlink=elt("a",{href:"/"},"Save");

functionupdate(){

try{

link.href=cx.canvas.toDataURL();

}catch(e){

if(einstanceofSecurityError)

link.href="javascript:alert("+

JSON.stringify("Can'tsave:"+e.toString())+")";

else

throwe;

}

}

link.addEventListener("mouseover",update);

link.addEventListener("focus",update);

returnlink;

};

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

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

Ноздесьмыопятьсталкиваемсясдеталямиреализациипесочницывбраузере.КогдакартинкагрузитсясURLсдругогодомена,еслиответсерверанесодержитзаголовок,разрешающийиспользованиересурсасдругихдоменов(см.главу17),холстбудетсодержатьинформацию,котораябудетвиднапользователю,ноневиднаскрипту.

ВыразительныйJavascript

369Проект:Paint

Page 370: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Мымоглизапроситькартинкусприватнойинформацией(графикизмененийбанковскогосчёта).Еслибыскриптмогполучитькнейдоступ,онмогбышпионитьзапользователем.

Дляпредотвращениятакихутечекинформации,когдакартинка,невидимаяскрипту,будетзагруженанахолст,браузерыпометятегокак«испорчен».Пиксельныеданные,включаяURLсданными,нельзябудетполучитьс«испорченного»холста.Нанегоможнописать,носнегонельзячитать.

Поэтомунамнужнаобработкаtry/catchвфункцииupdateдляссылкисохранения.Когдахолст«портится»,вызовtoDataURLвыброситисключение,являющеесяэкземпляромSecurityError.ВэтомслучаемыперенаправляемссылкунаещёодинвидURLспротоколомjavascript:.Такиессылкипростовыполняютскрипт,стоящийпоследвоеточия,инашассылкапокажетпредупреждение,сообщающееопроблеме.

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

functionloadImageURL(cx,url){

varimage=document.createElement("img");

image.addEventListener("load",function(){

varcolor=cx.fillStyle,size=cx.lineWidth;

cx.canvas.width=image.width;

cx.canvas.height=image.height;

cx.drawImage(image,0,0);

cx.fillStyle=color;

cx.strokeStyle=color;

cx.lineWidth=size;

});

image.src=url;

}

Намнадопоменятьразмерхолста,чтобыонсоответствовалкартинке.Почему-топрисменеразмерахолстаегоконтекстзабываетвсенастройки(fillStyleиlineWidth),всвязисчемфункциясохраняетихизагружаетобратнопослеобновленияразмерахолста.

Загрузкакартинок

ВыразительныйJavascript

370Проект:Paint

Page 371: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ЭлементуправлениядлязагрузкилокальногофайлаиспользуеттехникуFileReaderизглавы18.КромеиспользуемогоздесьметодаreadAsTextутакихобъектовестьметодподназваниемreadAsDataURL–аэтото,чтонамнужно.Мызагружаемфайл,которыйпользовательвыбирает,какURLсданными,ипередаёмеговloadImageURLдлявыводанахолст.

controls.openFile=function(cx){

varinput=elt("input",{type:"file"});

input.addEventListener("change",function(){

if(input.files.length==0)return;

varreader=newFileReader();

reader.addEventListener("load",function(){

loadImageURL(cx,reader.result);

});

reader.readAsDataURL(input.files[0]);

});

returnelt("div",null,"Openfile:",input);

};

ЗагружатьфайлсURLещёпроще.Ностекстовымполеммынезнаем,закончиллипользовательнабиратьвнёмURL,поэтомумынеможемпростослушатьсобытия“change”.Вместоэтогомыобернёмполевформуисреагируем,когдаонабудетотправлена–либопонажатиюEnter,либопонажатиюкнопкуload.

controls.openURL=function(cx){

varinput=elt("input",{type:"text"});

varform=elt("form",null,

"OpenURL:",input,

elt("button",{type:"submit"},"load"));

form.addEventListener("submit",function(event){

event.preventDefault();

loadImageURL(cx,form.querySelector("input").value);

});

returnform;

};

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

Закругляемся

ВыразительныйJavascript

371Проект:Paint

Page 372: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

tools.Text=function(event,cx){

vartext=prompt("Text:","");

if(text){

varpos=relativePos(event,cx.canvas);

cx.font=Math.max(7,cx.lineWidth)+"pxsans-serif";

cx.fillText(text,pos.x,pos.y);

}

};

Можнобылобыдобавитьполейдляразмератекстаишрифта,нодляпростотымывсегдаиспользуемшрифтsans-serifиразмершрифта,какутекущейкисти.Минимальныйразмер–7пикселей,потомучтоменьшетекстбудетнечитаемый.

Ещёодиннеобходимыйинструментдлякаляк-маляк–“аэрозоль”.Онарисуетслучайныеточкиподкистью,поканажатакнопкамыши,создаваяболееилименеегустыеточкивзависимостиотскоростидвижениякурсора.

tools.Spray=function(event,cx){

varradius=cx.lineWidth/2;

vararea=radius*radius*Math.PI;

vardotsPerTick=Math.ceil(area/30);

varcurrentPos=relativePos(event,cx.canvas);

varspray=setInterval(function(){

for(vari=0;i<dotsPerTick;i++){

varoffset=randomPointInRadius(radius);

cx.fillRect(currentPos.x+offset.x,

currentPos.y+offset.y,1,1);

}

},25);

trackDrag(function(event){

currentPos=relativePos(event,cx.canvas);

},function(){

clearInterval(spray);

});

};

АэрозольиспользуетsetIntervalдлявыплёвыванияцветныхточеккаждые25миллисекунд,поканажатакнопкамыши.ФункцияtrackDragиспользуетсядля

ВыразительныйJavascript

372Проект:Paint

Page 373: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

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

functionrandomPointInRadius(radius){

for(;;){

varx=Math.random()*2-1;

vary=Math.random()*2-1;

if(x*x+y*y<=1)

return{x:x*radius,y:y*radius};

}

}

Этафункциясоздаётточкивквадратемежду(-1,-1)и(1,1).ИспользуятеоремуПифагора,онапроверяет,лежитлисозданнаяточкавнутрикругасрадиусом1.Когдатакаяточканаходится,онавозвращаетеёкоординаты,умноженныенарадиус.

Циклнужендляравномерногораспределенияточек.Прощебылобысоздаватьточкивкруге,взявслучайныйуголирадиусивызвавMath.sinиMath.cosдлясозданияточки.Нотогдаточкисбольшейвероятностьюпоявлялисьбыближекцентрукруга.Этоограничениеможнообойти,норезультатбудетсложнее,чемпредыдущийцикл.

Теперьнашапрограммадлярисованияготова.Запуститекодипопробуйте.

<linkrel="stylesheet"href="css/paint.css">

<body>

<script>createPaint(document.body);</script>

</body>

Впрограммеещёоченьмногочегоможноулучшить.Давайтедобавимейвозможностей.

Упражнения

ВыразительныйJavascript

373Проект:Paint

Page 374: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ОпределитеинструментRectangle,заполняющийпрямоугольник(см.методfillRectизглавы16)текущимцветом.Прямоугольникдолженпоявлятьсяизтойточки,гдепользовательнажалкнопкумыши,идотойточки,гдеонотпустилкнопку.Заметьте,чтопоследнеедействиеможетпроизойтилевееиливышепервого.

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

Еслинепридумаете,вспомнитеостилеposition:absolute,которыймыобсуждаливглаве13.которыйможноиспользовать,чтобывыводитьузелповерхостальногодокумента.СвойстваpageXиpageYсобытиймышиможноиспользоватьдляточногорасположенияэлементаподмышью,записываянужныезначениявстилиleft,top,widthиheight.

<script>

tools.Rectangle=function(event,cx){

//Вашкод

};

</script>

<linkrel="stylesheet"href="css/paint.css">

<body>

<script>createPaint(document.body);</script>

</body>

Ещёодинчастовстречающийсяинструмент–выборцвета,которыйпозволяетщелчкоммышинакартинкевыбратьцвет,которыйнаходитсяподкурсором.Сделайтетакойинструмент.

Дляегоизготовленияпонадобитсядоступксодержимомухолста.МетодtoDataURLпримерноэтоиделал,нополучитьинформациюопикселеизURLсданнымисложно.Вместоэтогомывозьмёмметодконтекста

Прямоугольники

Выборцвета

ВыразительныйJavascript

374Проект:Paint

Page 375: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

getImageData,возвращающийпрямоугольныйкусоккартинкиввидеобъектасосвойствамиwidth,heightиdata.Всвойствеdataсодержитсямассивчиселот0до255,идлякаждогопикселяхранитсячетыреномера—red,green,blueиalpha(прозрачность).

Этотпримерполучаетчислаизодногопикселяхолста,одинраз,когдатотпуст(всепиксели–прозрачныечёрные),иодинраз,когдапиксельокрашенвкрасныйцвет.

functionpixelAt(cx,x,y){

vardata=cx.getImageData(x,y,1,1);

console.log(data.data);

}

varcanvas=document.createElement("canvas");

varcx=canvas.getContext("2d");

pixelAt(cx,10,10);

//→[0,0,0,0]

cx.fillStyle="red";

cx.fillRect(10,10,1,1);

pixelAt(cx,10,10);

//→[255,0,0,255]

АргументыgetImageDataпоказываютначальныекоординатыпрямоугольникаxиy,которыенамнадополучить,закоторымиидутширинаивысота.

Игнорируйтепрозрачностьвэтомупражнении,работайтетолькоспервымитремяцифрамидлязаданногопикселя.Такженеволнуйтесьпоповодуобновленияполяcolorпривыборецвета.Простоубедитесь,чтоfillStyleиstrokeStyleконтекстаустановленывцвет,оказавшийсяподкурсором.

Помните,чтоэтисвойствапринимаютлюбойцвет,которыйпонимаетCSS,включаязаписьвидаrgb(R,G,B),которуювывиделивглаве15.

МетодgetImageDataимееттежеограничения,чтоиtoDataURL–онвыдастошибку,когданахолстесодержатсяпикселикартинки,скачаннойсдругогодомена.Используйтезаписьtry/catchдлясообщенияобэтихошибкахчерезокноalert.

<script>

tools["Pickcolor"]=function(event,cx){

ВыразительныйJavascript

375Проект:Paint

Page 376: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

//Yourcodehere.

};

</script>

<linkrel="stylesheet"href="css/paint.css">

<body>

<script>createPaint(document.body);</script>

</body>

Этоупражнениеболеесложное,чемпредыдущие,ионопотребуетразработкинетривиальногорешенияхитройзадачи.Убедитесь,чтоувасестьсвободноевремяитерпениепередначаломработы,инеотчаивайтесь,еслисразуувасчто-тонебудетполучаться.

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

Следующаякартинкадемонстрируетнаборпикселей,окрашиваемых,когдаинструментзаливкиприменяетсякпомеченномупикселю:

Заливканепротекаетчерездиагональныеразрывыинекасаетсяпикселей,которыхнельзядостичь,дажееслионитогожецвета,чтоиисходный.

ВамвновьпонадобитсяgetImageDataдлявыясненияцветапикселя.Скореевсего,удобнеебудетполучитьвсюкартинкузараз,апотомужеполучатьданныепопикселямизполучившегосямассива.Пикселивмассивеорганизованысхожимобразомсрешёткойизглавы7,порядам,только

Заливка

ВыразительныйJavascript

376Проект:Paint

Page 377: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

каждыйпиксельописываетсячетырьмязначениями.Первоезначениедляпикселяскоординатами(x,y)находитсянапозиции(x+y×width)×4

Включайтеврассмотрениечетвёртоечисло(альфа),потомучтонамнужнобудетразличатьчёрныеипустые(прозрачные)пиксели.

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

Укартинкисреднегоразмерамногопикселей.Постарайтесьсвестиработупрограммыкминимуму,илижеонабудетработатьслишкомдолго.Кпримеру,игнорируйтепиксели,которыевыужеобрабатывали.

РекомендуюдляокраскиотдельныхпикселейвызыватьfillRect,ихранитькакую-тоструктуруданных,гдезаписано,какиепикселивыужеобошли.

<script>

tools["Floodfill"]=function(event,cx){

//Вашкод

};

</script>

<linkrel="stylesheet"href="css/paint.css">

<body>

<script>createPaint(document.body);</script>

</body>

ВыразительныйJavascript

377Проект:Paint

Page 378: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Ученикспросил:«Программистывстарьиспользовалитолькопростыекомпьютерыипрограммировалибезязыков,ноониделалипрекрасныепрограммы.Почемумыиспользуемсложныекомпьютерыиязыкипрограммирования?».Фу-Тзуответил:«Строителивстарьиспользовалитолькопалкииглину,ноониделалипрекрасныехижины».

МастерЮан-Ма,«Книгапрограммирования»

НатекущиймоментвыучилиязыкJavaScriptииспользовалиеговединственномокружении:вбраузере.ВэтойиследующейглавемыкраткопредставимвамNode.js,программу,котораяпозволяетприменятьнавыкиJavaScriptвнебраузера.Снейвыможетенаписатьвсё,отутилиткоманднойстрокидодинамическихHTTPсерверов.

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

Кодизпредыдущихглаввымоглиписатьиисполнятьпрямовбраузере,нокодизэтойглавынаписандляNodeивбраузереработатьнебудет.

Есливыхотитесразузапускатькодизэтойглавы,начнитесустановкиNodeссайтаnodejs.orgдлявашейоперационки.ТакженаэтомсайтевынайдётедокументациюпоNodeиеговстроенныммодулям.

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

Node.js

Вступление

ВыразительныйJavascript

378Node.js

Page 379: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Втрадиционномметодеобработкивводаивыводапринято,чтофункция,кпримеру,readFile,начинаетчитатьфайливозвращаетсятолькокогдафайлполностьюпрочитан.Этоназываетсясинхроннымвводом-выводом(synchronousI/O,input/output).

NodeбылзадумансцельюоблегчитьиупроститьиспользованиеасинхронногоI/O.Мыужевстречалисьсасинхроннымиинтерфейсами,такими,какобъектбраузераXMLHttpRequest,обсуждавшийсявглаве17.Такойинтерфейспозволяетскриптупродолжатьработу,покаинтерфейсделаетсвою,ивызываетфункциюобратноговызовапоокончаниюработы.ТакимобразомвNodeработаетвесьI/O.

JavaScriptлегковписываетсявсистемутипаNode.Этоодинизнемногихязыков,вкоторыеневстроенасистемаI/O.ПоэтомуJavaScriptлегковстраиваетсявдовольноэксцентричныйподходкI/OвNodeиврезультатенепорождаетдверазныхсистемывводаивывода.В2009годуприразработкеNodeлюдиужеиспользовалиI/Oвбраузере,основанныйнаобратныхвызовах,поэтомусообществовокругязыкабылопривычнокасинхронномустилюпрограммирования.

ПопробуюпроиллюстрироватьразницувсинхронномиасинхронномподходахвI/Oнанебольшомпримере,гдепрограммадолжнаполучитьдваресурсаизинтернета,изатемсделатьчто-тосданными.

Всинхронномокруженииочевиднымспособомрешениязадачибудетсделатьзапросыпоследовательно.Уэтогометодаестьминус–второйзапросначнётсятолькопослеокончанияпервого.Общеевремябудетнеменьше,чемсуммавременинаобработкудвухзапросов.Этонеэффективноеиспользованиекомпьютера,которыйбольшуючастьвременибудетпростаивать,покапроисходитпередачаданныхпосети.

Решениепроблемывсинхроннойсистеме–запускдополнительныхпотоковконтроляисполненияпрограммы(вглаве14мыихужеобсуждали).Второйпотокможетзапуститьвторойзапрос,изатемобапотокабудутждатьвозвратарезультата,послечегоонизановобудутсинхронизированыдля

Асинхронность

ВыразительныйJavascript

379Node.js

Page 380: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

сведенияработыводинрезультат.

Надиаграммежирныелинииобозначаютвремянормальнойработыпрограммы,атонкие–времяожиданияI/O.Всинхронноймоделивремя,затраченноенаI/O,входитвовременнойграфиккаждогоизпотоков.Васинхронной,запускдействияпоI/Oприводиткразветвлениювременнойлинии.Поток,запустившийI/O,продолжаетвыполнение,аI/Oвыполняетсяпараллельноему,поокончаниюработыделаяобратныйвызовфункции.

ПотоквыполненияпрограммыдлясинхронногоиасинхронногоI/O

Ещёодинспособвыразитьэтуразницу:всинхронноймоделиожиданиеокончанияI/Oнеявное,авасинхронной–явное,инаходитсяподнашимнепосредственнымконтролем.Ноасинхронностьработаетвобестороны.Сеёпомощьювыражатьпрограммы,неработающиепопринципупрямойлинии,проще,новыражатьпрямолинейныепрограммыстановитсясложнее.

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

Нодлясистемы,основаннойнаJavaScript,ябысказал,чтоиспользованиеасинхронностисобратнымивызовамиимеетсмысл.ОднаизсильныхсторонJavaScript–простота,ипопыткидобавитьвпрограммунесколькопотоковпривелибыксильномуусложнению.Хотяобратныевызовынеделаюткодпростым,ихидеяоченьпростаивтожевремядостаточносильнадлятого,чтобыписатьвысокопроизводительныевеб-серверы.

КогдаввашейсистемеустановленNode.js,уваспоявляетсяпрограммаподназваниемnode,котораязапускаетфайлыJavaScript.Допустим,увасестьфайлhello.jsсоследующимкодом:

varmessage="Helloworld";

console.log(message);

Командаnode

ВыразительныйJavascript

380Node.js

Page 381: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Выможетевыполнитьсвоюпрограммуизкоманднойстроки:

$nodehello.js

Helloworld

Методconsole.logвNodeдействуеттакже,каквбраузере.Выводиткусоктекста.НовNodeтекствыводитсянастандартныйвывод,аневконсольJavaScriptвбраузере.

Еслизапуститьnodeбезфайла,онвыдаствамстрокузапроса,вкоторойможнописатькоднаJavaScriptиполучатьрезультат.

$node

>1+1

2

>[-1,-2,-3].map(Math.abs)

[1,2,3]

>process.exit(0)

$

Переменнаяprocess,такжекакиconsole,доступнавNodeглобально.Онаобеспечиваетнесколькоспособовдляинспектированияиманипулированияпрограммой.Методexitзаканчиваетпроцесс,иемуможнопередатькодстатусаокончанияпрограммы,которыйсообщаетпрограмме,запустившейnode(вданномслучае,программнойоболочке),завершиласьлипрограммаудачно(нулевойкод)илисошибкой(любоедругоечисло).

Длядоступакаргументамкоманднойстроки,переданнымпрограмме,можночитатьмассивстрокprocess.argv.Внеготакжевключеныимякомандыnodeиимявашегоскрипта,поэтомусписокаргументовначинаетсясиндекса2.Еслифайлshowargv.jsсодержиттолькоинструкциюconsole.log(process.argv),егоможнозапуститьтак:

$nodeshowargv.jsone--andtwo

["node","/home/marijn/showargv.js","one","--and","two"]

ВсестандартныеглобальныепеременныеJavaScript—Array,Math,JSON,такжеестьвокруженииNode.Нотамотсутствуетфункционал,связанныйсработойбраузера,напримерdocumentилиalert.

ВыразительныйJavascript

381Node.js

Page 382: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Объектглобальнойобластивидимости,которыйвбраузереназываетсяwindow,вNodeимеетболееосмысленноеназваниеglobal.

Кроменесколькихупомянутыхпеременных,вродеconsoleиprocess,Nodeдержитмалофункционалавглобальнойобластивидимости.Длядоступакостальнымвстроеннымвозможностямвамнадообращатьсяксистемемодулей.

СистемаCommonJS,основаннаянафункцииrequire,былаописанавглаве10.ТакаясистемавстроенавNodeииспользуетсядлязагрузкивсего,отвстроенныхмодулейискачанныхбиблиотекдофайлов,являющихсячастямивашейпрограммы.

ПривызовеrequireNodeнужнопреобразоватьзаданнуюстрокувимяфайла.Пути,начинающиесяс"/","./"или"../",преобразуютсявпутиотносительнотекущего."./"означаеттекущуюдиректорию,"../"–директориювыше,а"/"–корневуюдиректориюфайловойсистемы.Есливызапросите"./world/world"изфайла/home/marijn/elife/run.js,Nodeпопробуетзагрузитьфайл/home/marijn/elife/world/world.js.Расширение.jsможноопускать.

Когдапередаётсястрока,котораяневыглядиткакотносительныйилиабсолютныйпуть,топредполагается,чтоэтолибовстроенныймодуль,илимодуль,установленныйвдиректорииnode_modules.Кпримеру,require(«fs»)выдаствамвстроенныймодульдляработысфайловойсистемой,аrequire(«elife»)попробуетзагрузитьбиблиотекуизnode_modules/elife/.Типичныйметодустановкибиблиотек–припомощиNPM,ккоторомуявернусьпозже.

Длядемонстрациидавайтесделаемпростойпроектиздвухфайлов.Первыйназовёмmain.js,ивнёмбудетопределёнскрипт,вызываемыйизкоманднойстроки,предназначенныйдляискажениястрок.

vargarble=require("./garble");

//Поиндексу2содержитсяпервыйаргументпрограммыизкоманднойстроки

varargument=process.argv[2];

Модули

ВыразительныйJavascript

382Node.js

Page 383: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

console.log(garble(argument));

Файлgarble.jsопределяетбиблиотекуискажениястрок,котораяможетиспользоватьсякакзаданнойранеепрограммойдлякоманднойстроки,такидругимискриптами,которымнуженпрямойдоступкфункцииgarble.

module.exports=function(string){

returnstring.split("").map(function(ch){

returnString.fromCharCode(ch.charCodeAt(0)+5);

}).join("");

};

Заменаmodule.exportsвместодобавлениякнемусвойствпозволяетнамэкспортироватьопределённоезначениеизмодуля.Вданномслучае,результатомзапросанашегомодуляполучитсясамафункцияискажения.

Функцияразбиваетстрокунасимволы,используяsplitспустойстрокой,изатемзаменяетвсесимволынадругие,скодомна5единицвыше.Затемонасоединяетрезультатобратновстроку.

Теперьмыможемвызватьнашинструмент:

$nodemain.jsJavaScript

Of{fXhwnuy

NPM,вскользьупомянутыйвглаве10,этоонлайн-хранилищемодулейJavaScript,многиеизкоторыхнаписаныспециальнодляNode.КогдавыставитеNodeнакомпьютер,выполучаетепрограммуnpm,котораядаётудобныйинтерфейскэтомухранилищу.

Кпримеру,одинизмодулейNPMзовётсяfiglet,ионпреобразуеттекств“ASCIIart”,рисунки,составленныеизтекстовыхсимволов.Воткакегоустановить:

$npminstallfiglet

УстановкачерезNPM

ВыразительныйJavascript

383Node.js

Page 384: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

npmGEThttps://registry.npmjs.org/figlet

npm200https://registry.npmjs.org/figlet

npmGEThttps://registry.npmjs.org/figlet/-/figlet-1.0.9.tgz

npm200https://registry.npmjs.org/figlet/-/figlet-1.0.9.tgz

[email protected]_modules/figlet

$node

>varfiglet=require("figlet");

>figlet.text("Helloworld!",function(error,data){

if(error)

console.error(error);

else

console.log(data);

});

_______

||||___|||_____________||__|||

||_||/_\||/_\\\/\//_\|'__||/_`||

|_|__/||(_)|\VV/(_)||||(_||_|

|_||_|\___|_|_|\___/\_/\_/\___/|_||_|\__,_(_)

ПослезапускаnpminstallNPMсоздастдиректориюnode_modules.Внутринеёбудетдиректорияfiglet,содержащийбиблиотеку.Когдамызапускаемnodeивызываемrequire(«figlet»),библиотеказагружаетсяимыможемвызватьеёметодtext,чтобывывестибольшиекрасивыебуквы.

Чтоинтересно,вместопростоговозвратастроки,вкоторойсодержатсябольшиебуквы,figlet.textпринимаетфункциюдляобратноговызова,которойонпередаётрезультат.Такжеонпередаёттудаещёодинаргумент,error,которыйвслучаеошибкибудетсодержатьобъектerror,авслучаеуспеха–null.

ТакойпринципработыпринятвNode.Длясозданиябуквfigletдолженпрочестьфайлсдиска,содержащийбуквы.Чтениефайла–асинхроннаяоперациявNode,поэтомуfiglet.textнеможетвернутьрезультатнемедленно.Асинхронностьзаразительна–любаяфункция,вызывающаяасинхронную,самастановитсяасинхронной.

NPM–этобольше,чемпростоnpminstall.Ончитаетфайлыpackage.json,содержащиеинформациювформатеJSONпропрограммуилибиблиотеку,вчастности,накакихбиблиотекахонаоснована.Выполнениеnpminstallвдиректории,содержащейтакойфайл,автоматическиприводиткустановкевсехзависимостей,ивсвоюочередьихзависимостей.Такжеинструментnpmиспользуетсядляразмещениябиблиотеквонлайновомхранилище

ВыразительныйJavascript

384Node.js

Page 385: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

NPM,чтобыдругиелюдимоглиихнаходить,скачиватьииспользовать.

БольшемынебудемуглублятьсявдеталииспользованияNPM.Обращайтесьнаnpmjs.orgзадокументациейпоискубиблиотек.

ОдинизсамыхвостребованныхвстроенныхмодулейNode–модуль“fs”,чтоозначает«файловаясистема».Модульобеспечиваетфункционалдляработысфайламиидиректориями.

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

varfs=require("fs");

fs.readFile("file.txt","utf8",function(error,text){

if(error)

throwerror;

console.log("Авфайлетомбыло:",text);

});

ВторойаргументreadFileзадаёткодировкусимволов,вкоторойнужнопреобразовыватьсодержимоефайлавстроку.Текстможнопреобразоватьвдвоичныеданныеразнымиспособами,носамымновымизнихявляетсяUTF-8.Еслиуваснетоснованийполагать,чтовфайлесодержитсятекствдругойкодировке,можносмелопередаватьпараметр«utf8».Есливынезадаликодировку,NodeвыдаствамданныевдвоичнойкодировкеввидеобъектаBuffer,анестроки.Этомассивоподобныйобъект,содержащийбайтыизфайла.

varfs=require("fs");

fs.readFile("file.txt",function(error,buffer){

if(error)

throwerror;

console.log("Вфайлебыло",buffer.length,"байт.",

"Первыйбайт:",buffer[0]);

});

Схожаяфункция,writeFile,используетсядлязаписифайланадиск.

Модульfilesystem

ВыразительныйJavascript

385Node.js

Page 386: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

varfs=require("fs");

fs.writeFile("graffiti.txt","ЗдесьбылNode",function(err){

if(err)

console.log("Ничегоневышло,ивотпочему:",err);

else

console.log("Записьуспешна.Всесвободны.");

});

Здесьзадаватькодировкуненужно,потомучтоwriteFileполагает,чтоеслиейназаписьдалистроку,анеобъектBuffer,тоеёнадовыводитьввидетекстаскодировкойпоумолчаниюUTF-8.

Модуль“fs”содержитмногополезного:функцияreaddirвозвращаетсписокфайловдиректорииввидемассивастрок,statвернётинформациюофайле,renameпереименовываетфайл,unlinkудаляет,ит.п.См.документациюнаnodejs.org

Многиефункции“fs”имеюткаксинхронный,такиасинхронныйвариант.Кпримеру,естьсинхронныйвариантфункцииreadFileподназваниемreadFileSync.

varfs=require("fs");

console.log(fs.readFileSync("file.txt","utf8"));

Синхронныефункциииспользоватьпрощеиполезнеедляпростыхскриптов,гдедополнительнаяскоростьасинхронногометоданеважна.Нозаметьте–навремявыполнениясинхронногодействиявашапрограммаполностьюостанавливается.Еслиейнадоотвечатьнавводпользователяилидругимпрограммампосети,затыкиожиданиясинхронногоI/Oприводяткраздражающимзадержкам.

Ещёодиносновноймодуль—«http».ОндаётфункционалдлясозданияHTTPсерверовиHTTPзапросов.

Вотвсё,чтонужнодлязапускапростейшегоHTTPсервера:

МодульHTTP

ВыразительныйJavascript

386Node.js

Page 387: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

varhttp=require("http");

varserver=http.createServer(function(request,response){

response.writeHead(200,{"Content-Type":"text/html"});

response.write("<h1>Привет!</h1><p>Вызапросили`"+

request.url+"`</p>");

response.end();

});

server.listen(8000);

Запустивскриптнасвоеймашины,выможетенаправитьбраузерпоадресуlocalhost:8000/hello,такимобразомсоздавзапросксерверу.ОнответитнебольшойHTML-страницей.

Функция,передаваемаякакаргументкcreateServer,вызываетсяприкаждойпопыткесоединенияссервером.Переменныеrequestиresponse–объекты,представляющиевходныеивыходныеданные.Первыйсодержитинформациюпозапросу,напримерсвойствоurlсодержитURLзапроса.

Чтобыотправитьчто-тоназад,используютсяметодыобъектаresponse.Первый,writeHead,пишетзаголовкиответа(см.главу17).Выдаётеемукодстатуса(вэтомслучае200для“OK”)иобъект,содержащийзначениязаголовков.Здесьмысообщаемклиенту,чтоондолженждатьдокументHTML.

Затемидёттелоответа(самдокумент),отправляемоечерезresponse.write.Этотметодможновызыватьнесколькораз,еслихотитеотправлятьответпокускам,кпримеру,передаваяпотоковыеданныепомереихпоступления.Наконец,response.endсигнализируетконецответа.

Вызовserver.listenзаставляетсерверслушатьзапросынапорту8000.Поэтомувамнадовбраузерезаходитьнаlocalhost:8000,анепростонаlocalhost(гдепортомпоумолчаниюбудет80).

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

Настоящийвеб-серверделаетгораздобольшетого,чтоописановпримере.Онсмотритнаметодзапроса(свойствоmethod),чтобыпонять,какоедействиепытаетсявыполнитьклиент,инаURLзапроса,чтобыпонять,на

ВыразительныйJavascript

387Node.js

Page 388: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

какомресурсеэтодействиедолжновыполняться.Далеевыувидитеболеепродвинутуюверсиюсервера.

ЧтобысделатьHTTP-клиент,мыможемиспользоватьфункциюмодуля“http”request.

varhttp=require("http");

varrequest=http.request({

hostname:"eloquentjavascript.net",

path:"/20_node.html",

method:"GET",

headers:{Accept:"text/html"}

},function(response){

console.log("Сервисответилскодом",

response.statusCode);

});

request.end();

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

Какиобъектresponseсервера,объект,возвращаемыйrequest,позволяетпередаватьданныеметодомwriteизаканчиватьзапросметодомend.Впримеренеиспользуетсяwrite,потомучтозапросыGETнедолжнысодержатьданныхвтеле.

ДлязапросовнабезопасныеURL(HTTPS),Nodeпредлагаетмодульhttps,вкотороместьсвояфункциязапроса,схожаясhttp.request.

МывиделидвапримерапотоковвпримерахHTTP–объектresponse,вкоторыйсерверможетвестизапись,иобъектrequest,которыйвозвращаетсяизhttp.request

Потокисвозможностьюзаписи–популярнаяконцепциявинтерфейсахNode.Увсехпотоковестьметодwrite,которомуможнопередатьстрокуили

Потоки

ВыразительныйJavascript

388Node.js

Page 389: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

объектBuffer.Методendзакрываетпоток,априналичииаргумента,выведетпередзакрытиемкусочекданных.Обоимметодамможнозадатьфункциюобратноговызовачерездополнительныйаргумент,которуюонивызовутпоокончаниюзаписиилизакрытиюпотока.

Возможносоздатьпоток,показывающийнафайл,припомощифункцииfs.createWriteStream.Затемможноиспользоватьметодwriteдлязаписивфайлпокусочкам,анецеликом,каквfs.writeFile.

Потокисвозможностьючтениябудутчутьсложнее.Какпеременнаяrequest,переданнаяфункциидляобратноговызовавсервереHTTP,такипеременнаяresponse,переданнаявHTTP-клиенте,являютсяпотокамисвозможностьючтения.(Серверчитаетзапросипотомпишетответы,аклиентпишетзапросичитаетответа).Чтениеизпотокаосуществляетсячерезобработчикисобытий,анечерезметоды.

Уобъектов,создающихсобытиявNode,естьметодon,схожийсметодомбраузераaddEventListener.Выдаётеемуимясобытияифункцию,ионрегистрируетэтуфункцию,чтобеёвызвалисразу,когдапроизойдётсобытие.

Употоковсвозможностьючтенияестьсобытия«data»и«end».Первоепроисходитприпоступленииданных,второе–поокончанию.Этамодельподходиткпотоковымданным,которыеможносразуобработать,дажееслиполученневесьдокумент.Файлможнопрочестьввидепотокачерезfs.createReadStream.

Следующийкодсоздаётсервер,читающийтелазапросовиотправляющийихвответпотокомввидетекстаиззаглавныхбукв.

varhttp=require("http");

http.createServer(function(request,response){

response.writeHead(200,{"Content-Type":"text/plain"});

request.on("data",function(chunk){

response.write(chunk.toString().toUpperCase());

});

request.on("end",function(){

response.end();

});

}).listen(8000);

ВыразительныйJavascript

389Node.js

Page 390: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Переменнаяchunk,передаваемаяобработчикуданных,будетбинарнымBuffer,которыйможнопреобразоватьвстроку,вызвавегометодtoString,которыйдекодируетегоизкодировкипоумолчанию(UTF-8).

Следующийкод,будучизапущеннымодновременноссервером,отправитзапроснасерверивыведетполученныйответ:

varhttp=require("http");

varrequest=http.request({

hostname:"localhost",

port:8000,

method:"POST"

},function(response){

response.on("data",function(chunk){

process.stdout.write(chunk.toString());

});

});

request.end("Helloserver");

Примерпишетвprocess.stdout(стандартныйвыводпроцесса,являющийсяпотокомсвозможностьюзаписи),аневconsole.log.Мынеможемиспользоватьconsole.log,таккакондобавляетлишнийпереводстрокипослекаждогокускакода–этоздесьненужно.

ДавайтескомбинируемнашиновыезнанияосерверахHTTPиработесфайловойсистемой,инаведёммостикмеждуними:HTTP-сервер,предоставляющийудалённыйдоступкфайлам.Утакогосерверамноговариантовиспользования.Онпозволяетвеб-приложениямхранитьиделитьсяданными,илиможетдатьгруппелюдейдоступкнаборуфайлов.

Когдамыотносимсякфайлам,каккресурсамHTTP,методыGET,PUTиDELETEможноиспользоватьдлячтения,записииудаленияфайлов.Мыбудеминтерпретироватьпутьвзапросекакпутькфайлу.

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

Простойфайловыйсервер

ВыразительныйJavascript

390Node.js

Page 391: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

/home/marijn/public/(илиC:\Users\marijn\public\наWindows),тозапросна/file.txtдолженуказатьна/home/marijn/public/file.txt(илиC:\Users\marijn\public\file.txt).

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

varhttp=require("http"),fs=require("fs");

varmethods=Object.create(null);

http.createServer(function(request,response){

functionrespond(code,body,type){

if(!type)type="text/plain";

response.writeHead(code,{"Content-Type":type});

if(body&amp;&amp;body.pipe)

body.pipe(response);

else

response.end(body);

}

if(request.methodinmethods)

methods[request.method](urlToPath(request.url),

respond,request);

else

respond(405,"Method"+request.method+

"notallowed.");

}).listen(8000);

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

Функцияrespondпередаётсяфункциям,обрабатывающимразныеметоды,иработаеткакобратныйвызовдляокончаниязапроса.ОнапринимаеткодстатусаHTTP,тело,и,возможно,типсодержимого.Еслипереданноетело–потоксвозможностьючтения,унегобудетметодpipe,которыйиспользуетсядляпередачичитаемогопотокавзаписываемый.Еслинет–предполагается,чтоэтолибоnull(телопустое),илистрока,итогдаонапередаётсянапрямуювметодответаend.

ЧтобыполучитьпутьизURLвзапросе,функцияurlToPath,используявстроенныймодульNode“url”,разбираетURL.Онапринимаетимяпути,

ВыразительныйJavascript

391Node.js

Page 392: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

нечтовроде/file.txt,декодирует,чтобыубратьэкранирующиекоды%20,ивставляетвначалеточку,чтобыполучитьпутьотносительнотекущегокаталога.

functionurlToPath(url){

varpath=require("url").parse(url).pathname;

return"."+decodeURIComponent(path);

}

Вамкажется,чтофункцияurlToPathнебезопасна?Выправы.Вернёмсякэтомувопросувупражнениях.

МыустроимметодGETтак,чтобыонвозвращалсписокфайловпричтениидиректории,исодержимоефайлапричтениифайла.

Вопросназасыпку–какойтипзаголовкаContent-Typeмыдолжнывозвращать,читаяфайл.Посколькувфайлеможетбытьвсё,чтоугодно,сервернеможетпростовернутьодинитотжетипдлявсех.НоNPMсэтимможетпомочь.Модульmime(индикаторытипасодержимогофайлавродеtext/plainтакженазываютсяMIMEtypes)знаетправильныйтипдляогромногоколичестварасширенийфайлов.

Запустивследующуюкомандуnpmвдиректории,гдеживётскриптсервера,высможетеиспользоватьrequire(«mime»)длязапросовкбиблиотекетипов.

$npminstallmime

npmhttpGEThttps://registry.npmjs.org/mime

npmhttp304https://registry.npmjs.org/mime

[email protected]_modules/mime

Когдазапрошенногофайланесуществует,правильнымкодомошибкидляэтогослучаябудет404.Мыбудемиспользоватьfs.statдлявозвратаинформациипофайлу,чтобывыяснить,естьлитакойфайл,инедиректориялиэто.

methods.GET=function(path,respond){

fs.stat(path,function(error,stats){

if(error&amp;&amp;error.code=="ENOENT")

respond(404,"Filenotfound");

elseif(error)

ВыразительныйJavascript

392Node.js

Page 393: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

respond(500,error.toString());

elseif(stats.isDirectory())

fs.readdir(path,function(error,files){

if(error)

respond(500,error.toString());

else

respond(200,files.join("\n"));

});

else

respond(200,fs.createReadStream(path),

require("mime").lookup(path));

});

};

Посколькузапросыкдискузанимаютвремя,fs.statработаетасинхронно.Когдафайланесуществует,fs.statпередастобъектerrorскодовымсвойством«ENOENT»вфункциюобратноговызова.Былобыздорово,еслибыNodeопределилразныетипыошибокдляразныхошибок,нотакогонет.ВместоэтогоонвыдаётзапутанныекодывстилеUnix.

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

Объектstatsвозвращаемыйfs.stat,рассказываетнамофайлевсё.Например,size–размерфайла,mtime–датамодификации.Здесьнамнужноузнать,директорияэтоилиобычныйфайл–этонамсообщитметодisDirectory.

Длячтенияспискафайловвдиректориимыиспользуемfs.readdir,ичерезещёодинобратныйвызов,возвращаемегопользователю.Дляобычныхфайловмысоздаёмчитаемыйпотокчерезfs.createReadStreamипередаёмеговответ,вместестипомсодержимого,которыймодуль“mime”выдалдляэтогофайла.

КодобработкиDELETEбудетпроще:

methods.DELETE=function(path,respond){

fs.stat(path,function(error,stats){

if(error&amp;&amp;error.code=="ENOENT")

ВыразительныйJavascript

393Node.js

Page 394: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

respond(204);

elseif(error)

respond(500,error.toString());

elseif(stats.isDirectory())

fs.rmdir(path,respondErrorOrNothing(respond));

else

fs.unlink(path,respondErrorOrNothing(respond));

});

};

Возможно,ваминтересно,почемупопыткаудалениянесуществующегофайлавозвращаетстатус204вместоошибки.Можносказать,чтоприпопыткеудалитьнесуществующийфайл,таккакфайлатамуженет,тозапросужеисполнен.СтандартHTTPпоощряетлюдейделатьидемпотентныезапросы–тоестьтакие,прикоторыхмногократныйповтородногоитогожедействиянеприводиткразнымрезультатам.

functionrespondErrorOrNothing(respond){

returnfunction(error){

if(error)

respond(500,error.toString());

else

respond(204);

};

}

КогдаответHTTPнесодержитданных,можноиспользоватькодстатуса204(“nocontent”).Таккакнамнужнообеспечитьфункцииобратноговызова,которыелибосообщаютобошибки,иливозвращаютответ204вразныхситуациях,янаписалспециальнуюфункциюrespondErrorOrNothing,котораясоздаёттакойобратныйвызов.

ВотобработчикзапросовPUT:

methods.PUT=function(path,respond,request){

varoutStream=fs.createWriteStream(path);

outStream.on("error",function(error){

respond(500,error.toString());

});

outStream.on("finish",function(){

respond(204);

});

request.pipe(outStream);

ВыразительныйJavascript

394Node.js

Page 395: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

};

Здесьнамненужнопроверятьсуществованиефайла–еслионесть,мыегопростоперезапишем.Опятьмыиспользуемpipeдляпередачиданныхизчитаемогопотокавзаписываемый,внашемслучае–иззапросавфайл.Еслисоздатьпотокнеудаётся,создаётсясобытие“error”,очёммысообщаемвответе.Когдаданныепереданыуспешно,pipeзакроетобапотока,чтоприведёткзапускусобытия“finish”.Апослеэтогомыможемотчитатьсяобуспехескодом204.

Полныйскриптсервералежиттут:eloquentjavascript.net/code/file_server.js.ЕгоможноскачатьизапуститьчерезNode.Конечно,егоможноменятьидополнятьдлярешенияупражненийилиэкспериментов.

Утилитакоманднойстрокиcurl,общедоступнаянаunix-системах,можетиспользоватьсядлясозданияHTTPзапросов.Следующийфрагменттестируетнашсервер.–Xиспользуетсядлязаданияметодазапроса,а–dдлявключениятелазапроса.

$curlhttp://localhost:8000/file.txt

Filenotfound

$curl-XPUT-dhellohttp://localhost:8000/file.txt

$curlhttp://localhost:8000/file.txt

hello

$curl-XDELETEhttp://localhost:8000/file.txt

$curlhttp://localhost:8000/file.txt

Filenotfound

Первыйзапроскfile.txtзавершаетсясошибкой,посколькуфайлаещёнет.ЗапросPUTсоздаётфайл,иглядите-ка–следующийзапросегоуспешнополучает.ПослеегоудалениячерезDELETEфайлсноваотсутствует.

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

Обработкаошибок

ВыразительныйJavascript

395Node.js

Page 396: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Чтобудет,когдачто-тореальновыброситисключениевсистеме?Мынеиспользуемблокиtry,потомуонобудетпереданонасамыйверхстекавызовов.ВNodeэтоприводиткпрекращениювыполненияпрограммыивыводуинформацииобисключении(вместесотслеживаниемстека)настандартныйвывод.

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

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

Ещёодинподход–использованиеобещаний,которыебылиописанывглаве17.Ониловятисключения,выброшенныефункциямиобратноговызоваипередаютихкакошибки.ВNodeможнозагрузитьбиблиотекуpromiseииспользоватьеёдляобработкиасинхронныхвызовов.НемногиебиблиотекиNodeинтегрируютобещания,нообычноихдовольнопростообернуть.Отличныймодуль“promise”сNPMсодержитфункциюdenodeify,котораяберётасинхроннуюфункциювродеfs.readFileипреобразовываетеёвфункцию,возвращающуюобещание.

varPromise=require("promise");

varfs=require("fs");

varreadFile=Promise.denodeify(fs.readFile);

readFile("file.txt","utf8").then(function(content){

console.log("Thefilecontained:"+content);

},function(error){

console.log("Failedtoreadfile:"+error);

});

Длясравнения,янаписалещёоднуверсиюфайловогосерверас

ВыразительныйJavascript

396Node.js

Page 397: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

использованиемобещаний,которуюможнонайтинаeloquentjavascript.net/code/file_server_promises.js.Онапочище,потомучтофункциитеперьмогутвозвращатьрезультаты,аненазначатьобратныевызовы,иисключенияпередаютсянеявно.

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

Объектfsp,использующийсявкоде,содержитвариантыфункцийfsсобещаниями,обёрнутымиприпомощиPromise.denodeify.Возвращаемыйизобработчикаметодаобъект,сосвойствамиcodeиbody,становитсяокончательнымрезультатомцепочкиобещаний,иониспользуетсядляопределениятого,какойответотправитьклиенту.

methods.GET=function(path){

returninspectPath(path).then(function(stats){

if(!stats)//Doesnotexist

return{code:404,body:"Filenotfound"};

elseif(stats.isDirectory())

returnfsp.readdir(path).then(function(files){

return{code:200,body:files.join("\n")};

});

else

return{code:200,

type:require("mime").lookup(path),

body:fs.createReadStream(path)};

});

};

functioninspectPath(path){

returnfsp.stat(path).then(null,function(error){

if(error.code=="ENOENT")returnnull;

elsethrowerror;

});

}

ФункцияinspectPath–простаяобёрткавокругfs.stat,обрабатывающаяслучай,когдафайлненайден.Вэтомслучаемызаменяемошибкунауспех,возвращающийnull.Всеостальныеошибкиможнопередавать.Когдаобещание,возвращаемоеизэтихобработчиков,обламывается,серверотвечаеткодом500.

ВыразительныйJavascript

397Node.js

Page 398: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Node–отличнаяпростаясистема,позволяющаязапускатьJavaScriptвнебраузера.Изначальноонаразрабатываласьдляработыпосети,чтобыигратьрольузлавсети.Ноонапозволяетделатьмноговсего,иесливынаслаждаетесьпрограммированиемнаJavaScript,автоматизацияежедневныхзадачсNodeработаетотлично.

NPMпредоставляетбиблиотекидлявсего,чтовамможетприйтивголову(идажедлякое-чего,чтовамнепридётвголову),ионапозволяетскачиватьиустанавливатьихпростойкомандой.Nodeтакжепоставляетсяснаборомвстроенныхмодулей,включая“fs”дляработысфайловойсистемой,и“http”длязапускаHTTPсерверовисозданияHTTPзапросов.

ВесьвводивыводвNodeделаетсяасинхронно,еслитольковынеиспользуетеявносинхронныйвариантфункции,напримерfs.readFileSync.Выпредоставляетефункцииобратноговызова,аNodeихвызываетвнужноевремя,когдаоперацииI/Oзаканчиваютработу.

Вглаве17первоеупражнениебылопосвященосозданиюзапросовкeloquentjavascript.net/author,спрашивавшихразныетипысодержимогопутёмпередачиразныхзаголовковAccept.

Сделайтеэтоснова,используяфункциюNodehttp.request.Запросите,покрайнеймере,типыtext/plain,text/htmlиapplication/json.Помните,чтозаголовкизапросаможнопередаватькакобъектвсвойствеheaders,первымаргументомhttp.request.

Выведитесодержимоекаждогоответа.

Дляупрощениядоступакфайламяоставилработатьсерверусебяна

Итог

Упражнения

Исновасогласованиесодержания

Устранениеутечек

ВыразительныйJavascript

398Node.js

Page 399: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

комьпютере,вдиректории/home/marijn/public.Однаждыяобнаружил,чтокто-тополучилдоступковсеммоимпаролям,которыеяхранилвбраузере.Чтослучилось?

Есливамэтонепонятно,вспомнитефункциюurlToPath,котораяопределяласьтак:

functionurlToPath(url){

varpath=require("url").parse(url).pathname;

return"."+decodeURIComponent(path);

}

Теперьвспомните,чтопути,передаваемыевфункцию“fs”,могутбытьотносительными.Онимогутсодержатьпуть“../”вверхнийкаталог.Чтобудет,есликлиентотправитзапросынаURLвродеследующих:

myhostname:8000/../.config/config/google-chrome/Default/Web%20Datamyhostname:8000/../.ssh/id_dsamyhostname:8000/../../../etc/passwd

ПоменяйтефункциюurlToPathдляустраненияподобнойпроблемы.Примитевовнимание,чтонаWindowsNodeразрешаеткакпрямыетакиобратныеслешидлязаданияпутей.

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

ХотяметодDELETEработаетиприудалениидиректорий(черезfs.rmdir),покасервернепредоставляетвозможностисозданиядиректорий.

ДобавьтеподдержкуметодаMKCOL,которыйдолженсоздаватьдиректориючерезfs.mkdir.MKCOLнеявляетсяосновнымметодомHTTP,ноонсуществует,именнодляэтого,встандартеWebDAV,которыйсодержитрасширенияHTTP,чтобыиспользоватьегодлязаписиресурсов,анетолькодляихчтения.

Созданиедиректорий

Общественноеместовсети

ВыразительныйJavascript

399Node.js

Page 400: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ТаккакфайловыйсервервыдаётлюбыефайлыидажевозвращаетправильныйзаголовокContent-Type,егоможноиспользоватьдляобслуживаниявеб-сайта.Таккаконразрешаетвсемудалятьизаменятьфайлы,этобылбыинтересныйсайт–которыйможноизменять,портитьиудалятьвсем,ктоможетсоздатьправильныйHTTP-запрос.Ноэтовсёравнобылбывеб-сайт.

НапишитепростуюHTMLстраницуспростымфайломJavaScript.Разместитеихвдиректории,обслуживаемойсерверомиоткройтевбраузере.

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

ИспользуйтеформуHTML(глава18)дляредактированияфайлов,составляющихсайт,позволяяпользователюобновлятьихнасерверечерезHTTP-запросы,какописановглаве17.

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

Неменяйтефайлынепосредственновкодефайловогосервера–есливысделаетеошибку,выскореевсегоиспортитетефайлы.Работайтевдиректории,недоступнойснаружи,икопируйтеихтудапослетестирования.

Есливашкомпьютерсоединяетсясинтернетомнапрямую,безfirewall,роутераилидругихустройств,высможетепригласитьдруганасвойсайт.Дляпроверкисходитенаwhatismyip.com,скопируйтеIPадресвадреснуюстрокуидобавьте:8000длявыборанужногопорта.Есливыпопалинасвойсайт,тоондоступендляпросмотравсем.

ВыразительныйJavascript

400Node.js

Page 401: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Навстречахпообменуопытомлюдисобщимиинтересамивстречаютсяиделаютнебольшиенеформальныепрезентациинатемусвоихзнаний.Навстречепообменуопытомсредифермеровкто-нибудьможетрассказатьовыращиваниисельдерея.НавстречепрограммистоввыможетевыступитьсрассказомпроNode.js

Такиевстречи–отличныйспособрасширитьсвойкругозор,узнатьоновинкахобласти,илипростопообщатьсяслюдьмисосхожимиинтересами.ВомногихгородахестьвстречилюбителейJavaScript.Обычноихпосещениебесплатное,иянашёлте,которыепосещал,дружелюбнымиигостеприимными.

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

Какивпредыдущейглаве,коднаписандляNode.jsизапуститьеговбраузеренеполучится.Полныйкоддоступенпоссылке.

Проект:веб-сайтпообменуопытом

Встречимоноциклистов

ВыразительныйJavascript

401Проект:веб-сайтпообменуопытом

Page 402: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Упроектаестьсервернаячасть,написаннаядляNode.js,иклиентская,написаннаядлябраузера.Сервернаяхранитсистемныеданныеипередаётихклиенту.ТакжеонаотдаётфайлыHTMLиJavaScript,которыесоздаютсистемунасторонеклиента.

Насервереестьсписоктемдляследующегособрания,иклиентихпоказывает.Укаждойтемыестьимявыступающего,название,описаниеисписоккомментариев.Клиентпозволяетпредлагатьновыетемы(добавлятьихвсписок),удалятьтемыикомментироватьсуществующие.Когдапользовательвноситэтоизменение,клиентделаетHTTP-запрос,чтобысообщитьобэтомсерверу.

Будетсозданоприложениедляпоказатекущихпредложенийтемикомментариипоним.Когдакто-тогде-тодобавляетновуютемуилиоставляеткомментарий,увсехлюдей,открывшихстраницувбраузере,изменениядолжныпроисходитьмгновенно.Этонепростаязадача,потомучтовеб-сервернеможетоткрыватьсоединениесклиентом,ипотомучтонетгодногоспособаузнать,ктоизклиентовсейчаспросматриваетданныйвеб-сайт.

Общепринятымрешениемпроблемыявляютсядлинныезапросы(longpolling),которыепослужилиоднойизмотивацийкразработкеNode.

Дизайн

ВыразительныйJavascript

402Проект:веб-сайтпообменуопытом

Page 403: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Чтобымгновеннооповеститьклиентаобизменениях,намнужносоединениесклиентом.Браузерытрадиционнонепринимаютзапросовнасоединения,иклиентывсёравноскрытызаустройствами,которыеэтисоединениянепринялибы,поэтомуначинатьсоединениессерверасмысланеимеет.

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

НозапросHTTPразрешаеттолькопростойобменинформацией–клиентотправляетзапрос,сервервозвращаетответ,ивсё.Естьтехнологияподназваниемwebsockets,котораяподдерживаетсясовременнымибраузерами,позволяющаяоткрыватьсоединениядляобменапроизвольнымиданными.Ноихдовольносложноиспользовать.

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

Покаклиентпостояннодержитоткрытыйзапрос,онбудетполучатьинформациюссерверанемедленно.Кпримеру,еслиуАлисывбраузереоткрытоприложениедляобменаопытом,браузерсделаетзапроснаобновленияибудетожидатьответа.КогдаБобизсвоегобраузераотправиттему«Экстремальныйспускнамоноциклесгоры»,серверзаметит,чтоАлисаждётобновлений,иотправитинформациюпоновойтемевответнаеёждущийзапрос.БраузерАлисыполучитданныеиобновитстраницу,показавновуютему.

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

Длинныезапросы

ВыразительныйJavascript

403Проект:веб-сайтпообменуопытом

Page 404: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

Передтем,какмыначнёмделатьсервериликлиент,подумаемобихточкесоприкосновения:интерфейсеHTTP,черезкоторыйонивзаимодействуют.

ИнтерфейсбудетоснованнаJSON,и,какивфайловомсерверевглаве20,мыбудемсвыгодойиспользоватьметодыHTTP.Интерфейссосредоточенвокругпути/talks.Пути,которыененачинаютсяс/talks,будутиспользоватьсядляотдачистатичныхфайлов–HTMLиJavaScript,определяющихклиентскуючасть.

ЗапросGETк/talksвозвращаетдокументJSONтипаэтого:

{"serverTime":1405438911833,

"talks":[{"title":"Unituning",

"presenter":"Васисуалий",

"summary":"Украшаемсвоймоноцикл",

"comment":[]}]}

ПолеserverTimeиспользуетсядлянадёжностидлинныхзапросов.Вернёмсякнемупозже.

СозданиеновойтемыпроисходитчереззапросPUTкURLвида/talks/Unituning,гдечастьпослевторогослеша–названиетемы.ТелозапросPUTдолжносодержатьобъектJSON,вкоторомописанысвойстваpresenterиsummary.

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

console.log("/talks/"+encodeURIComponent("HowtoIdle"));

//→/talks/How%20to%20Idle

ИнтерфейсHTTP

ВыразительныйJavascript

404Проект:веб-сайтпообменуопытом

Page 405: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Запроснасозданиетемыможетвыглядетьтак:

PUT/talks/How%20to%20IdleHTTP/1.1Content-Type:application/jsonContent-Length:92

{«presenter»:«Даша»,«summary»:«Неподвижностоимнамоноцикле»}

ТакиеURLподдерживаютзапросыGETдляполученияJSON-представлениятемыиDELETEдляудалениятемы.

ДобавлениекомментарияпроисходитчерезPOSTзапроскURLвида/talks/Unituning/comments,собъектомJSON,содержащимсвойстваauthorиmessageвтелезапроса.

POST/talks/Unituning/commentsHTTP/1.1Content-Type:application/jsonContent-Length:72

{«author»:«Alice»,«message»:«Willyoutalkaboutraisingacycle?»}

Дляподдержкидлинныхзапросов,запросыGETк/talksмогутвключатьпараметрподименемchangesSince,показывающий,чтоклиентунужныобновления,случившиесяпослезаданнойточкивовремени.Когдаобновленияпоявляются,онисразужевозвращаются.Когдаихнет,запросзадерживается,покачто-нибудьнеслучится,илипоканепройдётзаданныйпериодвремени(мызададим90секунд).

Времяиспользуетсявформатеколичествамиллисекундсначала1970года,втомжеформате,чтовозвращаетDate.now().Чтобыудостовериться,чтоклиентполучаетвсеобновления,инеполучаетодноитожеобновлениедважды,клиентдолженпередатьвремя,вкотороеонвпоследнийразполучилинформациюссервера.Часысерверамогутнесовпадатьсклиентом,идажееслибонисовпадали,клиентнемогбызнатьточноевремя,вкотороесерверотправлялответ,потомучтопередачаданныхпосетизанимаетвремя.

ПоэтомувответахназапросыGETк/talksисуществуетсвойствоserverTime.Оносообщаетклиентуточноевремяпочасамсервера,когдабылисозданыпередаваемыеданные.Клиентпростосохраняетвремяипередаётеговместесоследующимзапросом,чтобыубедиться,чтоонполучаеттолькотеобновления,которыхещёнеполучал.

ВыразительныйJavascript

405Проект:веб-сайтпообменуопытом

Page 406: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

GET/talks?changesSince=1405438911833HTTP/1.1

(прошловремя)

HTTP/1.1200OKContent-Type:application/jsonContent-Length:95

{«serverTime»:1405438913401,«talks»:[{«title»:«Unituning»,«deleted»:true}]}

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

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

Простымрешениембылобыразместитьсистемузаобратнымпрокси–этоHTTP-сервер,котораяпринимаетсоединенияснаружисистемыиперенаправляетихнасервераHTTP,работающиелокально.Такойproxyможнонастроить,чтобыонспрашивалимяипарольпользователя,ивымоглибыустроитьтак,чтобыпарольбылтолькоучленоввашейгруппы.

Начнёмснаписаниясервернойчастипрограммы.КодработаетнаNode.js

Длязапускасерверабудетиспользоватьсяhttp.createServer.Вфункции,обрабатывающейновыйзапрос,мыдолжныразличатьзапросы(определяемыеметодомипутём),которыемыподдерживаем.Этоможносделатьчерездлиннуюцепочкуif/else,номожноикрасивее.

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

Сервер

Роутинг

ВыразительныйJavascript

406Проект:веб-сайтпообменуопытом

Page 407: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

совпадающимсрегуляркой/^\/talks\/(\/+)$/(чтосовпадаетс/talks/,закоторымидётназваниетемы),могутбытьобработанызаданнойфункцией.Крометого,онможетпомочьизвлечьосмысленныечастипути,внашемслучае–названиетемы,заключённоевкавычки,ипередатьихвспомогательнойфункции.

ВNPMестьмногохорошихмодулейроутинга,нотутмысамисебетакойнапишем,чтобыпродемонстрироватьпринципегоработы.

Вотфайлrouter.js,которыйбудетзапрашиватьсячерезrequireизмодулясервера:

varRouter=module.exports=function(){

this.routes=[];

};

Router.prototype.add=function(method,url,handler){

this.routes.push({method:method,

url:url,

handler:handler});

};

Router.prototype.resolve=function(request,response){

varpath=require("url").parse(request.url).pathname;

returnthis.routes.some(function(route){

varmatch=route.url.exec(path);

if(!match||route.method!=request.method)

returnfalse;

varurlParts=match.slice(1).map(decodeURIComponent);

route.handler.apply(null,[request,response]

.concat(urlParts));

returntrue;

});

};

МодульэкспортируетконструкторRouter.Объектrouterпозволяетрегистрироватьновыеобработчикисметодомadd,ираспределятьзапросыметодомresolve.

Последнийвернётбулевскоезначение,показывающее,быллинайденобработчик.Методsomeмассивапутейбудетпробоватьихпоочереди(впорядке,вкакомонибылизаданы),иостановитсясвозвратомtrue,если

ВыразительныйJavascript

407Проект:веб-сайтпообменуопытом

Page 408: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

путьнайден.

Функцииобработчиковвызываютсясобъектамиrequestиresponse.Когдарегулярка,проверяющаяURL,возвращаетгруппы,топредставляющиеихстрокипередаютсявобработчиквкачестведополнительныхаргументов.ЭтистрочкинадодекодироватьизURL-стиля%20.

Когдатипзапросанесовпадаетнисоднимизтипов,которыеобрабатываетроутер,сервердолженинтерпретироватьегокакзапросфайлаизобщейдиректории.Можнобылобыиспользоватьфайловыйсерверизглавы20длявыдачиэтихфайлов,нонамненужнаподдержкаPUTиDELETE,затонамнужныдополнительныефункциитипаподдержкикеширования.Поэтому,давайтеиспользоватьпроверенныйипротестированныйфайловыйсерверизNPM.

Явыбралecstatic.ЭтонеединственныйсервернаNPM,ноонхорошоработаетиудовлетворяетнашимтребованиям.Модульecstaticэкспортируетфункцию,которуюможновызватьсобъектомконфигурации,чтобыонавыдалафункциюобработчика.Мыиспользуемопциюroot,чтобысообщитьсерверу,гденужноискатьфайлы.Обработчикпринимаетпараметрыrequestиresponse,иегоможнопередатьнапрямуювcreateServer,чтобысоздатьсервер,которыйотдаёттолькофайлы.Носначаланамнужнопроверитьтезапросы,которыемыобрабатываемособо–поэтомумыобёртываемеговещёоднуфункцию.

varhttp=require("http");

varRouter=require("./router");

varecstatic=require("ecstatic");

varfileServer=ecstatic({root:"./public"});

varrouter=newRouter();

http.createServer(function(request,response){

if(!router.resolve(request,response))

fileServer(request,response);

}).listen(8000);

ФункцииrespondиrespondJSONиспользуютсявкодесервера,чтобыможнобылоотправлятьответыоднимвызовомфункции.

Выдачафайлов

ВыразительныйJavascript

408Проект:веб-сайтпообменуопытом

Page 409: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functionrespond(response,status,data,type){

response.writeHead(status,{

"Content-Type":type||"text/plain"

});

response.end(data);

}

functionrespondJSON(response,status,data){

respond(response,status,JSON.stringify(data),

"application/json");

}

Серверхранитпредложенныетемывобъектеtalks,укоторогоименамисвойствявляютсяназваниятем.ОнибудутвыглядетькакресурсыHTTPпоадресу/talks/[title],поэтомунамнужнодобавитьвроутеробработчиков,реализующихразличныеметоды,которыеклиентымогутиспользоватьдляработысними.

ОбработчикдлязапросовGETоднойтемыдолженнайтиеёилибовернутьданныевJSON,либовыдатьошибку404.

vartalks=Object.create(null);

router.add("GET",/^\/talks\/([^\/]+)$/,

function(request,response,title){

if(titleintalks)

respondJSON(response,200,talks[title]);

else

respond(response,404,"Notalk'"+title+"'found");

});

Удалениетемыделаетсяудалениемизобъектаtalks.

router.add("DELETE",/^\/talks\/([^\/]+)$/,

function(request,response,title){

if(titleintalks){

deletetalks[title];

registerChange(title);

}

respond(response,204,null);

});

Темыкакресурсы

ВыразительныйJavascript

409Проект:веб-сайтпообменуопытом

Page 410: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ФункцияregisterChange,которуюмыопределимпозже,уведомляетдлинныезапросыобизменениях.

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

functionreadStreamAsJSON(stream,callback){

vardata="";

stream.on("data",function(chunk){

data+=chunk;

});

stream.on("end",function(){

varresult,error;

try{result=JSON.parse(data);}

catch(e){error=e;}

callback(error,result);

});

stream.on("error",function(error){

callback(error);

});

}

Одинизобработчиков,которомунужночитатьответывJSON–этообработчикPUT,которыйиспользуетсядлясозданияновыхтем.Ондолженпроверить,естьлиуданныхсвойстваpresenterиsummary,которыедолжныбытьстроками.Данные,приходящиеснаружи,всегдамогутоказатьсямусором,имынехотим,чтобыиз-заплохогозапросабыласломананашасистема.

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

router.add("PUT",/^\/talks\/([^\/]+)$/,

function(request,response,title){

readStreamAsJSON(request,function(error,talk){

if(error){

respond(response,400,error.toString());

ВыразительныйJavascript

410Проект:веб-сайтпообменуопытом

Page 411: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

}elseif(!talk||

typeoftalk.presenter!="string"||

typeoftalk.summary!="string"){

respond(response,400,"Badtalkdata");

}else{

talks[title]={title:title,

presenter:talk.presenter,

summary:talk.summary,

comments:[]};

registerChange(title);

respond(response,204,null);

}

});

});

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

router.add("POST",/^\/talks\/([^\/]+)\/comments$/,

function(request,response,title){

readStreamAsJSON(request,function(error,comment){

if(error){

respond(response,400,error.toString());

}elseif(!comment||

typeofcomment.author!="string"||

typeofcomment.message!="string"){

respond(response,400,"Badcommentdata");

}elseif(titleintalks){

talks[title].comments.push(comment);

registerChange(title);

respond(response,204,null);

}else{

respond(response,404,"Notalk'"+title+"'found");

}

});

});

Попыткадобавитькомментарийкнесуществующейтемедолжнавозвращатьошибку404.

Поддержкадлинныхзапросов

ВыразительныйJavascript

411Проект:веб-сайтпообменуопытом

Page 412: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Самыйинтересныйаспектсервера–часть,котораяподдерживаетдлинныезапросы.Когданаадрес/talksпоступаетзапросGET,этоможетбытьпростойзапросвсехтем,илизапроснаобновленияспараметромchangesSince.

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

functionsendTalks(talks,response){

respondJSON(response,200,{

serverTime:Date.now(),

talks:talks

});

}

ОбработчикдолженпосмотретьнавсепараметрызапросавегоURL,чтобыпроверить,незаданлипараметрchangesSince.Еслидатьфункцииparseмодуля“url”второйаргументзначенияtrue,онтакжераспарситвторуючастьURL–query,частьзапроса.Увозвращаемогообъектабудетсвойствоquery,вкоторомбудетещёодинобъект,сименамиизначениямипараметров.

router.add("GET",/^\/talks$/,function(request,response){

varquery=require("url").parse(request.url,true).query;

if(query.changesSince==null){

varlist=[];

for(vartitleintalks)

list.push(talks[title]);

sendTalks(list,response);

}else{

varsince=Number(query.changesSince);

if(isNaN(since)){

respond(response,400,"Invalidparameter");

}else{

varchanged=getChangedTalks(since);

if(changed.length>0)

sendTalks(changed,response);

else

waitForChanges(since,response);

}

}

});

ПриотсутствиипараметраchangesSinceобработчикпростостроитсписок

ВыразительныйJavascript

412Проект:веб-сайтпообменуопытом

Page 413: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

всехтемивозвращаетего.

Иначе,сперванадопроверитьпараметрchangeSinceнапредметтого,чтоэточисло.ФункцияgetChangedTalks,которуюмывскореопределим,возвращаетмассивизменённыхтемснекоегозаданноговремени.Еслионавозвращаетпустоймассив,тосерверунечеговозвращатьклиенту,такчтоонсохраняетобъектresponse(припомощиwaitForChanges),чтобыответитьпопозже.

varwaiting=[];

functionwaitForChanges(since,response){

varwaiter={since:since,response:response};

waiting.push(waiter);

setTimeout(function(){

varfound=waiting.indexOf(waiter);

if(found>-1){

waiting.splice(found,1);

sendTalks([],response);

}

},90*1000);

}

Методspliceиспользуетсядлявырезаниякускамассива.Емузадаётсяиндексиколичествоэлементов,ионизменяетмассив,удаляяэтоколичествоэлементовпослезаданногоиндекса.Вэтомслучаемыудаляемодинэлемент–объект,ждущийответ,чейиндексмыузналичерезindexOf.Есливыпередадитедополнительныеаргументывsplice,ихзначениябудутвставленывмассивназаданнойпозиции,изаместятудалённыеэлементы.

Когдаобъектresponseсохранёнвмассивеwaiting,задаётсятаймаут.После90секундонпроверяет,ждётлиещёзапрос,иеслида–отправляетпустойответиудаляетегоизмассиваwaiting.

Чтобынайтиименнотетемы,которыесменилисьпослезаданноговремени,намнадоотслеживатьисториюизменений.РегистрацияизмененияприпомощиregisterChangeзапомнитэтоизменение,вместестекущимвременем,вмассивеchanges.Когдаслучаетсяизменение,этозначит–естьновыеданные,поэтомувсемждущимзапросамможнонемедленноответить.

varchanges=[];

ВыразительныйJavascript

413Проект:веб-сайтпообменуопытом

Page 414: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functionregisterChange(title){

changes.push({title:title,time:Date.now()});

waiting.forEach(function(waiter){

sendTalks(getChangedTalks(waiter.since),waiter.response);

});

waiting=[];

}

Наконец,getChangedTalksиспользуетмассивchanges,чтобыпостроитьмассивизменившихсятем,включаяобъектысосвойствомdeletedдлятем,которыхуженесуществует.ПрипостроениимассиваgetChangedTalksдолжнаубедиться,чтооднаитажетеманевключаетсядважды,таккактемамоглаизменитьсянесколькоразсзаданногомоментавремени.

functiongetChangedTalks(since){

varfound=[];

functionalreadySeen(title){

returnfound.some(function(f){returnf.title==title;});

}

for(vari=changes.length-1;i>=0;i--){

varchange=changes[i];

if(change.time<=since)

break;

elseif(alreadySeen(change.title))

continue;

elseif(change.titleintalks)

found.push(talks[change.title]);

else

found.push({title:change.title,deleted:true});

}

returnfound;

}

Вотивсёскодомсервера.Запускнаписанногокодадаствамсервер,работающийнапорту8000,которыйвыдаётфайлыизпубличнойподдиректориииуправляетинтерфейсомтемпоадресу/talks.

Клиентскаячастьвеб-сайтапоуправлениютемамисостоитизтрёхфайлов:HTML-страница,таблицастилейифайлJavaScript.

Клиент

ВыразительныйJavascript

414Проект:веб-сайтпообменуопытом

Page 415: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Серверыпообщепринятойсхемевслучаезапросапути,соответствующегодиректории,отдаютфайлподименемindex.htmlизэтойдиректории.Модульфайловогосервераecstaticподдерживаетэтосоглашение.Призапросепути/серверищетфайл./public/index.html(где./public–этокорневаядиректория)ивозвращаетего,еслионтаместь.

Значит,еслинадопоказатьстраницу,когдабраузербудетзапрашиватьнашсервер,еёнадоположитьвpublic/index.html.Вотначалофайлаindex:

<!doctypehtml>

<title>Обменопытом</title>

<linkrel="stylesheet"href="skillsharing.css">

<h1>Обменопытом</h1>

<p>Вашеимя:<inputtype="text"id="name"></p>

<divid="talks"></div>

Определяетсязаголовокивключаетсятаблицастилей,гдеопределяютсястили–вчислепрочего,рамочкавокругтем.Затемдобавлензаголовокиполеname.Пользовательдолженвписатьсвоёимя,чтобыонобылоприсоединенокеготемамикомментариям.

Элемент<div>сID“talks”будетсодержатьсписоктем.Скриптзаполняетсписок,когдаонполучаетегоссервера.

Затемидётформадлясозданияновойтемы.

<formid="newtalk">

<h3>Submitatalk</h3>

Заголовок:<inputtype="text"style="width:40em"name="title">

<br>

Summary:<inputtype="text"style="width:40em"name="summary">

<buttontype="submit">Отправить</button>

</form>

Скриптдобавитобработчиксобытия“submit”вформу,изкоторогоонсможет

HTML

ВыразительныйJavascript

415Проект:веб-сайтпообменуопытом

Page 416: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

сделатьHTTP-запрос,сообщающийсерверупротему.

Затемидётзагадочныйблок,укоторогостильdisplayустановленвnone,икоторыйпоэтомуневиденнастранице.Догадаетесь,зачемоннужен?

<divid="template"style="display:none">

<div>

<h2>{{title}}</h2>

<div>by<span>{{presenter}}</span></div>

<p>{{summary}}</p>

<div></div>

<form>

<inputtype="text"name="comment">

<buttontype="submit">Добавитькомментарий</button>

<buttontype="button">Удалитьтему</button>

</form>

</div>

<div>

<span>{{author}}</span>:{{message}}

</div>

</div>

СозданиесложныхструктурDOMчерезJavaScriptприводиткуродливомукоду.Можносделатьегопокрасивееприпомощивспомогательныхфункцийтипаeltизглавы13,норезультатвсёравнобудетвыглядетьхуже,чемHTML,которыйвкаком-тосмыслеявляетсяязыкомдляпостроенияDOM-структур.

ДлясозданияDOM-структурдлятемобсуждений,нашапрограммаопределитпростуюсистемушаблонов,котораяиспользуетскрытыеструктуры,включаемыевдокумент,длясозданияновыхструктур–заменяяметкивфайлемеждудвойнымифигурнымикавычкаминазначениядляконкретнойтемы.

Инаконец,HTMLвключаетфайлскрипта,содержащегоклиентскийкод.

<scriptsrc="skillsharing_client.js"></script>

Запуск

ВыразительныйJavascript

416Проект:веб-сайтпообменуопытом

Page 417: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Первое,чтоклиентдолженсделатьпризагрузкестраницы,этозапроситьссервератекущийнабортем.ТаккакмыбудемделатьмногоHTTP-запросов,мыопределимнебольшуюобёрткувокругXMLHttpRequest,котораяприметобъектдлянастройкизапросаиобратноговызовапоокончаниюзапроса.

functionrequest(options,callback){

varreq=newXMLHttpRequest();

req.open(options.method||"GET",options.pathname,true);

req.addEventListener("load",function(){

if(req.status<400)

callback(null,req.responseText);

else

callback(newError("Requestfailed:"+req.statusText));

});

req.addEventListener("error",function(){

callback(newError("Networkerror"));

});

req.send(options.body||null);

}

Начальныйзапроспоказываетполученныетемынаэкранеиначинаетпроцессдлинныхзапросов,вызываяwaitForChanges.

varlastServerTime=0;

request({pathname:"talks"},function(error,response){

if(error){

reportError(error);

}else{

response=JSON.parse(response);

displayTalks(response.talks);

lastServerTime=response.serverTime;

waitForChanges();

}

});

ПерменнаяlastServerTimeиспользуетсядляотслеживаниявременипоследнегообновления,полученногоссервера.Посленачальногозапроса,видтемуклиентасоответствуетвидутемсервера,которыебылунеговмоментзапроса.Такимобразом,свойствоserverTime,включаемоевответ,предоставляетправильноеначальноезначениеlastServerTime.

Когдазапроснеудался,намненадо,чтобыстраницапростосиделаиничего

ВыразительныйJavascript

417Проект:веб-сайтпообменуопытом

Page 418: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

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

functionreportError(error){

if(error)

alert(error.toString());

}

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

Чтобыиметьвозможностьобновлятьсписоктемприпоступленииизменений,клиентдолженотслеживатьтемы,которыеонпоказываетсейчас.Тогда,еслипоступаетноваяверсиятемы,котораяужеестьнаэкране,еёможнозаменитьпрямонаместеобновлённойверсией.Сходнымобразом,когдапоступаетинформацияобудалениитемы,нужныйэлементDOMможноудалитьиздокумента.

ФункцияdisplayTalksиспользуетсякакдляпостроенияначальногоэкрана,такидляегообновленияприизменениях.ОнабудетиспользоватьобъектshownTalks,связывающийзаголовкитемсузламиDOM,чтобызапомнитьтемы,которыеужеестьнаэкране.

vartalkDiv=document.querySelector("#talks");

varshownTalks=Object.create(null);

functiondisplayTalks(talks){

talks.forEach(function(talk){

varshown=shownTalks[talk.title];

if(talk.deleted){

if(shown){

talkDiv.removeChild(shown);

deleteshownTalks[talk.title];

}

}else{

varnode=drawTalk(talk);

Показтем

ВыразительныйJavascript

418Проект:веб-сайтпообменуопытом

Page 419: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

if(shown)

talkDiv.replaceChild(node,shown);

else

talkDiv.appendChild(node);

shownTalks[talk.title]=node;

}

});

}

СтруктураDOMдлятемстроитсяпошаблону,включённомувHTMLдокумент.СначаланужноопределитьinstantiateTemplate,которыйнаходитизаполняетшаблон.

Параметрname–имяшаблона.Чтобынайтиэлементшаблона,мыищемэлементы,укоторыхимяклассасовпадаетсименемшаблона,которыйявляетсядочернимуэлементасID“template”.МетодquerySelectorоблегчаетэтотпроцесс.Настраницеестьшаблоны“talk”и“comment”.

functioninstantiateTemplate(name,values){

functioninstantiateText(text){

returntext.replace(/\{\{(\w+)\}\}/g,function(_,name){

returnvalues[name];

});

}

functioninstantiate(node){

if(node.nodeType==document.ELEMENT_NODE){

varcopy=node.cloneNode();

for(vari=0;i<node.childNodes.length;i++)

copy.appendChild(instantiate(node.childNodes[i]));

returncopy;

}elseif(node.nodeType==document.TEXT_NODE){

returndocument.createTextNode(

instantiateText(node.nodeValue));

}

}

vartemplate=document.querySelector("#template."+name);

returninstantiate(template);

}

МетодcloneNode,которыйестьувсехузловDOM,создаёткопиюузла.Оннескопируетдочерниеузлы,еслинепередатьемупервымаргументомtrue.Функцияinstantiateрекурсивносоздаёткопиюшаблона,заполняяегопоходудела.

ВыразительныйJavascript

419Проект:веб-сайтпообменуопытом

Page 420: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

ВторойаргументinstantiateTemplateдолженбытьобъектом,чьисвойствасодержатстроки,которыенадоввестившаблон.Меткавродебудетзамененазначениемсвойства“title”.

Этотподходкшаблонамдовольногруб,нодлясозданияdrawTalkегобудетдостаточно.

functiondrawTalk(talk){

varnode=instantiateTemplate("talk",talk);

varcomments=node.querySelector(".comments");

talk.comments.forEach(function(comment){

comments.appendChild(

instantiateTemplate("comment",comment));

});

node.querySelector("button.del").addEventListener(

"click",deleteTalk.bind(null,talk.title));

varform=node.querySelector("form");

form.addEventListener("submit",function(event){

event.preventDefault();

addComment(talk.title,form.elements.comment.value);

form.reset();

});

returnnode;

}

Послезавершенияобработкишаблона“talk”нужномногочегоподлатать.Во-первых,нужновывестикомментарии,путёммногократногодобавленияшаблона“comment”идобавлениярезультатовкузлукласса«comments».Затем,обработчикисобытийнужноприсоединитьккнопке,котораяудаляетзадачуикформе,добавляющейкомментарий.

Обработчикисобытий,зарегистрированныевdrawTalk,вызываютфункцииdeleteTalkиaddCommentнепосредственнодлядействий,необходимыхдляудалениятемыилидобавлениякомментария.ЭтобудетнужнодляпостроенияURL,которыессылаютсянатемысзаданнымименем,длякоторыхмыопределяемвспомогательнуюфункциюtalkURL.

Обновлениесервера

ВыразительныйJavascript

420Проект:веб-сайтпообменуопытом

Page 421: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

functiontalkURL(title){

return"talks/"+encodeURIComponent(title);

}

ФункцияdeleteTalkзапускаетзапросDELETEисообщаетобошибкевслучаенеудачи.

functiondeleteTalk(title){

request({pathname:talkURL(title),method:"DELETE"},

reportError);

}

ДлядобавлениякомментариянужнопостроитьегопредставлениевформатеJSONиотправитьегокакчастьPOST-запроса.

functionaddComment(title,comment){

varcomment={author:nameField.value,message:comment};

request({pathname:talkURL(title)+"/comments",

body:JSON.stringify(comment),

method:"POST"},

reportError);

}

ПеременнаяnameField,используемаядляустановкисвойствакомментарияauthor,ссылаетсянаполе<input>вверхустраницы,котороепозволяетпользователюзадатьегоимя.МытакжеподключаемэтополекlocalStorage,чтобыегонеприходилосьзаполнятькаждыйразприперезагрузкестраницы.

varnameField=document.querySelector("#name");

nameField.value=localStorage.getItem("name")||"";

nameField.addEventListener("change",function(){

localStorage.setItem("name",nameField.value);

});

Формавнизустраницыдлясозданияновойтемыполучаетобработчиксобытий“submit”.Этотобработчикзапрещаетдействиепоумолчанию(чтопривелобыкперезагрузкестраницы),очищаетформуизапускаетPUT-запросдлясозданиятемы.

vartalkForm=document.querySelector("#newtalk");

talkForm.addEventListener("submit",function(event){

event.preventDefault();

request({pathname:talkURL(talkForm.elements.title.value),

ВыразительныйJavascript

421Проект:веб-сайтпообменуопытом

Page 422: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

method:"PUT",

body:JSON.stringify({

presenter:nameField.value,

summary:talkForm.elements.summary.value

})},reportError);

talkForm.reset();

});

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

Учитываясозданнуюнасервересистемуито,какмыопределилиdisplayTalksдляобработкиизмененийтем,которыеужеестьнастранице,саммеханизмдлинныхзапросовоказываетсянеожиданнопростым.

functionwaitForChanges(){

request({pathname:"talks?changesSince="+lastServerTime},

function(error,response){

if(error){

setTimeout(waitForChanges,2500);

console.error(error.stack);

}else{

response=JSON.parse(response);

displayTalks(response.talks);

lastServerTime=response.serverTime;

waitForChanges();

}

});

}

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

Обнаружениеизменений

ВыразительныйJavascript

422Проект:веб-сайтпообменуопытом

Page 423: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

отладки),иделаетсяследующаяпопыткачерез2.5секунды.

Когдазапросудаётся,наэкранвыводятсяновыеданные,иlastServerTimeобновляется,чтобыотражатьтотфакт,чтомыполучилиданныевсоответствиисэтимновыммоментомвремени.Запроссразустартуетзаново,иждётследующегообновления.

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

Следующиеупражнениязаключаютсявизменениисистемы,описаннойвэтойглаве.Дляработынадними,убедитесь,чтовыскачаликодиустановилиNode.js.

Сервердержитвседанныевпамяти.Еслионупадётилиперезапустится,всетемыикомментариибудутпотеряны.

Расширьтеегофункциональностьстем,чтобыонсохранялданныенадискеиавтоматическизагружалихприперезагрузке.Неволнуйтесьнасчётэффективности,сделайтесамыйпростойвариант.

Общаяперерисовкавсехтемработаетнеплохо,потомучтонетразличиямеждуузломDOMиегозаменой,когдаониодинаковые.Ноестьисключения.Есливыначнётёпечататьчто-либовполекомментарияктемеводномокнебраузера,азатемвдругомокнедобавитекомментарийкэтойтеме,полевпервомокнебудетперерисовано,ибудетпотеряноиегосодержимое,ифокус.

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

Упражнения

Сохранениесостояниянадиск

Обнулениеполейкомментариев

ВыразительныйJavascript

423Проект:веб-сайтпообменуопытом

Page 424: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

избежатьэтого?

Большинствошаблонизаторовделаютбольше,чемпростозаполняютшаблоныстроками.Поменьшеймереонипозволяютдобавлятьвшаблоныусловия,аналогичнооператоруif,иповторениячастейшаблона,аналогичноциклам.

Еслибмымоглиповторятькусокшаблонадлякаждогоэлементамассива,второйшаблон(«comment»)былбынамненужен.Мымоглипростосказатьшаблону“talk”,чтобыонповторялсядлямассива,содержащегосявсвойствеcomments,исоздавалбыузлы,которыеявляютсякомментариями,длякаждогоэлементамассива.

Этомоглобывыглядетьтак:

<div>

<divtemplate-repeat="comments">

<span>{{author}}</span>:{{message}}

</div>

</div>

Идеявследующем:когдаприобработкешаблонавстречаетсяатрибутtemplate-repeat,повторяющимшаблон,кодпроходитцикломпомассиву,содержащемусявсвойстве,названномтакже,какэтотатрибут.Контекстшаблона(переменнаяvaluesвinstantiateTemplate)приработециклапоказывалабынатекущийэлементмассиватак,чтобыметкуискалибывобъектеcomment,аневтеме.

ПерепишитеinstantiateTemplateтак,чтобыонаэтоумела,ипотомпоменяйтешаблоны,чтобонииспользовалиэтувозможность,иуберителишниестрокидлясозданиякомментариевизфункцииdrawTalk.

Какбывыорганизовалиусловноесозданиеузлов,чтобыможнобылоопускатьчастишаблона,еслиопределённоезначениеравноtrueилиfalse?

Улучшенныешаблоны

Актобезскрипта?

ВыразительныйJavascript

424Проект:веб-сайтпообменуопытом

Page 425: Marijn Haverbeke - Выразительный Javascript, 2-е Издание - 2015

Есликто-нибудьзайдётнанашсайтсотключеннымJavaScript,ониполучатсломаннуюнеработающуюстраницу.Этонеочень-тохорошо.

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

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

Незаставляювасегореализовывать.Достаточноописатьвозможноерешение.Кажетсяливамтакойвариантсайтаболееилименееэлегантным,чемтот,чтомыужесделали?

ВыразительныйJavascript

425Проект:веб-сайтпообменуопытом