В течение первых нескольких лет использования JavaScript я чувствовал себя чуть ли не самозванцем. Даже хотя я и мог создавать веб-сайты с помощью фреймворков, я ощущал, что мне чего-то не хватает. Собеседования по JavaScript внушали мне страх из-за того, что у меня не было чёткого понимания основ этого языка.
За многие годы я сформировал ментальную модель JavaScript, которая дала мне ощущение уверенности. Здесь я собираюсь поделиться с вами весьма сжатым вариантом этой модели. Её структура напоминает словарь. Каждое понятие описано в нескольких предложениях.
По мере того, как вы будете читать этот материал, попробуйте мысленно оценить то, насколько вы уверенно чувствуете себя по отношению к каждому рассматриваемому здесь вопросу. И если окажется так, что многое отсюда покажется вам не особенно знакомым, я вас за это не осужу. Но если это и правда так — в конце материала есть то, что поможет вам исправить ситуацию.
Ментальная модель JavaScript
- Значение. Концепция значения немного абстрактна. Это — «нечто». Значение в JavaScript — это то же самое, что число в математике, или точка в геометрии. Когда ваша программа выполняется — мир этой программы полон значений. Числа, вроде
1
,2
и420
— это значения. Но значениями являются и другие сущности. Например — предложение"Cows go moo"
. Правда, не всё является значением. Число — это значение, но инструкцияif
— это уже не значение. Ниже мы ещё поговорим о различных видах значений.- Тип значения. Существуют различные «типы» значений. Например, числа — вроде
420
, строки — такие, как"Cows go moo"
, объекты. Есть и другие типы значений. Узнать тип значения можно с помощью оператораtypeof
. Например, командаconsole.log(typeof 2)
приведёт к выводу в консольnumber
. - Примитивные значения. Некоторые значения имеют «примитивные» типы. Это — числа, строки, и ещё некоторые значения. Одно интересное свойство примитивных значений заключается в том, что нельзя создать больше таких значений, чем есть в языке, нельзя и менять существующие примитивные значения. Например, каждый раз, когда вы используете в коде число
2
— это будет одно и то же значение2
. В программе нельзя «создать» ещё одно значение2
, или сделать так, чтобы2
«превратилось» бы в3
. Это справедливо и для строк. - Значения
null
иundefined
. Это — два особых значения. Они не такие, как другие, из-за того, что с ними много чего нельзя делать — их появление часто приводит к ошибкам. Обычно использованиеnull
представляет собой указание на то, что некое значение не было назначено переменной умышленно, аundefined
говорит о том, что некое значение отсутствует по случайности. Однако то, как именно использовать эти значения, программист решает сам. Эти значения существуют из-за того, что иногда лучше, чтобы в ходе выполнения некоей операции произошла бы ошибка, а не случилось бы так, что выполнение программы продолжилось бы после «обработки» несуществующего значения.
- Тип значения. Существуют различные «типы» значений. Например, числа — вроде
- Равенство. Как и понятие «значение», понятие «равенство» является одной из фундаментальных концепций JavaScript. Мы говорим о том, что два значения равны в том случае, если они… на самом деле, не буду этого говорить. Если два значения равны, то это значит, что они являются одним и тем же значением. Не двумя разными значениями, а одним! Например, справедливы равенства
"Cows go moo" === "Cows go moo"
и2 === 2
. И тут всё понятно:2
— это2
. Обратите внимание на то, что мы используем три знака равенства, которые представляют вышеописанную концепцию равенства значений в JavaScript.- Строгое равенство. О нём мы только что говорили в предыдущем пункте.
- Равенство ссылок. И о нём мы тоже только что говорили.
- Нестрогое равенство. О, а вот это — уже кое-что совсем другое. В JavaScript проверка на нестрогое равенство значений производится с использованием оператора, состоящего из двух знаков равенства (
==
). Сущности могут быть признаны нестрого равными друг другу даже в том случае, если они представлены различными значениями, выглядящими похожими друг на друга (нечто вроде2
и"2"
). Оператор нестрогого равенства был добавлен в JavaScript на ранних стадиях становления языка, для удобства. С тех пор он является бездонным источником путаницы. Концепцию нестрогого равенства нельзя назвать фундаментальной, но она является типичным источником ошибок. Вы можете изучить оператор нестрогого равенства в какой-нибудь дождливый день, но многие стараются попросту не использовать оператор==
.
- Литерал. Литералы используют тогда, когда на значение ссылаются, записывая его в коде программы. Например,
2
— это числовой литерал, а"Banana"
— это строковой литерал. - Переменная. Переменные позволяют ссылаться на значения, используя имена. Например —
let message = "Cows go moo"
. После того, как в коде была использована подобная конструкция, везде, где понадобится предложение"Cows go moo"
, можно писать простоmessage
, а не повторять это предложение. Позже можно поменятьmessage
, сделав так, чтобы переменная указывала бы на что-то другое. Например, воспользовавшись такой конструкцией:message = "I am the walrus"
. Обратите внимание на то, что это не меняет самого значения. Это влияет лишь на то, на что именно ссылается переменная. Это — вроде «подключения» имени переменной к чему-то другому. Сначала переменная была «подключена» к"Cows go moo"
, а теперь — к"I am the walrus"
.- Область видимости переменной. Если бы во всей программе можно было бы использовать лишь одну переменную с именем
message
— это было бы очень плохо. Когда мы объявляем переменную, она оказывается доступной лишь в некоторой части программы. Эта часть называется «областью видимости переменной». Существуют правила, описывающие особенности работы областей видимости. Обычно выявить область видимости переменной можно, выяснив то, в каком блоке, ограниченном фигурными скобками ({}
), она объявлена. Этот блок и можно назвать областью видимости переменной. - Присваивание значений переменным. Когда мы пишем в коде
message = "I am the walrus"
— это приводит к тому, что мы меняем переменнуюmessage
так, чтобы она указывала бы на значение"I am the walrus"
. Эту операцию называют присвоением переменной значения, или записью чего-либо в переменную, или установкой переменной. - Ключевые слова
let
,const
иvar
. Обычно для объявления переменных лучше всего подходит ключевое словоlet
. Если нужно сделать так, чтобы в переменную нельзя было бы записать ничего нового — можно воспользоваться ключевым словомconst
. (В некоторых кодовых базах и командах педантично относятся к этому вопросу, заставляя всех, в том случае, если значение записывается в переменную лишь один раз, использоватьconst
.) Постарайтесь не пользоваться ключевым словомvar
, так как с переменными, объявленными с его помощью, связаны запутанные правила, касающиеся определения области видимости переменных.
- Область видимости переменной. Если бы во всей программе можно было бы использовать лишь одну переменную с именем
- Тип
Object
. ТипObject
, сущности, принадлежащие к которому, называют объектами, играет в JavaScript особую роль. Примечательная особенность объектов заключается в том, что они могут быть связаны с другими значениями. Например, объект{flavor: "vanilla"}
имеет свойствоflavor
, которое указывает на значение"vanilla"
. Объекты можно воспринимать как самостоятельные значения, из которых тянутся связи к другим значениям.- Свойство объекта. Свойство — это нечто вроде «связи», которая идёт из объекта и указывает на некое значение. Это может напомнить вам идею переменной: у свойства есть имя (вроде
flavor
), оно указывает на некое значение (вроде"vanilla"
). Но, в отличие от переменной, свойства «живут» внутри самого объекта, а не где-то в коде (в некоей области видимости переменной). Свойство считается частью объекта, а значение, на которое ссылается свойство, частью объекта не считается. - Объектный литерал. Объектный литерал — это механизм, позволяющий создавать объекты, внося в код соответствующие конструкции. Например — это
{}
или{flavor: "vanilla"}
. В фигурных скобках может быть объявлено множество пар видасвойство: значение
, разделённых запятыми. Это позволяет нам указывать значения, на которые ссылаются свойства объектов. - Идентичность объектов. Мы уже говорили о том, что
2
равно2
(другими словами —2 === 2
), так как мы, где бы ни записали число2
, «призываем» в это место одно и то же значение. Но каждый раз, когда мы пишем{}
, мы всегда получаем разные значения. Как результат, один объект вида{}
не равен другому объекту, который тоже выглядит как{}
. Попробуйте записать в консоли следующее:{} === {}
(результатом будетfalse
). Когда компьютер встречает в коде число2
— он всегда работает с одной и той же двойкой. Но объектные литералы — это уже кое-что другое. Когда компьютер встречает{}
, он создаёт новый объект, который всегда является новым значением. Как же проверять объекты на равенство? Понятие «равенство» можно рассматривать как понятие «идентичность значений». Когда мы говорим: «a
иb
идентичны» — это значит, что мы имеем в виду то, чтоa
иb
указывают на одно и то же значение (то есть —a === b
). Когда же мы говорим о том, чтоa
иb
не идентичны, это значит, чтоa
иb
указывают на различные значения (то есть —a !== b
). - Точечная нотация. Когда нужно прочитать значение свойства объекта или что-то записать в свойство, можно использовать точечную нотацию (
.
). Например, если переменнаяiceCream
указывает на объект, свойство которогоflavor
содержит строку"chocolate"
, то конструкцияiceCream.flavor
даст нам"chocolate"
. - Скобочная нотация. Иногда заранее неизвестно имя свойства объекта, к которому нужно обратиться. Например, иногда нужно читать значение свойства
iceCream.flavor
, а иногда — значение свойстваiceCream.taste
. Скобочная нотация ([]
) позволяет обращаться к свойствам объектов, задавая их имена с помощью переменных. Например, предположим, что в коде есть такая переменная:let ourProperty = 'flavor'
. Это значит, что конструкция видаiceCream[ourProperty]
даст нам значение"chocolate"
. Что интересно, скобочной нотацией можно пользоваться и при создании объектов:{ [ourProperty]: "vanilla" }
. - Мутация. Мы говорим о том, что объект мутирует (или изменяется) в том случае, если кто-то записывает в его свойство новое значение. Например, если мы создали объект
let iceCream = {flavor: "vanilla"}
, позже мы можем его изменить командойiceCream.flavor = "chocolate"
. Обратите внимание на то, что даже если бы мы объявили переменнуюiceCream
с использованием ключевого словаconst
, это, всё равно, не помешало бы нам изменить свойство объектаiceCream.flavor
. Это так из-за того, что использованиеconst
защищает от перезаписи лишь саму переменнуюiceCream
, а мы меняем свойство (flavor
) объекта, на который ссылается переменная. Некоторые люди отказались от использованияconst
только из-за того, что это ключевое слово способно ввести программиста в заблуждение. - Массив. Массив — это объект, который представляет собой набор неких значений. Массивы можно объявлять с использованием литералов массивов, например — так:
["banana", "chocolate", "vanilla"]
. Использование подобной конструкции приводит к созданию объекта, свойство которого с именем0
указывает на строку"banana"
, свойство1
— на строку"chocolate"
, свойство2
— на значение"vanilla"
. Утомительно было бы записывать то же самое примерно так:{0: ..., 1: ..., 2: ...}
. Поэтому массивы — это полезные структуры. Массивы имеют встроенные механизмы, которые предназначены для работы с их элементами. Среди них — методыmap
,filter
иreduce
. Не расстраивайтесь, если имяreduce
кажется вам непонятным. Оно всем кажется непонятным. - Прототип. Что происходит в том случае, если попытаться обратиться к несуществующему свойству объекта? Например, что случится, если мы обращаемся к
iceCream.taste
, а в объекте есть только свойствоflavor
? Если ответить на этот вопрос, не вдаваясь в детали, то можно сказать, что, попытавшись обратиться к несуществующему свойству, мы получим особое значениеundefined
. Если дать на этот вопрос развёрнутый ответ, то начать надо с того, что большинство объектов в JavaScript имеют так называемый «прототип». Прототип объекта можно воспринимать как «скрытое» свойство, которое указывает системе на то, где нужно искать запрашиваемое свойство в том случае, если в самом объекте его нет. В нашем примере, когда оказывается, что в объектеiceCream
нет свойстваtaste
, JavaScript будет искать это свойство в прототипе объекта, который тоже является объектом. А если и там его не найдёт — то в прототипе прототипа, и так далее. Значениеundefined
будет выдано только тогда, когда будет достигнут конец «цепочки прототипов», и при этом свойство.taste
так и не будет найдено. Вам редко придётся напрямую работать с этим механизмом, но, зная о прототипах, можно понять то, почему у объектаiceCream
есть методtoString
, который мы никогда не объявляли. Этот метод берётся из прототипа объекта.
- Свойство объекта. Свойство — это нечто вроде «связи», которая идёт из объекта и указывает на некое значение. Это может напомнить вам идею переменной: у свойства есть имя (вроде
- Функция. Функция — это особое значение, существующее с единственной целью: представление некоего фрагмента кода программы. Функции удобны в тех ситуациях, когда программист не хочет постоянно писать один и тот же код. «Вызов» функции, выглядящий как
sayHi()
, сообщает компьютеру о том, что ему нужно выполнить код, находящийся внутри функции, а потом — вернуться туда, где была вызвана функция. В JavaScript существует множество способов объявления функций, которые немного отличаются друг от друга.- Аргументы (или параметры) функции. Аргументы позволяют передавать в функцию некие данные из того места, где вызывается функция. Например, это может выглядеть так:
sayHi("Amelie")
. Поведение аргументов в функции похоже на поведение переменных. Слова «параметры» и «аргументы» используют в зависимости от того, о чём именно идёт речь — об объявлении функции, или о её вызове. Однако эта тонкость терминологии важна для тех, кто педантично подходит к программированию, на практике эти термины используются взаимозаменяемо. - Функциональное выражение. Ранее мы записывали в переменные строковые значения. Например —
let message = "I am the walrus"
. Как оказывается, в переменную можно записать и функцию:let sayHi = function() { }
. То, что находится после знака=
, называется функциональным выражением. Оно даёт нам особое значение (функцию), которое представляет собой фрагмент кода. Если нам нужно выполнить этот код — мы можем вызвать соответствующую функцию. - Объявление функции. Программисту может надоесть постоянно писать нечто вроде
let sayHi = function() { }
. Если это так — то тут можно воспользоваться более краткой формой описания функции:function sayHi() { }
. Эта конструкция называется объявлением функции. Вместо того чтобы указывать в левой части выражении имя переменной, мы помещаем это имя после ключевого словаfunction
. Обычно два вышеописанных стиля создания функций взаимозаменяемы. - Поднятие функций в верхнюю часть области видимости. Обычно переменной можно пользоваться только после того, как она была объявлена помощью
let
илиconst
, ниже места её объявления. В случае с функциями это может оказаться неудобным. Функции могут вызывать друг друга. Непростой задачей способно оказаться выяснение того, какая из них должна быть создана первой. Хорошо то, что при использовании объявлений функций (и только при использовании этого метода!), порядок описания функций неважен. Дело в том, что при таком подходе функции «поднимаются» в верхнюю часть области видимости. То есть оказывается, что функции, даже при попытке их вызова из кода, который идёт до их объявления, уже оказываются определёнными и готовыми к работе. - Ключевое слово
this
. Возможно, ключевое словоthis
— это концепция JavaScript, которую чаще других понимают неправильно. Это ключевое слово можно сравнить с особым аргументом функции. Но сами мы его функциям не передаём. Его передаёт JavaScript. Значениеthis
зависит от того, как именно вызывают функцию. Например, при вызове метода объекта с использованием точечной нотации, вродеiceCream.eat()
,this
будет указывать на то, что находится перед точкой. В нашем примере это — объектiceCream
. Значениеthis
в функции зависит от того, как вызвана функция, а не от того, где она была объявлена. Существуют особые методы, такие, как.bind
,.call
и.apply
, которые дают программисту возможность управлять тем, что попадёт вthis
. - Стрелочные функции. Стрелочные функции напоминают функциональные выражения. Объявляют их так:
let sayHi = () => { }
. Они компактны и часто используются для оформления однострочных конструкций. Возможности стрелочных функций ограничены сильнее, чем возможности обычных функций. Например, у них нет ключевого словаthis
. Когда в стрелочной функции используют ключевое словоthis
— оно берётся из той функции, в которую вложена стрелочная функция. Это похоже на обращение к аргументу или к переменной из функции, вложенной в другую функцию. На практике это означает, что стрелочными функциями пользуются тогда, когда хотят, чтобы в них было бы видно то же значениеthis
, которое существует в окружающем их коде. - Привязка значения
this
к функциям. Обычно привязка некоей функцииf
к конкретному значениюthis
и к некоему набору аргументов означает, что создаётся новая функция, которая вызывает функциюf
с этими заранее заданными значениями. В JavaScript есть вспомогательный механизм для привязки функций — метод.bind
, но привязыватьthis
к функции можно и другими способами. Привязка была популярным способом достижения того, чтобы вложенные функции «видели» бы то же значениеthis
, что и внешние по отношению к ним функции. Теперь в подобной ситуации используются стрелочные функции, в результате привязка функций используется в наше время нечасто. - Стек вызовов. Вызвать функцию — это как войти в комнату. Каждый раз, когда мы вызываем функцию, переменные внутри неё снова инициализируются. В результате каждый вызов функции — это нечто вроде строительства новой «комнаты» с кодом функции. Когда «комната» «построена», в неё «входят», выполняется код функции. Переменные, объявленные в функции, «живут» в этой «комнате». Когда осуществляется возврат из функции — «комната» исчезает вместе со всем её содержимым. Все эти «комнаты», создаваемые при вызовах функций, можно представить в виде высокой «башни». Это — стек вызовов. Когда мы выходим из некоей функции — мы попадаем в функцию, которая расположена «ниже» её в стеке вызовов.
- Рекурсия. Рекурсия — это когда функция сама себя вызывает. Эта методика полезна в тех случаях, когда то, что уже было сделано функцией, надо повторить, но с использованием других аргументов. Например, если мы пишем поисковую систему, которая исследует веб-сайты, то у нас может быть функция
collectLinks(url)
. Эта функция сначала собирает ссылки, находящиеся на странице какого-то сайта, а потом сама себя вызывает, передавая себе каждую из найденных ссылок. Это происходит до тех пор, пока не будут посещены все страницы некоего сайта. Опасность рекурсии заключается в том, что вполне можно случайно написать функцию, которая будет вызывать саму себя бесконечно. Правда, если в программе и правда оказывается бесконечная рекурсия, это приведёт к переполнению стека вызовов и выполнение программы остановится с ошибкойstack overflow
. Стек переполняется из-за того, что в него попадает слишком много записей о вызванных функциях. - Функция высшего порядка. Функция высшего порядка — это функция, которая работает с другими функциями, принимая их в виде аргументов или возвращая их в виде результатов своей работы. Поначалу это может показаться странным, но тут стоит помнить о том, что функции — это значения. А значит — обращаться с ними можно так же, как и с другими значениями — с числами, строками, объектами. Если при использовании функций высшего порядка руководствоваться чувством меры, то в результате получается хороший выразительный код.
- Функция обратного вызова. Функция обратного вызова (коллбэк) — это термин, который имеет отношение не только к JavaScript. Это, скорее, паттерн. Он выглядит так: одну функцию передают другой функции в виде аргумента при её вызове. Вызванная функция на некоем этапе своей работы вызовет переданную ей функцию. Например, функция
setTimeout
принимает коллбэк, который… вызывается после истечения тайм-аута. Но надо отметить, что в функциях обратного вызова нет ничего особенного. Это — обычные функции. И когда мы называем их «функциями обратного вызова», это говорит лишь о том, что мы ожидаем их вызова другими функциями. - Замыкание. Обычно, когда мы выходим из функции, переменные, объявленные в ней, просто исчезают. Это происходит из-за того, что они уже никому не нужны. А что если объявить функцию в другой функции и вернуть эту новую функцию при выходе из внешней функции? При таком подходе внутреннюю функцию можно будет когда-нибудь вызвать. А значит — можно будет и обратиться к переменным внешней по отношению к ней функции. На практике это очень полезно. Но для того чтобы эта схема работала, переменные внешней функции должны где-то храниться. Решение этой задачи берёт на себя JavaScript, не уничтожая их, а поддерживая их существование. Эти переменные хранятся в так называемом «замыкании». Хотя замыкания часто относят к сложным для понимания концепциям JavaScript, вы, вероятно, пользуетесь ими по многу раз в день, даже не зная об этом.
- Аргументы (или параметры) функции. Аргументы позволяют передавать в функцию некие данные из того места, где вызывается функция. Например, это может выглядеть так:
Итоги
JavaScript сделан из всех тех концепций, которые мы обсудили. Но состоит этот язык не только из них. Меня очень беспокоили мои знания в области JavaScript. Продолжалось это до тех пор, пока мне не удавалось построить правильную ментальную модель языка. Этим материалом я хочу помочь будущим поколениям разработчиков поскорее понять JavaScript. А вот — мой проект Just JavaScript. Он создан для тех, кто хочет как следует разобраться в том, как работает JavaScript.
Уважаемые читатели! Как вы изучали JavaScript?
Автор: ru_vds