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
2-еиздание
Автор:MarijnHaverbekeПеревод:ВячеславГолованов
РаспространяетсяподлицензиейCreativeCommonsAttribution-Noncommercial.ИсходныйкодвкнигераспростроняетсяподлицензиейMIT.
СборкавGitbook:АнтонКармазин
ВыразительныйJavascript
ВыразительныйJavascript
3
Этокнигарассказывает,какзаставитькомпьютерыделатьто,чтовамотнихнужно.Компьютерысегоднятакжераспространены,какотвёртки–носодержатгораздобольшескрытыхсложностей,ипоэтомуихсложнеепонятьиснимисложнееработать.Длямногихониостаютсячуждыми,слегкаугрожающимиштуками.
Мыобнаружилидваэффективныхспособауменьшитькоммуникационныйразрывмеждунами–водянистымибиологическимиорганизмами,укоторыхестьталантксоциальнымсвязямипространнымрассуждениям,икомпьютерами–бесчувственнымиманипуляторами,работающимисбессмысленнымиданными.Первый–обратитьсякнашемуощущениюфизическогомира,истроитьинтерфейсы,имитирующиеего,чтобымымоглиприпомощипальцевманипулироватьформаминаэкране.Дляпростоговзаимодействияскомпьютеромэтонеплохоподходит.
Номыненашлихорошийспособпередаватькомпьютеруприпомощиперемещенийинажатиймышьютевещи,которыедизайнеринтерфейсанепредусмотрел.Длятого,чтобывзаимодействоватьскомпьютеромнаболеесложныхуровнях,напримерзадаватьемупроизвольныезадачинавыполнение,лучшеподходитнашталанткобщению:мыобучаемкомпьютерязыку.
Человеческиеязыкипозволяюткомбинироватьсловавеликиммножествомспособов,так,чтомыможемсказатьоченьмногоразныхвещей.Компьютерныеязыкиустроеныпримернотакже,хотяименеегибкиграмматически.
Введение
ВыразительныйJavascript
4
Запоследние20летработаскомпьютеромсталаоченьраспространённымявлением,иинтерфейсы,построенныенаязыке(акогда-тоэтобылединственныйспособобщенияскомпьютером)почтивытесненыграфическими.Ноонивсёещёесть–есливызнаете,гдеихискать.Одинизтакихязыков,JavaScript,встроенпочтивлюбойвеб-браузер,ипотомудоступенпочтинакаждомвычислительномустройстве.
Этакнигаставитцельюпознакомитьвассэтимязыкомдостаточнодлятого,чтобывымоглизаставитькомпьютерделатьто,чтовамнужно.
ВыразительныйJavascript
5
Янепросвещаютех,ктонежаждетучиться,инепобуждаютех,ктонехочетискатьответысамостоятельно.Еслияпокажуодинуголквадрата,ионинеприходяткомнесостальнымитремя–мнененужнодаватьповторныхобъяснений.
Конфуций
КромеобъясненияJavaScriptятакжехочуобъяснитьосновныепринципыпрограммирования.Каквыясняется,программироватьтяжело.Обычнобазовыепринципыпростыипонятны.Нопрограммы,построенныенаэтихпринципах,становятсясложныминастолько,чтовводятсвоисобственныеправилаиуровнисложности.Выстроитесвойсобственныйлабиринт,иможетевнёмпотеряться.
Возможно,временамичтениебудетразочаровыватьвас.Есливыновичоквпрограммировании,вамнужнобудетмногочегопереварить.Многоматериалабудетскомбинированотакимобразом,чтовамнужнобудетустановитьновыесвязимеждуегочастями.
Высамидолжныобосноватьнеобходимостьэтихусилий.Есливамтяжелопродиратьсячерезкнигу,ненужнодуматьосебеплохо.Свамивсёвпорядке–вамнужнопростопродолжатьдвижение.Сделайтеперерыв,вернитесьназад–ивсегдаудостоверяйтесь,чтовыпрочлиипонялипримерыпрограмм.Обучение–этосложнаяработа,норазвычто-товыучили,оноужепринадлежитвам,иоблегчаетдальнейшиешаги.
Программистсоздаётвселенные,закоторыеонодинвответе.Вкомпьютерныхпрограммахмогутбытьсозданывселенныепрактическинеограниченнойсложности.
ДжозефВайзенбаум,«СилакомпьютеровиРазумлюдей»
Программа–сложноепонятие.Этокусоктекста,набранныйпрограммистом,этонаправляющаясила,заставляющаякомпьютерчто-тоделать,этоданныевпамятикомпьютера,иприэтомонаконтролируетработусэтойже
Опрограммировании
ВыразительныйJavascript
6Опрограммировании
самойпамятью.Аналогии,которыепытаютсясравниватьпрограммысознакомыминамобъектамиобычнонесправляютсясэтим.Однаболее-менееподходящая–аналогиясмашиной.Множествоотдельныхчастейсоставляютодноцелое,ичтобызаставитьеёработать,намнужнопредставлятьсебеспособы,которымиэтичастивзаимодействуютичтоонипривносятвработуцелоймашины.
Компьютер–этомашина,котораяустроенатак,чтобысодержатьвсебеэтинематериальныемашинки.Компьютерысамипосебемогутвыполнятьтолькопростыедействия.Пользаихвтом,чтоонимогутделатьэтооченьбыстро.Программаможеточеньхитрымобразомкомбинироватьэтидействиятак,чтобыврезультатевыполнялисьоченьсложныедействия.
Длянекоторыхизнаспрограммирование–этоувлекательнаяигра.Программа–этомысленнаяконструкция.Ничегонестоитеёпостроить,онаничегоневесит,ионалегковырастаетподнашимипальцами.
Еслинебытьосторожным,размерисложностьвыходятиз-подконтроля,запутываядажетого,ктоеёпишет.Этоосновнаяпроблемапрограммирования:сохранятьконтрольнадпрограммами.Когдапрограммаработает–этопрекрасно.Искусствопрограммирования–этоумениеконтролироватьсложность.Большаяпрограмманаходитсяподконтролем,ивыполненапростовсвоейсложности.
Многиепрограммистыверят,чтоэтойсложностьюлучшевсегоуправлять,используявпрограммахнебольшойнаборхорошоизвестныхтехник.Ониописалистрогиеправила(«наилучшиепрактики»)того,какуюформупрограммыдолжныиметь.Исамыеревностныесрединихсчитаюттех,ктоотклоняетсяотэтихпрактик,плохимипрограммистами.
Чтозавраждебностьпоотношениюкбогатствупрограммирования–попыткипринизитьегодочего-топрямолинейногоипредсказуемого,наложитьтабунавсякиестранныеипрекрасныепрограммы!Ландшафттехникпрограммированияогромен,увлекателенсвоимразнообразием,идосихпоризученмало.Этоопасноепутешествие,заманивающееизапутывающеенеопытногопрограммиста,ноэтовсеголишьозначает,чтовыдолжныследоватьэтимпутёмосторожноидуматьголовой.Помереобучениявамвсегдабудутвстречатьсяновыезадачииновыенеизведанныетерритории.
ВыразительныйJavascript
7Опрограммировании
Программисты,неизучающиеновое,стагнируют,забываютсвоюрадость,ихработанаскучиваетим.
Вначале,призарождениикомпьютерныхдисциплин,небылоязыковпрограммирования.Программывыгляделитак:
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Опрограммировании
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Опрограммировании
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Опрограммировании
веб-страницывбраузереNetscapeNavigator.Стехпорязыкприжилсявовсехосновныхграфическихбраузерах.Ондалвозможностьпоявитьсясовременнымвеб-приложениям–браузерныее-мейл-клиенты,карты,социальныесети.Аещёониспользуетсянаболеетрадиционныхсайтахдляобеспеченияинтерактивностиивсякихнаворотов.
Важноотметить,чтоJavaScriptпрактическинеимеетотношениякдругомуязыкуподназваниемJava.Похожееимябыловыбраноизмаркетинговыхсоображений.КогдапоявилсяJavaScript,языкJavaширокорекламировалсяинабиралпопулярность.Кое-кторешил,чтонеплохобыприцепитьсякэтомупаровозу.Атеперьмыуженикуданеденемсяотэтогоимени.
Послетого,какязыквышелзапределыNetscape,былсоставлендокумент,описывающийработуязыка,чтобыразныепрограммы,заявляющиеоегоподдержке,работалиодинаково.ОнназываетсястандартECMAScriptпоимениорганизацииECMA.НапрактикеможноговоритьоECMAScriptиJavaScriptкакободномитомже.
МногиеругаютJavaScriptиговорятонёммногоплохого.Имногоеизэтого–правда.КогдамнепервыйразпришлосьписатьпрограммунаJavaScript,ябыстропочувствовалотвращение–языкпринималпрактическивсё,чтояписал,приэтоминтерпретировалэтововсенетак,какяподразумевал.Восновномэтобылоиз-затого,чтоянеимелпонятияотом,чтоделаю,нотутестьипроблема:JavaScriptслишкомлиберален.Задумывалосьэтокакоблегчениепрограммированиядляначинающих.Вреальности,этозатрудняетрозыскпроблемвпрограмме,потомучтосистемаонихнесообщает.
Гибкостьимеетсвоипреимущества.Онаоставляетместодляразныхтехник,невозможныхвболеестрогихязыках.Иногда,какмыувидимвглаве«модули»,еёможноиспользоватьдляпреодолениянекоторыхнедостатковязыка.Послетого,какяпонастоящемуизучилипоработалсним,янаучилсялюбитьJavaScript.
ВышлоуженескольковерсийязыкаJavaScript.ECMAScript3быладоминирующей,распространённойверсиейвовремяподъёмаязыка,примернос2000до2010.Вэтовремяготовиласьамбициозная4-яверсия,вкоторойбылозапланированонесколькорадикальныхулучшенийи
ВыразительныйJavascript
11Опрограммировании
расширенийязыка.Однакополитическиепричинысделалиизменениеживогопопулярногоязыкаоченьсложным,иработанад4-йверсиейбылапрекращенав2008.Вместонеёвышламенееамбициозная5-яверсияв2009.Сейчасбольшинствобраузеровподдерживает5-юверсию,которуюмыибудемиспользоватьвкниге.
JavaScriptподдерживаютнетолькобраузеры.БазыданныхтипаMongoDBandCouchDBиспользуютеговкачествескриптовогоязыкаиязыказапросов.Естьнесколькоплатформдлядекстоповисерверов,наиболееизвестнаяизкоторыхNode.js,предоставляютмощноеокружениедляпрограммированиявнебраузера.
Код–этотекст,изкоторогосостоятпрограммы.Вбольшинствеглавкнигиестькод.Чтениеинаписаниекода–этонеотъемлемаячастьобученияпрограммированию.Старайтесьнепростопробегатьглазамипримеры–читайтеихвнимательноиразбирайтесь.Сначалаэтобудетпроисходитьмедленноинепонятно,новыбыстроовладеетенавыками.Тоже–насчётупражнений.Неподразумевайте,чтовывнихразобрались,поканенапишетеработающийвариант.
Рекомендуюпробоватьваширешениявнастоящеминтерпретатореязыка,чтобысразуполучатьотзыв,и,надеюсь,подвергатьсяискушениюпоэкспериментироватьдалее.
ВыможетеустановитьNode.jsивыполнятьпрограммысегопомощью.Такжевыможетеделатьэтовконсолибраузера.В12главебудетобъяснено,каквстраиватьпрограммывHTML-страницы.Такжеестьсайтытипаjsbin.com,позволяющиепростозапускатьпрограммывбраузере.Насайтекнигиестьпесочницадлякода.
Код,ичтоснимделать
ВыразительныйJavascript
12Опрограммировании
Подповерхностьюмашиныдвижетсяпрограмма.Безусилий,онарасширяетсяисжимается.Находясьввеликойгармонии,электронырассеиваютсяисобираются.Формынамониторе–лишьрябьнаводе.Сутьостаётсяскрытойвнутри…
МастерЮан-Ма,Книгапрограммирования
Вкомпьютерноммиреестьтолькоданные.Можночитатьданные,изменятьданные,создаватьновые–нокромеданныхничегонет.Вседанныехранятсякакдлинныепоследовательностибит,этимонисходнымеждусобой.
Биты–этосущностисдвумясостояниями,обычноописываемыекакнулииединицы.Вкомпьютереониживутввидевысокихинизкихэлектрическихзарядов,сильногоилислабогосигнала,илиблестящегоиматовогоучастканаповерхностиCD.Каждаячастьинформацииможетбытьпредставленаввидепоследовательностинулейиединиц,тоестьбит.
Кпримеру,номер13.Вместодесятичнойсистемы,состоящейиз10цифр,увасестьдвоичнаясистемасдвумяцифрами.Значениекаждойпозициичислаудваиваетсяпридвижениисправаналево.Биты,составляющиечисло13,вместесихвесами:
00001101
1286432168421
Получаетсядвоичноечисло00001101,или8+4+1,чторавно13.
Представьтеокеанбит.Типичныйсовременныйкомпьютерхранитболее30миллиардовбитвоперативнойпамяти.Постояннаяпамять(жёсткийдиск)обычноещёнапарупорядковобъёмнее.
Величины,типыиоператоры
Величины.
ВыразительныйJavascript
13Величины,типыиоператоры
Чтобыработатьснимиинезаблудиться,выможетеделитьихнакуски,представляющиеединицыинформации.ВJavaScriptэтиединицыназываютсявеличинами.Всеихзначениясостоятизбит,ноиграютразныероли.Укаждойвеличиныестьтип,определяющийегороль.Всегоестьшестьосновныхтипов:числа,строки,булевыевеличины,объекты,функцииинеопределённыевеличины.
Длясозданиявеличинывамнужноуказатьеёимя.Этоудобно.Вамненадособиратьстройматериалыилиплатитьзаних.Нужнопростопозвать–иоп-па,готово.Онинесоздаютсяизвоздуха–каждаявеличинагде-тохранится,иесливыхотитеиспользоватьогромноеихколичество,увасмогутзакончитьсябиты.Ксчастью,этотолькоеслионивсенужнывамодновременно.Когдавеличинавамстанетненужна,онарастворяется,ииспользованныееюбитыпоступаютвпереработкукакстройматериалдляновыхвеличин.
ВэтойглавемызнакомимсясатомамипрограммJavaScript–простыетипывеличиниоператоры,которыекнимприменимы.
Величинычисловыхтипов,это–сюрприз–числа.ВпрограммеJavaScriptонизаписываютсякак
13
Используйтеэтузаписьвпрограмме,ионавызоветкжизнивкомпьютерной
Числа
ВыразительныйJavascript
14Величины,типыиоператоры
памятицепочкубит,представляющуючисло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Величины,типыиоператоры
конечногочислацифр,такимногиедробинельзяпредставитьвслучае,когдаунасестьтолько64бита.Плохо,ноэтомешаетвоченьспецифическихслучаях.Важнопомнитьобэтомиотноситьсякдробямкаккприближённымзначениям.
Главное,чтоможноделатьсчислами–этоарифметическиевычисления.Сложенияиумноженияиспользуютдвачислаивыдаюттретье.КакэтозаписываетсявJavaScript:
100+4*11
Символы+и*называютсяоператорами.Первый–сложение,второй–умножение.Помещаемоператормеждудвумявеличинамииполучаемзначениевыражения.
Авпримереполучается«сложить4и100изатемумножитьрезультатна11»илиумножениевыполняетсясначала?Каквымоглидогадаться,умножениевыполняетсяпервым.Нокакивматематике,этоможноизменитьприпомощискобок:
(100+4)*11
Длявычитанияиспользуетсяоператор-,адляделения-/
Когдаоператорыиспользуютсябезскобок,порядокихвыполненияопределяетсяихприоритетом.Уоператоров*и/приоритетодинаковый,выше,чему+и-,которыемеждусобойравныпоприоритету.Привычисленииоператоровсравнымприоритетомонивычисляютсяслеванаправо:
1-2+1
вычисляетсякак(1-2)+1
Покабеспокоитьсяоприоритетахненадо.Еслисомневаетесь–используйте
Арифметика
ВыразительныйJavascript
16Величины,типыиоператоры
скобки.
Естьещёодиноператор,которыйвынесразуузнаете.Символ%используетсядляполученияостатка.X%Y–остатокотделенияXнаY.314%100даёт14,и144%12даёт0.Приоритетуоператоратакойже,какуумноженияиделения.Егоещёчастоназывают«делениепомодулю»,хотяболееправильно«состатком».
ВJavaScriptестьтриспециальныхзначения,которыесчитаютсячислами,новедутсебянекакобычныечисла.
ЭтоInfinityи-Infinity,которыепредставляютположительнуюиотрицательнуюбесконечности.Infinity-1=Infinity,итакдалее.Ненадейтесьсильнонавычислениясбесконечностями,онинеслишкомстрогие.
Третьечисло:NaN.Обозначает«notanumber»(нечисло),хотяэтовеличиначисловоготипа.Выможетеполучитьеёпослевычисленийтипа0/0,Infinity–Infinity,илидругихопераций,которыеневедуткточнымосмысленнымрезультатам.
Следующийбазовыйтипданных–строки.Онииспользуютсядляхранениятекста.Записываютсяонивкавычках:
"Чтопосеешь,тоизпруда"
'Бабасвозу,потехечас'
Можноиспользоватькакдвойные,такиодинарныекавычки–главноеиспользоватьихвместе.Почтивсёможнозаключитьвкавычкиисделатьизэтогостроку.Нонекоторыесимволывызываютпроблемы.Например,сложнозаключитькавычкивкавычки.Переводстрокитоженельзяпростотакзаключитьвних–строкадолжнаидтиоднойстрокой.
Длязаключенияспециальныхсимволовиспользуетсяобратныйслеш.Он
Специальныечисла
Строки
ВыразительныйJavascript
17Величины,типыиоператоры
обозначает,чтосимвол,идущийзаним,имеетспециальноезначение–этоназывается«экранированиесимволов»(escapecharacter).\”можнозаключатьвдвойныекавычки.\nобозначаетпереводстроки,\t–табуляцию.
Строка“Междупервойивторой\nсимволбудетнебольшой”насамомделебудетвыглядетьтак:
Междупервойивторой
символбудетнебольшой
Есливамнужновключитьвстрокуобратныйслеш,еготоженужноэкранировать:\.Инструкцию“Символновойстроки—это“\n””нужнобудетнаписатьтак:
"Символновойстроки–это\"\\n\""
Строкинельзяделить,умножатьискладывать.Однакоснимиможноиспользоватьоператор+,которыйбудетсоединятьихдругсдругом.Следующеевыражениевыдастслово«соединение»:
"сое"+"ди"+"н"+"ение"
Естьмногоспособовманипуляцийсостроками,которыемыобсудимвглаве4.
Невсеоператорызаписываютсясимволами–некоторыесловами.Одинизтакихоператоров–typeof,которыйвыдаётназваниетипавеличины,ккоторойонприменяется.
console.log(typeof4.5)
//→number
console.log(typeof"x")
//→string
Унарныеоператоры
ВыразительныйJavascript
18Величины,типыиоператоры
Будемиспользоватьвызов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Величины,типыиоператоры
Строкисравниваютсяпоалфавиту:буквывверхнемрегистревсегда«меньше»букввнижнемрегистре.Сравнениеоснованонастандарте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Величины,типыиоператоры
«Нет»записываетсяприпомощивосклицательногознака“!”.Этоунарныйоператор,которыйобращаетданнуювеличинунаобратную.!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Величины,типыиоператоры
Ранееяупоминал,что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Величины,типыиоператоры
Последнийпримердемонстрируетполезныйприём.Когдавамнадопроверить,имеетливеличинареальноезначениевместо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Величины,типыиоператоры
случай.
Оператор&&работаетсходнымобразом,нонаоборот.Есливеличинаслевапреобразовываетсявfalse,онвозвращаетэтувеличину,аиначе–величинусправа.
Ещёодноважноеихсвойство–выражениевправойчастивычисляетсятолькопринеобходимости.Вслучаеtrue||Xневажно,чемуравноX.Дажееслиэтокакое-тоужасноевыражение.РезультатвсегдаtrueиXневычисляется.Такжеработаетfalse&&X–Xпростоигнорируется.Этоназываетсякороткимвычислением.
Операторусловияработаеттакже.Первоевыражениевсегдавычисляется,аизвторогоитретьегозначения–толькото,котороеоказываетсявыбраннымврезультате.
МырассмотреличетыретипавеличинJavaScript:числа,строки,булевскиеинеопределённые.
Этивеличиныполучаются,когдамыпишемихимена(true,null)илизначения(13,“ёпрст”).Ихможнокомбинироватьиизменятьприпомощиоператоров.Дляарифметикиестьбинарныеоператоры(+,-,*,/,and%),объединениестрок(+),сравнение(==,!=,===,!==,<,>,<=,>=),илогическиеоператоры(&&,||),также,какинесколькоунарныхоператоров(-дляотрицательногозначения,!длялогическогоотрицания,иtypeofдляопределениятипавеличины).
ЭтизнанияпозволяютиспользоватьJavaScriptвкачествекалькулятора,ноитолько.Вследующейглавемыбудемсвязыватьэтипростыезначениявместе,чтобысоставлятьпростыепрограммы.
Итог
ВыразительныйJavascript
24Величины,типыиоператоры
Сердцемоёсияетярко-краснымсветомподмоейтонкой,прозрачнойкожей,иимприходитсявколотьмнедесятькубиковJavaScript,чтобывернутьменякжизни(яхорошореагируюнатоксинывкрови).Отэтойфигниувасвразжабрыпобледнеют!
_why,Why's(Poignant)GuidetoRuby
Вэтойглавемыначнёмзаниматьсятем,чтоужеможноназватьпрограммированием.МырасширимиспользованиеязыкаJavaScriptзапределысуществительныхифрагментовпредложенийкболее-менееосмысленнойпрозе.
Впервойглавемысоздаваливеличиныиприменяликнимоператоры,получаяновыевеличины.Этоважнаячастькаждойпрограммы,нотольколишьчасть.
Фрагменткода,результатомработыкоторогоявляетсянекаявеличина,называетсявыражением.Каждаявеличина,записаннаябуквально(например,22или“психоанализ”)тожеявляетсявыражением.Выражение,записанноевскобках,такжеявляетсявыражением,какибинарныйоператор,применяемыйкдвумвыражениямилиунарный–кодному.
Эточастькрасотыязыковогоинтерфейса.Выражениямогутвключатьдругиевыражениятакже,каксложноподчинённоепредложениесостоитизпростых.Этопозволяетнамкомбинироватьвыражениядлясозданиявычисленийлюбойсложности.
Есливыражение–этофрагментпредложения,тоинструкция–этопредложениеполностью.Программа–этопростосписокинструкций.
Простейшаяинструкция–этовыражениесточкойсзапятойпосленего.Это—программа:
Структурапрограмм
Выраженияиинструкции
ВыразительныйJavascript
25Структурапрограмм
1;
!false;
Правда,этобесполезнаяпрограмма.Выражениеможноиспользоватьтолькодляполучениявеличины,котораяможетбытьиспользованавдругомвыражении,охватывающемэто.Инструкциястоитсамапосебеиеёприменениеизменяетчто-товмирепрограммы.Онаможетвыводитьчто-тонаэкран(изменениевмире),илименятьвнутреннеесостояниемашинытакимобразом,чтоэтоповлияетнаследующиезаниминструкции.Этиизмененияназываютсяпобочнымиэффектами.Инструкциивпредыдущемпримерепростовыдаютвеличины1иtrue,исразуихвыбрасывают.Онинеоказываютникакоговлияниянамирпрограммы.Привыполнениипрограммыничегозаметногонепроисходит.
ВнекоторыхслучаяхJavaScriptпозволяетопускатьточкусзапятойвконцеинструкции.Вдругихслучаяхонаобязательно,илиследующаястрокабудетрасцениватьсякакчастьтойжеинструкции.Правила,согласнокоторымможноилинельзяопускатьточкусзапятой,довольносложныиувеличиваютвероятностьошибиться.Вэтойкнигемынебудемопускатьточкусзапятой,иярекомендуюделатьтакжевсвоихпрограммах,покавыненакопитеопыт.
Какжепрограммахранитвнутреннеесостояние?Каконаегозапоминает?Мыполучалиновыевеличиныизстарых,ностарыевеличиныэтонеменяло,ановыенужнобылоиспользоватьсразу,илижеониисчезали.Чтобызахватитьихранитьих,JavaScriptпредлагаетнечтоподназванием«переменная».
varcaught=5*5;
Иэтодаётнамвторойвидинструкций.Специальноеключевоеслово(keyword)varпоказывает,чтовэтойинструкциимыобъявляемпеременную.Занимидётимяпеременной,и,еслимысразухотимназначитьейзначение–оператор=ивыражение.
Переменные
ВыразительныйJavascript
26Структурапрограмм
Примерсоздаётпеременнуюподименемcaughtииспользуетеёдлязахватачисла,котороеполучаетсяврезультатеперемножения5и5.
Послеопределенияпеременнойеёимяможноиспользоватьввыражениях.Величинапеременнойбудеттакой,какоезначениевнейсейчассодержится.Пример:
varten=10;
console.log(ten*ten);
//→100
Переменныеможноназыватьлюбымсловом,котороенеявляетсяключевым(типаvar).Нельзяиспользоватьпробелы.Цифрытожеможноиспользовать,нонепервымсимволомвназвании.Нельзяиспользоватьзнакипунктуации,кромесимволов$и_.
Переменнойприсваиваютзначениененавсегда.Оператор=можноиспользоватьнасуществующихпеременныхвлюбоевремя,чтобыприсвоитьимновоезначение.
varmood="лёгкое";
console.log(mood);
//→лёгкое
mood="тяжёлое";
console.log(mood);
//→тяжёлое
Представляйтесебепеременныеневвидекоробочек,аввидещупалец.Онинесодержатзначения–онихватаютих.Двепеременныемогутссылатьсянаоднозначение.Программаимеетдоступтолькокзначениям,которыеонисодержат.Когдавамнужночто-тозапомнить,выотращиваетещупальцеидержитесьзаэто,иливыиспользуетесуществующеещупальце,чтобыудержатьэто.
ВыразительныйJavascript
27Структурапрограмм
Пример.Длязапоминанияколичестваденег,которыевамдолженВасилий,высоздаётепеременную.Затем,когдаонвыплачиваетчастьдолга,выдаётеейновоезначение.
varvasyaDebt=140;
vasyaDebt=vasyaDebt-35;
console.log(vasyaDebt);
//→105
Когдавыопределяетепеременнуюбезприсваиванияейзначения,щупальцунезачтодержаться,оновиситввоздухе.Есливызапроситезначениепустойпеременной,выполучитеundefined.
Однаинструкцияvarможетсодержатьнесколькопеременных.Определениянужноразделятьзапятыми.
varone=1,two=2;
console.log(one+two);
//→3
Словасоспециальнымсмыслом,типаvar–ключевые.Ихнельзя
Переменныекакщупальца
Ключевыеизарезервированныеслова
ВыразительныйJavascript
28Структурапрограмм
использоватькакименапеременных.Такжеестьнесколькослов,«зарезервированныхдляиспользования»вбудушихверсияхJavaScript.Ихтоженельзяиспользовать,хотявнекоторыхсредахисполненияэтовозможно.Полныйихсписокдостаточнобольшой.
breakcasecatchcontinuedebuggerdefaultdeletedoelsefalsefinally
forfunctionifimplementsininstanceofinterfaceletnewnullpackage
privateprotectedpublicreturnstaticswitchthrowtruetrytypeofvar
voidwhilewithyieldthis
Ненужноихзапоминать,ноимейтеввиду,чтоошибкаможеткрытьсяздесь,есливашиопределенияпеременныхнеработают,какнадо.
Коллекцияпеременныхиихзначений,котораясуществуетвопределённыймомент,называетсяокружением.Когдапрограммазапускается,окружениенепустое.Тамвсегдаестьпеременные,являющиесячастьюпрограммногостандарта,ибольшуючастьвременитаместьпеременные,помогающиевзаимодействоватьсокружающейсистемой.Кпримеру,вбраузереестьпеременныеифункциидляизучениясостояниязагруженнойвеб-страницыивлияниянанеё,длячтениявводасмышииклавиатуры.
Многиевеличиныизстандартногоокруженияимеюттипfunction(функция).Функция–отдельныйкусочекпрограммы,которыйможноиспользоватьвместесдругимивеличинами.Кпримеру,вбраузерепеременнаяalertсодержитфункцию,котораяпоказываетнебольшоеокноссообщением.Используютеготак:
alert("Сдобрымутром!");
Окружение
Функции
ВыразительныйJavascript
29Структурапрограмм
Выполнениефункцииназываютвызовом.Выможетевызватьфункцию,записавскобкипослевыражения,котороевозвращаетзначениефункции.Обычновынапрямуюиспользуетеимяфункциивкачествевыражения.Величины,которыеможнонаписатьвнутрискобок,передаютсяпрограммномукодувнутрифункции.Впримере,функция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Структурапрограмм
Показдиалоговогоокнаиливыводтекстанаэкран–этопобочныйэффект.Множествофункцийполезныоттого,чтоонипроизводятэтиэффекты.Функциитакжемогутпроизводитьзначения,ивэтомслучаеимненуженпобочныйэффектдлятого,чтобыбытьполезной.Кпримеру,функция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Структурапрограмм
Функциюpromptможноиспользовать,чтобызадатьоткрытыйвопрос.Первыйаргумент–вопрос,второй–текст,скоторогопользовательначинает.Вдиалоговоеокноможновписатьстрокутекста,ифункциявернётеговвидестроки.
prompt("Расскажимневсё,чтознаешь.","...");
Этифункциинечастоиспользуют,потомучтонельзяизменятьвнешнийвидэтихокон—ноонимогутпригодитьсядляэкспериментальныхпрограмм.
Когдавпрограммебольшеоднойинструкции,онивыполняютсясверхувниз.Вэтомпримереупрограммыдвеинструкции.Перваяспрашиваетчисло,вторая,выполняемаяследом,показываетегоквадрат.
vartheNumber=Number(prompt("Выберичисло",""));
alert("Твоёчисло–квадратныйкореньиз"+theNumber*theNumber);
ФункцияNumberпреобразовываетвеличинувчисло.Намэтонужно,потомучтоpromptвозвращаетстроку.ЕстьсходныефункцииStringиBoolean,преобразующиевеличинывсоответствующиетипы.
Простаясхемапрямогопорядкаисполненияпрограммы:
Условноевыполнение
Выполнятьинструкциипопорядку–неединственнаявозможность.Вкачествеальтернативысуществуетусловноевыполнение,гдемывыбираем
Управлениепорядкомвыполненияпрограммы
ВыразительныйJavascript
32Структурапрограмм
издвухвозможныхпутей,основываясьнабулевскойвеличине:
Условноевыполнениезаписываетсяприпомощиключевогослова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Структурапрограмм
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Структурапрограмм
Этотвидконтролянадпорядкомвыполненияназываетсяциклом.
Зацикливаниедаётвозможностьвернутьсяназадккакой-тоинструкциииповторитьвсёзановосновымсостояниемпрограммы.Еслискомбинироватьэтоспеременнойдляподсчёта,можносделатьследующее:
varnumber=0;
while(number<=12){
console.log(number);
number=number+2;
}
//→0
//→2
//…ит.д.
Инструкция,начинающаясясключевогословаwhile–этоцикл.Заwhileследуетвыражениевскобках,изатеминструкция(телоцикла)–также,какуif.Циклвыполняетинструкцию,покавыражениевыдаётистинныйрезультат.
Вцикленамнужновыводитьзначениеиприбавлятькнему.Еслинамнужновыполнятьвцикленесколькоинструкций,мызаключаемеговфигурныескобки{}.Фигурныескобкидляинструкций–каккруглыескобкидлявыражений.Онигруппируютихипревращаютвединое.Последовательностьинструкций,заключённаявфигурныескобки,называетсяблоком.
Многопрограммистовзаключаютлюбоетелоциклавскобки.Ониделаютэтодляединообразия,идлятого,чтобыненужнобылодобавлятьиубиратьскобки,еслиприходитсяизменятьколичествоинструкцийвцикле.Вкнигеянебудуписатьскобкивокругединичныхинструкцийвцикле,таккаклюблюкраткость.Выможетеделать,какугодно.
Переменнаяnumberпоказывает,какпеременнаяможетотслеживатьпрогресспрограммы.Прикаждомповторениициклаnumberувеличиваетсяна2.Передкаждымповторениемоносравниваетсяс12,чтобыпонять,сделалалипрограммавсё,чтотребовалось.
ВыразительныйJavascript
35Структурапрограмм
Дляпримераболееполезнойработымыможемнаписатьпрограммувычисления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Структурапрограмм
люблюотделятькаждыйблокдвумяпробелами,новкусыразличаются–некоторыеиспользуютчетыре,некоторые–табуляцию.Чембольшепробеловиспользовать,темзаметнееотступ,нотембыстреевложенныеблокиубегаютзаправыйкрайэкрана.
Многоцикловстроятсяпотакомушаблону,каквпримере.Создаётсяпеременная-счётчик,потомидётцикл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Структурапрограмм
Дождаться,покаусловиецикланестанетложным–неединственныйспособзакончитьцикл.Специальнаяинструкцияbreakприводиткнемедленномувыходуизцикла.
Вследующемпримеремыпокидаемцикл,когданаходимчисло,большее20иделящеесяна7безостатка.
for(varcurrent=20;;current++){
if(current%7==0)
break;
}
console.log(current);
//→21
Конструкцияforнеимеетпроверочнойчасти–поэтомуциклнеостановится,поканесработаетинструкцияbreak.
Есливынеукажетеэтуинструкцию,илислучайнонапишетеусловие,котороевсегдавыполняется,программазависнетвбесконечномциклеиникогданезакончитработу–обычноэтоплохо.
Есливысделаетебесконечныйцикл,обычночерезнесколькосекундсредаисполненияпредложитвампрерватьего.Еслинет,вампридётсязакрытьзакладку,илидажевесьбраузер.
Ключевоесловоcontinueтакжевлияетнаисполнениецикла.Когдаэтослововстречаетсявцикле,оннемедленнопереходитнаследующуюитерацию.
Особенночастовциклахпрограмменужнообновитьпеременную,основываясьнаеёпредыдущемсостоянии.
counter=counter+1;
Выходизцикла
Короткоеобновлениепеременных
ВыразительныйJavascript
38Структурапрограмм
В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Структурапрограмм
console.log("Идигуляй.");
break;
default:
console.log("Непонятнаяпогода!");
break;
}
Вблокswitchможнопоместитьлюбоеколичествометокcase.Программаперепрыгиваетнаметку,соответствующуюзначениюпеременнойвswitch,илинаметкуdefault,еслиподходящихметокненайдено.Послеэтогоинструкцииисполняютсядопервойинструкцииbreak–дажееслимыужепрошлидругуюметку.Иногдаэтоможноиспользоватьдляисполненияодногоитогожекодавразныхслучаях(вобоихслучаях«солнечно»и«облачно»программапорекомендуетпойтипогулять).Однако,оченьлегкозабытьзаписьbreak,чтоприведётквыполнениюнежелательногоучасткакода.
Именапеременныхнемогутсодержатьпробелы,однакочастоудобноиспользоватьнесколькословдляпонятногоописанияпеременной.Выможетевыбиратьизнесколькихвариантов:
fuzzylittleturtle
fuzzy_little_turtle
FuzzyLittleTurtle
fuzzyLittleTurtle
Первыйдовольносложночитать.Мненравятсяподчёркивания,хотяихнеоченьудобнопечатать.СтандартныефункцииJavaScriptибольшинствопрограммистовиспользуютпоследнийвариант–каждоесловосбольшойбуквы,кромепервого.
Внекоторыхслучаях,напримервслучаефункцииNumber,первуюбуквутожепишутбольшой–когданужновыделитьфункциюкакконструктор.Оконструкторахмыпоговоримвглаве6.Сейчаспростонеобращайтенаэтовнимания.
Регистримён
ВыразительныйJavascript
40Структурапрограмм
Частокоднесодержитвсюинформацию,которуюхотелосьбыпередатьчитателям-людям,илидоноситеёвнепонятномвиде.Иногдавычувствуетепоэтическоевдохновение,илипростохотитеподелитьсямыслямивсвоейпрограмме.Дляэтогослужаткомментарии.
Комментарий–этотекст,которыйзаписанвпрограмме,ноигнорируетсякомпьютером.ВJavaScriptкомментарииможнописатьдвумяспособами.Дляоднострочногокомментарияможноиспользоватьдваслеша:
varaccountBalance=calculateBalance(account);
//Издалекадолго
accountBalance.adjust();
//ТечётрекаВолга
varreport=newReport();
//ТечётрекаВолга
addToReport(accountBalance,report);
//Концаикраянет
Комментарийпродолжаетсятолькодоконцастроки.Кодмеждусимволами/и/будетигнорироватьсявместесвозможнымипереводамистроки.Этоподходитдлявключенияцелыхинформационныхблоковвпрограмму:
/*
Этотгород–самыйлучший
ГороднаЗемле.
Онкакбудтонарисован
Меломнастене.
*/
varmyCity=‘Челябинск’;
Теперьвызнаете,чтопрограммасостоитизинструкций,которыесамимогутсодержатьинструкции.Винструкцияхсодержатсявыражения,которыемогутсостоятьизвыражений.
Записываяинструкцииподряд,мыполучаемпрограмму,которая
Комментарии
Итог
ВыразительныйJavascript
41Структурапрограмм
выполняетсясверхувниз.Выможетеизменятьэтотпотоквыполнения,используяусловные(if,else,иswitch)операторыиоператорыцикла(while,do,иfor).
Переменныеможноиспользоватьдляхранениякусочковданныхподопределённымназваниемидляотслеживаниясостоянияпрограммы.Окружение–наборопределённыхпеременных.Системы,исполняющиеJavaScript,всегдадобавляютнесколькостандартныхпеременныхввашеокружение.
Функции–особыепеременные,включающиечастипрограммы.ИхможновызватькомандойfunctionName(argument1,argument2).Такойвызов–этовыражение,иможетвыдаватьзначение.
Каждоеупражнениеначинаетсясописаниязадачи.Прочтитеипостарайтесьвыполнить.Всложныхситуацияхобращайтеськподсказкам.Готовыерешениязадачможнонайтинасайтекнигиeloquentjavascript.net/code/.Чтобыобучениебылоэффективным,незаглядывайтевответы,поканерешитезадачусами,илихотябынепопытаетесьеёрешитьдостаточнодолгодлятого,чтобыувасслегказаболелаголова.Тамжеможнописатькодпрямовбраузереивыполнятьего.
Напишитецикл,которыйза7вызововconsole.logвыводиттакойтреугольник:
#
##
###
####
#####
######
#######
Будетполезнознать,чтодлинустрокиможноузнать,приписавкпеременной.length.
Упражнения
Треугольниквцикле
ВыразительныйJavascript
42Структурапрограмм
varabc="abc";
console.log(abc.length);
//→3
Напишитепрограмму,котораявыводитчерезconsole.logвсецифрыот1до100,сдвумяисключениями.Длячисел,нацелоделящихсяна3,онадолжнавыводить‘Fizz’,адлячисел,делящихсяна5(нонена3)–‘Buzz’.
Когдасумеете–исправьтееётак,чтобыонавыводила«FizzBuzz»длявсехчисел,которыеделятсяина3ина5.
(Насамомделе,этотвопросподходитдлясобеседований,иговорят,онпозволяетотсеиватьдовольнобольшоечислокандидатов.Поэтому,когдавырешитеэтузадачу,можетесебяпохвалить)
Напишитепрограмму,создающуюстроку,содержащуюрешётку8х8,вкоторойлинииразделяютсясимволаминовойстроки.Накаждойпозициилибопробел,либо#.Врезультатедолжнаполучитьсяшахматнаядоска.
####
####
####
####
####
####
####
####
Когдасправитесь,сделайтеразмердоскипеременным,чтобыможнобылосоздаватьдоскилюбогоразмера.
FizzBuzz
Шахматнаядоска
ВыразительныйJavascript
43Структурапрограмм
Людисчитают,чтокомпьютерныенауки–этоискусстводлягениев.Вреальностивсёнаоборот–простомножестволюдейделаютвещи,которыестоятдругнадруге,будтосоставляястенуизмаленькихкамушков.
ДональдКнут
Выужевиделивызовыфункций,такихкакalert.Функции–этохлебсмасломпрограммированиянаJavaScript.Идеяоборачиваниякускапрограммыивызоваеёкакпеременнойоченьвостребована.Этоинструментдляструктурированиябольшихпрограмм,уменьшенияповторений,назначенияимёнподпрограммам,иизолированиеподпрограммдруготдруга.
Самоеочевидноеиспользованиефункций–созданиеновогословаря.Придумыватьсловадляобычнойчеловеческойпрозы–дурнойтон.Вязыкепрограммированияэтонеобходимо.
Среднийвзрослыйрусскоговорящийчеловекзнаетпримерно10000слов.Редкийязыкпрограммированиясодержит10000встроенныхкоманд.Исловарьязыкапрограммированияопределёнчётче,поэтомуонменеегибок,чемчеловеческий.Поэтомунамобычноприходитсядобавлятьвнегосвоислова,чтобыизбежатьизлишнихповторений.
Определениефункции–обычноеопределениепеременной,гдезначение,котороеполучаетпеременная,являетсяфункцией.Например,следующийкодопределяетпеременнуюsquare,котораяссылаетсянафункцию,подсчитывающуюквадратзаданногочисла:
varsquare=function(x){
returnx*x;
};
Функции
Определениефункции
ВыразительныйJavascript
44Функции
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Функции
задаютсяпривызовефункции,аневеёкоде.
Важноесвойствофункцийвтом,чтопеременные,созданныевнутрифункции(включаяпараметры),локальнывнутриэтойфункции.Этоозначает,чтовпримерес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Функции
переменнуюповторно,выбыстолкнулисьсостраннымиэффектами,когдастороннийкодпортитзначениявашейпеременной.Относяськлокальнымдляфункцийпеременнымтак,чтоонисуществуюттольковнутрифункции,языкделаетвозможнымработусфункциямибудтосотдельнымималенькимивселенными,чтопозволяетневолноватьсяпровеськодцеликом.
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Функции
переменныеcountдругдруга,потомучтопеременныеоднойфункциинаходятсявнеобластивидимостидругой.Аокружениеснаружифункцииlandscapeневидитниоднойизпеременных,определённыхвнутриэтойфункции.
Корочеговоря,вкаждойлокальнойобластивидимостиможноувидетьвсеобласти,которыееёсодержат.Наборпеременных,доступныхвнутрифункции,определяетсяместом,гдеэтафункцияописанавпрограмме.Всепеременныеизблоков,окружающихопределениефункции,видны–включаяите,чтоопределенынаверхнемуровневосновнойпрограмме.Этотподходкобластямвидимостиназываетсялексическим.
Люди,изучавшиедругиеязыкипрограммирования,могутподумать,чтолюбойблок,заключённыйвфигурныескобки,создаётсвоёлокальноеокружение.НовJavaScriptобластьвидимостисоздаюттолькофункции.Выможетеиспользоватьотдельностоящиеблоки:
varsomething=1;
{
varsomething=2;
//Делаемчто-либоспеременнойsomething...
}
//Вышлиизблока...
Ноsomethingвнутриблока–этотажепеременная,чтоиснаружи.Хотятакиеблокииразрешены,имеетсмыслиспользоватьихтолькодлякомандыifициклов.
Еслиэтокажетсявамстранным–таккажетсянетольковам.ВверсииJavaScript1.7появилосьключевоесловоlet,котороеработаеткакvar,носоздаётпеременные,локальныедлялюбогоданногоблока,анетолькодляфункции.
Именафункцийобычноиспользуюткакимядлякусочкапрограммы.Такаяпеременнаяоднаждызадаётсяинеменяется.Такчтолегкоперепутатьфункциюиеёимя.
Функциикакзначения
ВыразительныйJavascript
48Функции
Ноэто–дверазныевещи.Вызовфункцииможноиспользовать,какпростуюпеременную–например,использоватьихвлюбыхвыражениях.Возможнохранитьвызовфункциивновойпеременной,передаватьеёкакпараметрдругойфункции,итакдалее.Такжепеременная,хранящаявызовфункции,остаётсяобычнойпеременнойиеёзначениеможнопоменять:
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Функции
области.Иногдаэтоудобно,потомучтовыможетеписатькодвтакомпорядке,которыйвыглядитнаиболееосмысленно,небеспокоясьпоповодунеобходимостиопределятьвсефункциивышетогоместа,гдеонииспользуются.
Ачтобудет,еслимыпоместимобъявлениефункциивнутрьусловногоблокаилицикла?Ненадотакделать.ИсторическиразныеплатформыдлязапускаJavaScriptобрабатывалитакиеслучаипоразному,атекущийстандартязыказапрещаеттакделать.Есливыхотите,чтобывашипрограммыработалипоследовательно,используйтеобъявленияфункцийтольковнутридругихфункцийилиосновнойпрограммы.
functionexample(){
functiona(){}//Нормуль
if(something){
functionb(){}//Ай-яй-яй!
}
}
Полезнымбудетприсмотретьсяктому,какпорядоквыполненияработаетсфункциями.Вотпростаяпрограммаснесколькимивызовамифункций:
functiongreet(who){
console.log("Привет,"+who);
}
greet("Семён");
console.log("Покеда");
Обрабатываетсяонапримернотак:вызовgreetзаставляетпроходпрыгнутьнаначалофункции.Онвызываетвстроеннуюфункциюconsole.log,котораяперехватываетконтроль,делаетсвоёделоивозвращаетконтроль.Потомондоходитдоконцаgreet,ивозвращаетсякместу,откудаеговызвали.Следующаястрочкаопятьвызываетconsole.log.
Схематичноэтоможнопоказатьтак:
top
Стеквызовов
ВыразительныйJavascript
50Функции
greet
console.log
greet
top
console.log
top
Посколькуфункциядолжнавернутьсянатоместо,откудаеёвызвали,компьютердолжензапомнитьконтекст,изкоторогобылавызванафункция.Водномслучае,console.logдолжнавернутьсяобратновgreet.Вдругом,онавозвращаетсявконецпрограммы.
Место,гдекомпьютерзапоминаетконтекст,называетсястеком.Каждыйразпривызовефункции,текущийконтекстпомещаетсянаверхстека.Когдафункциявозвращается,оназабираетверхнийконтекстизстекаииспользуетегодляпродолженияработы.
Хранениестекатребуетместавпамяти.Когдастекслишкомсильноразрастается,компьютерпрекращаетвыполнениеивыдаётчто-товроде“stackoverflow”или“toomuchrecursion”.Следующийкодэтодемонстрирует–онзадаёткомпьютеруоченьсложныйвопрос,которыйприводиткбесконечнымпрыжкаммеждудвумяфункциями.Точнее,этобылибыбесконечныепрыжки,еслибыукомпьютерабылбесконечныйстек.Вреальностистекпереполняется.
functionchicken(){
returnegg();
}
functionegg(){
returnchicken();
}
console.log(chicken()+"camefirst.");
//→??
Следующийкодвполнеразрешёнивыполняетсябезпроблем:
alert("Здрасьте","Добрыйвечер","Всемпривет!");
Необязательныеаргументы
ВыразительныйJavascript
51Функции
Официальнофункцияпринимаетодинаргумент.Однако,притакомвызовеонанежалуется.Онаигнорируетостальныеаргументыипоказывает«Здрасьте».
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Функции
Возможностьиспользоватьвызовыфункцийкакпеременныевкупестемфактом,чтолокальныепеременныекаждыйразпривызовефункциисоздаютсязаново,приводитнаскинтересномувопросу.Чтопроисходитслокальнымипеременными,когдафункцияперестаётработать?
Следующийпримериллюстрируетэтотвопрос.Внёмобъявляетсяфункция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Функции
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Функции
Примернотакматематикиопределяютвозведениевстепень,и,возможно,этоописываетконцепциюболееэлегантно,чемцикл.Функциявызываетсебямногоразсразнымиаргументамидлядостижениямногократногоумножения.
Однако,утакойреализацииестьпроблема–вобычнойсредеJavaScriptонаразв10медленнее,чемверсиясциклом.Проходпоциклувыходитдешевле,чемвызовфункции.
Дилемма«скоростьпротивэлегантности»довольноинтересна.Естьнекийпромежутокмеждуудобствомдлячеловекаиудобствомдлямашины.Любуюпрограммуможноускорить,сделавеёбольшеизамысловатее.Отпрограммистатребуетсянаходитьподходящийбаланс.
Вслучаеспервымвозведениемвстепень,неэлегантныйциклдовольнопростипонятен.Неимеетсмыслазаменятьегорекурсией.Часто,однако,программыработаютстакимисложнымиконцепциями,чтохочетсяуменьшитьэффективностьпутёмповышениячитаемости.
Основноеправило,котороеуженеразповторяли,искоторымяполностьюсогласен–небеспокойтесьнасчётбыстродействия,покавыточнонеуверены,чтопрограмматормозит.Еслитак,найдитетечасти,которыеработаютдольшевсех,именяйтетамэлегантностьнаэффективность.
Конечно,мынедолжнысразужеполностьюигнорироватьбыстродействие.Вомногихслучаях,каксвозведениемвстепень,особойпростотыотэлегантныхрешениймынеполучаем.Иногдаопытныйпрограммистсразувидит,чтопростойподходникогданебудетдостаточнобыстрым.
Язаостряюнаэтомвниманиеоттого,чтослишкоммногоначинающихпрограммистовхватаютсязаэффективностьдажевмелочах.Результатполучаетсябольше,сложнееичастонебезошибок.Такиепрограммыдольшеписать,аработаютоничастонесильнобыстрее.
Норекурсияневсегдалишьменееэффективнаяальтернативациклам.Некоторыезадачипрощерешитьрекурсией.Чащевсегоэтообходнесколькихветокдерева,каждаяизкоторыхможетветвиться.
Вотвамзагадка:можнополучитьбесконечноеколичествочисел,начинаяс
ВыразительныйJavascript
55Функции
числа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Функции
онвозвращается.Вдругомслучаевозвращаетсявторой.
Чтобылучшепонять,какфункциядостигаетнужногоэффекта,давайтепросмотримеёвызовы,которыепроисходятвпоискахрешениядлячисла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Функции
Второйспособ–выобнаруживаетепотребностьвнекоейновойфункциональности,котораядостойнапомещениявотдельнуюфункцию.Выначинаетесназванияфункции,изатемпишетееётело.Можнодаженачинатьснаписаниякода,использующегофункцию,дотого,каксамафункциябудетопределена.
То,насколькосложновамподобратьимядляфункции,показывает,какхорошовыпредставляетесебееёфункциональность.Возьмёмпример.Намнужнонаписатьпрограмму,выводящуюдвачисла,количествокоровикурицнаферме,закоторымиидутслова«коров»и«куриц».Кчисламнужноспередидобавитьнулитак,чтобыкаждоезанималоровнотрипозиции.
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Функции
Можно,конечно.Нокогдамыначинаемкопироватьивставлятькодизэтихчетырёхстрочек,мыпонимаем,чтонадоостановитьсяиподумать.Долженбытьспособлучше.Пытаемсяулучшитьпрограмму:
//выводСДобавлениемНулейИМеткой
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Функции
еёможноиспользоватьвомногихситуациях,нетольковнашемслучае.Кпримеру,длявыводаотформатированныхтаблицсчислами.
Насколькоумнымииуниверсальнымидолжныбытьфункции?Мыможемнаписатькакпростейшуюфункцию,котораядополняетчислонулямидотрёхпозиций,такинавороченнуюфункциюобщегоназначениядляформатированияномеров,поддерживающуюдроби,отрицательныечисла,выравниваниепоточкам,дополнениеразнымисимволами,ит.п.
Хорошееправило–добавляйтетолькотуфункциональность,котораявамточнопригодится.Иногдапоявляетсяискушениесоздаватьфреймворкиобщегоназначениядлякаждойнебольшойпотребности.Сопротивляйтесьему.Выникогданезакончитеработу,апростонапишетекучукода,которыйниктонебудетиспользовать.
Функцииможногруборазделитьнате,чтовызываютсяиз-засвоихпобочныхэффектов,ите,чтовызываютсядляполучениянекоторогозначения.Конечно,возможноиобъединениеэтихсвойствводнойфункции.
Перваявспомогательнаяфункциявпримересфермой,printZeroPaddedWithLabel,вызываетсяиз-запобочногоэффекта:онавыводитстроку.Вторая,zeroPad,из-завозвращаемогозначения.Иэтонесовпадение,чтовтораяфункцияпригождаетсячащепервой.Функции,возвращающиезначения,легчекомбинироватьдругсдругом,чемфункции,создающиепобочныеэффекты.
Чистаяфункция–особыйвидфункции,возвращающейзначения,котораянетольконеимеетпобочныхэффектов,ноинезависитотпобочныхэффектовостальногокода–кпримеру,неработаетсглобальнымипеременными,которыемогутбытьслучайноизмененыгде-тоещё.Чистаяфункция,будучивызваннойсоднимиитемижеаргументами,возвращаетодинитотжерезультат(ибольшеничегонеделает)–чтодовольноприятно.Снейпростоработать.Вызовтакойфункцииможномысленнозаменятьрезультатомеёработы,безизменениясмыслакода.Когдавыхотитепроверитьтакуюфункцию,выможетепростовызватьеё,ибытьуверенным,чтоеслиона
Функцииипобочныеэффекты
ВыразительныйJavascript
60Функции
работаетвданномконтексте,онабудетработатьвлюбом.Нетакиечистыефункциимогутвозвращатьразныерезультатывзависимостиотмногихфакторов,ииметьпобочныеэффекты,которыесложнопроверятьиучитывать.
Однако,ненадостеснятьсяписатьнесовсемчистыефункции,илиначинатьсвященнуючисткукодаоттакихфункций.Побочныеэффектычастополезны.Нетспособанаписатьчистуюверсиюфункцииconsole.log,иэтафункциявесьмаполезна.Некоторыеоперациилегчевыразить,используяпобочныеэффекты.
Этаглавапоказалавам,какписатьсобственныефункции.Когдаключевоесловоfunctionиспользуетсяввидевыражения,возвращаетуказательнавызовфункции.Когдаоноиспользуетсякакинструкция,выможетеобъявлятьпеременную,назначаяейвызовфункции.
//Создаёмfсоссылкойнафункцию
varf=function(a){
console.log(a+2);
};
//Объявляемфункциюg
functiong(a,b){
returna*b*3.5;
}
Ключевоймоментвпониманиифункций–локальныеобластивидимости.Параметрыипеременные,объявленныевнутрифункции,локальныдлянеё,пересоздаютсякаждыйразприеёвызове,иневидныснаружи.Функции,объявленныевнутридругойфункции,имеютдоступкеёобластивидимости.
Оченьполезноразделятьразныезадачи,выполняемыепрограммой,нафункции.Вамнепридётсяповторяться,функцииделаюткодболеечитаемым,разделяяегонасмысловыечасти,также,какглавыисекциикнигипомогаютворганизацииобычноготекста.
Итог
ВыразительныйJavascript
61Функции
Впредыдущейглавебылаупомянутафункция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Функции
СимволномерNстрокиможнополучить,добавивкней.charAt(N)(“строчка”.charAt(5))–схожимобразомсполучениемдлиныстрокиприпомощи.length.Возвращаемоезначениебудетстроковым,состоящимизодногосимвола(кпримеру,“к”).Упервогосимволастрокипозиция0,чтоозначает,чтоупоследнегосимволапозициябудетstring.length–1.Другимисловами,устрокииздвухсимволовдлина2,апозицииеёсимволовбудут0и1.
НапишитефункциюcountBs,котораяпринимаетстрокувкачествеаргумента,ивозвращаетколичествосимволов“B”,содержащихсявстроке.
ЗатемнапишитефункциюcountChar,котораяработаетпримернокакcountBs,толькопринимаетвторойпараметр—символ,которыймыбудемискатьвстроке(вместотого,чтобыпростосчитатьколичествосимволов“B”).ДляэтогопеределайтефункциюcountBs.
Считаембобы.
ВыразительныйJavascript
63Функции
Дваразаменяспрашивали:«Скажите,м-рБэббидж,аесливывведётевмашинунеправильныеданные,получитсялиправильныйответ?».Непостижиматапутаницавголовах,котораяприводитктакимвопросам.
ЧарльзБэббидж,«Отрывкиизжизнифилософа»(1864)
Числа,булевскиезначенияистроки–кирпичики,изкоторыхстроятсяструктурыданных.Нонельзясделатьдомизодногокирпича.Объектыпозволяютнамгруппироватьзначения(втомчислеидругиеобъекты)вместе–истроитьболеесложныеструктуры.
Написаниепрограмм,которыммыдосегомоментазанимались,сильнозатруднялтотфакт,чтоониработалитолькоспростымиданными.Этаглавадобавитвамвинструментарийпониманиеструктурданных.Кеёконцувыбудетезнатьдостаточнодлятого,чтобыначатьписатьполезныепрограммы.
Главапройдётсяпоболее-менеереалистичномупримерупрограммирования,вводяпонятияпомеренеобходимости.Кодпримеровбудетстроитьсяизфункцийипеременных,которыемыопределялиранее.
Иногда,обычномеждувосемьюидесятьючасамивечера,Жакпротивсвоейволипревращаетсявнебольшогогрызунаспушистымхвостом.
Соднойстороны,Жакрад,чтооннепревращаетсявклассическоговолка.Превращениевбелкувлечётменьшепроблем.Вместотого,чтобыволноватьсяотом,несъешьлитысоседа(этобылобынеловко),онволнуется,какбыегонесъелсоседскийкот.Послетого,какондваждыпросыпалсянаоченьтонкойветкевкронедуба,голыйидезориентированный,онприучилсязапиратьокнаидверивсвоейкомнате
Структурыданных:объектыимассивы
Белка-оборотень
ВыразительныйJavascript
64Структурыданных:объектыимассивы
наночь,икластьнесколькоорешковнапол,чтобычем-тозанятьсебя.
Такрешаютсяпроблемыскотомидубом.НоЖаквсёещёстрадаетотсвоегозаболевания.Нерегулярныеобращениянаводятегонамысль,чтоонидолжныбытьчем-товызваны.Сначалаондумал,чтоэтопроисходиттольковтедни,когдаонприкасалсякдеревьям.Онпересталэтоделать,идажестализбегатьподходитькним.Нопроблеманеисчезла.
Перейдякболеенаучномуподходу,Жакрешилвестиежедневныйдневниквсего,чемонзанимался,записываятуда,обращалсялионвбелку.Таконнадеетсясузитькругвещей,приводящихктрансформации.
Сперваонрешилразработатьструктуруданныхдляхраненияэтойинформации.
Дляработыскускомданныхнамвначаленужнонайтиспособпредставлятьихвпамятимашины.Кпримеру,намнужнозапомнитьколлекциючисел:
2,3,5,7,11
Можнопоигратьсостроками–строкимогутбытьлюбойдлины,внихможнопоместитьмногоданных,ииспользоватьдляпредставленияэтогонабора«235711».Ноэтонеудобно.Намнужнобудеткак-товыниматьоттудачислаиливставлятьновыевстроку.
Наборыданных
ВыразительныйJavascript
65Структурыданных:объектыимассивы
Ксчастью,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Структурыданных:объектыимассивы
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Структурыданных:объектыимассивы
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Структурыданных:объектыимассивы
можемдобавлятьиудалятьсвойстваобъектапожеланию.Одинизспособовсоздатьобъект–использоватьфигурныескобки:
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Структурыданных:объектыимассивы
Возвращаяськнашеймоделисощупальцамиипеременными,мывидим,чтосвойстватожепохожинаних.Онихватаютзначения,нонаэтижезначениямогутссылатьсядругиепеременныеисвойства.Объекты–этоосьминогиспроизвольнымколичествомщупалец,накаждомизкоторыхнаписаноимясвойства.
Оператор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Структурыданных:объектыимассивы
{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Структурыданных:объектыимассивы
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Структурыданных:объектыимассивы
идеалеузнатьизихкорреляцийчто-тополезное.
Корреляция–этомеразависимостимеждупеременнымивеличинами(переменнымивстатистическомсмысле,аневсмыслеJavaScript).Онаобычновыражаетсяввидекоэффициента,принимающегозначенияот-1до1.Нулеваякорреляцияобозначает,чтопеременныевообщенесвязаны,акорреляция1означает,чтоониполностьюсвязаны–есливызнаетеодну,выавтоматическизнаетедругую.Минусодинтакжеозначаетпрочнуюсвязьпеременных,ноиихпротивоположность–когдаоднаtrue,втораявсегдаfalse.
Дляизмерениякорреляциибулевскихпеременныххорошоподходиткоэффициентфи(ϕ),ктомуже,егосравнительнолегкоподсчитать.Дляэтогонамнужнатаблица,содержащаяколичествораз,когданаблюдалисьразличныекомбинациидвухпеременных.Кпримеру,мыможемвзятьсобытия«поелпиццы»и«обращение»ипредставитьихвследующейтаблице:
ϕможновычислитьпоследующейформуле,гдеnотноситсякячейкамтаблицы:
ВыразительныйJavascript
73Структурыданных:объектыимассивы
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Структурыданных:объектыимассивы
окружения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Структурыданных:объектыимассивы
выдаётсяличтоизсписка.Нокакхранитьэтикорреляции?
Одинизспособов–хранитькорреляциивмассиве,используяобъектысосвойствами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Структурыданных:объектыимассивы
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Структурыданных:объектыимассивы
Большинствокорреляцийлежатблизкокнулю.Морковки,хлебипудинг,очевидно,несвязанысобращениемвбелку.Нооновродебыболеечастопроисходитнавыходных.Давайтеотфильтруемрезультаты,чтобывыводитьтолькокорреляциибольше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)&&
!hasEvent("чистказубов",entry))
entry.events.push("арахисзубы");
}
console.log(phi(tableFor("арахисзубы",JOURNAL)));
//→1
Ошибкибытьнеможет!Феноменслучаетсяименнотогда,когдаЖакестьарахисинечиститзубы.Еслибонтольконебылтакимнеряхойотносительнооральнойгигиены,онбывообщенезаметилсвоегонесчастья.
Знаяэто,Жакпростоперестаётестьарахисиобнаруживает,чтотрансформациипрекратились.
УЖакакакое-товремявсёхорошо.Ночерезнескольколетонтеряетработу,
ВыразительныйJavascript
78Структурыданных:объектыимассивы
ивконцеконцовемуприходитсянанятьсявцирк,гдеонвыступаеткакУдивительныйЧеловек-белка,набираяполныйротарахисовогомаслапередшоу.Однажды,уставотстольжалкогосуществования,Жакнеобращаетсяобратновчеловека,пробираетсячерездырувцирковомтентеиисчезаетвлесу.Большеегониктоневидел.
Вконцеглавыхочупознакомитьвасещёснесколькимиконцепциями,относящимисякобъектам.Начнёмсполезныхметодов,имеющихсяумассивов.
Мывиделиметоды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Структурыданных:объектыимассивы
Обаметода,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Структурыданных:объектыимассивы
Величинытипастрока,числоибулевские–необъекты,ихотяязыкнежалуетсянапопыткиназначитьимновыесвойства,оннасамомделеихнесохраняет.Величинынеизменяемы.
Ноунихестьсвоивстроенныесвойства.Укаждойстрокиестьнаборметодов.Самыеполезные,пожалуй–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Структурыданных:объектыимассивы
Когдавызываетсяфункция,кокружениюисполняемоготелафункциидобавляетсяособаяпеременнаяподназванием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Структурыданных:объектыимассивы
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Структурыданных:объектыимассивы
близкаяаппроксимация,помещающаясявчисло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Структурыданных:объектыимассивы
//→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Структурыданных:объектыимассивы
Такжевмассивахестьименованныесвойства,такиекак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Структурыданных:объектыимассивы
Умассивовестьметод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Структурыданных:объектыимассивы
Спискиудобнытем,чтоонимогутделитьсячастьюсвоейструктуры.Например,можносделатьдвасписка,{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Структурыданных:объектыимассивы
Напишитефункцию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Структурыданных:объектыимассивы
Цу-лииЦу-супохвалялисьразмерамисвоихновыхпрограмм.«Двеститысячстрок»,-сказалЦу-ли,-«несчитаякомментариев!»Цу-суответил:«Пф-ф,моя–почтимиллионстрок».МастерЮнь-Масказал:«Моялучшаяпрограммазанимаетпятьсотстрок».Услышавэто,Цу-лииЦу-суиспыталипросветление.
МастерЮнь-Ма,Книгапрограммирования
Естьдваспособапостроенияпрограмм:сделатьихнастолькопростыми,чтотамочевиднонебудетошибок,илиженастолькосложными,чтотамнебудеточевидныхошибок.
ЭнтониХоар,1980лекциянавручениипремииТьюринга
Большаяпрограмма–затратнаяпрограмма,инетолькоиз-завремениеёнаписания.Большойразмеробычноозначаетсложность,асложностьсбиваетстолкупрограммистов.Сбитыестолкупрограммистыделаютошибкивпрограммах.Большаяпрограммаозначает,чтобагаместьгдеспрятатьсяиихполучаетсятруднееотыскать.
Вернёмсяненадолгокдвумпримерамизвведения.Первыйсамодостаточен,изанимаетшестьстрок.
vartotal=0,count=1;
while(count<=10){
total+=count;
count+=1;
}
console.log(total);
Второйоснованнадвухвнешнихфункцияхизанимаетоднустроку.
console.log(sum(range(1,10)));
Вкакомизнихскореевстретитсяошибка?
Функциивысшегопорядка
ВыразительныйJavascript
90Функциивысшегопорядка
Еслимыдобавимразмеропределенийsumиrange,втораяпрограмматожеполучитсябольшой–большепервой.Ноявсёравноутверждаю,чтоонаскореевсегобудетправильной.
Этобудетпотому,чтовыражениерешениянепосредственноотноситсякрешаемойзадаче.Суммированиечисловогопромежутка–этонециклыисчётчики.Этосуммыипромежутки.
Определенияэтогословаря(функцииsumиrange)будутвключатьциклы,счётчикиидругиеслучайныедетали.Нопотому,чтоонивыражаютболеепростыеконцепции,чемвсяпрограмма,ихпрощесделатьправильно.
Впрограммномконтекстеэти«словарные»определениячастоназываютабстракциями.Абстракциипрячутдеталиидаютнамвозможностьразговариватьозадачахнавысшем,илиболееабстрактном,уровне.
Сравнитедварецептагороховогосупа:
Добавьтевёмкостьпооднойчашкесухогогороханапорцию.Добавьтеводытак,чтобыонапокрылагорох.Оставьтееготакминимумна12часов.Выньтегорохизводыипоместитеихнасковороду.Добавьте4чашкиводынапорцию.Закройтесковородуитушитегорохдвачаса.Возьмитепополовинелуковицынапорцию.Порежьтенакускиножом,добавьтекгороху.Возьмитепоодномустеблюсельдереянапорцию.Порежьтенакускиножом,добавьтекгороху.Возьмитепоморковкенапорцию.Порежьтенакускиножом,добавьтекгороху.Готовьтеещё10минут.
Второйрецепт:
Напорцию:1чашкасухогогороха,половиналуковицы,стебельсельдерея,морковка.Вымачивайтегорох12часов.Тушите2часав4чашкахводынапорцию.Порежьтеидобавьтеовощи.Готовьтеещё10минут.
Второй–корочеипроще.Новамнужнознатьчутьбольшепонятий,связанныхсготовкой–вымачивание,тушение,резка(иовощи).
Абстракции
ВыразительныйJavascript
91Функциивысшегопорядка
Программируя,мынеможемрассчитыватьнато,чтовсенеобходимыесловабудутвнашемсловаре.Из-заэтоговыможетескатитьсядошаблонапервогорецепта:диктоватькомпьютерувсемелкиешажкидругзадругом,незамечаяпонятийболеевысокогоуровня,которыеонивыражают.
Второйнатуройпрограммистадолжностатьумениезамечать,когдапонятиеумоляетпридуматьдлянегоновоесловоивынестивабстракцию.
Простыефункции,которыемыиспользовалираньше,хорошидляпостроенияабстракций.Ноиногдаихбываетнедостаточно.
Впредыдущейглавемынесколькоразвстречалитакойцикл:
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Функциивысшегопорядка
Ночто,еслинамнадоделатьчто-тодругое,нежеливыводитьэлементывконсоль?Поскольку«делатьчто-то»можнопредставитькакфункцию,афункции–этопростопеременные,мыможемпередатьэтодействиекакаргумент:
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Функциивысшегопорядка
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Функциивысшегопорядка
}
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Функциивысшегопорядка
Важноеотличиевтом,чтопеременные,объявленныевнутривнутреннихфункций,непопадаютвокружениевнешней.Иобычноэтотолькоклучшему.
Функция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Функциивысшегопорядка
Первыйаргументметода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Функциивысшегопорядка
двойныекавычки,атакжедопускаютсятолькопростыевеличины–никакихвызововфункций,переменных,ничегочтовключалобывычисления.Такженедопускаютсякомментарии.
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&&person.born<1925;
Фильтруеммассив
ВыразительныйJavascript
98Функциивысшегопорядка
}));
//→[{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Функциивысшегопорядка
##Суммированиес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Функциивысшегопорядка
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Функциивысшегопорядка
(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Функциивысшегопорядка
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Функциивысшегопорядка
Мысчитали,чтотолькопоследнеепоколениелюдейдожилодо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.Также,как&&возвращает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Функциивысшегопорядка
Проблемаобъектно-ориентированныхязыковвтом,чтоонитащатссобойвсёсвоёнеявноеокружение.Вамнуженбылбанан–авыполучаетегориллусбананом,ицелыеджунгливпридачу.
ДжоАрмстронг,винтервьюCodersatWork
Термин«объект»впрограммированиисильноперегружензначениями.Вмоейпрофессииобъекты–стильжизни,темасвященныхвойнилюбимоезаклинание,нетеряющийсвоеймагическойсилы.
Стороннемучеловекувсёэтонепонятно.Начнёмжескраткойисторииобъектовкакконцепциивпрограммировании.
Этаистория,какбольшинствоисторийопрограммировании,начинаетсяспроблемысложности.Однаизидейговорит,чтосложностьможносделатьуправляемой,разделивеёненебольшиечасти,изолированныедруготдруга.Этичастисталиназыватьобъектами.
Объект–твёрдаяскорлупа,скрывающаялипкуюсложностьвнутри,ивместонеёпредлагающаянамнесколькоручекнастройкииконтактов(вродеметодов),представляющихинтерфейс,посредствомкоторогообъектнужноиспользовать.Идеявтом,чтоинтерфейсотносительнопрост,иприработеснимпозволяетигнорироватьвсесложныепроцессы,происходящиевнутриобъекта.
Простойинтерфейсможетспрятатьмногосложного.
Тайнаяжизньобъектов
История
ВыразительныйJavascript
105Тайнаяжизньобъектов
Дляпримерапредставьтеобъект,обеспечивающийинтерфейскучасткуэкрана.Сегопомощьюможнорисоватьфигурыиливыводитьтекстнаэтотучасток,ноприэтомвседетали,касающиесяпревращениятекстаилифигурвпиксели,скрыты.Увасестьнаборметодов,кпримеруdrawCircle,иэтовсё,чтовамнужнознатьдляиспользованиятакогообъекта.
Такиеидеиполучилиразвитиев70-80годах,ав90-хихвынесланаповерхностьрекламнаяволна–революцияобъектно-ориентированногопрограммирования.Внезапнобольшойкланлюдейобъявил,чтообъекты–этоправильныйспособпрограммирования.Авсё,чтонеимеетобъектов,являетсяустаревшейерундой.
Такойфанатизмвсегдаприводитккучебесполезнойчуши,истехпоридётчто-товродеконтрреволюции.Внекоторыхкругахобъектывообщеимеюткрайнеплохуюрепутацию.
ВыразительныйJavascript
106Тайнаяжизньобъектов
Япредпочитаюрассматриватьихспрактической,анеидеологическойточкизрения.Естьнесколькополезныхидей,вчастностиинкапсуляция(различиемеждувнутреннейсложностьюивнешнейпростотой),которыебылипопуляризованыобъектно-ориентированнойкультурой.Ихстоитизучать.
Этаглаваописываетдовольноэксцентричныйподход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Тайнаяжизньобъектов
можноиспользоватьдляэмуляциивызоваметодов.Этотпервыйаргументкакраздаётзначениепеременной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Тайнаяжизньобъектов
Какиследовалоожидать,функция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Тайнаяжизньобъектов
Болееудобныйспособсозданияобъектов,наследуемыхотнекоегопрототипа–конструктор.В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Тайнаяжизньобъектов
Егосвойство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Тайнаяжизньобъектов
какобычныеобъектыпростоиспользуютстандартныезначения,взятыеупрототипов.
Такжеонаиспользуетсядляназначенияфункциямимассивамразныхметодов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Тайнаяжизньобъектов
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Тайнаяжизньобъектов
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Тайнаяжизньобъектов
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Тайнаяжизньобъектов
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Тайнаяжизньобъектов
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Тайнаяжизньобъектов
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Тайнаяжизньобъектов
}
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Тайнаяжизньобъектов
Работает!Нотаккакувсехячеекодинразмер,кодформатированиятаблицынеделаетничегоинтересного.
Исходныеданныедлятаблицыгор,которуюмыстроим,содержатсявпеременной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Тайнаяжизньобъектов
});
});
return[headers].concat(body);
}
console.log(drawTable(dataTable(MOUNTAINS)));
//→nameheightcountry
//-------------------------------
//Kilimanjaro5895Tanzania
//…итакдалее
СтандартнаяфункцияObject.keysвозвращаетмассивимёнсвойствобъекта.Верхняястрокатаблицыдолжнасодержатьподчёркнутыеячейкисназваниямистолбцов.Значениявсехобъектовизнабораданныхвыглядятподзаголовкомкакнормальныеячейки–мыизвлекаемихпроходомфункцииmapпомассивуkeys,чтобыбытьувереннымвсохраненииодногопорядкаячееквкаждойстроке.
Итоговаятаблицанапоминаеттаблицуизпримера,тольковотчисланевыровненыпоправомукраю.Мызаймёмсяэтимчутьпозже.
Присозданииинтерфейсаможноввестисвойства,неявляющиесяметодами.МымоглипростоопределитьminHeightиminWidthкакпеременныедляхраненияномеров.Ноэтопотребовалобыотнасписатькодвычисленияихзначенийвконструкторе–аэтоплохо,посколькуконструированиеобъектанесвязаносниминапрямую.Могливозникнутьпроблемы,когданапримервнутренняяячейкаилиподчёркнутаяячейкаизменяются–итогдаихразмертожедолженменяться.
Этисоображенияпривеликтому,чтосвойства,неявляющиесяметодами,многиеневключаютвинтерфейс.Вместопрямогодоступаксвойствам-значениям,используютсяметодытипаgetSomethingиsetSomethingдлячтенияизаписизначенийсвойств.Ноестьиминус–приходитсяписать(ичитать)многодополнительныхметодов.
Ксчастью,JavaScriptдаётнамтехнику,использующуюлучшееизобоихподходов.Мыможемзадатьсвойства,которыеснаруживыглядят
Геттерыисеттеры
ВыразительныйJavascript
121Тайнаяжизньобъектов
обыкновенными,новтайнеимеютсвязанныеснимиметоды.
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Тайнаяжизньобъектов
выровненпоправомукраю.Намнужносоздатьещёодинтипячееквроде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Тайнаяжизньобъектов
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Тайнаяжизньобъектов
этого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Тайнаяжизньобъектов
полезнаяштука.
Определяянесколькотипов,различающихсятольковмелкихдеталях,бываетудобнопростонаследоватьпрототипновоготипаотпрототипастароготипа,чтобыновыйконструкторвызывалстарый.Этодаётвамтипобъекта,сходныйсостарым,ноприэтомкнемуможнодобавлятьсвойстваилипереопределятьстарые.
Напишитеконструктор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Тайнаяжизньобъектов
былабыменьше.
//Вашкод.
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Тайнаяжизньобъектов
Вопросотом,могутлимашиныдуматьтакжеуместен,каквопросотом,могутлиподводныелодкиплавать.
ЭдсгерДейкстра,Угрозывычислительнойнауке
Вглавах-проектахяперестанузакидыватьвастеорией,ибудуработатьвместесваминадпрограммами.Теориянезаменимаприобучениипрограммированию,ноонадолжнасопровождатьсячтениемипониманиемнетривиальныхпрограмм.
Нашпроект–постройкавиртуальнойэкосистемы,небольшогомира,населённогосуществами,которыедвигаютсяиборютсязавыживание.
Чтобызадачасталавыполнимой,мыкардинальноупростимконцепциюмира.Аименно–мирбудетдвумернойсеткой,гдекаждаясущностьзанимаетоднуклетку.Накаждомходусуществаполучатвозможностьвыполнитькакое-либодействие.
Такимобразом,мыпорубимвремяипространствонаединицыфиксированногоразмера:клеткидляпространстваиходыдлявремени.Конечно,этогрубоеинеаккуратноеприближение.Нонашасимуляциядолжнабытьразвлекательной,анеаккуратной,поэтомумысвободно«срезаемуглы».
Определитьмирмыможемприпомощиплана–массивастрок,которыйраскладываетмировуюсетку,используяодинсимволнаклетку.
varplan=["############################",
"###o##",
"##",
"#######",
"#######",
"#######",
Проект:электроннаяжизнь
Определение
ВыразительныйJavascript
128Проект:электроннаяжизнь
"######",
"######",
"###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Проект:электроннаяжизнь
доступксвойствам:
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&&vector.x<this.width&&
vector.y>=0&&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Проект:электроннаяжизнь
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Проект:электроннаяжизнь
рядомссуществом,илиже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Проект:электроннаяжизнь
объект,сообщающий,чтоозначаеткаждыйизсимволовкарты.Внёместьконструктордлякаждогосимвола–кромепробела,которыйссылаетсяна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Проект:электроннаяжизнь
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Проект:электроннаяжизнь
которуювнутренняяфункцияполучаетдоступксетке.
Этопромахвдизайне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Проект:электроннаяжизнь
Этоработаеттолькостемифункциямивысшегопорядка,укоторыхестьтакойконтекстныйпараметр.Еслинет–приходитсяиспользоватьдругиеупомянутыеподходы.
Внашейсобственнойфункциивысшегопорядкамыможемвключитьподдержкуконтекстногопараметра,используяметод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&&acted.indexOf(critter)==-1){
Оживляеммир
ВыразительныйJavascript
136Проект:электроннаяжизнь
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&&action.type=="move"){
vardest=this.checkDestination(action,vector);
if(dest&&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Проект:электроннаяжизнь
(вводпользователяиличтениефайла),ноонотакжеполезнодляизолированияподсистемдруготдруга.Внашемслучаеегоцель–учесть,чтосуществамогутбытьзапрограммированынеаккуратно.Имненадопроверять,имеютлиихнамерениясмысл.Онипростозапрашиваютвозможностьдействия,амирсамрешает,разрешатьлиего.
Этидваметоданепринадлежатквнешнемуинтерфейсумировогообъекта.Ониявляютсядеталямивнутреннейреализации.Некоторыеязыкипредусматриваютспособыобъявлятьопределённыеметодыисвойства«приватными»,ивыдаватьошибкуприпопыткеихиспользованияснаружиобъекта.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Проект:электроннаяжизнь
Методlookвычисляеткоординаты,накоторыемыпытаемсяпосмотреть.Еслионинаходятсявнутрисетки,тополучаетсимвол,соответствующийэлементу,находящемусятам.Длякоординатснаружисеткиlookпростопритворяется,чтотамстена–есливызададитемирбезокружающихстен,существанесмогутсойтискрая.
Мысоздалиэкземплярмировогообъекта.Теперь,когдавсенеобходимыеметодыготовы,унасдолжнополучитьсязаставитьегодвигаться.
for(vari=0;i<5;i++){
world.turn();
console.log(world.toString());
}
//→…пятьходов
Простовыводитьпятькопийкарты–неоченьудобныйспособнаблюдениязамиром.Поэтомувпесочницедлякниги(иливфайлахдляскачивания)естьволшебнаяфункцияanimateWorld,котораяпоказываетмиркаканимациюнаэкране,делаяпотришагавсекунду,покавыненажмётестоп.
animateWorld(world);
//→…заработало!
РеализацияanimateWorldпокаостанетсятайной,нопослепрочтенияследующихглавкниги,обсуждающихинтеграциюJavaScriptвбраузеры,онауженебудетвыглядетьтакзагадочно.
Однаизинтересныхситуаций,происходящихвмире,случается,когдадвасуществаотскакиваютдруготдруга.Можетепридуматьдругуюинтереснуюформувзаимодействий?
Япридумалсущество,двигающеесяпостенке.Онодержитсвоюлевуюруку
Онодвигается
Большеформжизни
ВыразительныйJavascript
139Проект:электроннаяжизнь
(лапу,щупальце,чтоугодно)настенеидвигаетсявдольнеё.Это,какоказалось,нетак-топростозапрограммировать.
Намнужнобудетвычислять,используянаправлениявпространстве.Таккакнаправлениязаданынаборомстрок,намнадозадатьсвоюоперацию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Проект:электроннаяжизнь
начинаемвпереди,поэтомувпустомпространствеонбудетидтипрямо.
Инаконец,естьпроверканасовпадениеthis.dirиstartнакаждомпроходецикла,чтобыоннезациклился,когдасуществунекудаидтииз-застенилидругихсуществ,иононеможетнайтипустуюклетку.
Этотнебольшоймирпоказываетсуществ,двигающихсяпостенам.:
animateWorld(newWorld(
["############",
"###",
"#~~#",
"####",
"###o####",
"##",
"############"],
{"#":Wall,
"~":WallFollower,
"o":BouncingCritter}
));
Чтобысделатьжизньвнашеммиркеболееинтересной,добавимпонятияедыиразмножения.Укаждогоживогосуществапоявляетсяновоесвойство,energy(энергия),котораяуменьшаетсяприсовершениидействий,иувеличиваетсяприпоеданииеды.Когдаусуществадостаточноэнергии,онможетразмножаться,создаваяновоесуществотогожетипа.Дляупрощениярасчётовнашисуществаразмножаютсясамипосебе.
Еслисуществатолькодвигаютсяиедятдругдруга,мирвскореподдастсявозрастающейэнтропии,внёмзакончитсяэнергияионпревратитсявпустыню.Дляпредотвращенияэтогофинала(илиоттягивания),мыдобавляемвнегорастения.Онинедвигаются.Онипростозанимаютсяфотосинтезомирастут(нарабатываютэнергию),иразмножаются.
Чтобыэтозаработало,намнуженмирсдругимметодомletAct.МымоглибыпростозаменитьметодпрототипаWorld,нояпривыккнашейсимуляцииходящихпостенамсуществинехотелбыеёразрушать.
Болеежизненнаяситуация
ВыразительныйJavascript
141Проект:электроннаяжизнь
Одноизрешений–использоватьнаследование.Мысоздаёмновыйконструктор,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&&
action.typeinactionTypes&&
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Проект:электроннаяжизнь
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&&this.grid.get(dest);
if(!atDest||atDest.energy==null)
returnfalse;
critter.energy+=atDest.energy;
this.grid.set(dest,null);
returntrue;
};
Поеданиедругогосуществатакжетребуетпредоставлениядопустимойклеткинаправления.Вэтомслучаеклеткадолжнасодержатьчто-либос
ВыразительныйJavascript
143Проект:электроннаяжизнь
энергией,напримерсущество(нонестену,ихестьнельзя).Еслиэтоподтверждается,энергиясъеденногопереходиткедоку,ажертваудаляетсяссетки.
Инаконец,мыпозволяемсуществамразмножаться.
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Проект:электроннаяжизнь
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&&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Проект:электроннаяжизнь
"###########",
"##*****##",
"#*##****O*##",
"#***O##***#",
"#O##***#",
"###**#",
"#O#*#",
"#*#**O#",
"#***##**O**#",
"##****###****###",
"############################"],
{"#":Wall,
"O":PlantEater,
"*":Plant}
);
Большуючастьвременирастенияразмножаютсяиразрастаются,нозатемизобилиеедыприводитквзрывномуроступопуляциитравоядных,которыесъедаютпочтивсюрастительность,чтоприводиткмассовомувымираниюотголода.Иногдаэкосистемавосстанавливаетсяиначинаетсяновыйцикл.Вдругихслучаяхкакой-тоизвидоввымирает.Еслитравоядные,тогдавсёпространствозаполняетсярастениями.Еслирастения–оставшиесясуществаумираютотголода,идолинапревращаетсявнеобитаемуюпустошь.О,жестокостьприроды…
Грустно,когдажителинашегомиравымираютзанесколькоминут.Чтобысправитьсясэтим,мыможемпопробоватьсоздатьболееумногопоедателярастений.
Унашихтравоядныхестьнесколькоочевидныхпроблем.Во-первых,онижадные—поедаюткаждоерастение,котороенаходят,покаполностьюнеуничтожатвсюрастительность.Во-вторых,ихслучайноедвижение(вспомните,чтометодview.findвозвращаетслучайноенаправление)заставляетихболтатьсянеэффективноипомиратьсголоду,еслирядомнеокажетсярастений.Инаконец,онислишкомбыстроразмножаются,чтоделаетциклыотизобилиякголодуслишкомбыстрыми.
Упражнения
Искусственныйидиот
ВыразительныйJavascript
146Проект:электроннаяжизнь
Напишитеновыйтипсущества,которыйстараетсясправитсясоднимилинесколькимипроблемамиизаменитеимстарыйтипPlantEaterвмиредолины.Последитезаними.Выполнитенеобходимыеподстройки.
//Вашкод
functionSmartPlantEater(){}
animateWorld(newLifelikeWorld(
["############################",
"###########",
"##*****##",
"#*##****O*##",
"#***O##***#",
"#O##***#",
"###**#",
"#O#*#",
"#*#**O#",
"#***##**O**#",
"##****###****###",
"############################"],
{"#":Wall,
"O":SmartPlantEater,
"*":Plant}
));
Влюбойсерьёзнойэкосистемепищеваяцепочкадлиннееодногозвена.Напишитеещёодносущество,котороевыживает,поедаятравоядных.Вызаметите,чтостабильностиещётруднеедостичь,когдациклыпроисходятнаразныхуровнях.Попытайтесьнайтистратегию,котораяпозволитэкосистемеработатьплавнонекотороевремя.
Увеличениемираможетпомочьвэтом.Тогдалокальныедемографическиевзрывыилиуменьшениечисленностиимеютменьшешансовполностьюизничтожитьпопуляцию,иестьместодляотносительнобольшойпопуляциижертв,котораяможетподдерживатьнебольшуюпопуляциюхищников.
//Вашкодтут
functionTiger(){}
animateWorld(newLifelikeWorld(
["####################################################",
Хищники
ВыразительныйJavascript
147Проект:электроннаяжизнь
"#####****###",
"#*@##########OO##",
"#*##OO*****#",
"###*##########*#",
"###**********#",
"#***#****#########**#",
"#***#*#***#",
"####O#***######",
"#*@##*O##",
"#*#######**#",
"###*********#",
"#O@O#",
"#*###########*#",
"#**#*#####O#",
"##**OO##******###**#",
"####*********#",
"####################################################"],
{"#":Wall,
"@":Tiger,
"O":SmartPlantEater,//изпредыдущегоупражнения
"*":Plant}
));
ВыразительныйJavascript
148Проект:электроннаяжизнь
Отладкаизначальновдвоесложнеенаписаниякода.Поэтому,есливыпишетекоднастолькозаумный,насколькоможете,топоопределениювынеспособныотлаживатьего.БрайанКерниганиП.Ж.Плауэр,«Основыпрограммногостиля»
Юан-Манаписалнебольшуюпрограмму,использующуюмногоглобальныхпеременныхиужасныххаков.Ученик,читаяпрограмму,спросилего:«Выпредупреждалинасоподобныхтехниках,ноприэтомянахожуихввашейжепрограмме.Какэтовозможно?»Мастерответил:«Ненужнобежатьзаполивальнымшлангом,еслидомнегорит».
МастерЮан-Ма,«Книгапрограммирования».
Программа–этокристаллизованнаямысль.Иногдамыслипутаются.Иногдаприпревращениимыслейвпрограммувкодвкрадываютсяошибки.Вобоихслучаяхполучаетсяповреждённаяпрограмма.
Недостаткивпрограммахобычноназываютошибками.Этомогутбытьошибкипрограммистаилипроблемывсистемах,скоторымипрограммавзаимодействует.Некоторыеошибкиочевидны,другие–трудноуловимы,имогутскрыватьсявсистемахгодами.
Частопроблемавозникаетвтехситуациях,возникновениекоторыхпрограммистизначальнонепредвидел.Иногдаэтихситуацийнельзяизбежать.Когдапользователяпросятввестиеговозраст,аонвводит«апельсин»,этоставитпрограммувнепростуюситуацию.Этиситуациинеобходимопредвидетьикак-тообрабатывать.
Вслучаеошибокпрограммистовнашацельясна.Намнадонайтиихиисправить.Таковыеошибкиварьируютсяотпростыхопечаток,накоторыекомпьютерпожалуетсясразуже,кактолькоувидитпрограмму,доскрытыхошибоквнашемпониманиитого,какпрограммаработает,которыеприводят
Поискиобработкаошибок
Ошибкипрограммистов
ВыразительныйJavascript
149Поискиобработкаошибок
кнеправильнымрезультатамвособыхслучаях.Ошибкипоследнегородаможноискатьнеделями.
Разныеязыкипо-разномумогутпомогатьвамвпоискеошибок.Ксожалению,JavaScriptнаходитсянаконцеэтойшкалы,обозначенномкак«вообщепочтинепомогает».Некоторымязыкамнадоточнознатьтипывсехпеременныхивыраженийещёдозапускапрограммы,ионисразусообщатвам,еслитипыиспользованынекорректно.JavaScriptрассматриваеттипытолькововремяисполненияпрограмм,идажетогдаонразрешаетделатьнеоченьосмысленныевещибезвсякихжалоб,например
x=true*"обезьяна"
НанекоторыевещиJavaScriptвсё-такижалуется.Написаниесинтаксическинеправильнойпрограммысразувызоветошибку.Другиеошибки,напримервызовчего-либо,неявляющегосяфункцией,илиобращениексвойствунеопределённойпеременной,возникнутпривыполнениипрограммы,когдаонасталкиваетсястакойбессмысленнойситуацией.
НочастовашибессмысленныевычисленияпростопородятNaN(notanumber)илиundefined.Программарадостнопродолжит,будучиувереннойвтом,чтоонаделаетчто-тоосмысленное.Ошибкапроявитсебяпозже,когдатакоефиктивноезначениеужепройдётчерезнесколькофункций.Онаможетвообщеневызватьсообщениеобошибке,апростопривестикнеправильномурезультатувыполнения.Поискисточникатакихпроблем–сложнаязадача.
Процесспоискаошибок(bugs)впрограммахназываетсяотладкой(debugging).
JavaScriptможнозаставитьбытьпостроже,переведяеговстрогийрежим.Дляэтогонаверхуфайлаилителафункциипишется«usestrict».Пример:
functioncanYouSpotTheProblem(){
"usestrict";
Строгийрежим(strictmode)
ВыразительныйJavascript
150Поискиобработкаошибок
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Поискиобработкаошибок
несколькимипараметрамисоднимитемжеименем,иудаляетнекоторыепотенциальнопроблемныесвойстваязыка(например,инструкцию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Поискиобработкаошибок
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Поискиобработкаошибок
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Поискиобработкаошибок
разработчикаактивны,исполнениепрограммыбудетприостановленонаэтойинструкции,ивысможетеизучитьсостояниепрограммы.
Ксожалению,программистможетпредотвратитьпоявлениеневсехпроблем.Есливашапрограммаобщаетсясвнешниммиром,онаможетполучитьнеправильныевходныеданные,илижесистемы,скоторымионапытаетсявзаимодействовать,окажутсясломаннымиилинедоступными.
Простыепрограммы,илипрограммы,работающиеподвашимнадзором,могутпросто«сдаваться»втакоймомент.Выможетеизучитьпроблемуипопробоватьснова.«Настоящие»приложениянедолжныпросто«падать».Иногдаприходитсяприниматьнеправильныевходныеданныеикак-тоснимиработать.Вдругихслучаях,нужносообщитьпользователю,чточто-топошлонетак–ипотомужесдаваться.Влюбомслучаепрограммадолжначто-тосделатьвответнавозникновениепроблемы.
Допустим,увасестьфункцияpromptInteger,котораязапрашиваетцелоечислоивозвращаетего.Чтоонадолжнасделать,еслипользовательвведёт«апельсин»?
Одинизвариантов–вернутьособоезначение.Обычнодляэтихцелейиспользуютnullиundefined.
functionpromptNumber(question){
varresult=Number(prompt(question,""));
if(isNaN(result))returnnull;
elsereturnresult;
}
console.log(promptNumber("Сколькопальцеввидите?"));
Этонадёжнаястратегия.Теперьлюбойкод,вызывающийpromptNumber,долженпроверять,былоливозвращеночисло,иеслинет,как-товыйтиизситуации–спроситьснова,илизадатьзначениепо-умолчанию.Иливернутьспециальноезначениеужетому,ктоеговызвал,сообщаяонеудаче.
Вомногихтакихслучаях,когдаошибкивозникаютчастоивызывающий
Распространениеошибок
ВыразительныйJavascript
155Поискиобработкаошибок
функциюкоддолженприниматьихвовнимание,совершеннодопустимовозвращатьспециальноезначениекакиндикаторошибки.Ноестьиминусы.Во-первых,что,еслифункцияитакможетвернутьлюбойтипзначения?Длянеёсложнонайтиспециальноезначение,котороебудетотличатьсяотдопустимогорезультата.
Втораяпроблема–работасоспециальнымизначениямиможетзамусоритькод.ЕслифункцияpromptNumberвызывается10раз,тонадо10разпроверить,невернулалионаnull.Еслиреакциянаnullзаключаетсяввозвратеnullнауровеньвыше,тогдатам,гдевызывалсяэтоткод,тоженужновстраиватьпроверкунаnull,итакдалее.
Когдафункциянеможетработатьнормально,мыбыхотелиостановитьработуиперепрыгнутьтуда,гдетакаяошибкаможетбытьобработана.Этимзанимаетсяобработкаисключений.
Код,встретившийпроблемувмоментвыполнения,можетподнять(иливыкинуть)исключение(raiseexception,throwexception),котороепредставляетизсебянекоезначение.Возвратисключениянапоминаетнекий«прокачанный»возвратизфункции–онвыпрыгиваетнетолькоизсамойфункции,ноиизвсехвызывавшихеёфункций,дотогоместа,скоторогоначалосьвыполнение.Этоназываетсяразвёртываниемстека(unwindingthestack).Можетбыть,выпомнитестекфункцийизглавы3…Исключениебыстропроматываетстеквниз,выкидываявсеконтекстывызовов,которыевстречает.
Еслибыисключениясразудоходилидосамогонизастека,пользыотнихбылобынемного.Онибыпростопредоставлялиинтересныйспособвзорватьпрограмму.Ихсилавтом,чтонаихпутивстекеможнопоставить«препятствия»,которыебудутловитьисключения,мчащиесяпостеку.Итогдасэтимможносделатьчто-тополезное,послечегопрограммапродолжаетвыполнятьсястойточки,гдебылопойманоисключение.
Пример:
Исключения
ВыразительныйJavascript
156Поискиобработкаошибок
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Поискиобработкаошибок
ошибка,итам,гдеонаобрабатывается.Промежуточныефункциипростонеобращаютнаэтовнимания.
Ну,почти.
Представьтеследующуюситуацию:функция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Поискиобработкаошибок
Заметьте,чтонамбольшененужносохранятьрезультатвызова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Поискиобработкаошибок
переменную,запроссвойствупеременной,равной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Поискиобработкаошибок
Значит,намнадопойматьопределённоеисключение.Мыможемвблоке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Поискиобработкаошибок
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Поискиобработкаошибок
Утверждения–способубедитьсявтом,чтоошибкипровоцируютпрерываниепрограммывтомместе,гдеонисовершены,анепростовыдаютстранныевеличины,которыепередаютсяпосистемеивызываютпроблемывкаких-тодругих,несвязанныхсэтим,местах.
Ошибкиинедопустимыевходныеданныеслучаютсявжизни.Ошибкивпрограммахнадоискатьиисправлять.Ихлегченайти,используяавтоматическиесистемыпроверокидобавляяутвержденияввашипрограммы.
Проблемы,вызванныечем-то,чтонеподвластновашейпрограмме,нужнообрабатыватьдостойно.Иногда,когдапроблемуможнорешитьлокально,допустимовозвращатьспециальныезначениядляотслеживаниятакихслучаев.Вдругихслучаяхпредпочтительноиспользоватьисключения.
Выбросисключенияприводиткразматываниюстекадотехпор,поканебудетвстреченблокtry/catchилипокамынедойдёмдоднастека.Значениеисключениябудетпередановблокcatch,которыйсможетудостоверитьсявтом,чтоэтоисключениедействительното,котороеонждёт,иобработатьего.Дляработыснепредсказуемымисобытиямивпотокепрограммыможноиспользоватьблокиfinally,чтобыопределённыечастикодабыливыполненывлюбомслучае.
Допустим,увасестьфункцияprimitiveMultiply,котораяв50%случаевперемножает2числа,авостальныхслучаяхвыбрасываетисключениетипаMultiplicatorUnitFailure.Напишитефункцию,обёртывающуюэту,ипростовызывающуюеёдотехпор,поканебудетполученуспешныйрезультат.
Убедитесь,чтовыобрабатываететольконужныевамисключения.
Итог
Упражнения
Повтор
ВыразительныйJavascript
163Поискиобработкаошибок
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Поискиобработкаошибок
}
withBoxUnlocked(function(){
box.content.push("золотишко");
});
try{
withBoxUnlocked(function(){
thrownewError("Пиратынагоризонте!Отмена!");
});
}catch(e){
console.log("Произошлаошибка:",e);
}
console.log(box.locked);
//→true
Вкачествепризовойигрыубедитесь,чтопривызовеwithBoxUnlocked,когдакоробканезаперта,коробкаостаётсянезапертой.
ВыразительныйJavascript
165Поискиобработкаошибок
Некоторыелюди,столкнувшисьспроблемой,думают:«О,аиспользую-каярегулярныевыражения».Теперьунихестьдвепроблемы.ДжеймиЗавински
Юан-Масказал:«Требуетсябольшаясила,чтобырезатьдеревопоперёкструктурыдревесины.Требуетсямногокода,чтобыпрограммироватьпоперёкструктурыпроблемы.МастерЮан-Ма,«Книгапрограммирования»
Инструментыитехникипрограммированиявыживаютираспространяютсяхаотично-эволюционнымспособом.Иногдавыживаютнекрасивыеигениальные,апростотакие,которыедостаточнохорошоработаютвсвоейобласти–кпримеру,еслиихинтегрируютвдругуюуспешнуютехнологию.
Вэтойглавемыобсудимтакойинструмент–регулярныевыражения.Этоспособописыватьшаблонывстроковыхданных.Онисоздаютнебольшойотдельныйязык,которыйвходитвJavaScriptивомножестводругихязыковиинструментов.
Регуляркиодновременнооченьстранныеикрайнеполезные.Ихсинтаксисзагадочен,апрограммныйинтерфейсвJavaScriptдлянихнеуклюж.Ноэтомощныйинструментдляисследованияиобработкистрок.Разобравшисьсними,выстанетеболееэффективнымпрограммистом.
Регулярка–типобъекта.Еёможносоздать,вызвавконструкторRegExp,илинаписавнужныйшаблон,окружённыйслешами.
varre1=newRegExp("abc");
varre2=/abc/;
Обаэтихрегулярныхвыраженияпредставляютодиншаблон:символ“a”,закоторымследуетсимвол“b”,закоторымследуетсимвол“c”.
Регулярныевыражения
Создаёмрегулярноевыражение
ВыразительныйJavascript
166Регулярныевыражения
ЕсливыиспользуетеконструкторRegExp,тогдашаблонзаписываетсякакобычнаястрока,поэтомудействуютвсеправилаотносительнообратныхслешей.
Втораязапись,гдешаблоннаходитсямеждуслешами,обрабатываетобратныеслешипо-другому.Во-первых,таккакшаблонзаканчиваетсяпрямымслешем,тонужноставитьобратныйслешпередпрямымслешем,которыймыхотимвключитьвнашшаблон.Крометого,обратныеслеши,неявляющиесячастьюспециальныхсимволовтипа\n,будутсохранены(анепроигнорированы,каквстроках),иизменятсмыслшаблона.Унекоторыхсимволов,таких,какзнаквопросаилиплюс,естьособоезначениеврегулярках,иесливамнужнонайтитакойсимвол,еготакженадопредварятьобратнымслешем.
vareighteenPlus=/eighteen\+/;
Чтобызнать,какиесимволынадопредварятьслешем,вамнадовыучитьсписоквсехспециальныхсимволовврегулярках.Покаэтонереально,поэтомувслучаесомненийпростоставьтеобратныйслешпередлюбымсимволом,неявляющимсябуквой,числомилипробелом.
Урегулярокестьнесколькометодов.Простейший–test.Еслипередатьемустроку,онвернётбулевскоезначение,сообщая,содержитлистрокавхождениезаданногошаблона.
console.log(/abc/.test("abcde"));
//→true
console.log(/abc/.test("abxde"));
//→false
Регулярка,состоящаятолькоизнеспециальныхсимволов,простопредставляетсобойпоследовательностьэтихсимволов.Еслиabcестьгде-товстроке,которуюмыпроверяем(нетольковначале),testвернётtrue.
Проверяемнасовпадения
ВыразительныйJavascript
167Регулярныевыражения
Выяснить,содержитлистрока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Регулярныевыражения
Обратныеслешиможноиспользоватьивквадратныхскобках.Например,[\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Регулярныевыражения
встретитьсянольилиодинраз.Вследующемпримересимвол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Регулярныевыражения
Буква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Регулярныевыражения
еёпозициявмассивесодержит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Регулярныевыражения
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Регулярныевыражения
Еслинамнадопринудитьсовпадениевзятьвсюстрокуцеликом,мыиспользуемметки^и$.^совпадаетсначаломстроки,а$сконцом.Поэтому/^\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Регулярныевыражения
Скобкиограничиваютчастьшаблона,ккоторойприменяется|,иможнопоставитьмноготакихоператоровдругзадругом,чтобыобозначитьвыборизболеечемдвухвариантов.
Регулярныевыраженияможнорассматриватькакблок-схемы.Следующаядиаграммаописываетпоследнийживотноводческийпример.
Выражениесовпадаетсострокой,еслиможнонайтипутьслевойчастидиаграммывправую.Мызапоминаемтекущееположениевстроке,икаждыйраз,проходяпрямоугольник,проверяем,чточастьстрокисразузанашимположениемвнейсовпадаетссодержимымпрямоугольника.
Значит,проверкасовпадениянашейрегуляркивстроке«the3pigs»припрохождениипоблок-схемевыглядиттак:
—напозиции4естьграницаслова,ипроходимпервыйпрямоугольник—начинаяс4позициинаходимцифру,ипроходимвторойпрямоугольник—напозиции5одинпутьзамыкаетсяназадпередвторымпрямоугольником,авторойпроходитдалеекпрямоугольникуспробелом.Унаспробел,анецифра,имывыбираемвторойпуть.—теперьмынапозиции6,начало“pigs”,инатройномразветвлениипутей.Встрокенет“cow”или“chicken”,затоесть“pig”,поэтомумывыбираемэтотпуть.—напозиции9послетройногоразветвления,одинпутьобходит“s”инаправляетсякпоследнемупрямоугольникусграницейслова,авторойпроходитчерез“s”.Унасесть“s”,поэтомумыидёмтуда.—напозиции10мывконцестроки,исовпастьможеттолькограницаслова.Конецстрокисчитаетсяграницей,имыпроходимчерезпоследнийпрямоугольник.Ивотмыуспешнонашлинашшаблон.
Механизмпоиска
ВыразительныйJavascript
175Регулярныевыражения
Впринципе,работаютрегулярныевыраженияследующимобразом:алгоритмначинаетвначалестрокиипытаетсянайтисовпадениетам.Внашемслучаетаместьграницаслова,поэтомуонпроходитпервыйпрямоугольник–нотамнетцифры,поэтомунавторомпрямоугольникеонспотыкается.Потомондвигаетсяковторомусимволувстроке,ипытаетсянайтисовпадениетам…Итакдалее,покаонненаходитсовпадениеилинедоходитдоконцастроки,вкакомслучаесовпадениенанайдено.
Регулярка/\b([01]+b|\d+|[\da-f]h)\b/совпадаетлибосдвоичнымчислом,закоторымследуетb,либосдесятичнымчисломбезсуффикса,либошестнадцатеричным(цифрыот0до9илисимволыотaдоh),закоторымидётh.Соответствующаядиаграмма:
Впоискахсовпаденияможетслучиться,чтоалгоритмпошёлповерхнемупути(двоичноечисло),дажеесливстрокенеттакогочисла.Еслитаместьстрока“103”,кпримеру,понятно,чтотолькодостигнувцифры3алгоритмпоймёт,чтооннанеправильномпути.Вообщестрокасовпадаетсрегуляркой,простоневэтойветке.
Откаты
ВыразительныйJavascript
176Регулярныевыражения
Тогдаалгоритмсовершаетоткат.Наразвилкеонзапоминаеттекущееположение(внашемслучае,этоначалостроки,сразупослеграницыслова),чтобыможнобыловернутьсяназадипопробоватьдругойпуть,есливыбранныйнесрабатывает.Длястроки“103”послевстречистройкойонвернётсяипопытаетсяпройтипутьдлядесятичныхчисел.Этосработает,поэтомусовпадениебудетнайдено.
Алгоритмостанавливается,кактольконайдётполноесовпадение.Этозначит,чтодажееслинескольковариантовмогутподойти,используетсятолькоодинизних(втомпорядке,вкакомонипоявляютсяврегулярке).
Откатыслучаютсяприиспользованииоператоровповторения,таких,как+и.Есливыищете/^.x/встроке«abcxe»,частьрегулярки.*попробуетпоглотитьвсюстрочку.Алгоритмзатемсообразит,чтоемунуженещёи“x”.Таккакникакого“x”послеконцастрокинет,алгоритмпопробуетпоискатьсовпадение,откатившисьнаодинсимвол.Послеabcxтоженетx,тогдаонсноваоткатывается,ужекподстрокеabc.Ипослестрочкионнаходитxидокладываетобуспешномсовпадении,напозицияхс0по4.
Можнонаписатьрегулярку,котораяприведёткомножественнымоткатам.Такаяпроблемавозникает,когдашаблонможетсовпастьсвходнымиданнымимножествомразныхспособов.Например,еслимыошибёмсяпринаписаниирегуляркидлядвоичныхчисел,мыможемслучайнонаписатьчто-товроде/([01]+)+b/.
Еслиалгоритмбудетискатьтакойшаблонвдлиннойстрокеизнолейиединиц,несодержащейвконце“b”,онсначалапройдётповнутреннейпетле,покаунегонекончатсяцифры.Тогдаонзаметит,чтовконценет“b”,сделаетоткатнаоднупозицию,пройдётповнешнейпетле,опятьсдастся,попытаетсяоткатитьсянаещёоднупозициюповнутреннейпетле…Ибудетдальшеискатьтакимобразом,задействуяобепетли.Тоесть,количествоработыскаждымсимволомстрокибудетудваиваться.Дажедлянесколькихдесятковсимволовпоисксовпадениязаймёточеньдолгоевремя.
Методreplace
ВыразительныйJavascript
177Регулярныевыражения
Устрокестьметод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Регулярныевыражения
найденныегруппы(ивсясовпадающаячастьстрокицеликом),аеёрезультатбудетвставленвновуюстроку.
Простойпример:
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Регулярныевыражения
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Регулярныевыражения
Множествоошибоквозникаетприиспользованиижадныхоператороввместонежадных.Прииспользованииоператораповторасначалавсегдарассматривайтевариантнежадногооператора.
Внекоторыхслучаяхточныйшаблоннеизвестенвовремянаписаниякода.Например,вамнадобудетискатьимяпользователявтексте,изаключатьеговподчёркивания.Таккаквыузнаетеимятолькопослезапускапрограммы,вынеможетеиспользоватьзаписьсослешами.
Новыможетепостроитьстрокуииспользоватьконструктор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,"\\$&");
ДинамическоесозданиеобъектовRegExp
ВыразительныйJavascript
181Регулярныевыражения
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Регулярныевыражения
//→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Регулярныевыражения
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Регулярныевыражения
строки,заключённыевквадратныескобки,начинаютновуюсекцию—строки,содержащиеалфавитно-цифровойидентификатор,закоторымследует=,добавляютнастройкувданнойсекции
Всёостальное–неверныеданные.
Нашазадача–преобразоватьтакуюстрокувмассивобъектов,каждыйсосвойством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Регулярныевыражения
совпадаетскомментариями,а?делаеттак,чторегуляркасовпадётисострочками,состоящимиизоднихпробелов.
Еслистрока–некомментарий,кодпроверяет,начинаетлионановуюсекцию.Еслида,онсоздаётновыйобъектдлятекущейсекции,ккоторомудобавляютсяпоследующиенастройки.
Последняяосмысленнаявозможность–строкаявляетсяобычнойнастройкой,ивэтомслучаеонадобавляетсяктекущемуобъекту.
Еслиниодинвариантнесработал,функциявыдаётошибку.
Заметьте,какчастоеиспользование^и$заботитсяотом,чтовыражениесовпадаетсовсейстрокойцеликом,анесчастью.Еслиихнеиспользовать,кодвцеломбудетработать,ноиногдабудетвыдаватьстранныерезультаты,итакуюошибкубудеттрудноотследить.
Конструкцияif(match=string.match(...))похожанатрюк,использующийприсвоениекакусловиевциклеwhile.Частовынезнаете,чтовызовmatchбудетуспешным,поэтомувыможетеполучитьдоступкрезультирующемуобъектутольковнутриблокаif,которыйэтопроверяет.Чтобнеразбиватькрасивуюцепочкупроверокif,мыприсваиваемрезультатпоискапеременной,исразуиспользуемэтоприсвоениекакпроверку.
Из-заизначальнопростойреализацииязыка,ипоследующейфиксациитакойреализации«вграните»,регуляркиJavaScriptтупятссимволами,невстречающимисяванглийскомязыке.Кпримеру,символ«буквы»сточкизрениярегулярокJavaScript,можетбытьоднимиз26букванглийскогоалфавита,ипочему-тоещёподчёркиванием.Буквытипаéилиβ,однозначноявляющиесябуквами,несовпадаютс\w(исовпадутс\W,тоестьсне-буквой).
Постранномустечениюобстоятельств,исторически\s(пробел)совпадаетсовсемисимволами,которыевUnicodeсчитаютсяпробельными,включаятакиештуки,какнеразрывныйпробелилимонгольскийразделительгласных.
Международныесимволы
ВыразительныйJavascript
186Регулярныевыражения
Унекоторыхреализацийрегуляроквдругихязыкахестьособыйсинтаксисдляпоискаспециальныхкатегорийсимволов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Регулярныевыражения
однизадачи,имогутстатьнеуправляемымиприрешениидругих,сложныхзадач.Частьуменияпользоватьсярегуляркамисостоитвтом,чтобыуметьсопротивлятьсяискушениюзапихнутьвнихзадачу,длякоторойонинепредназначены.
Неизбежноприрешениизадачувасвозникнутнепонятныеслучаи,ивыможетеиногдаотчаиваться,видянепредсказуемоеповедениенекоторыхрегулярок.Иногдапомогаетизучитьповедениерегуляркичерезонлайн-сервистипаdebuggex.com,гдеможнопосмотретьеёвизуализациюисравнитьсжелаемымэффектом.
«Гольфом»вкоденазываютигру,гденужновыразитьзаданнуюпрограммуминимальнымколичествомсимволов.Регулярныйгольф–практическоеупражнениепонаписаниюнаименьшихвозможныхрегулярокдляпоисказаданногошаблона,итолькоего.
Длякаждойизподстрочекнапишитерегуляркудляпроверкиихнахождениявстроке.Регуляркадолжнанаходитьтолькоэтиуказанныеподстроки.Неволнуйтесьнасчётграницслов,еслиэтонеупомянутоособо.Когдаувасполучитсяработающаярегулярка,попробуйтееёуменьшить.
—carиcat—popиprop—ferret,ferry,иferrari—Любоеслово,заканчивающеесянаious—Пробел,закоторымидётточка,запятая,двоеточиеилиточкасзапятой.—Словодлинеешестибукв—Словобезбуквe
//Впишитесвоирегулярки
verify(/.../,
["mycar","badcats"],
["camper","highart"]);
verify(/.../,
["popculture","madprops"],
["plop"]);
Упражнения
Регулярныйгольф
ВыразительныйJavascript
188Регулярныевыражения
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Регулярныевыражения
Последовательностицифрможнонайтипростойрегуляркой/\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Регулярныевыражения
Начинающийпрограммистпишетпрограммытак,какмуравьистроятмуравейник–покусочку,безразмышлениянадобщейструктурой.Егопрограммыкакпесок.Онимогутнедолгопростоять,новырастая,ониразваливаются.
Понявпроблему,программисттратитмноговременинаразмышленияоструктуре.Егопрограммыполучаютсяжёсткоструктурированными,каккаменныеизваяния.Онитверды,нокогдаихнужноменять,наднимиприходитсясовершатьнасилие.
Мастер-программистзнает,когданужнаструктура,акогданужнооставитьвещивпростомвиде.Егопрограммысловноглина–твёрдые,ноподатливые.
МастерЮан-Ма,Книгапрограммирования
Укаждойпрограммыестьструктура.Вчастностяхонаопределяетсятем,какпрограммистделиткоднафункциииблокивнутриэтихфункций.Программистывольнывсозданииструктурысвоейпрограммы.Структураопределяетсябольшевкусомпрограммиста,нежелифункциональностьюпрограммы.
Вслучаебольшихпрограммотдельныефункцииужетеряютсявкоде,инамнеобходимаединицаорганизациикодабольшихмасштабов.Модулигруппируютпрограммныйкодпокаким-тоопределённымпризнакам.ВэтойглавемырассмотримпреимуществатакогоделенияитехникисозданиямодулейвJavaScript.
Естьнесколькопричин,покоторымавторыделятсвоикнигинаглавыисекции.Этопомогаетчитателюпонять,какпостроенакнига,инайтинужныеимчасти.Авторуэтопомогаетконцентрироватьсянакаждойконкретнойчасти.Преимуществаорганизациипрограммвнесколькихфайлах,или
Модули
Зачемнужнымодули
ВыразительныйJavascript
191Модули
модулях,примернотеже.Структуризацияпомогаетнезнакомымскодомлюдямнайтито,чтоимнужно,ипомогаетпрограммистамхранитьсвязанныедругсдругомвещиводномместе.
Некоторыепрограммыорганизованыпомоделиобычноготекста,гдепорядокследованиячёткоопределён,игдечитателюпредлагаетсяпоследовательноеизучениепрограммыимножествопрозы(комментариев)дляописаниякода.Этоделаетчтениекодаменеепугающим(ачтениечужогокодаобычнопугает),нотребуетбольшихусилийприсозданиикода.Такжетакуюпрограммусложнееменять,потомучточастипрозысвязанымеждусобойсильнее,чемчастикода.Этотстильназываетсялитературнымпрограммированием.Теглавыкниги,вкоторыхобсуждаютсяпроекты,можносчитатьлитературнымкодом.
Обычноструктурированиечего-либотребуетзатратэнергии.Нараннихстадияхпроекта,когдавыещёнеуверены,чтогдебудет,икакиемодуливообщенужны,япропагандируюбесструктурнуюминималистическуюорганизациюкода.Просторазмещайтевсечаститам,гдеудобно,покакоднестабилизируется.Такимобразомнепридётсятратитьвремянаперестановкукусковпрограммы,ивынепоймаетесебявтакуюструктуру,котораянеподходитдлявашейпрограммы.
УбольшинствасовременныхЯПестьпромежуточныеобластивидимости(ОВ)междуглобальной(видновсем)илокальной(виднотолькоэтойфункции).УJavaScriptтакогонет.Поумолчанию,всё,чтонеобходимовидетьснаружифункцииверхнегоуровня,находитсявглобальнойОВ.
Загрязнениепространстваимён(ПИ),когданесвязанныедругсдругомчастикодаделятодиннаборпеременных,упоминаласьвглаве4.ТамобъектMathбылприведёнвкачествепримераобъекта,которыйгруппируетфункциональность,связаннуюсматематикой,ввидемодуля.
ХотяJavaScriptнепредлагаетнепосредственноконструкциидлясозданиямодуля,мыможемиспользоватьобъектыдлясозданияподпространствимён,доступныхотовсюду.Афункцииможноиспользоватьдлясоздания
Пространствоимён
ВыразительныйJavascript
192Модули
изолированныхчастныхпространствимёнвнутримодуля.Чутьдальшемыобсудимспособпостроениядостаточноудобныхмодулей,изолирующихПИприпомощибазовыхконцепцийязыка.
Впроекте,неразбитомнамодули,непонятно,какиечастикоданеобходимыдляконкретнойфункции.Вмоейпрограмме,шпионящейзаврагами(глава9),янаписалфункциючтенияфайловснастройками.Еслимнепонадобитсяиспользоватьеёвдругомпроекте,ядолженбудускопироватьчастистаройпрограммы,которыевродебысвязанысэтойфункцией,вмойновыйпроект.Аеслиянайдутамошибку,яеёисправлютольковтомпроекте,надкоторымработаювданныймомент,изабудуисправитьеёвовсехостальных.
Когдаувасбудетмножествотакихсдублированныхкусковкода,выобнаружите,чтотратитекучувремениисилнаихкопированиеиобновление.Еслиразместитьсвязанныемеждусобойчастипрограммвотдельныемодули,ихбудетпрощеотслеживать,исправлятьиобновлять,потомучтовезде,гдеэтотфункционалпотребуется,высможетепростозагрузитьэтотмодульизфайла.
Этуидеюможноиспользоватьещёлучше,есличёткопрописатьвзаимоотношенияразныхмодулей–ктооткогозависит.Тогдаможноавтоматизироватьпроцессустановкииобновлениявнешнихмодулей(библиотек).
Еслиещёразвитьидею–представьтесебеонлайн-сервис,которыйотслеживаетираспространяетсотнитысячтакихбиблиотек,ивыможетеискатьнужнуювамфункциональностьсрединих,акогданайдёте–вашпроектавтоматическискачаетеё.
Итакойсервисесть!ОнназываетсяNPM(npmjs.org).NPM–онлайн-базамодулейиинструментдляскачиванияиапгрейдамодулей,откоторыхзависитвашапрограмма.онвыросизNode.js,окруженияJavaScript,нетребующегобраузера,котороемыобсудимвглаве20,нотакжеможетиспользоватьсяивбраузерныхпрограммах.
Повторноеиспользование
ВыразительныйJavascript
193Модули
Ещёодназадачамодулей–изолироватьнесвязанныемеждусобойчастикодатак,какэтоделаютинтерфейсыобъектов.Хорошопродуманныймодульпредоставляетинтерфейсдляегоиспользованиявовне.Когдамодульобновляютилиисправляют,существующийинтерфейсостаётсянеизменным,чтобыдругиемодулимоглииспользоватьновую,обновлённуюверсиюбезизмененийвнихсамих.
Стабильныйинтерфейснеозначает,чтовнегонедобавляютновыефункции,методыилипеременные.Главное,чтосуществующаяфункциональностьнеудаляетсяиеёсмыслнеменяется.Хорошийинтерфейспозволяетмодулюрасти,неломаястарыйинтерфейс.Аэтозначит–выставлятьнаружукакможноменьшевнутреннейкухнимодуля,приэтомязыкинтерфейсадолженбытьдостаточногибкимимощнымдляприменениявразличныхситуациях.
Интерфейсы,выполняющиепростуюзадачу,вродечтениянастроекизфайла,выходяттакимиестественнымобразом.Длядругих–кпримеру,дляредакторатекстов,укоторогоестьмножестворазныхаспектов,требующихдоступаизвне(содержимое,стили,действияпользователяит.п.)интерфейснеобходимоскрупулёзнопродумывать.
Функции–единственнаявещьвJavaScript,создающаяновуюобластьвидимости.Еслинамнужно,чтобыумодулейбыласвояобластьвидимости,придётсяосновыватьихнафункциях.
Обратитевниманиенапростейшиймодуль,связывающийименасномерамиднейнедели–какделаетметодgetDayобъектаDate.
varnames=["Понедельник","Вторник","Среда","Четверг","Пятница","Суббота","Воскресенье"];
functiondayName(number){
returnnames[number];
}
Устранениесвязей(Decoupling)
Использованиефункцийвкачествепространствимён
ВыразительныйJavascript
194Модули
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Модули
глобальнойОВиспользуемымиимпеременными.
Азачеммызаключилифункциювкруглыескобки?Этосвязаносглюкомсинтаксиса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Модули
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Модули
строкикаккод.
Естьнесколькоспособовполучитьданные(строкукода)ивыполнитьихкакчастьтекущейпрограммы.
Самыйочевидный–оператор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Модули
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Модули
экспортироватьтолькоконструкторобъекта,объявленноговнём.Сейчасэтоневозможно,поскольку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Модули
Значит,еслибыкаждыйвызов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Модули
естьфункция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Модули
толькораз,когдапоследняязависимостьзагрузится.Онатакжевызываетсясразуизсамого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Модули
НастоящаяреализацияAMDгораздоумнееподходиткпревращениюимёнмодулейвURLиболеенадёжна,чемпоказановпримере.ПроектRequireJSпредоставляетпопулярнуюреализациютакогостилязагрузчикамодулей.
Разработкаинтерфейсов–одинизсамыхтонкихмоментоввпрограммировании.Любуюнетривиальнуюфункциональностьможнореализоватьмножествомспособов.Поискработающегоспособатребуетпроницательностиипредусмотрительности.
Лучшийспособпознатьзначимостьхорошегоинтерфейса–использоватьмногоинтерфейсов.Некоторыебудутплохие,некоторыехорошие.Опытпокажетвам,чтоработает,ачто–нет.Никогданепринимайтекакдолжноеплохойинтерфейс.Исправьтеего,илизаключитевдругойинтерфейс,которыйлучшевамподходит.
Еслипрограммистможетпредсказать,какработаетвашинтерфейс,емунепридётсячастоотвлекатьсяисмотретьподсказкупоегоиспользованию.Постарайтесьследоватьобщепринятымсоглашениям.ЕслиестьмодульиличастьязыкаJavaScript,котораяделаетчто-топохожеенато,чтовыпытаетесьреализовать–будетнеплохо,есливашинтерфейсбудетнапоминатьсуществующий.Такимобразом,онбудетпривычендлялюдей,знакомыхссуществующиминтерфейсом.
Вповедениивашегокодапредсказуемостьтакжеважна.Васможетпостичьискушениесделатьинтерфейсслишкомзаумнымякобыпотому,чтоегоудобнееиспользовать.Кпримеру,выможетеприниматьлюбыевидытиповикомбинацийаргументовипроделыватьсними«то,чтонадо».Илипредоставлятьдесяткиспециализированныхфункций,которыепредлагаютнезначительноотличающуюсяфункциональность.Этоможетсделатькод,опирающийсянавашинтерфейс,немногокороче,затозатруднитьлюдям,работающимсним,строитьчёткуюмысленнуюмодельработывашегомодуля.
Разработкаинтерфейса
Предсказуемость
ВыразительныйJavascript
204Модули
Старайтесьиспользоватьвинтерфейсахнастолькопростыеструктурыданных,насколькоэтовозможно.Делайтетак,чтобыфункциивыполнялипростыеипонятныевещи.Еслиэтоприменимо,делайтефункциичистыми(см.Главу3).
Кпримеру,частенькомодулипредлагаютсвоюверсиюмассивоподобныхколлекцийобъектовсосвоиминтерфейсомдляподсчётаиизвлеченияэлементов.УтакихобъектовнетметодовmapилиforEach,иникакаяфункция,ожидающаянастоящиймассив,несможетснимиработать.Этопримерплохойкомпонуемости–модульнельзялегкоскомпоноватьсдругимкодом.
Примеромможетслужитьмодульдляорфографическойпроверкитекста,которыйможетпригодитьсявтекстовомредакторе.Проверочныймодульможносделатьтаким,чтобыонработалслюбымисложнымиструктурами,используемымисамимредактором,ивызывалвнутренниефункцииредакторадляпредоставленияпользователювыборавариантовнаписания.Есливыпоступитетакимобразом,модульнельзябудетиспользоватьсдругимипрограммами.Сдругойстороны,еслимыопределиминтерфейсмодуляпроверки,которыйпринимаетпростуюстрокуивозвращаетпозицию,накоторойвстрокеестьвозможнаяошибка,авпридачу–массивпредлагаемыхпоправок,тогдаунасбудетинтерфейс,которыйможноскомпоноватьсдругимисистемами,потомучтострокиимассивывсегдадоступнывJavaScript.
Разрабатываяинтерфейсдлясложнойсистемы(кпримеру,отправкаемейл),частоприходишькдилемме.Соднойстороны,ненужноперегружатьпользователяинтерфейсадеталями.Ненадозаставлятьихизучатьего20минутпередтем,каконисмогутотправитьемейл.Сдругойстороны,нехочетсяипрятатьвседетали–когдалюдямнадосделатьчто-тосложноеприпомощивашегомодуля,унихдолжнабытьтакаявозможность.
Компонуемость
Многослойныеинтерфейсы
ВыразительныйJavascript
205Модули
Частоприходитсяпредлагатьдваинтерфейса:детализированныйнизкоуровневыйдлясложныхситуаций,ипростойвысокоуровневыйдляобычногоиспользования.Второйможнопостроитьнаосновепервого.Вмодуледляотправкиемейловвысокоуровневыйинтерфейсможетбытьпростофункцией,котораяпринимаетсообщение,адресполучателяиотправителя,иотправляетписьмо.Низкоуровневыйдолжендаватьдоступкзаголовкам,приложеннымфайлам,HTMLписьмамит.д.
Модулипозволяютструктурироватьбольшиепрограммы,разделяякодпоразнымфайламипространствамимён.Еслиобеспечитьиххорошоразработаннымиинтерфейсами,ихбудетпростоиспользовать,применятьвдругихпроектахипродолжатьиспользоватьприразвитиииэволюциисамогопроекта.
ХотяJavaScriptсовершеннонепомогаетделатьмодули,егогибкиефункциииобъектыпозволяютсделатьдостаточнонеплохуюсистемумодулей.Областьвидимостифункцийиспользуетсякаквнутреннеепространствоимёнмодуля,аобъектыиспользуютсядляхранениянаборовпеременных.
Естьдвапопулярныхподходакиспользованиюмодулей.Один–CommonJS,построенныйнафункцииrequire,котораявызываетмодулипоимениивозвращаетихинтерфейс.Другой–AMD,использующийфункциюdefine,принимающуюмассивимёнмодулейи,послеихзагрузки,исполняющуюфункцию,аргументамикоторойявляютсяихинтерфейсы.
Напишитепростоймодультипаweekday,преобразующийномерамесяцев(начинаяснуля)вназванияиобратно.Выделитеемусобственноепространствоимён,т.к.емупотребуетсявнутренниймассивсназваниямимесяцев,ииспользуйтечистыйJavaScript,безсистемызагрузкимодулей.
Итог
Упражнения
Названиямесяцев
ВыразительныйJavascript
206Модули
//Вашкод
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Модули
Запутаннаятемавуправлениизависимостями–круговыезависимости,когдамодульАзависитотБ,аБзависитотА.Многиесистемымодулейэтопростозапрещают.МодулиCommonJSдопускаютограниченныйвариант:этоработает,покамодулинезаменяютобъектexports,существующийпо-умолчанию,другимзначением,иначинаютиспользоватьинтерфейсыдругдругатолькопослеокончаниязагрузки.
Можетеливыпридуматьспособ,которыйпозволилбывоплотитьсистемуподдержкитакихзависимостей?Посмотритенаопределениеrequireиподумайте,чтонужносделатьэтойфункциидляэтого.
Круговыезависимости
ВыразительныйJavascript
208Модули
То,чтопроверяетиопределяетсмыслвыраженийвязыкепрограммирования,являетсявсвоюочередьпростопрограммой.
ХэлАбельсониЖеральдСасман,«Структураиинтерпретациякомпьютерныхпрограмм».
КогдаучениеспросилучителяоприродециклаДанныхиКонтроля,Юань-Маответил:«Подумайокомпиляторе,компилирующемсамогосебя».
МастерЮань-Ма,«Книгапрограммирования»
Создатьсвойязыкпрограммированияудивительнолегко(покавынеставитезапредельныхцелей)идовольнопоучительно.
Главное,чтояхочупродемонстрироватьвэтойглаве–впостроенииязыканетникакоймагии.Мнечастоказалось,чтонекоторыечеловеческиеизобретениянастолькосложныизаумны,чтомнеихникогданепонять.Однакопосленебольшогосамообразованияиковыряниятакиештукичастооказываютсядовольнообыденными.
МыпостроимязыкпрограммированияEgg(Яйцо).Онбудетнебольшим,простым,нодостаточномощнымдлявыражениялюбыхрасчётов.Онтакжебудетосуществлятьпростыеабстракции,основанныенафункциях.
То,чтолежитнаповерхностиязыка–синтаксис,запись.Грамматическийанализатор,илипарсер–программа,читающаякусоктекстаивыдающаяструктуруданных,описывающуюструктурыпрограммы,содержавшейсявтексте.Еслитекстнеописываеткорректнуюпрограмму,парсердолженпожаловатьсяиуказатьнаошибку.
Унашегоязыкабудетпростойиоднородныйсинтаксис.ВEggвсёбудетявлятьсявыражением.Выражениеможетбытьпеременной,число,строка
Проект:языкпрограммирования
Разбор(parsing)
ВыразительныйJavascript
209Проект:языкпрограммирования
илиприложение.Приложенияиспользуютсядлявызовафункцийиконструкцийтипа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Проект:языкпрограммирования
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Проект:языкпрограммирования
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Проект:языкпрограммирования
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Проект:языкпрограммирования
моглобыпригодитьсяприразбореошибок–нодлянасиэтогохватит.
Ачтонамделатьссинтаксическимдеревомпрограммы?Запускатьеё!Этимзанимаетсяинтерпретатор.Выдаётеемусинтаксическоедеревоиобъектокружения,которыйсвязываетименасозначениями,аонинтерпретируетвыражение,представляемоедеревом,ивозвращаетрезультат.
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Проект:языкпрограммирования
функцию,обрабатывающуюформу.Еслиэтопростойвызов,мыинтерпретируемоператор,проверяем,чтоэтофункцияивызываемегосрезультатоминтерпретацииаргументов.
Дляпредставлениязначенийфункций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Проект:языкпрограммирования
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Проект:языкпрограммирования
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Проект:языкпрограммирования
функциюиназовёмеё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Проект:языкпрограммирования
Языкпрограммированиябезфункций–плохойязык.
Ксчастью,несложнодобавитьконструкцию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Проект:языкпрограммирования
"print(pow(2,10)))");
//→1024
Мысвамипостроилиинтерпретатор.Вовремяинтерпретациионработаетспредставлениемпрограммы,созданнымпарсером.
Компиляция–добавлениеещёодногошагамеждупарсеромизапускомпрограммы,котораяпревращаетвпрограммувнечто,чтоможновыполнятьболееэффективно,путёмпроделываниябольшинстваработызаранее.Кпримеру,вхорошоорганизованныхязыкахприкаждомиспользованиипеременнойочевидно,ккакойпеременнойобращаются,дажебеззапускапрограммы.Этоможноиспользовать,чтобынеискатьпеременнуюпоименикаждыйраз,когдакнейобращаются,анапрямуювызыватьеёизкакой-тозаранееопределённойобластипамяти.
Потрадициикомпиляциятакжепревращаетпрограммувмашинныйкод–сыройформат,пригодныйдляисполненияпроцессором.Нокаждыйпроцесспревращенияпрограммывдругойвид,посути,являетсякомпиляцией.
МожнобылобысоздатьдругойинтерпретаторEgg,которыйсначалапревращаетпрограммувпрограммунаязыкеJavaScript,используетnewFunctionдлявызовакомпилятораJavaScriptивозвращаетрезультат.ПриправильнойреализацииEggвыполнялсябыоченьбыстроприотносительнопростойреализации.
Есливамэтоинтересно,ивыхотитепотратитьнаэтовремя,япоощряюваспопробоватьсделатьтакойкомпиляторвкачествеупражнения.
Когдамыопределялиifиwhile,вымоглизаметить,чтоонипредставлялисобойпростыеобёрткивокругifиwhileвJavaScript.ЗначениявEgg–такжеобычныезначенияJavaScript.
СравниваяреализациюEgg,построеннуюнаJavaScript,собъёмомработы,
Компиляция
Мошенничество
ВыразительныйJavascript
220Проект:языкпрограммирования
необходимойдлясозданияязыкапрограммированиянепосредственнонамашинномязыке,торазницастановитсяогромной.Темнеменее,этотпример,надеюсь,даётвампредставлениеоработеязыковпрограммирования.
Икогдавамнадочто-тосделать,смошенничатьбудетболееэффективно,нежелиделатьвсёснулясамому.ИхотяигрушечныйязыкничемнелучшеJavaScript,внекоторыхситуацияхнаписаниесвоегоязыкапомогаетбыстреесделатьработу.
ТакойязыкнеобязаннапоминатьобыныйЯП.ЕслибыJavaScriptнесодержалрегулярныхвыражений,вымоглибынаписатьсвоипарсериинтерпретатордлятакогосуб-языка.
Илипредставьте,чтовыстроитегигантскогоробота-динозавраивамнужнозапрограммироватьегоповедение.JavaScript–несамыйэффективныйспособсделатьэто.Можновместоэтоговыбратьязыкпримернотакогосвойства:
behaviorwalk
performwhen
destinationahead
actions
moveleft-foot
moveright-foot
behaviorattack
performwhen
Godzillain-view
actions
firelaser-eyes
launcharm-rockets
Обычноэтоназываютязыкомдлявыбраннойобласти(domain-specificlanguage)–язык,специальнопредназначенныйдляработывузкомнаправлении.Такойязыкможетбытьболеевыразительным,чемязыкобщегоназначения,потомучтоонразработандлявыраженияименнотехвещей,которыенадовыразитьвэтойобласти–ибольшеничего.
Упражнения
ВыразительныйJavascript
221Проект:языкпрограммирования
Добавьтеподдержкумассивовв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Проект:языкпрограммирования
Хорошобылобыиметькомментариив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Проект:языкпрограммирования
сегомоментабывшаяудобной,теперьбудетваммешать.Вамможетпонадобитьсяфункция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Проект:языкпрограммирования
Браузер–крайневраждебнаяпрограммнаясреда
ДугласКрокфорд,«ЯзыкпрограммированияJavaScript»(видеолекция)
Следующаячастькнигирасскажетовеб-браузерах.БезнихнебылобыJavaScript.Аеслибыибыл,никтобынеобратилнанеговнимания.
Технологиивебассамогоначалабылидецентрализованными–нетолькотехнически,ноисточкизренияихэволюции.Различныеразработчикибраузеровдобавлялиновуюфункциональность«послучаю»,непродуманно,ичастоэтафункциональностьобреталаподдержкувдругихбраузерахистановиласьстандартом.
Этоиблагословлениеипроклятие.Соднойстороны,здоровонеиметьконтролирующегоцентра,чтобытехнологияразвиваласьразличнымисторонами,иногдасотрудничающими,иногдаконкурирующими.Сдругой–бессистемноеразвитиеязыкапривелоктому,чторезультатнеявляетсяяркимпримеромвнутреннейсогласованности.Некоторыечастипривносятпутаницуибеспорядок.
Компьютерныесетипоявилисьв1950-х.Есливыпроложитекабельмеждудвумяилинесколькимикомпьютерамииразрешитеимпередаватьданные,выможетделатьмногоудивительныхвещей.Аеслисвязьдвухмашинводномзданиипозволяетделатьмногоразного,тосвязькомпьютеровповсейпланетедолжнапозволятьещёбольше.Технология,позволяющаяэтосделать,быласозданав1980-х,иполучившаясясетьзовётсяинтернетом.Ионаоправдалаожидания.
Компьютерможетиспользоватьэтусеть,чтобыкидатьсябитамивдругойкомпьютер.Чтобыобщениевышлоэффективным,обакомпьютерадолжнызнать,чтоэтибитыозначают.Значениелюбойзаданнойпоследовательностибитовзависитоттого,чтопытаютсяимивыразить,икакоймеханизм
JavaScriptибраузер
Сетииинтернет
ВыразительныйJavascript
225JavaScriptибраузер
кодированияиспользуется.
Стильобщенияпосетиописываетсетевойпротокол.Естьпротоколыдляотправкие-мейлов,дляполученияе-мейлов,дляраспространенияфайловидажедляконтролянадкомпьютерами,заражённымивредоноснымсофтом.
Кпримеру,простойпротоколчатаможетсостоятьизодногокомпьютера,отправляющегобиты,представляющиетекст«ЧАТ?»надругой,авторогоотвечающеготекстом«ОК!»,дляподтверждениятого,чтоонпонялпротокол.Дальшеонимогутперейтикотправкедругдругутекстов,чтенияполученныхтекстовивыводаихнаэкран.
Большинствопротоколовпостроенонаосноведругихпротоколов.Нашпротоколчатаизпримерарассматриваетсетькакпотоковоеустройство,вкотороеможновводитьбитыизаказыватьихприходнаконкретныйадресвправильномпорядке.Аобеспечениеэтогопроцесса–самопосебеявляетсясложнойзадачей.TransmissionControlProtocol(TCP)–протокол,решающийэтузадачу.Всеустройства,подключённыекинтернету,говорятнанём,ибольшинствообщениявинтернетепостроенонаегооснове.
СоединениепоTCPработаеттак:одинкомпьютерждёт,или«слушает»,покадругиененачнутснимговорить.Чтобыможнобылослушатьразныевидыобщенияводноитожевремя,длякаждогоизнихназначаетсяномер(называемыйпортом).Большинствопротоколовустанавливаютпорт,используемыйпоумолчанию.Кпримеру,еслимыотправляеме-мейлпопротоколуSMTP,компьютер,черезкоторыймыегошлём,долженслушатьпорт25.
Тогдадругойкомпьютерможетустановитьсоединение,связавшисьскомпьютеромназначенияпоправильномупорту.Еслимашинаназначениядоступна,ионаслушаетэтотпорт,соединениеустанавливается.Слушающийкомпьютерзовётсясервером,асоединяющийся–клиентом.
Такоесоединениеработаеткакдвусторонняятруба,покоторойтекутбиты–обемашинымогутпомещатьвнеёданные.Когдабитыпереданы,другаямашинаможетихпрочесть.Этоудобнаямодель.Можносказать,чтоTCPобеспечиваетабстракциюсети.
ВыразительныйJavascript
226JavaScriptибраузер
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ибраузер
спрашиваетегоресурспоимени/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ибраузер
Естьещё
–,заголовкиразныхразмеров)идвапараграфа.
Утеговможетбытьнесколькоформ.Элементвродетела,параграфаиссылкиначинаетсяоткрывающимтегом
изаканчиваетсязакрывающим
.Некоторыеоткрывающиетеги,типассылки,содержатдополнительнуюинформациюввидеимя=”значение”.Онаназывается«атрибутами».Внашемслучаеадресссылкизаданкакhref="http://eloquentjavascript.net",гдеhrefозначает«гипертекстоваяссылка»,“hypertextreference”.
Некоторыетегиничегонеокружают,иихненадозакрывать.Пример–тегкартинки
<imgsrc="http://example.com/image.jpg">
Чтобывключатьвтекстдокументаугловыескобки,нужнопользоватьсяспециальнойзаписью,таккаквHTMLониимеютособоезначение.Открывающаяскобка(онажезнак«меньше»)записываетсякак<(«lessthan»,«меньше,чем»),закрывающая—>(“greaterthat”,«больше,чем»).ВHTMLамперсанд&,закоторымидётсловоиточкасзапятой,зовётсясущностьюизаменяетсясимволом,которыйкодируетсяэтойпоследовательностью.
Этопохоженаобратныеслэши,используемыевстрокахJavaScript.Из-заспециальногозначенияамперсандаегосамоговтекстможновключатьввиде&.Ватрибуте,заключаемомвдвойныекавычки,символкавычекзаписываетсякак".
HTMLразбираетсяпарсеромдовольнолиберальнопоотношениюквозможнымошибкам.Есликакие-тотегипропущены,браузерихвоссоздаёт.Какименноэтопроисходит,записановстандартах,поэтомуможноожидать,чтовсесовременныебраузерыбудутделатьэтоодинаково.
ВыразительныйJavascript
229JavaScriptибраузер
ВконтекстенашейкнигисамыйглавныйтегHTML—
Следующийдокументбудетобработантакже,какипредыдущий.
<!doctypehtml>
<title>Моядомашняястраничка</title>
<h1>Моядомашняястраничка</h1>
<p>Привет,яМарийниэтомоядомашняястраничка.
<p>Аещёякнижкунаписал!Читайтееё
<ahref=http://eloquentjavascript.net>here</a>.
Отсутствуюттеги,и.Браузерзнает,что
Вэтойкнигемыопустимтеги,идлякраткости.Ноябудузакрыватьтеги,изаключатьатрибутывкавычки.
Такжеобычноябудуопускатьdoctype.Янесоветуюделатьэтовам–браузерыиногдамогуттворитьстранныевещи,когдавыихопускаете.Считайте,чтоониприсутствуютвпримерахпоумолчанию.
HTMLиJavaScript
ВыразительныйJavascript
230JavaScriptибраузер
Когдавыоткрываетевеб-страницувбраузере,онполучаетисходныйтекст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
Структураданных,использующаясябраузеромдляпредставлениядокумента,отражаетегоформу.Длякаждойкоробкиестьобъект,скоторыммыможемвзаимодействоватьиузнаватьпронегоразныеданные–какойтегонпредставляет,какиекоробкиитекстсодержит.ЭтопредставлениеназываетсяDocumentObjectModel(объектнаямодельдокумента),илисокращённоDOM.
Мыможемполучитьдоступкэтимобъектамчерезглобальнуюпеременнуюdocument.ЕёсвойствоdocumentElementссылаетсянаобъект,представляющийтег.Онтакжепредоставляетсвойстваheadиbody,вкоторыхсодержатсяобъектыдлясоответствующихэлементов.
Вспомнитесинтаксическиедеревьяизглавы11.Ихструктураудивительнопохожанаструктурудокументабраузера.Каждыйузелможетссылатьсянадругиеузлы,укаждогоизответвленийможетбытьсвоёответвление.Этаструктура–типичныйпримервложенныхструктур,гдеэлементысодержатподэлементы,похожиенанихсамих.
Деревья
ВыразительныйJavascript
232DocumentObjectModel
Мызовёмструктуруданныхдеревом,когдаонаразветвляется,неимеетциклов(узелнеможетсодержатьсамсебя),иимеетединственныйярковыраженный«корень».Вслучае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
Листья–текстовыеузлы,астрелкипоказываютвзаимоотношенияотец-ребёнокмеждуузлами.
Использоватьзагадочныецифрыдляпредставлениятипаузла–этоподходневстилеJavaScript.ПозжемывстретимсясдругимичастямиинтерфейсаDOM,которыетожекажутсячуждымиинескладными.Причинавтом,чтоDOMразрабатывалсянетолькодляJavaScript.Онпытаетсяопределитьинтерфейс,независящийотязыка,которыйможноиспользоватьивдругихсистемах–нетольковHTML,ноивXML,которыйпредставляетизсебяформатданныхобщегоназначенияссинтаксисом,напоминающимHTML.
Получаетсянеудобно.Хотястандарты–ивесьмаполезнаяштука,внашемслучаепреимуществонезависимостиотязыканетакоеужиполезное.Лучшеиметьинтерфейс,хорошоприспособленныйкязыку,которыйвыиспользуете,чеминтерфейс,которыйбудетзнакомприиспользованииразныхязыков.
Чтобыпоказатьнеудобнуюинтеграциюсязыком,рассмотримсвойствоchildNodes,котороеестьуузловDOM.Внёмсодержитсяобъект,похожийнамассив,сосвойствомlength,ипронумерованныесвойствадлядоступакдочернимузлам.Ноэто–экземпляртипаNodeList,ненастоящиймассив,поэтомуунегонетметодоввродеforEach.
Естьтакжепроблемы,связанныесплохойпродуманностьюсистемы.К
Стандарт
ВыразительныйJavascript
234DocumentObjectModel
примеру,нельзясоздатьновыйузелисразудобавитькнемусвойстваилидочерниеузлы.Сначаланужноегосоздать,затемдобавитьдочерниепоодному,ивконценазначитьсвойствапоодному,сиспользованиемпобочныхэффектов.Код,плотноработающийсDOM,получаетсядлинным,некрасивымисомножествомповторов.
Ноэтипроблемынефатальные.JavaScriptпозволяетсоздаватьабстракции.Легконаписатьвспомогательныефункции,позволяющиевыражатьоперацииболеепонятноикоротко.Вообще,такогородаинструментыпредоставляютмногобиблиотек,направленныхнапрограммированиедлябраузера.
УзлыDOMсодержатмногоссылокнасоседние.Этопоказанонадиаграмме:
Хотятутпоказанотолькопооднойссылкекаждоготипа,укаждогоузлаестьсвойствоparentNode,указывающегонаегородительскийузел.Такжеукаждогоузла-элемента(тип1)естьсвойствоchildNodes,указывающеенамассивоподобныйобъект,содержащийегодочерниеузлы.
Втеорииможнопройтивлюбуючастьдерева,используятолькоэтиссылки.НоJavaScriptпредоставляетнаммногодополнительныхвспомогательныхссылок.СвойстваfirstChildиlastChildпоказываютнапервыйипоследнийдочернийэлементы,илисодержатnullутехузлов,укоторыхнетдочерних.
Обходдерева
ВыразительныйJavascript
235DocumentObjectModel
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
программечто-товроде:«второйребёнокшестогоребёнка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
<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
</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
}
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
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
}
}
</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
<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
}
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
Текстпоказан<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
}
</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
всемтегам<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
необходимодинконкретныйэлемент.Онвернёттолькопервоесовпадение,или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
Скриптиспользует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
Анимациякотахранитсчётчикangleдлятекущегоуглаповоротаанимации,иувеличиваетегопропорциональнопрошедшемувременикаждыйразпривызовефункцииanimation.Этотуголиспользуетсядляподсчётатекущейпозицииэлементаimage.СтильtopподсчитываетсячерезMath.sinиумножаетсяна20–этовертикальныйрадиуснашегоэллипса.СтильleftсчитаетсячерезMath.cosиумножаетсяна200,такчтоширинаэллипсасильнобольшевысоты.
Стилямобычнотребуютсяединицыизмерения.Внашемслучаеприходитсядобавлятьpxкчислу,чтобыобъяснитьбраузеру,чтомысчитаемвпикселях(аневсантиметрах,emsилидругихединицах).Этолегкозабыть.Использованиечиселбезединицизмеренияприведёткигнорированиюстиля–еслитолькочислонеравно0,чтонезависитотединицизмерения.
ПрограммыJavaScriptмогутизучатьиизменятьтекущийотображаемыйбраузеромдокументчерезструктуруподназваниемDOM.Этаструктураданныхпредставляетмодельдокументабраузера,апрограммаJavaScriptможетизменятьеёдляизменениявидимогодокумента.DOMорганизованввидедерева,вкоторомэлементырасположеныиерархическивсоответствиисоструктуройдокумента.УобъектовэлементовестьсвойстватипаparentNodeиchildNodes,которыиспользуютсядляориентированиянадереве.
Использованиесинусаикосинусадлявычислениякоординат
Итог
ВыразительныйJavascript
250DocumentObjectModel
Внешнийвиддокументаможноизменятьчерезстили,либодобавляястиликузламнапрямую,либоопределяяправиладлякаких-либоузлов.Устилейестьоченьмногосвойств,таких,как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
Когдавыразберётесьсосновами,выровняйтеячейкисчисламипоправомукраю,изменивихсвойство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
</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
Вывластнынадсвоимразумом,ноненадвнешнимисобытиями.Когдавыпоймётеэто,выобретётесилу.МаркАврелий,«Медитации».
Некоторыепрограммыработаютсвводомпользователя,мышьюиклавиатурой.Времявозникновениятакоговводаипоследовательностьданныхнельзяпредсказатьзаранее.Этотребуетиногоподходакконтролюнадпорядкомвыполненияпрограммы,чемужепривычныйнам.
Представьтеинтерфейс,вкоторомединственнымспособомузнать,нажалилинакнопкуклавиатуры,былобысчитываниетекущегосостояниякнопки.Чтобыреагироватьнанажатия,вампришлосьбыпостоянносчитыватьсостояниякнопок,чтобывымоглипойматьэтосостояние,покакнопканеотжалась.Былобыопаснопроводитьдругиеподсчёты,отнимающиепроцессорноевремя,таккакможнобылобыпропуститьмоментнажатия.
Такимобразомвводобрабатывалсянапримитивныхустройствах.Шагомвперёдбылобы,еслижелезоилиоперационказамечалибынажатиекнопкиипередавалибыеговочередь.Затемпрограммапериодическимоглабыпроверятьочередьнановыесобытияиреагироватьнато,чтонаходитсявочереди.
Разумеется,онадолжнапомнитьопроверке,иделатьэтодостаточночасто,потомучтоналичиедлительногопромежуткавременимеждунажатиемкнопкиитем,когдапрограммазамечаетиреагируетнаэто,ведётквосприятиюэтойпрограммыкакмедленноработающей.Такойподходиспользуетсядостаточноредко.
Вариантполучше–некаяпромежуточнаясистема,котораяпозволяеткодуреагироватьнасобытиявмоментихвозникновения.Браузерыпозволяютэтоделатьпутёмрегистрациифункцийкакобработчиковзаданныхсобытий.
Обработкасобытий
Обработчикисобытий
ВыразительныйJavascript
254Обработкасобытий
<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Обработкасобытий
<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Обработкасобытий
некоторыесобытия,случившиесясихдетьми.Есликликнутьнакнопкувнутрипараграфа,обработчикисобытийпараграфаполучатсобытие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Обработкасобытий
содержащийдлинныйсписоккнопок,былобыудобнеезарегистрироватьодинобработчиксобытийдляузла,ивнёмвыяснять,нажалилинакнопку–вместотого,чтобырегистрироватьобработчикикаждойкнопкипоотдельности.
<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Обработкасобытий
вашейстраницыбудеточеньнеудобно,когдаонистолкнутсяснеожиданнымирезультатамисвоихдействий.Взависимостиотбраузера,некоторыесобытияперехватитьнельзя.В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Обработкасобытий
//→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&&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Обработкасобытий
получитьфокусввода(еслитольковынезадалииматрибут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Обработкасобытий
</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Обработкасобытий
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Обработкасобытий
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Обработкасобытий
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Обработкасобытий
Приполученииэлементомфокусабраузерзапускаетсобытие“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Обработкасобытий
Уэлементовтипакартинокилитеговскрипта,которыезагружаютвнешнийфайл,тожеестьсобытие“load”,котороепоказывает,чтофайлзагружен.Какисобытияфокуса,событиязагрузкинераспространяются.
Когдастраницазакрываетсяилиснеёуходят(например,поссылке),запускаетсясобытие«beforeunload».Основнаяцель–защититьпользователяотслучайнойпотериданныхпризакрытиидокумента.Предотвращениезакрытиястраницынепроизводится,каквымоглиподумать,припомощиpreventDefault.Вместоэтогоиспользуетсявозвратстрокиизобработчика.Строкабудетиспользованавдиалоге,которыйспрашиваетпользователя,хочетлионостатьсянастраницеилипокинутьеё.Этотмеханизмгарантирует,чтопользовательможетпокинутьстраницу,дажееслинанейработаетзловредныйскрипт,которыйбыхотелнеотпускатьпользователя,авместоэтогопоказывалбыемумошенническуюрекламупоснижениювеса.
Нескольковещеймогутпривестикстартускрипта.Чтениетега<script>—однаизних.Запусксобытия–ещёодна.Вглаве13обсуждаетсяфункцияrequestAnimationFrame,котораяпланируетзапускфункциипередследующейперерисовкойстраницы.Этоещёодинспособзапуститьскрипт.
Важнопонять,чтохотясобытияизапускаютсявлюбоймомент,дваразныхскриптаодновременноработатьнемогут.Еслискриптработает,обработчикисобытийизапланированныедругимспособомкускикодабудутждатьсвоейочереди.Поэтомудокументподвисает,когдаскриптработаетслишкомдолго.Браузернеобрабатываетщелчкиидругиесобытиявнутридокументапотому,чтооннеможетзапуститьобработчикисобытий,покаработаеттекущийскрипт.
Внекоторыхпрограммныхокруженияхможнозапускатьнесколькопотоководновременно.Можносделатьпрограммубыстрее,есливыполнятьнескольковещейодновременно.Нокогданесколькодействующихлицтрогаютодниитежечастисистемыводноитожевремя,продумыватьпрограммустановитсянапорядоксложнее.
Графиквыполненияскрипта
ВыразительныйJavascript
267Обработкасобытий
То,чтопрограммы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Обработкасобытий
<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Обработкасобытий
надобытьосторожныминеделатьничего«тяжёлого»,иливашобработчикзаймётстольковременинавыполнение,чтовзаимодействиесдокументомбудетмедленнымипрерывистым.
Есливтакомобработчикенадосделатьчто-тонетривиальное,можноиспользовать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Обработкасобытий
<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Обработкасобытий
черезсобытия«focus»и«blur».Когдазаканчиваетсязагрузкадокумента,уобъектаwindowзапускаетсясобытие“load”.
Водноитожевремяможетработатьодинучастокпрограммы.Поэтомуобработчикисобытийидругиезапланированныескриптыбудутждатьокончанияработытекущих.
Впромежуткес1928по2013годтурецкиезаконызапрещалииспользованиебуквQ,WиXвофициальныхдокументах.Этоявлялосьчастьюобщейинициативыподавлениякурдскойкультуры–этибуквыиспользуютсявязыкекурдов,нонеутурков.
Вкачествеупражнениянатемустранногоиспользованиятехнологий,япрошувасзапрограммироватьполедлявводатекстатак,чтобыэтибуквынельзябылотудавписать.Насчеткопированияивставкиидругихподобныхвозможныхобходовправиланебеспокойтесь.
<inputtype="text">
<script>
varfield=document.querySelector("input");
//Yourcodehere.
</script>
ВранниедниJavaScript,когдабыловремякричащихдомашнихстраницсобилиеманимированныхкартинок,людииспользовалиязыкоченьвдохновляющимиспособами.Однимизнихбыл«следмыши»—сериякартинок,которыеследовализакурсоромприегодвижениипостранице.
Яхочу,чтобывывупражнениисделалитакойслед.Используйтесабсолютнымпозиционированием,фиксированнымразмеромицветомфона.Создайтекучкуэлементовипридвижениимышипоказывайтеихследомза
Упражнения
Цензураклавиатуры
Следмыши
ВыразительныйJavascript
272Обработкасобытий
курсором.
Кэтомуможноподойтимногимиспособами.Можносделатьоченьпростоеилиоченьсложноерешение,какугодно.Простое–хранитьфиксированноеколичествоэлементовипроходитьпонимвцикле,двигаякаждыйследующийнатекущееместокурсора,каждыйразкогдаслучаетсясобытие«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Обработкасобытий
<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Обработкасобытий
Всянашажизнь–игра.ИйенБэнкс,«Игрок»
Впервыеяувлёксякомпьютерами,какибольшинстводетей,черезкомпьютерныеигры.Менязатянулововселеннуюсимулированныхмиров,которымиможнобылоуправлять,ивкоторыхрассказывалисьистории–мнекажетсябольшепотому,чтовнихбылданпростормоемувоображению,чемиз-зареальныхвозможностей,которыеонипредоставляли.
Никомубынепожелалкарьеруигровогопрограммиста.Какивмузыкальнойиндустрии,несоответствиемеждуколичествоммолодыхлюдей,желающихпопастьтудаиреальнымспросомнаних,создаётнездоровуюсреду.Нонаписаниеигрдляразвлечения–этооченьздорово.
Вэтойглавемыизучимреализациюпростогоплатформера.Вплатформерах(или«прыгайибеги»)отигрокатребуетсядвигатьфигуркупо(обычно)двумерномумиру,которыймывидимсбоку,ичастоперепрыгиватьчерезразныештуки.
НашаиграбудетпримернобазироватьсянаигреDarkBlueотТомасаПалефа.Явыбралеё,потомучтоонакакразвлекательная,такиминималистичная,иеёможносделатьминимумомкода.Выглядитонатак:
Чёрныйпрямоугольникпредставляетигрока,чьязадача–собиратьжёлтыеквадраты(монеты),избегаякрасныхучастков(лава?).Уровеньзаканчивается,когдаигроксобралвсемонеты.
Игрокможетходитьклавишамивлевоивправо,ипрыгатьклавишейвверх.Прыжки–этоспециальностьнашегоперсонажа.Онможетпрыгатьвнесколькоразвышесвоегоростаименятьнаправлениедвиженияввоздухе.Этонеочень-тореалистично,нопомогаетигрокупочувствоватьполныйконтрольнадегоэкраннымаватаром.
Проект:игра-платформер
Игра
ВыразительныйJavascript
275Проект:игра-платформер
Уигрыфиксированныйфонввидерешётки,гдедвижущиесяэлементынакладываютсянафон.Каждаяячейкарешёткилибопустая,либоявляетсястеной,либолавой.Движущиесяэлементы–игрок,монеты,инекоторыекусочкилавы.Вотличиеотсимуляциижизниизглавы7,позицииэтихэлементовнепривязаныкрешётке.Ихкоординатымогутбытьдробными,обеспечиваяплавноедвижение.
МыиспользуемDOMбраузерадляграфики,ичитаемвводпользователя,обслуживаясобытияклавиатуры.
Код,относящийсякэкрануиклавиатуре–небольшаячастьработы,которуюнамнадпроделатьдлясозданияигры.Таккаквсёсостоитизцветныхквадратиков,рисоватьэтопросто:мысоздаёмэлементыDOMииспользуемстили,чтобызадатьимцветфона,размерирасположение.
Мыпредставляемфонкактаблицу,посколькуэто–неизменнаярешёткаизквадратов.Свободнодвигающиесяэлементыможнонакладыватьсверху,используяабсолютноепозиционирование.
Виграхидругихинтерактивныхпрограммахсграфическойанимацией,которыедолжныреагироватьнадействияпользователябеззадержки,оченьважнаэффективность.ХотяDOMнебылзадумандлявыводавысокоскоростнойграфики,онсправляетсясэтимлучше,чемможноожидать.Вглаве13вывиделинемножкоанимации.Насовременномкомпьютеретакаяпростаяиграидётнеплохо,дажееслинесильномучитьсясоптимизацией.
Вследующейглавемыизучимдругуютехнологиюбраузера,тег,которыйпредоставляетболеетрадиционныйспособдлярисования,иработаетсформамиипикселямивместоэлементовDOM.
Вглаве7мыиспользовалимассивыстрокдляописаниядвумернойрешётки.
Технология
Уровни
ВыразительныйJavascript
276Проект:игра-платформер
Мыможемсделатьтожеиздесь.Этопозволитнамразрабатыватьуровнибезтого,чтобысначалаписатьредакторуровней.
Простойуровеньможетвыглядетьтак:
varsimpleLevelPlan=[
"",
"",
"x=x",
"xoox",
"x@xxxxxx",
"xxxxxx",
"x!!!!!!!!!!!!x",
"xxxxxxxxxxxxxx",
""
];
Фиксированнаярешёткаидвижущиесяэлементывключены.Символыxобозначаютстены,пробелы–пустоеместо,авосклицательныезнаки–фиксированнаялава.
@отмечаетместо,гдеигрокначинает.o–монетки,знакравенства=означаетблокдвижущейсялавы,которыйдвигаетсяпогоризонталитудаисюда.Заметьте,чторешётканаэтихпозицияхбудетсодержатьпустоепространство,идляотслеживанияпозицииэтихподвижныхэлементовиспользуетсяещёоднаструктураданных.
Мыбудемподдерживатьещёдвавидалавы:вертикальнаячерта|—длякусочков,двигающихсяповертикали,иvдлякапающейлавы.Онабудетдвигатьсявниз,нонеотскакиватьобратно,апростоперепрыгиватьнаначальнуюпозициюподостижениюпола.
Играсостоитизнесколькихуровней,которыенадозакончить.Уровеньзакончен,когдасобранывсемонетки.Еслиигроккасаетсялавы,текущийуровеньвозвращаетсякисходномусостоянию,иигрокначинаетзаново.
Чтениеуровня
Следующийконструкторсоздаётобъектуровня.Аргументомдолженбытьмассивстрок,задающихуровень.
ВыразительныйJavascript
277Проект:игра-платформер
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Проект:игра-платформер
Послепостроениярешёткимыиспользуемметодfilter,чтобынайтиобъектигрока,хранящийсявсвойствеуровня.Свойствоstatusотслеживает,выигралигрокилипроиграл.Когдаэтослучается,используетсяfinishDelay,котороедержитуровеньактивнымнекотороевремядляпоказапростойанимации.(Простосразувосстанавливатьсостояниеуровняилиначинатьследующий–этовыглядитнекрасиво).Этотметодможноиспользовать,чтобыузнать,законченлиуровень:
Level.prototype.isFinished=function(){
returnthis.status!=null&&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Проект:игра-платформер
};
Трисимволассылаютсяна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Проект:игра-платформер
}
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Проект:игра-платформер
первых,инкапсуляциятребуетдополнительныхусилий.Программыстановятсябольше,требуютбольшеконцепцийиинтерфейсов.Атаккакотслишкомбольшогообъёмакодаглазачитателястекленеют,япостаралсясохранитьпрограммунебольшой.
Во-вторых,различныеэлементыигрытаксвязанывместе,чтоеслибыменялосьповедениеодногоизних,врядлиоставшийсякодоставалсябынеизменным.Созданиеинтерфейсовмеждуэлементамипривелобыкиспользованиюслишкомбольшогоколичествапредположенийпоповодутого,какработаетигра.Итогдаонибылибынеэффективными–меняяоднучастьсистемы,вамприходилосьбыдумать,какэтовлияетнадругиечасти,потомучтоихинтерфейсынеохватывалибыновуюситуацию.
Некоторыечастисистемыхорошоподдаютсяразделениюнакусочкисострогопрописаннымиинтерфейсами,адругие–нет.Впопыткахинкапсулироватьнечто,неимеющеечёткихграниц,выгарантированнопотратитемногосил.Совершивтакуюошибку,выувидите,чтоинтерфейсыстановятсячересчурбольшимиидетальными,ичтоихнадочастоменятьвпроцессеэволюциипрограммы.
Однувещьмывсё-такиинкапсулируем–подсистемурисования.Этосделаноспециальнодлятого,чтобывследующейглавемымогливыводитьнаэкрантужеигрудругимспособом.Спрятаврисованиезаинтерфейс,мыможемпростозагрузитьтужепрограммуиподключитькнейновыймодульвыводанаэкран.
Инкапсулироватькоддлярисованиямыбудем,введяобъектdisplay,которыйвыводитуровеньнаэкран.Типэкрана,которыймыопределяем,зовётсяDOMDisplay,потомучтоониспользуетэлементыDOMдляпоказауровня.
Мыиспользуемтаблицустилейдлязаданияцветовидругихфиксированныхсвойствэлементов,составляющихигру.Былобывозможнонепосредственноназначатьстильэлементучерезсвойствоstyleприегосоздании,нопрограммавэтомслучаесталабыизлишнемногословной.
Рисование
ВыразительныйJavascript
282Проект:игра-платформер
Следующаявспомогательнаяфункциядаётпростойспособсозданияэлементасназначениемкласса.
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Проект:игра-платформер
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Проект:игра-платформер
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Проект:игра-платформер
игрокаестьпотомоксзаданнымклассом.
.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Проект:игра-платформер
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Проект:игра-платформер
<linkrel="stylesheet"href="css/game.css">
<script>
varsimpleLevel=newLevel(simpleLevelPlan);
vardisplay=newDOMDisplay(document.body,simpleLevel);
</script>
Тэг<link>прииспользованиисrel="stylesheet"позволяетзагружатьфайлсCSS.Файлgame.cssсодержитнеобходимыедляигрыстили.
Теперьнамнадодобавитьобработкудвижений–самоеинтересноевигре.Простойподход,которыйиспользуюбольшинствоигр–разделитьвремянанебольшиеотрезки,инакаждомшагесдвигатьактёровнадистанцию,соответствующуюихскорости(расстояниевсекунду),умноженноенадлительностьвременногоотрезка(всекундах).
Этопросто.Сложностьвтом,чтонадообрабатыватьвзаимодействиепредметов.Когдаигроккасаетсяполаилистены,оннедолженпроходитьнасквозь.Иградолжназамечать,когдадвижениеодногообъектаприводиткстолкновениюсдругимиреагироватьсоответственно.Стеныостанавливаютдвижение,монетысобираются,итакдалее.
Вобщемслучаеэтазадачанетакаяпростая.Можнонайтибиблиотеки,обычноназывающиеся«физическимидвижками»,симулирующиевзаимодействиямеждуфизическимиобъектамивдвухилитрёхизмерениях.Вэтойглавемыпоступимпроще,таккакнамнужнообрабатыватьстолкновениятолькопрямоугольныхобъектов.
Передтем,каксдвинутьигрокаилиблоклавы,мыпроверяем,неприведётлинасдвижениевнутрьнепустойчастифона.Еслида–мыотменяемдвижение.Реакциянаэтобудетзависетьоттипаактёра–игрокостанавливается,лаваотскакивает.
Подходтребуетиспользованиянебольшихотрезковвремени,чтобыобъектыостанавливалисьдосоприкосновения.Есливзятьслишкомбольшиеотрезки,игрокбудетзависатьнадземлёй.Можнобылобыиспользоватьболее
Движениеистолкновение
ВыразительныйJavascript
288Проект:игра-платформер
сложныйвариант–вычислитьместонепосредственногосоприкосновенияиподвинутьактёратуда.Мыпоступимпроще,искроемегопроблемы,выбравнебольшиевременныеотрезки.
Методсообщает,непересекаетсялипрямоугольник(заданныйпозициейиразмером)скаким-либонепустымпространствомфоновойрешётки.
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Проект:игра-платформер
гибель).
Этотметодсканируетмассивактёров,впоискахтого,которыйнакладываетсяназаданныйаргумент:
Level.prototype.actorAt=function(actor){
for(vari=0;i<this.actors.length;i++){
varother=this.actors[i];
if(other!=actor&&
actor.pos.x+actor.size.x>other.pos.x&&
actor.pos.x<other.pos.x+other.size.x&&
actor.pos.y+actor.size.y>other.pos.y&&
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Проект:игра-платформер
Цикл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Проект:игра-платформер
Остаётсяигрок.Движениеигрокаобрабатываетсяпоразнымосямотдельно,потомучтовстречасполомнедолжнамешатьгоризонтальномуперемещению,австречасостеной–падениюилипрыжку.Этотметодработаетсгоризонтальнымперемещением.
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&&this.speed.y>0)
this.speed.y=-jumpSpeed;
else
this.speed.y=0;
}else{
this.pos=newPos;
ВыразительныйJavascript
292Проект:игра-платформер
}
};
Вначалеметодаигрокускоряетсяповертикали,чтобыобеспечитьгравитацию.Гравитация,скоростьпрыжкаивсеостальныеконстантывигребылиподобраныметодомпробиошибок.Япроверялразныезначения,покаменянеудовлетворилрезультат.
Затеммысновапроверяемпрепятствия.Еслимыеговстретили,возможныдваварианта.Когданажатаклавиша«вверх»,имыдвигаемсявниз(тоесть,мывстретилисьсчем-то,чтонаходитсяподнами),скоростиприсваиваетсядовольнобольшоеотрицательноезначение.Врезультатеигрокпрыгает.Виномслучае,мыпростовочто-товрезалисьискоростьобнуляется.
Самметод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Проект:игра-платформер
объектами:
Level.prototype.playerTouched=function(type,actor){
if(type=="lava"&&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Проект:игра-платформер
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Проект:игра-платформер
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Проект:игра-платформер
начинаетсязаново.Когдауровеньзакончен,мыпереходимнаследующий.Этоможновыразитьследующейфункцией,принимающеймассивплановуровней(массивстрок)иконструктор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Проект:игра-платформер
Попробуйтевыиграть.Яздоровоповеселился,сочиняяих.
Потрадиции,платформерыдаютигрокуограниченноеколичествожизней,ивычитаютпооднойкаждыйразпригибелиигрока.Когдажизникончаются,играначинаетсязаново.
Подредактируйте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Проект:игра-платформер
другойобработчиксобытийклавиатуры,ипрерывалаивозобновлялаанимациюпонажатию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Проект:игра-платформер
Рисование—этообман.М.К.Эшер
Браузерыпозволяютнамрисоватьграфикуразнымиспособами.ПрощевсегоиспользоватьстилидлярасположенияирасцветкистандартныхэлементовDOM.Такможнодобитьсямногого,какпоказалпримеригрыизпредыдущейглавы.Добавляячастичнопрозрачныекартинкиузлам,мыможемпридатьимлюбойнужныйвид.Возможнодажеповорачиватьилиискажатьузлычерезстильtransform.
НотакоеиспользованиеDOM–нето,длячегоонсоздавался.Некоторыезадачи,типарисованиялиниимеждудвумяпроизвольнымиточками,крайненеудобновыполнятьприпомощиобычныхэлементовHTML.
Естьдвеальтернативы.Первая–SVG,масштабируемаявекторнаяграфика,такжеоснованнаянаDOM,нобезучастияHTML.SVG–диалектдляописаниядокументов,которыйконцентрируетсянаформах,анетексте.SVGможновстроитьвHTML,иливключитьчерезтег<img>.
Втораяальтернатива–холст(canvas).Холст–этоодинэлементDOM,вкоторомнаходитсякартинка.ОнпредоставляетAPIдлярисованияформнатомместе,котороезанимаетэлемент.РазницамеждухолстомиSVGвтом,чтовSVGхранитсяначальноеописаниеформ–ихможновлюбоймоментсдвигатьилименятьразмер.Холстжепреобразовываетформывпиксели(цветныеточкирастра),кактольконарисуетих,инезапоминает,чтоэтипикселиизсебяпредставляют.Единственнымспособомсдвинутьформанахолстеявляетсяочиститьхолст(илитучасть,котораяокружаетформу)иперерисоватьеёнадругомместе.
ЭтакниганеуглубляетсядетальновSVG,нократкояпояснюеёработу.Вконцеглавыявернуськсравнительнымнедостаткамметодов,которыенужнопринятьвовнимание,выбираямеханизмрисованиядляконкретного
Рисованиенахолсте
SVG
ВыразительныйJavascript
300Рисованиенахолсте
применения.
Вотдокумент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Рисованиенахолсте
эффективнуюграфикупрямоиз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Рисованиенахолсте
Свойство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Рисованиенахолсте
Призаливкепутикаждаяизформзаливаетсяотдельно.Путьможетсодержатьнесколькоформ–каждоедвижение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Рисованиенахолсте
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Рисованиенахолсте
пятьаргументов.Первыечетыре–похожинааргументы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Рисованиенахолсте
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Рисованиенахолсте
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Рисованиенахолсте
Вкомпьютернойграфикепроводитсяразличиемеждувекторнойирастровойграфикой.Первая–то,чеммызанималисьвэтойглаве,рисованиеприпомощилогическихописанийформ.Вторая–незадаётформы,аработаетнауровнепикселей.
Метод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Рисованиенахолсте
Перебираяпозы,мыможемвывестианимациюидущегоперсонажа.
Дляанимациинахолстепригодитсяметод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Рисованиенахолсте
онрисовалкартинкузеркально.
Вызовметода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Рисованиенахолсте
градусов(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Рисованиенахолсте
Затемпроисходитмасштабирование,итреугольникоказываетсянаместе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Рисованиенахолсте
Функция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Рисованиенахолсте
Мыопределимтипобъекта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Рисованиенахолсте
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Рисованиенахолсте
Приочисткедисплеямыиспользуемдругойцвет,взависимостиоттого,выигранаиграилипроиграна.
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Рисованиенахолсте
Клеткифона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Рисованиенахолсте
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Рисованиенахолсте
Следующиймаленькийдокументподключаетновый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Рисованиенахолсте
(размытиеилиискажение),которыеможносделатьтолькоприпомощипопиксельногорисования.
Внекоторыхслучаяхможнокомбинироватьэтитехники.Например,можнонарисоватьграфчерезSVGилихолст,атекстовуюинформациюпоказывать,позиционируяэлементыHTMLповерхкартинки.
Длянепривередливыхприложенийневажно,какойвыиспользуетеинтерфейс.Дисплей,построенныйнамидлянашейигры,можносделатьлюбымизтрёхграфическихспособов,таккаконневыводиттекстинеобрабатываетнажатиямыши,инеобслуживаетогромноеколичествоэлементов.
Вэтойглавемыобсудилитехникирисованияграфикивбраузере,сконцентрировавшисьнаэлементе<canvas>.
Узелхолстапредставляетобластьдокумента,гдепрограммаможетрисовать.Этоделаетсячерезобъектcontext,создаваемыйметодомgetContext.Интерфейсдвумерногорисованияпозволяетзакрашиватьиобводитьразныеформы.СвойствоfillStyleзадаётзаливкуформ.СвойстваstrokeStyleиlineWidthуправляюттем,какрисуютсялинии.
Прямоугольникиикускитекстаможнорисоватьоднимвызовомметода.МетодыfillRectиstrokeRectрисуютпрямоугольники,аfillTextиstrokeTextвыводяттекст.Длясозданияпроизвольныхформнамнужностроитьпути.
ВызовbeginPathначинаетпуть.Несколькометодовдобавляютлинииикривыектекущемупути.Например,lineToдобавляетпрямую.Когдапутьзакончен,егоможнозаполитьметодомfillилиобвестиметодомstroke.
ПеремещениепикселейскартинкиилидругогохолстананашделаетсяметодомdrawImage.Поумолчанию,онрисуетвсюисходнуюкартинку,носбольшимколичествомпараметроввыможетескопироватьнужныйучастокизображения.Внашейигремыиспользовалиэтувозможность,копируяразныепозыигровогоперсонажаизчастейкартинки,содержавшеймногопоз.
Итог
ВыразительныйJavascript
321Рисованиенахолсте
Перемещенияпозволяютрисоватьформу,ориентированнуюпо-разному.Двумерныйконтекстхраниттекущеепреобразование,котороеможноменятьчерезметоды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Рисованиенахолсте
</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Рисованиенахолсте
<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Рисованиенахолсте
Мечта,радикоторойсоздаваласьСеть–этообщееинформационноепространство,вкотороммыобщаемся,делясьинформацией.Егоуниверсальностьявляетсяегонеотъемлемойчастью:ссылкавгипертекстеможетвестикудаугодно,будьтоперсональная,локальнаяилиглобальнаяинформация,черновикиливыверенныйтекст.
ТимБернес-Ли,Всемирнаяпаутина:Оченькороткаяличнаяистория
Есливадреснойстрокебраузеранабрать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
определённыйресурс.Другиераспространённыеметоды–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
Побольшейчастиклиентилисерверопределяют,какиезаголовкинеобходимовключатьвзапросилиответ,хотянекоторыезаголовкиобязательны.Кпримеру,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
помещаетсяв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&goodbye"));
//→Hello%20%26%20goodbye
console.log(decodeURIComponent("Hello%20%26%20goodbye"));
//→Hello&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
дляпользователяделаютзапросы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
Чтобыотправитьпростойзапрос,мысоздаёмобъектзапросасконструктором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
“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
тоткод,которыйдолженбытьвыполненпослезапроса,иустраиваявызовэтойфункциивнужноевремя.Мывернёмсякэтомупозже.
Когдаресурс,возвращённыйобъектом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
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
callback(req.responseText);
});
req.send(null);
}
ПростаяабстракцияупрощаетиспользованиеXMLHttpRequestдляпростыхGET-запросов.Есливыпишетепрограмму,котораяделаетHTTP-запросы,будетнеплохоиспользоватьвспомогательнуюфункцию,чтобывамнеприходилосьвсёвремяповторятьуродливыйшаблонXMLHttpRequest.
Аргументcallback(обратныйвызов)–термин,частоиспользующийсядляописанияподобныхфункций.Функцияобратноговызовапередаётсявдругойкод,чтобыонмогпозватьнасобратнопозже.
НесложнонаписатьсвоювспомогательнуюфункциюHTTP,специальноскроеннуюподвашупрограмму.ПредыдущаяделаеттолькоGET-запросы,инедаётнамконтролянадзаголовкамиилителомзапроса.МожнонаписатьещёодинвариантдлязапросаPOST,илиболееобщий,поддерживающийразныезапросы.МногиебиблиотекиJavaScriptпредлагаютобёрткидляXMLHttpRequest.
Основнаяпроблемасприведённойобёрткой–обработкаошибок.Когдазапросвозвращаеткодстатуса,обозначающийошибку(от400ивыше),онничегонеделает.Внекоторыхслучаяхэтонормально,нопредставьте,чтомыпоставилииндикаторзагрузкинастранице,показывающий,чтомыполучаеминформацию.Еслизапроснеудался,потомучтосерверупалилисоединениепрервано,страницабудетделатьвид,чтооначем-тозанята.Пользовательподождётнемного,потомемунадоестионрешит,чтосайткакой-тодурацкий.
Намнуженвариант,вкотороммыполучаемпредупреждениеонеудачномзапросе,чтобымымоглипринятьмеры.Например,мыможемубратьсообщениеозагрузкеисообщитьпользователю,чточто-топошлонетак.
Обработкаошибоквасинхронномкодеещёсложнее,чемвсинхронном.Посколькунамчастоприходитсяотделятьчастьработыиразмещатьеёвфункцииобратноговызова,областьвидимостиблокаtryтеряетсмысл.Вследующемкодеисключениенебудетпоймано,потомучтовызовbackgroundReadFileвозвращаетсясразуже.Затемуправлениеуходитиз
ВыразительныйJavascript
334HTTP
блока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
else
console.log("nonsense.txt:"+content);
});
Сисключениямиэтонепомогает.Когдамысовершаемпоследовательнонесколькоасинхронныхдействий,исключениевлюбойточкецепочкивлюбомслучае(еслитольковынеобернуликаждыйобработчиквсвойблокtry/catch)вывалитсянаверхнемуровнеипрервётвсюцепочку.
Тяжелописатьасинхронныйкоддлясложныхпроектовввидепростыхобратныхвызовов.Оченьлегкозабытьпроверкунаошибкуилипозволитьнеожиданномуисключениюрезкопрерватьпрограмму.Крометого,организацияправильнойобработкиошибокипроходошибкичерезнесколькопоследовательныхобратныхвызововоченьутомительна.
Предпринималосьмножествопопытокрешитьэтупроблемудополнительнымиабстракциями.Однаизнаиболееудачныхпопытокназываетсяобещаниями(promises).Обещанияоборачиваютасинхронноедействиевобъект,которыйможетпередаватьсяикоторомунужносделатькакие-товещи,когдадействиезавершаетсяилинеудаётся.ТакойинтерфейсужесталчастьютекущейверсииJavaScript,адлястарыхверсийегоможноиспользоватьввидебиблиотеки.
Интерфейсобещанийнеособенноинтуитивнопонятный,номощный.Вэтойглавемылишьчастичноопишемего.Большеинформацииможнонайтинаwww.promisejs.org
ДлясозданияобъектаpromisesмывызываемконструкторPromise,задаваяемуфункциюинициализацииасинхронногодействия.Конструкторвызываетэтуфункциюипередаётейдвааргумента,которыесамитакжеявляютсяфункциями.Перваядолжнавызыватьсявудачномслучае,другая–внеудачном.
ИвотнашаобёрткадлязапросовGET,котораянаэтотразвозвращаетобещание.Теперьмыпростоназовёмегоget.
Обещания
ВыразительныйJavascript
336HTTP
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
качестверезультатаиспользуяэтозначение.
Значит,выможетеиспользовать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
});
</script>
Итоговаяпрограммаотносительнокомпактнаичитаема.Методcatchсхожсthen,нооножидаеттолькообработчикнеудачногорезультатаивслучаеуспехапередаётдальшенеизменённыйрезультат.Исполнениепрограммыбудетпродолженообычнымпутёмпослеотловаисключения–также,каквслучаесtry/catch.Такимобразом,последнийthen,удаляющийсообщениеозагрузке,выполняетсявлюбомслучае,дажевслучаенеудачи.
Можнопредставлятьсебе,чтоинтерфейсобещаний–этоотдельныйязыкдляасинхроннойобработкиисполненияпрограммы.Дополнительныевызовыметодовифункций,которыенужныдляегоработы,придаюткодунесколькостранныйвид,ноненастольконеудобный,какобработкавсехошибоквручную.
Присозданиисистемы,вкоторойпрограмманаJavaScriptвбраузере(клиентская)общаетсяссервернойпрограммой,можноиспользоватьнескольковариантовмоделированиятакогообщения.
Общепринятыйметод–удалённыевызовыпроцедур.Вэтоймоделиобщениеидётпошаблонуобычныхвызововфункций,толькофункцииэтивыполняютсянадругомкомпьютере.Вызовзаключаетсявсозданиизапросанасервер,вкоторыйвходятимяфункциииаргументы.Ответназапросвключаетвозвращаемоезначение.
ПрииспользованииудалённыхвызововпроцедурHTTPслужитлишьтранспортомдляобщения,ивы,скореевсего,напишетеслойабстракции,которыйспрячетегополностью.
Другойподход–построитьсвоюсистемуобщениянаконцепцииресурсовиметодовHTTP.ВместоудалённоговызовапроцедурыпоимениaddUserвыделаетезапросPUTк/users/larry.Вместокодированиясвойствпользователяваргументахфункциивыопределяетеформатдокументаилииспользуетесуществующийформат,которыйбудетпредставлятьпользователя.Тело
ЦенитеHTTP
ВыразительныйJavascript
339HTTP
PUT-запроса,создающегоновыйресурс,будетпростодокументомэтогоформата.РесурсполучаетсячереззапросGETкегоURL(/user/larry),которыйвозвращаетпредставляющийэтотресурсдокумент.
ВторойподходупрощаетиспользованиенекоторыхвозможностейHTTP,напримерподдержкикешированияресурсов(копияресурсахранитсянасторонеклиента).Такжеонспособствуетсозданиюсогласованногоинтерфейса,потомучтодуматьвтерминахресурсовпроще,чемвтерминахфункций.
Данныепутешествуютпоинтернетуподлинномуиопасномупути.Чтобыдобратьсядопунктаназначения,имнадопопрыгатьчерезвсякиеместа,начинаяотWi-Fiсетикофейнидосетей,контролируемыхразнымиорганизациямиигосударствами.Влюбойточкепутиихмогутпрочитатьилидажепоменять.
Еслинужнохранитьчто-либовсекрете,напримерпароликемейлу,илиданнымнеобходимоприйтивпунктназначениявнеизменномвиде—таким,например,какномербанковскогосчёта,накоторыйвыпереводитеденьги,-простогоHTTPнедостаточно.
БезопасныйпротоколHTTP,URLкоторогоначинаютсясhttps://,оборачиваетHTTP-трафиктак,чтобыегобылосложнеепрочитатьипоменять.Сначалаклиентподтверждает,чтосервер–тот,закогосебявыдаёт,требуяссерверапредставитькриптографическийсертификат,выданныйавторитетнойстороной,которуюпризнаётбраузер.Потом,вседанные,проходящиечерезсоединение,шифруютсятак,чтобыпредотвратитьпрослушкуиизменение.
Такимобразом,когдавсёработаетправильно,HTTPSпредотвращаеткакслучаи,когдакто-топритворяетсядругимвеб-сайтом,скоторымвыобщаетесь,такислучаипрослушкивашегообщения.Оннеидеален,иужебылислучаи,когдаHTTPSнесправлялсясработойиз-заподдельныхиликраденыхсертификатовилисломанныхпрограмм.Темнеменее,сHTTPоченьлегкосделатьчто-топлохое,авзломHTTPSтребуеттакихусилий,которыемогутприкладыватьтолькогосударственныеструктурыилиочень
БезопасностьиHTTPS
ВыразительныйJavascript
340HTTP
серьёзныекриминальныеорганизации(амеждуэтимиорганизациямииногдасовсемнетразличий).
Вэтойглавемыувидели,что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
Однаизвещей,которые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
ошибокввашемобещании.
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
ЯнынчежнаученомкутежеТвоедоверьеслужбойзавоюю,Тыжмнечеркнираспискудолговую,Чтобмненесомневатьсявплатеже.
Мефистофель,в«Фаусте»Гёте
Формыбыликраткопредставленывпредыдущейглавевкачествеспособапередачиинформации,введённойпользователем,черезHTTP.ОнибылиразработаныввебедопоявленияJavaScript,стемрасчётом,чтовзаимодействиессерверомпроисходитприпереходенадругуюстраницу.
НоихэлементыявляютсячастямиDOM,какиостальныечастистраницы,аэлементыDOM,представляющиеполяформы,поддерживаютнесколькосвойствисобытий,которыхнетудругихэлементов.ЭтоделаетвозможнымпросматриватьиуправлятьполямивводаизпрограммJavaScriptидобавлятьфункциональностикклассическимформамилииспользоватьформыиполякакосновудляпостроенияприложения.
Веб-формасостоитизлюбогочислаполейввода,окружённыхтегом<form>.HTMLпредлагаетмногоразныхполей,отпростыхгалочексозначениямивкл/выклдовыпадающихсписковиполейдлявводатекста.Вэтойкнигенебудутподробнообсуждатьсявсевидыполей,номысделаемнебольшойихобзор.
Многотиповполейвводаиспользуюттег<input>.Егоатрибутtypeиспользуетсядлявыборастиляполя.Вотнесколькораспространённыхтипов:
textтекстовоеполенаоднустрокуpasswordтоже,чтотекст,нопрячетвводcheckboxпереключательвкл/выклradioчастьполясвозможностьювыбораизнесколькихвариантовfileпозволяетпользователювыбратьфайлнаегокомпьютере
Формыиполяформ
Поля
ВыразительныйJavascript
344Формыиполяформ
Поляформнеобязательнодолжныпоявлятьсявнутритега<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Формыиполяформ
Вотличиеотбольшинстваэлементовдокумента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Формыиполяформ
кнопку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Формыиполяформ
<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Формыиполяформ
Полястегами<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Формыиполяформ
былопродолжатьпечатать.
Событие“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Формыиполяформ
Радиокнопкасхожасгалочкой,ноонасвязанасдругимирадиокнопкамистемжеименем,такчтотолькооднаизнихможетбытьвыбрана.
Цвет:
<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Формыиполяформ
Вбольшинствебраузероввнешнийвидполябудетотличатьсяотполясединственнымвариантомвыбора,которыйобычновыглядиткаквыпадающееменю.
Атрибут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Формыиполяформ
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Формыиполяформ
дисказанимаетдлительноевремя,интерфейсдолженбытьасинхронным,чтобыдокументнезамирал.Конструктор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Формыиполяформ
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Формыиполяформ
перезаписанытолькоскриптамисэтогожесайта.
Такжебраузерыограничиваютобъёмхранимыхданных,обычновнесколькомегабайт.Этоограничение,вкупестемфактом,чтозабиваниежёсткихдисковулюдейнеприноситприбыли,предотвращаетотъеданиеместанадиске.
Следующийкодреализуетпростуюпрограммудляведениязаметок.Онахранитзаметкиввидеобъекта,ассоциируязаголовкиссодержимым.Онкодируетсяв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Формыиполяформ
});
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Формыиполяформ
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Формыиполяформ
<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Формыиполяформ
одновременно,анеккаждойизклетокпоочереди.Тоесть,подсчётколичествасоседейпроисходитводинмоментпередследующимшагом,иизменения,происходящиенасоседнихклетках,невлияютнановоесостояниеклетки.
Реализуйтеигру,используялюбыеподходящиеструктуры.ИспользуйтеMath.randomдлясозданияслучайныхначальныхпопуляций.Выводитеполекакрешёткуизгалочекскнопкой«перейтинаследующийшаг».Когдапользовательвключаетиливыключаетгалочки,этиизменениянужноучитыватьприподсчётеследующегопоколения.
<divid="grid"></div>
<buttonid="next">Следующеепоколение</button>
<script>
//Вашкод.
</script>
ВыразительныйJavascript
360Формыиполяформ
Ясмотрюнамногообразиецветов.Ясмотрюнапустойхолст.Затемяпытаюсьнанестицветакакслова,изкоторыхвозникаютпоэмы,какноты,изкоторыхвозникаетмузыка.
ЖоанМиро
Материалпредыдущихглавдаётвамвсёнеобходимоедлясозданияпростоговеб-приложения.Именноэтиммыизаймёмся.
Нашеприложениебудетпрограммойдлярисованиявбраузере,схожейсMicrosoftPaint.Сегопомощьюможнобудетоткрыватьфайлысизображениями,малеватьнанихмышкойисохранятьобратно.Вот,какэтобудетвыглядеть:
Рисоватьнакомпьютереклёво.Ненадоволноватьсянасчётматериалов,умения,таланта.Простоберёшь,иначинаешькалякать.
Проект:Paint
Простаяпрограммарисования
ВыразительныйJavascript
361Проект:Paint
Интерфейспрограммывыводитвверхубольшойэлемент<canvas>,подкоторыместьнесколькополейввода.Пользовательрисуетнакартинке,выбираяинструментизполя<select>,азатемнажимаянахолстемышь.Естьинструментыдлярисованиялиний,стираниякусочковкартинки,добавлениятекстаит.п.
Щелчокнахолстепередаётсобытие«mousedown»текущемуинструменту,которыйобрабатываетего,каксчитаетнужным.Рисованиелиний,например,будетслушатьсобытия«mousemove»,покакнопкамышинебудетотпущена,инарисуетлиниюпопутимышитекущимцветомиразмеромкисти.
Цветиразмеркистивыбираютсявдополнительныхполяхввода.ОнивыполняютобновлениесвойствконтекстарисованиянахолстеfillStyle,strokeStyle,иlineWidthкаждыйразприихизменении.
Загрузитькартинкувпрограммуможнодвумяспособами.Первыйиспользуетполеfile,гдепользовательвыбираетфайлсосвоегодиска.ВтораязапрашиваетURLискачиваеткартинкуизинтернета.
Картинкихранятсянестандартнымспособом.Ссылкаsaveсправойстороныведётнатекущуюкартинку.Понейможнопроходить,делитьсяейилисохранятьфайлчерезнеё.Яскорообъясню,какэтоработает.
Интерфейспрограммысостоитизболеечем30элементовDOM.Нужноихкак-тособратьвместе.
ОчевиднымформатомдлясложныхструктурDOMявляетсяHTML.НоразделятьпрограммунаHTMLискриптнеудобно–дляэлементовDOMпонадобитсямножествообработчиковсобытийилидругихнеобходимыхвещей,которыенадобудеткак-тообрабатыватьизскрипта.ДляэтогопридётсяделатьмноговызововquerySelectorиимподобных,чтобынайтинужныйэлементDOMдляработы.
Реализация
СтроимDOM
ВыразительныйJavascript
362Проект:Paint
Былобыудобноопределятьчасти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
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
});
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
Унеёдвааргумента.Один–функция,котораявызываетсяприкаждомсобытии«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
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
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
сперванакакой-либосервер.
Уэлементахолстаестьудобныйметод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
Мымоглизапроситькартинкусприватнойинформацией(графикизмененийбанковскогосчёта).Еслибыскриптмогполучитькнейдоступ,онмогбышпионитьзапользователем.
Дляпредотвращениятакихутечекинформации,когдакартинка,невидимаяскрипту,будетзагруженанахолст,браузерыпометятегокак«испорчен».Пиксельныеданные,включая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
Элементуправлениядлязагрузкилокальногофайлаиспользуеттехнику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
Оченьпростоможнодобавитьинструментдлявыводатекста,которыйвыводитзапроспользователю,кудаондолженввеститекст.
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
того,чтобы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
Определитеинструмент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
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
//Yourcodehere.
};
</script>
<linkrel="stylesheet"href="css/paint.css">
<body>
<script>createPaint(document.body);</script>
</body>
Этоупражнениеболеесложное,чемпредыдущие,ионопотребуетразработкинетривиальногорешенияхитройзадачи.Убедитесь,чтоувасестьсвободноевремяитерпениепередначаломработы,инеотчаивайтесь,еслисразуувасчто-тонебудетполучаться.
Инструментзаливкиокрашиваетпиксельподкурсороммышииподцелойгруппойпикселейвокругнего,имеющихтотжецвет.Дляцелейнашегоупражнениямыбудемсчитать,чтоэтагруппавключаетвсепиксели,докоторыхможнодобратьсяотначального,двигаясьпоодномупикселюпогоризонталиивертикали(нонеподиагонали),неприкасаяськпикселям,чейцветотличаетсяотисходного.
Следующаякартинкадемонстрируетнаборпикселей,окрашиваемых,когдаинструментзаливкиприменяетсякпомеченномупикселю:
Заливканепротекаетчерездиагональныеразрывыинекасаетсяпикселей,которыхнельзядостичь,дажееслионитогожецвета,чтоиисходный.
ВамвновьпонадобитсяgetImageDataдлявыясненияцветапикселя.Скореевсего,удобнеебудетполучитьвсюкартинкузараз,апотомужеполучатьданныепопикселямизполучившегосямассива.Пикселивмассивеорганизованысхожимобразомсрешёткойизглавы7,порядам,только
Заливка
ВыразительныйJavascript
376Проект:Paint
каждыйпиксельописываетсячетырьмязначениями.Первоезначениедляпикселяскоординатами(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
Ученикспросил:«Программистывстарьиспользовалитолькопростыекомпьютерыипрограммировалибезязыков,ноониделалипрекрасныепрограммы.Почемумыиспользуемсложныекомпьютерыиязыкипрограммирования?».Фу-Тзуответил:«Строителивстарьиспользовалитолькопалкииглину,ноониделалипрекрасныехижины».
МастерЮан-Ма,«Книгапрограммирования»
НатекущиймоментвыучилиязыкJavaScriptииспользовалиеговединственномокружении:вбраузере.ВэтойиследующейглавемыкраткопредставимвамNode.js,программу,котораяпозволяетприменятьнавыкиJavaScriptвнебраузера.Снейвыможетенаписатьвсё,отутилиткоманднойстрокидодинамическихHTTPсерверов.
Этиглавыпосвященыобучениюважнымидеям,составляющимNode.jsипредназначеныдляпередачивамдостаточногоколичестваинформации,чтобывымоглиписатьполезныепрограммывэтойсреде.ОнинепытаютсябытьвсеобъемлющимисправочникамипоNode.
Кодизпредыдущихглаввымоглиписатьиисполнятьпрямовбраузере,нокодизэтойглавынаписандляNodeивбраузереработатьнебудет.
Есливыхотитесразузапускатькодизэтойглавы,начнитесустановкиNodeссайтаnodejs.orgдлявашейоперационки.ТакженаэтомсайтевынайдётедокументациюпоNodeиеговстроенныммодулям.
Однаизнаиболеесложныхпроблемпринаписаниисистем,общающихсяпосети–обработкавводаивывода.Чтениеизаписьданныхвсетьиизсети,надиск,идругиеустройства.Перемещениеданныхтребуетвремени,играмотноепланированиеэтихдействийможетсильноповлиятьнавремяоткликасистемыдляпользователяилисетевыхзапросов.
Node.js
Вступление
ВыразительныйJavascript
378Node.js
Втрадиционномметодеобработкивводаивыводапринято,чтофункция,кпримеру,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
сведенияработыводинрезультат.
Надиаграммежирныелинииобозначаютвремянормальнойработыпрограммы,атонкие–времяожидания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
Выможетевыполнитьсвоюпрограммуизкоманднойстроки:
$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
Объектглобальнойобластивидимости,которыйвбраузереназывается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
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
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
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
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
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
какомресурсеэтодействиедолжновыполняться.Далеевыувидитеболеепродвинутуюверсиюсервера.
Чтобысделать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
объект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
Переменная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
/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&&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
нечтовроде/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&&error.code=="ENOENT")
respond(404,"Filenotfound");
elseif(error)
ВыразительныйJavascript
392Node.js
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&&error.code=="ENOENT")
ВыразительныйJavascript
393Node.js
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
};
Здесьнамненужнопроверятьсуществованиефайла–еслионесть,мыегопростоперезапишем.Опятьмыиспользуем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
персонально.Этосводитнанетпреимуществообработкиисключений,аименно,возможностьцентрализованнообрабатыватьошибки.
Чтобудет,когдачто-тореальновыброситисключениевсистеме?Мынеиспользуемблоки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
использованиемобещаний,которуюможнонайтина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
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
комьпютере,вдиректории/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
ТаккакфайловыйсервервыдаётлюбыефайлыидажевозвращаетправильныйзаголовокContent-Type,егоможноиспользоватьдляобслуживаниявеб-сайта.Таккаконразрешаетвсемудалятьизаменятьфайлы,этобылбыинтересныйсайт–которыйможноизменять,портитьиудалятьвсем,ктоможетсоздатьправильныйHTTP-запрос.Ноэтовсёравнобылбывеб-сайт.
НапишитепростуюHTMLстраницуспростымфайломJavaScript.Разместитеихвдиректории,обслуживаемойсерверомиоткройтевбраузере.
Затем,вкачествепродвинутогоупражнения,скомбинируйтевсеполученныезнанияизкниги,чтобыпостроитьболеедружественныйинтерфейсдлямодификациивеб-сайтаизнутрисамогосайта.
ИспользуйтеформуHTML(глава18)дляредактированияфайлов,составляющихсайт,позволяяпользователюобновлятьихнасерверечерезHTTP-запросы,какописановглаве17.
Начнитесодногофайла,редактированиекоторогоразрешено.Затемсделайтетак,чтобыможнобыловыбиратьфайлдляредактирования.Используйтетотфакт,чтонашфайловыйсервервозвращаетспискифайловпозапросудиректории.
Неменяйтефайлынепосредственновкодефайловогосервера–есливысделаетеошибку,выскореевсегоиспортитетефайлы.Работайтевдиректории,недоступнойснаружи,икопируйтеихтудапослетестирования.
Есливашкомпьютерсоединяетсясинтернетомнапрямую,безfirewall,роутераилидругихустройств,высможетепригласитьдруганасвойсайт.Дляпроверкисходитенаwhatismyip.com,скопируйтеIPадресвадреснуюстрокуидобавьте:8000длявыборанужногопорта.Есливыпопалинасвойсайт,тоондоступендляпросмотравсем.
ВыразительныйJavascript
400Node.js
Навстречахпообменуопытомлюдисобщимиинтересамивстречаютсяиделаютнебольшиенеформальныепрезентациинатемусвоихзнаний.Навстречепообменуопытомсредифермеровкто-нибудьможетрассказатьовыращиваниисельдерея.НавстречепрограммистоввыможетевыступитьсрассказомпроNode.js
Такиевстречи–отличныйспособрасширитьсвойкругозор,узнатьоновинкахобласти,илипростопообщатьсяслюдьмисосхожимиинтересами.ВомногихгородахестьвстречилюбителейJavaScript.Обычноихпосещениебесплатное,иянашёлте,которыепосещал,дружелюбнымиигостеприимными.
Впоследнейглаве-проектемыустроимвеб-сайтпообслуживаниювыступлений,которыеделаютсянатакихвстречах.Представьтесебегруппулюдей,которыерегулярновстречаютсявофисеодногоизучастников,чтобыпоговоритьомоноциклах.Проблемавтом,чтокогдапредыдущийорганизаторвстречпереехалвдругойгород,никтонезанялегоместо.Намнужнасистема,котораяпозволитучастникампредлагатьиобсуждатьтемыдругсдругом,безучастияорганизатора.
Какивпредыдущейглаве,коднаписандляNode.jsизапуститьеговбраузеренеполучится.Полныйкоддоступенпоссылке.
Проект:веб-сайтпообменуопытом
Встречимоноциклистов
ВыразительныйJavascript
401Проект:веб-сайтпообменуопытом
Упроектаестьсервернаячасть,написаннаядляNode.js,иклиентская,написаннаядлябраузера.Сервернаяхранитсистемныеданныеипередаётихклиенту.ТакжеонаотдаётфайлыHTMLиJavaScript,которыесоздаютсистемунасторонеклиента.
Насервереестьсписоктемдляследующегособрания,иклиентихпоказывает.Укаждойтемыестьимявыступающего,название,описаниеисписоккомментариев.Клиентпозволяетпредлагатьновыетемы(добавлятьихвсписок),удалятьтемыикомментироватьсуществующие.Когдапользовательвноситэтоизменение,клиентделаетHTTP-запрос,чтобысообщитьобэтомсерверу.
Будетсозданоприложениедляпоказатекущихпредложенийтемикомментариипоним.Когдакто-тогде-тодобавляетновуютемуилиоставляеткомментарий,увсехлюдей,открывшихстраницувбраузере,изменениядолжныпроисходитьмгновенно.Этонепростаязадача,потомучтовеб-сервернеможетоткрыватьсоединениесклиентом,ипотомучтонетгодногоспособаузнать,ктоизклиентовсейчаспросматриваетданныйвеб-сайт.
Общепринятымрешениемпроблемыявляютсядлинныезапросы(longpolling),которыепослужилиоднойизмотивацийкразработкеNode.
Дизайн
ВыразительныйJavascript
402Проект:веб-сайтпообменуопытом
Чтобымгновеннооповеститьклиентаобизменениях,намнужносоединениесклиентом.Браузерытрадиционнонепринимаютзапросовнасоединения,иклиентывсёравноскрытызаустройствами,которыеэтисоединениянепринялибы,поэтомуначинатьсоединениессерверасмысланеимеет.
Можносделатьтак,чтобыклиентоткрывалсоединениеидержалего,чтобысерверимелвозможностьотправлятьчерезнегоинформациюпонеобходимости.
НозапросHTTPразрешаеттолькопростойобменинформацией–клиентотправляетзапрос,сервервозвращаетответ,ивсё.Естьтехнологияподназваниемwebsockets,котораяподдерживаетсясовременнымибраузерами,позволяющаяоткрыватьсоединениядляобменапроизвольнымиданными.Ноихдовольносложноиспользовать.
Вэтойглавемыобратимсякотносительнопростойтехнологии,длиннымзапросам,когдаклиентыпостояннозапрашиваютсервероновойинформациичерезобычныеHTTP-запросы,асерверпростомедлитсответом,когдаемунечегосообщить.
Покаклиентпостояннодержитоткрытыйзапрос,онбудетполучатьинформациюссерверанемедленно.Кпримеру,еслиуАлисывбраузереоткрытоприложениедляобменаопытом,браузерсделаетзапроснаобновленияибудетожидатьответа.КогдаБобизсвоегобраузераотправиттему«Экстремальныйспускнамоноциклесгоры»,серверзаметит,чтоАлисаждётобновлений,иотправитинформациюпоновойтемевответнаеёждущийзапрос.БраузерАлисыполучитданныеиобновитстраницу,показавновуютему.
Дляпредотвращениязавершениясоединенийпотаймауту(поистечениювременинеактивныесоединенияобрываются),технологиядлинныхзапросовобычноустанавливаетмаксимальноевремядлякаждогозапроса,попрошествиикоторогосервервлюбомслучаеответит,дажееслиемунечегосообщить,азатемклиентзапуститновыйзапрос.Периодическоеобновлениезапросаделаеттехникуболеенадёжной,позволяяклиентамвосстанавливатьсяпослевременныхобрывовилипроблемнасервере.
Длинныезапросы
ВыразительныйJavascript
403Проект:веб-сайтпообменуопытом
Узанятогосервера,использующегодлинныезапросы,могутвисетьоткрытымитысячизапросов,а,следовательно,и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Проект:веб-сайтпообменуопытом
Запроснасозданиетемыможетвыглядетьтак:
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Проект:веб-сайтпообменуопытом
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Проект:веб-сайтпообменуопытом
совпадающимсрегуляркой/^\/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Проект:веб-сайтпообменуопытом
путьнайден.
Функцииобработчиковвызываютсясобъектами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Проект:веб-сайтпообменуопытом
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Проект:веб-сайтпообменуопытом
Функция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Проект:веб-сайтпообменуопытом
}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Проект:веб-сайтпообменуопытом
Самыйинтересныйаспектсервера–часть,котораяподдерживаетдлинныезапросы.Когданаадрес/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Проект:веб-сайтпообменуопытом
всехтемивозвращаетего.
Иначе,сперванадопроверитьпараметр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Проект:веб-сайтпообменуопытом
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Проект:веб-сайтпообменуопытом
Серверыпообщепринятойсхемевслучаезапросапути,соответствующегодиректории,отдаютфайлподименем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Проект:веб-сайтпообменуопытом
сделать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Проект:веб-сайтпообменуопытом
Первое,чтоклиентдолженсделатьпризагрузкестраницы,этозапроситьссервератекущийнабортем.Таккакмыбудемделатьмного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Проект:веб-сайтпообменуопытом
неделала.Мыопределимпростуюфункциюподназванием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Проект:веб-сайтпообменуопытом
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Проект:веб-сайтпообменуопытом
Второйаргумент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Проект:веб-сайтпообменуопытом
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Проект:веб-сайтпообменуопытом
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Проект:веб-сайтпообменуопытом
отладки),иделаетсяследующаяпопыткачерез2.5секунды.
Когдазапросудаётся,наэкранвыводятсяновыеданные,иlastServerTimeобновляется,чтобыотражатьтотфакт,чтомыполучилиданныевсоответствиисэтимновыммоментомвремени.Запроссразустартуетзаново,иждётследующегообновления.
Есливызапуститесервер,иоткроетедваокнабраузерасадресомlocalhost:8000,выувидите,чтодействия,выполняемыевамиводномокне,моментальноотображаютсявдругом.
Следующиеупражнениязаключаютсявизменениисистемы,описаннойвэтойглаве.Дляработынадними,убедитесь,чтовыскачаликодиустановилиNode.js.
Сервердержитвседанныевпамяти.Еслионупадётилиперезапустится,всетемыикомментариибудутпотеряны.
Расширьтеегофункциональностьстем,чтобыонсохранялданныенадискеиавтоматическизагружалихприперезагрузке.Неволнуйтесьнасчётэффективности,сделайтесамыйпростойвариант.
Общаяперерисовкавсехтемработаетнеплохо,потомучтонетразличиямеждуузломDOMиегозаменой,когдаониодинаковые.Ноестьисключения.Есливыначнётёпечататьчто-либовполекомментарияктемеводномокнебраузера,азатемвдругомокнедобавитекомментарийкэтойтеме,полевпервомокнебудетперерисовано,ибудетпотеряноиегосодержимое,ифокус.
Пригорячемобсуждении,когданесколькочеловекдобавляюткомментариикоднойтеме,этооченьраздражалобы.Можетеливыпридумать,как
Упражнения
Сохранениесостояниянадиск
Обнулениеполейкомментариев
ВыразительныйJavascript
423Проект:веб-сайтпообменуопытом
избежатьэтого?
Большинствошаблонизаторовделаютбольше,чемпростозаполняютшаблоныстроками.Поменьшеймереонипозволяютдобавлятьвшаблоныусловия,аналогичнооператору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Проект:веб-сайтпообменуопытом
Есликто-нибудьзайдётнанашсайтсотключеннымJavaScript,ониполучатсломаннуюнеработающуюстраницу.Этонеочень-тохорошо.
Некоторыеразновидностивеб-приложенийнеполучитсясделатьбезJavaScript.Длядругихнехватаетфинансированияилитерпения,чтобызаботитьсяопосетителяхбезскриптов.Нодляпосещаемыхстраницсчитаетсявежливымподдержатьтакихпользователей.
Попробуйтепридуматьспособ,которымбывеб-сайтпообменуопытомможнобылобысделатьработающимбезJavaScript.Придётсяввестиавтоматическиеобновлениястраниц,аперезагружатьстраничкипользователямпридётсяпостаринке.Нобылобынеплохоуметьпросматриватьтемы,создаватьновыеиотправлятькомментарии.
Незаставляювасегореализовывать.Достаточноописатьвозможноерешение.Кажетсяливамтакойвариантсайтаболееилименееэлегантным,чемтот,чтомыужесделали?
ВыразительныйJavascript
425Проект:веб-сайтпообменуопытом