автордың кітабын онлайн тегін оқу Вы пока еще не знаете JS} Область видимости и замыкания
Переводчик Е. Матвеев
Художник В. Мостипан
Корректоры М. Одинокова, Г. Шкатова
Верстка Е. Неволайнен
Кайл Симпсон
{Вы пока еще не знаете JS} Область видимости и замыкания. 2-е межд. издание. — СПб.: Питер, 2021.
ISBN 978-5-4461-1876-2
© ООО Издательство "Питер", 2021
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Благодарности
Прежде всего спасибо моей жене и детям. Их постоянная поддержка позволила мне продолжать работу. Также хочу поблагодарить 500 бэкеров первого издания «Вы не знаете JS» (YDKJS) на Kickstarter, а также сотни тысяч людей, которые купили и прочли эти книги. Без вашей финансовой поддержки второе издание не состоялось бы. Также спасибо интервьюеру из одной соцсети с птичьим названием, который сказал, что я «недостаточно знаю JS», чем помог мне выбрать название для серии книг.
Своей карьерой я в значительной мере обязан Марку Грабански (Marc Grabanski) и FrontendMasters. Много лет назад Марк оказал мне доверие и помог сделать первые шаги в области преподавания. Если бы не он, я не начал бы писать книги! Frontend Masters является главным спонсором «Вы все еще не знаете JS») (2-е издание). Спасибо вам, Frontend Masters (и Марк!).
Наконец, мой редактор Саймон Сен-Лоран (Simon St.Laurent) помог мне определиться с первоначальным замыслом серии YDKJS и стал редактором моей первой книги. Поддержка и советы Саймона оказали на меня серьезное влияние, и именно благодаря им я в значительной мере сформировался как автор. Прошло много лет с тех пор, как за выпивкой в Driskill родился замысел YDKJS. Спасибо тебе, Саймон, за все эти годы, что ты указывал мне путь и улучшал эти книги!
Предисловие
Когда я смотрю на книги на полке, сразу вижу любимые. Любимые книги всегда потертые. Переплет надорван, на замусоленных страницах — пятна от пролитых напитков. Удивительно, что самые любимые книги выглядят так, словно о них меньше всего заботятся, хотя, честно говоря, все совсем наоборот.
Первое издание этой книги — одно из самых моих любимых. Она невелика, но переплет уже начал разваливаться. Страницы потрепаны, уголки загибаются. Это явно не книга на один раз. Я снова и снова возвращалась к ней в течение многих лет, прошедших с момента ее издания.
Для меня она также стала вехой моего личного прогресса в изучении JavaScript. Впервые она попалась мне в руки в 2014 году; на тот момент я была знакома с основными концепциями, но, откровенно говоря, глубина моего понимания не могла сравниться с тем, что описано в этой тоненькой книжице.
Шли годы. И хотя мне порой казалось, будто мои профессиональные навыки вовсе не улучшаются, мне все же удалось разобраться со всеми концепциями из книги. Я улыбаюсь, сознавая, какой путь прошла под этим руководством. Стало очевидно, что между моей любовью к этой книге и моим бережным отношением к ней была обратно пропорциональная связь.
Когда Кайл предложил написать вступление ко 2-му изданию, я была ошеломлена. Нечасто вам предлагают написать что-то о книге, которая оказала такое влияние на ваше собственное понимание и карьеру. Помню тот день, когда впервые поняла суть замыканий; первый раз, когда я успешно воспользовалась ими. Тогда я была горда собой, отчасти из-за того, что меня привлекала симметрия этой идеи. Я была восхищена замыканиями еще до того, как взялась за эту книгу. Но просто написать рабочий код — совсем не то же самое, что глубоко изучить концепции. Эта книга улучшила мое понимание фундаментальных вещей и помогла их мастерски освоить.
Книга получилась обманчиво короткой. То, что она настолько мала, — весьма удобно, так как материал очень информационно насыщен. Рекомендую побольше времени выделить на усвоение каждой страницы. Не торопитесь. Относитесь к книге со всем вниманием — чтобы она стала такой же потрепанной и зачитанной, как и моя.
Сара Дрейснер (Sarah Drasner), руководитель группы DX, Netlify
Вступление
Вашему вниманию предлагается 2-е издание снискавшей популярность серии книг «Вы не знаете JS»: «Вы пока еще не знаете JS» (YDKJSY).
Если вы уже читали предыдущее издание, то заметите, что в этом появился обновленный подход к изложению с подробными описаниями того, что изменилось в JS за последние 5 лет.
Я надеюсь и верю, что вы все еще сохраняете стремление изучить JS и разобраться в том, как же он устроен.
Если вы читаете эти книги впервые, я рад, что они попались вам на глаза. Подготовьтесь к увлекательному путешествию по закоулкам JavaScript.
Если вы недавно занимаетесь программированием или JS, то учтите, что эти книги не задумывались как «деликатный вводный курс по JavaScript». Временами материал становится сложным и требующим серьезных усилий, и многие темы рассматриваются намного глубже, чем в книгах для новичков. Книга может пригодиться всем читателям независимо от уровня подготовки, но я писал ее с прицелом на то, что вы уже знакомы с JS, а ваш практический опыт работы с этим языком составляет хотя бы полгода, если не больше.
Части языка
В этих книгах я намеренно отошел от традиционного подхода, в котором рассматриваются хорошие части языка. Нет, это не означает, что мы будем рассматривать только плохиечасти — скорее рассматриваться будут все части.
Возможно, вы слышали (или сами считаете), что JS — глубоко ущербный язык, плохо спроектированный и непоследовательно реализованный. Многие считают, что это худший из популярных языков; что никто не пишет код JS добровольно, а только из-за того, что он занял свое место в сети. Это смехотворные, нездоровые и высокомерные утверждения.
Миллионы разработчиков ежедневно пишут код JavaScript, и многие из них уважают и ценят этот язык.
Как и у любого великого языка, у него есть как выдающиеся достоинства, так и недостатки. Даже сам создатель JavaScript Брендан Эйх сожалеет по поводу некоторых частей и называет их ошибками. Но он заблуждается: они вовсе не были ошибками. В наши дни JS стал тем, чем он стал — самым распространенным, а следовательно, самым влиятельным языком программирования, — именно из-за всех этих частей.
Не ведитесь на утверждения, будто вам следует изучить и использовать только небольшой набор хороших частей, а от всего плохого нужно держаться подальше. Не ведитесь на шарлатанство «X — это новый Y», будто с появлением в языке некоторой новой возможности все предшествующее использование старой функциональности мгновенно устаревает и отмирает. Не слушайте, когда кто-то вам говорит, что ваш код «не современен», потому что в нем еще не используется функция стадии 0, предложенная лишь несколько недель назад!
Все части JS полезны. Некоторые части полезнее других. Некоторые требуют действовать более внимательно и осознанно.
На мой взгляд, абсурдно даже пытаться стать по-настоящему эффективным разработчиком JavaScript, используя только узкий срез возможностей языка. Можно ли представить рабочего с полным ящиком инструментов, который пользуется только молотком, а отвертку и рулетку презирает, считая их недостойными? Это просто глупо.
Я утверждаю, что изучать нужно все части JavaScript и пользоваться ими там, где они уместны! И я даже наберусь смелости предложить: выбросьте все книги, в которых говорится обратное.
Название?
Какой же смысл заложен в название серии?
Я не пытаюсь обидеть вас, ставя под сомнение ваш уровень знания или понимания JavaScript. Я не предполагаю, что вы не можете или не сможете изучить JavaScript. Я не хвастаюсь некими секретными тайными знаниями, которыми обладаю только я и еще несколько избранных.
Серьезно, все это реальные реакции на название оригинальной серии, которые появились еще до того, как книги увидели свет. И они совершенно необоснованны.
Главный смысл названия «Вы пока еще не знаете JS» — подчеркнуть, что большинство разработчиков JS не тратит время на то, чтобы по-настоящему понять, как работает написанный ими код. Они знают, что код работает — он выдает желаемый результат. Но они либо не понимают, как он работает, либо, что еще хуже, руководствуются неточной ментальной моделью, которая дает сбой при ближайшем рассмотрении.
Я предлагаю вам спокойно, но вдумчиво отложить все свои допущения по поводу JS, взглянуть на язык свежим взглядом и подойти к нему с заново пробужденной любознательностью. Спрашивайте себя «почему?» каждый раз, когда пишете строчку.Почему она работает именно так, а не иначе? Почему один способ лучше или уместнее пяти-шести других возможных решений? Почему все «лидеры мнений» предлагают делать X в вашем коде, но выясняется, что вариант Y оказывается лучше?
Я добавил в название «пока» не только потому, что это второе издание, но и из-за того, что в конечном итоге я хочу, чтобы книги вселяли в вас надежду, а не убивали ее.
Не думаю, что JS вообще возможно знать полностью. Это не достижение, которое необходимо получить, а цель, к которой нужно стремиться. Не думайте, что вы все узнаете о JS и на этом все закончится; нет, вы просто продолжаете учиться, все чаще практикуясь в написании кода. И чем глубже вы погружаетесь, тем чаще возвращаетесь к тому, что изучали ранее, и переосмысливаете его с позиций более опытного разработчика.
Рекомендую сформировать особую систему взглядов на JavaScript (и на разработку в целом): вы никогда не освоите его полностью, но можете (и должны) работать над тем, чтобы приблизиться к этой цели. Этот путь растянется на всю вашу карьеру разработчика и даже дальше.
Вы всегда можете знать JS лучше, чем сейчас. Надеюсь, именно эту мысль передают книги серии YDKJSY.
Миссия
На самом деле не нужно обосновывать, почему разработчики должны относиться к JS серьезно — думаю, язык уже доказал, что заслуживает статуса первоклассного среди языков программирования.
Важно обосновать другое, более глобальное утверждение, и эти книги пытаются справиться с этой задачей.
Я обучал более 5000 разработчиков из групп и компаний по всему миру более чем в 25 странах на шести континентах. Мне часто приходилось видеть, что главным фактором считается только результат программы, а не то, как программа написана или как/почему она работает.
Мой опыт не только как разработчика, но и как преподавателя говорит мне: вы всегда можете повысить эффективность своего труда, если четко будете понимать, как работает ваш код (а не просто добиваться того, чтобы он выдавал желаемый результат).
Иначе говоря, «код достаточно хорош, чтобы работать» — не то же самое, что «код достаточно хорош» (и не должно быть тем же самым).
Всем разработчикам постоянно приходится мучиться с каким-нибудь блоком кода, который по неизвестной причине работает неправильно. Но слишком часто разработчики JS обвиняют язык, вместо того чтобы винить себя за нехватку понимания. Эти книги служат вопросом и ответом: почему произошло именно это и как нужно действовать, чтобы произошло вот это.
Моя миссия — дать возможность каждому разработчику JS полностью контролировать написанный им код, понять его и программировать сознательно и ясно.
Путь
Некоторые из вас начали читать эту книгу с целью изучить все шесть книг от начала и до конца.
Давайте немного скорректируем этот план. Последовательное чтение книг серии не входило в мои намерения. Материал в них освоить не так-то просто, потому что JavaScript — язык мощный, замысловатый и порой достаточно сложный. Никому не удастся загрузить всю эту информацию в мозг за один проход, вы неизбежно забудете почти все прочитанное. Лучше даже не пытаться.
Мой совет: не торопитесь. Возьмите одну главу, прочитайте ее полностью от начала до конца, потом вернитесь и перечитайте раздел за разделом. Разберите код и идеи в каждом разделе. Если вы столкнетесь с чем-то сложным, лучше провести несколько дней за усвоением, повторным чтением и тренировками, а потом продолжить изучение.
На каждую главу можно выделить неделю или две, на каждую книгу — месяц или два, на всю серию — год и более, и даже в этом случае вы еще не выжмете из YDKJSY все возможное.
Не читайте эти книги взахлеб; будьте терпеливы. Чередуйте чтение с практикой: применяйте знания в рабочих задачах или собственных проектах. Оспаривайте мои идеи, возражайте, а самое главное — не соглашайтесь со мной! Организуйте учебную группу или клуб. Проводите мини-семинары в своем офисе. Пишите посты о прочитанном. Обсудите эти темы на локальных встречах JS.
Моя цель не навязать вам свое мнение. Скорее я хочу выработать у вас собственное мнение и умение его отстаивать. Вы не сможете достичь этой цели скоростным чтением. На это уйдет немало времени. Вы будете двигаться вперед шаг за шагом, пока изучаете, размышляете и возвращаетесь к прочитанному. Эти книги были задуманы как путеводитель по JavaScript от вашего текущего местонахождения в знаниях о языке до точки более глубокого понимания. А теперь самая интересная часть: чем глубже вы понимаете JS, тем больше вопросов у вас появится и тем больше придется изучать!
Я очень рад, что вы отправляетесь в путешествие, и для меня большая честь, что вы сочли мои книги достойными своего внимания и решили довериться им. Пришло время начать изучение JS!
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.
Мы снабдили некоторые изображения QR-кодами, чтобы вы могли посмотреть их цветные версии.
1. Что такое область видимости?
Вероятно, к тому моменту, когда вы уже написали несколько первых программ, то уже освоились с созданием переменных и хранением в них значений. Работа с переменными — один из фундаментальных навыков в программировании!
Но возможно, вы не уделяли особого внимания механизмам, которые используются движком для организации и управления этими переменными. Я говорю не о выделении памяти компьютером, а совсем о другом: как JS узнает, какие переменные доступны для любой конкретной команды, и как она поступает, обнаруживая две переменные с одинаковыми именами?
Ответы на подобные вопросы воплощаются в системе четко определенных правил, называемых областью видимости (scope). В этой книге будут подробно рассмотрены все аспекты области видимости — как она работает, для чего она нужна, каких скрытых в ней ловушек стоит остерегаться. После этого будут представлены распространенные паттерны области видимости, определяющие структуру программ.
А начнем мы с описания того, как движок JS обрабатывает наши программы перед запуском.
О книге
Добро пожаловать во вторую книгу серии «Вы пока еще не знаете JS»! Если вы уже прочитали первую книгу «Познакомьтесь, JavaScript», вы на верном пути! Если нет, рекомендую начать с первой книги, которая подготовит вас к этому материалу.
Книга посвящена первому из трех столпов языка JS: системе областей видимости и ее функциональным замыканиям, а также мощному паттерну проектирования «Модуль».
JS обычно относят к категории интерпретируемых языков сценариев, поэтому предполагается, что большинство программ JS обрабатывается за один проход «сверху вниз». Но в действительности JS разбирается/компилируется в отдельной фазе до начала выполнения. Решения автора кода в отношении того, как размещать переменные, функции и блоки относительно друг друга, анализируются с учетом правил области видимости в исходной фазе разбора/компиляции. Полученная структура кода обычно не зависит от условий стадии выполнения.
Функции JS сами по себе являются полноправными значениями; их можно присваивать и передавать точно так же, как числа или строки. Но так как эти функции содержат переменные и обращаются к ним, они поддерживают свою исходную область видимости независимо от того, в какой точке программы эти функции будут выполняться в конечном итоге. Эта концепция называется замыканием.
Модули представляют паттерн организации кода, для которого характерны открытые методы с привилегированным доступом (через замыкание) к скрытым переменным и функциям во внутренней области видимости модуля.
Компилируемые и интерпретируемые языки
Конечно, вы уже слыхали о компиляции кода. Но скорее всего, процесс компиляции кажется вам чем-то вроде «черного ящика», в который с одного конца подается исходный код, а с другого — выскакивают исполняемые программы.
Но ничего загадочного или волшебного здесь нет. Компиляция кода — последовательность шагов, которая обрабатывает исходный код и преобразует его в набор инструкций, понятных компьютеру. Как правило, весь исходный код преобразуется одновременно, и эти инструкции сохраняются как результат (обычно в файле), который может быть выполнен позднее.
Возможно, вы также слышали, что код может интерпретироваться. Чем же интерпретация отличается от компиляции?
Интерпретация решает примерно ту же задачу, что и компиляция, — в том смысле, что преобразует вашу программу в набор инструкций, понятных машине. При этом используется другая модель обработки. Если при компиляции обрабатывается сразу вся программа, при интерпретации исходный код преобразуется строка за строкой; после выполнения каждой строки или команды происходит немедленный переход к следующей строке исходного кода.
Рис. 1. Компилируемый и интерпретируемый код
На рис. 1 представлена сравнительная схема интерпретации и компиляции программ.
Являются ли эти две модели обработки взаимоисключающими? В общем случае да. Но проблема скрывает ряд нюансов, потому что интерпретация в действительности может принимать другие формы помимо простой построчной обработки исходного кода. Современные движки JS в действительности применяют различные сочетания компиляции и интерпретации при обработке программ JS.
Напомню, что эта тема рассматривалась в главе 1 книги «Познакомьтесь, JavaScript». Там был сделан вывод, что JS точнее всего описывать как компилируемый язык. Для удобства читателей в следующих разделах мы вернемся к этому утверждению и расширим его.
Компиляция кода
Но для начала необходимо понять, зачем мы вообще говорим о том, компилируется JS или нет?
Видимость в основном определяется в фазе компиляции, поэтому понимание связи между компиляцией и выполнением играет ключевую роль для понимания областей видимости.
В классической теории компиляторов обработка программы компилятором состоит из трех основных этапов:
1. Разбиение на лексемы/лексический разбор: строка символов разбивается на осмысленные (для языка) фрагменты, называемые лексемами. Для примера возьмем программу: var a = 2;. Скорее всего, эта программа будет разбита на следующие лексемы: var, a, =, 2 и ;. Пробелы могут сохраняться как лексемы, а могут и не сохраняться — это зависит от того, содержательны они или нет.
Различия между разбиением на лексемы (tokenizing) и лексическим разбором (lexing) достаточно тонкие и академические, но это зависит от того, как идентифицируются лексемы — с учетом состояния или без. Проще говоря, если для определения того, должен ли символ a считаться отдельной лексемой или просто частью другой лексемы, подсистема разбиения на лексемы должна активизировать правила разбора с учетом состояния, это будет лексический разбор.
2. Разбор: поток (массив) лексем преобразуется в дерево вложенных элементов, которые совместно представляют грамматическую структуру программы. Оно называется абстрактным синтаксическим деревом (AST).
Например, дерево для программы var a = 2; может начинаться с узла верхнего уровня с именем VariableDeclaration, имеющего дочерний узел с именем Identifier (со значением a) и еще один дочерний узел AssignmentExpression, у которого есть дочерний узел NumericLiteral (со значением 2).
3. Генерирование кода: AST преобразуется в исполняемый код. Эта часть очень сильно изменяется в зависимости от языка, целевой платформы и других факторов. Движок JS получает только что описанное дерево AST для var a = 2; и преобразует его в набор машинных команд для фактического создания переменной с именем a (включая резервирование памяти и т.д.) и последующего сохранения значения в a.
Подробности реализации движка JS (использование ресурсов системной памяти и т.д.) намного глубже, чем мы здесь рассматриваем. Основное внимание будет уделяться наблюдаемому поведению наших программ, а управление более глубокими абстракциями системного уровня будет доверено движку JS.
Принципы работы движка JS намного сложнее, они далеко не сводятся только к этим трем стадиям. В процессе разбора и генерирования кода выполняются особые действия для оптимизации быстродействия (например, исключение избыточных элементов). Более того, код даже может быть перекомпилирован и заново оптимизирован в ходе выполнения.
В общем, я привожу только общую картину. Но вскоре вы увидите, почему те подробности, которые здесь рассматриваются (даже на высоком уровне), важны для изучаемой темы.
Движку JS недоступна такая роскошь, как лишнее время для выполнения их работы и оптимизаций, потому что компиляция JS не выполняется на отдельной стадии до выполнения, как в других языках. Обычно она должна выполняться за считаные микросекунды (или менее!) прямо перед выполнением кода. Чтобы обеспечить наилучшее быстродействие в этих условиях, движок JS использует всевозможные трюки (например, метод JIT с отложенной компиляцией и даже горячей перекомпиляцией); все они выходят за рамки нашего изложения.
Две фазы
В самой простой формулировке самое важное замечание, которое можно сделать по поводу обработки программ JS, заключается в том, что она выполняется (как минимум) в две фазы: сначала разбор/компиляция, затем выполнение.
Отделение фазы разбора/компиляции от последующей фазы выполнения — наблюдаемый факт, а не какая-то теория или субъективное мнение. Хотя спецификация JS не требует компиляции явно, она требует поведения, которое по сути реально только при подходе «компиляция с последующим выполнением».
Чтобы убедиться в этом, можно понаблюдать за тремя характеристиками программ: синтаксические ошибки, ранние ошибки и поднятие (hoisting).
Синтаксические ошибки
Рассмотрим следующую программу:
var greeting = "Hello";
console.log(greeting);
greeting = ."Hi";
// SyntaxError: unexpected token .
Программа ничего не выводит (сообщение "Hello" не выводится), а вместо этого выдает ошибку SyntaxError о неожиданной лексеме . прямо перед строкой "Hi". Так как синтаксическая ошибка происходит после правильно сформированной команды console.log(..), если бы код JS выполнялся при построчном выполнении программы сверху вниз, можно было бы ожидать, что сообщение "Hello" будет выведено перед выдачей синтаксической ошибки. Но этого не происходит.
Движок JS может узнать о синтаксической ошибке в третьей строке, перед выполнением первой и второй строк, только в одном случае: если движок JS сначала разбирает всю программу до того, как будет выполнена любая из ее частей.
Ранние ошибки
Теперь следующая программа:
console.log("Howdy");
saySomething("Hello","Hi");
// Неперехваченная ошибка SyntaxError: одинаковые имена
// параметров недопустимы в этом контексте
function saySomething(greeting,greeting) {
"use strict";
console.log(greeting);
}
Сообщение "Howdy" не выводится, несмотря на правильно сформированную команду.
Вместо этого, как и во фрагменте из предыдущего раздела, перед выполнением программы выдается ошибка SyntaxError. В данном случае это объясняется тем, что строгий режим (включенный только для функции saySomething(..)) запрещает среди прочего функции с одинаковыми именами параметров; в нестрогом режиме это всегда было разрешено.
Выданная ошибка не является синтаксической ошибкой, обусловленной неправильно сформированной последовательностью лексем (как ."Hi" выше), но в строгом режиме спецификация требует выдавать раннюю ошибку до начала выполнения.
Но как движок JS узнает, что параметр greeting повторяется? Откуда он знает, что функция saySomething(..) выполняется в строгом режиме во время обработки списка параметров (директива "use strict" появляется позже в теле функции)?
И снова возможно только одно разумное объяснение: код полностью разбирается до начала выполнения.
Поднятие
Наконец, рассмотрим пример:
function saySomething() {
var greeting = "Hello";
{
greeting = "Howdy"; // здесь происходит ошибка
let greeting = "Hi";
console.log(greeting);
}
}
saySomething();
// ReferenceError: невозможно обратиться к 'greeting'
// до инициализации
Источником ошибки ReferenceError является строка с командой greeting = "Howdy". Дело в том, что переменная greeting из этой команды относится к объявлению в следующей стр
