Обновлено 26 февраля 2022: с момент написания оригинального поста Дэн Абрамов запустил обучающий проект Just JavaScript с иллюстрациями от Maggie Appleton.
Перевод поста «What Is JavaScript Made Of?» Дэна Абрамова, разработчика React и активного участника open-source сообщества.
В статье рассмотрены все основные понятия, на которых строится современный JavaScript — значения, типы, равенство, литералы, переменные, объекты, массивы и функции. Кратко и доступно объяснены такие понятия, как хоистинг переменных, мутабельность, прототипы, стек вызовов, функции высшего порядка, коллбеки и замыкания.
Материал будет полезен новичками и разработчикам, пребывающим в начале своего пути изучения JavaScript.
В первые несколько лет работы с JavaScript я чувствовал себя самозванцем. Я вполне мог создавать сайты с помощью фреймворков, но что-то ускользало от моего понимания. Я страшился технических собеседований по JavaScript, поскольку не владел фундаментальным знаниями.
Спустя годы, я сформировал ментальную модель JavaScript, которая вселила в меня уверенность в своих знаниях. Здесь я хочу поделиться очень сжатой версией этой модели. Она структурирована, как словарь, с несколькими предложениями по каждой теме.
В процессе чтения постарайтесь мысленно оценивать, насколько уверенно вы себя чувствуете с каждым обозначенным понятием. Не стоит расстраиваться, если многие из них окажутся для вас сложными. В конце статьи вы найдете кое-что, что может вам помочь.
Значение
Концепция значения довольно абстрактна. Это «сущность». Значение в JavaScript — это то же самое, что число в математике или точка в геометрии. Во время работы вашей программы она наполнена значениями. Числа вроде 1
, 2
и 420
являются значениями, так же как и множество других вещей, вроде этого предложения: "Cows go moo"
. Однако, не все является значением. Число — это значение, а выражение с if
— нет.
Рассмотрим разные типы значений.
Тип значения
Существует несколько разных «типов» значений. Например, числа вроде 420
, строки вроде "Cows go moo"
, объекты и несколько других. Вы можете узнать тип некоторого значения с помощью оператора typeof
. Например, вызов typeof 2
вернет "number"
.
Примитивные значения
Некоторые значения являются примитивами. Они включают в себя числа, строки и некоторые другие. Специфика примитивных значений — вы не можете создать больше примитивов, чем уже существует, или изменить их каким-либо образом. Например, каждый раз обращаясь к примитиву 2
, вы получаете одно и то же значение 2
. Вы не можете «создать» еще одно 2
в своей программе, или превратить 2
в 3
. Этот же принцип верен и для строк.
null
и undefined
Это два специальных значения. Они специальные потому, что многое с ними сделать нельзя — это приведет к ошибке. Обычно, null
представляет некое значение, которое отсутствует намеренно, а undefined
— значение, отсутствующее ненамеренно. Однако, вопрос использования каждого из них оставлен за программистом. Эти значения существуют, так как иногда лучше, чтобы операция завершилась с ошибкой, чем сработала с неверным значением.
Равенство
Так же как и «значение», равенство является фундаментальным кирпичиком в JavaScript. Мы считаем, что два значения равны между собой, когда они... окей, я никогда этого не произнесу. Если два значения равны, это значит, что они являются одним и тем же значением. Не двумя значениями, а одним! Например, "Cows go moo" === "Cows go moo"
и 2 === 2
, потому что 2
является тем же самым 2
. Обратите внимание, что мы используем три знака равенства для представления понятия равенства в JavaScript.
- Строгое равенство: то же, что выше.
- Равенство по ссылке: то же, что выше.
- Нестрогое равенство: а вот это совсем другое! Нестрогое равенство — это когда мы используем два знака равенства (
==
). Сущности могут считаться нестрого равными, даже если они относятся к разным значениям, которые выглядят похожими (например,2
и"2"
). Нестрогое сравнение появилось в JavaScript в давние времена для удобства и с тех самых пор вызывает путаницу. Это не фундаментальный принцип языка, но частый источник ошибок. Вы можете поинтересоваться, как оно работает в какой-нибудь дождливый день, но многие просто его избегают.
Литерал
Литерал — это когда вы используете некое значение посредством его написания в своей программе. Например, 2
— это численный литерал, а "Banana"
— строковый литерал.
Переменная
Переменная позволяет обращаться к некоторому значению по имени. Например, let message = "Cows go moo"
. Теперь вы можете написать message
вместо того, чтобы повторять одно и то же предложение в своем коде. Где-нибудь ниже вы можете изменить message
для обращения к другому значению, например message = "I am the walrus"
. Обратите внимание, что это не приведет к изменению значений, но изменится указание для message
, как при смене соединяющего кабеля. Прежде оно указывало на "Cows go moo"
, а теперь указывает на "I am the walrus"
.
Область видимости
Было бы плохо, если бы во всей программе могла существовать всего одна переменная message
. Поэтому когда вы определеяете переменную, она становится доступна лишь в части вашей программы. Эта часть называется «областью видимости». Существует ряд правил, по которым работает область видимости, однако в большинстве случаев вы можете найти ближайшие фигурные скобки {
и }
, в которые заключено определение переменной. Этот «блок» кода будет ее областью видимости.
Присваивание
Когда мы пишем message = "I am the walrus"
, мы назначаем переменную message
указывать на значение "I am the walrus"
. Это называется присваиванием, записью или назначением переменной.
let
vs const
vs var
Обычно вам нужен let
. Если вы хотите запретить последующее присваивание для этой переменной, можно использовать const
. (В некоторых проектах с устоявшимися кодовыми базами коллеги могут быть педантичны и требовать использование const
в случае единственного присваивания). Избегайте использование var
, так как его область видимости зачастую запутанна.
Объект
Объект — это особый тип значения в JavaScript. Крутой особенностью объектов является то, что они могут иметь связи с другими значениями. Например, объект {flavor: "vanilla"}
имеет свойство flavor
, которое указывает на значение "vanilla"
. Представляйте себе объекты как «ваше собственное» значение с комплектом соединяющих кабелей.
Свойство
Свойство похоже на соединяющий кабель, тянущийся из объекта к некоторому другому значению. Это может напомнить вам переменную: у свойство есть имя (например, flavor
) и оно указывается на значение (например, "vanilla"
). Но в отличие от переменной, свойство объекта «живет» внутри объекта, тогда как переменная — в некоторой части вашего кода (своей области видимости). Свойство считается частью объекта, но значение, на которое оно указывает — нет.
Объектный литерал
Объектный литерал — это способ создания объекта посредством его написания в коде программы, например {}
или {flavor: "vanilla"}
. Внутри {}
мы можем иметь несколько пар свойство: значение
, разделенных запятыми. Это позволяет нам определять, на что указывает свойство нашего объекта.
Тождественность объектов
Ранее мы упомянули, что 2
равно 2
(другими словами, 2 === 2
) потому, что каждый раз, когда мы пишем 2
, мы «призываем» одно и то же значение. Но когда мы пишем {}
, мы всегда получаем разные значения! Поэтому {}
не равно другому {}
. Попробуйте вывести в консоли: {} === {}
(результат будет false
). Когда компьютер встречает 2
в вашем коде, он всегда возвращает одинаковое значение 2
. Однако, объектный литерал устроен иначе: когда компьютер встречает {}
, он создает новый объект, который всегда является новым значением. Что же тогда является тождественностью объектов? Это еще одно определение равенства (или — однозначности) значений. Когда мы говорим «a
и b
тождественны», мы имеем в виду, что «a
и b
указывают на одно и то же значение» (a === b
). Когда мы говорим «a
и b
не тождественны», мы имеем в виду, что «a
и b
указывают на разные значения» (a !== b
).
Нотация через точку (Dot Notation)
Когды вы хотите прочитать или записать свойство объекта, можно использовать нотацию через точку .
. Например, если переменная iceCream
указывает на объект, чье свойство flavor
указывает на значение "chocolate"
, то обратившись к iceCream.flavor
— вы получите "chocolate"
.
Нотация через скобки (Bracket Notation)
Иногда заранее неизвестно название нужного свойства. Например, иногда вы хотите получить значение свойства iceCream.flavor
, а иногда — iceCream.taste
. Нотация через скобки []
позволяет прочитать свойство, когда его название является переменной. Например, пусть let ourProperty = "flavor"
. Тогда iceCream[ourProperty]
вернет нам "chocolate"
. Это может показаться странным, но мы можем использовать нотацию через скобки в том числе для создания объектов: { [ourProperty]: "vanilla" }
.
Мутабельность
Мы говорим, что объект мутировал, когда кто-то изменил его свойство для указания на другое значение. Например, мы задали объект let iceCream = {flavor: "vanilla"}
, затем мы мутировали его — iceCream.flavor = "chocolate"
. Обратите внимание, что мы можем мутировать объект, даже если объявляли его через const
. Это возможно, так как 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 имееют «прототип». К прототипу можно относиться как к скрытому свойству у каждого объекта, который определяет «где искать дальше». Поэтому если свойства taste
в объекте iceCream
не существует, JavaScript будет искать taste
в прототипе iceCream
, затем в прототипе того прототипа и так далее, и вернет 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
Наверное самая трудная для понимания концепция JS, this
является своего рода специальным аргументом функции. Вы не передаете его в функции напрямую. Вместо этого, JavaScript сам передает его в функцию, в зависимости от того, как она была вызвана. Например, вызовы нотацией через точку — например, iceCream.eat()
— получит специальное значение в this
исходя из того, что стояло перед точкой (в нашем примере iceCream
). Значение this
внутри функции зависит от того, где и как она была вызвана, и не зависит от того, где она определена. Хелперы вроде .bind
, .call
и .apply
позволяют точнее управлять значением this
.
Стрелочные функции
Стрелочные функции похожи на функциональные выражения. Вы задаете их таким образом: let sayHi = () => { }
. Их синтаксис прост, и они часто используется для маленьких функций, состоящих из одной строки. Стрелочные функции более ограничены, чем обычные — например, у них вообще нет понятия this
. Когда вы обращаетесь к this
внутри стрелочной функции, туда попадает значение из ближайшей «обычной» функции выше. Это похоже на то, что происходит при обращении внутри функции к аргументу, который существует только в функции выше. На практике это означает, что люди используют стрелочные функции, когда хотят иметь в них тот же this
, что и в окружающем коде.
Биндинг функции
Обычно биндинг функции f
к определенному значению this
и аргументов означает создание новой функции, которая вызывает f
с предопределенными значениями. В JavaScript есть встроенный хелпер .bind
, но вы можете делать это и вручную. Биндинг был популярным способом заставить вложенные функции «видеть» то же значение this
, что и во внешних функциях. Но теперь этот кейс закрывают стрелочные функции, поэтому биндинг отошел на второй план.
Стек вызовов
Вызов функции напоминает то, как мы входим в комнату. Каждый раз, когда мы вызываем функцию, все переменные внутри нее инициализируются заново. Поэтому новый вызов функции похож на «конструирование» новой комнаты и вход в нее. Переменные нашей функции «живут» в этой комнате. Когда мы возвращаемся из функции, «комната» исчезает со всеми своими переменными. Эти комнаты можно визуализировать в качестве вертикального стека комнат — стека вызовов. Когда мы выходим из функции, мы попадаем в функцию на один уровне ниже предыдущей в стеке вызовов.
Рекурсия
Рекурсия означает, что функция вызывает саму себя внутри себя. Это бывает полезно, когда необходимо повторить то, что вы только что сделали в своей функции, но с другими аргументами. Например, если вы пишите поисковую машину для индексирования интернета, ваша функция collectLinks(url)
будет сначала собирать ссылки с исходной страницы, а затем вызывать саму себя для каждой найденной ссылки — и так пока не посетит все страницы. Ловушка при рекурсии заключается в том, что можно довольно легко написать код, который никогда не завершится потому, что функция будет бесконечно вызывать саму себя. Если такое произойдет, то JavaScript прервет выполнение с ошибкой «stack overflow». Она называется так из-за того, что у нас стало слишком много вызовов в стеке, и он стал в прямом смысле переполнен.
Функции высшего порядка
Функции высшего порядка — это функции, которые взаимодействуют с другими функциями, принимая их в качестве аргументов или возвращая в качестве значения. На первый взгляд это может показаться странным, но мы помним, что функции — это значения, поэтому мы можем их передавать — точно так же, как числа, строки или объекты. Этим стилем можно легко злоупотребить, но он бывает очень выразителен при умеренном использовании.
Обратный вызов (коллбек)
Обратный вызов на самом деле не является понятием из JavaScript. Скорее, это паттерн. Идея в том, что вы передаете одну функцию в качестве аргумента в другую функцию, ожидая что вторая функция вызовет первую позднее. Вы ожидаете обратный вызов. Например, setTimeout
принимает коллбек-функцию и... вызывает ее обратно после таймаута. Но ничего специфичного в коллбеках нет. Это обычные функции, и когда мы говорим коллбек, мы говорим только о наших ожиданиях касательно их выполнения.
Замыкание
Обычно, когда мы выходим из функции, все ее переменные "исчезают". Это так, ведь больше они нигде не требуются. Но что, если вы объявите функцию внутри другой функции? Тогда внутренняя функция может быть по-прежнему вызвана позднее, и иметь доступ к переменнным из внешней функции. На практике это бывает очень полезно! Но чтобы это сработало, переменные внешней функции нужно где-то "хранить". В этом случае JavaScript сам заботится о том, чтобы переменные продолжали существовать — вместо того, чтобы "забыть" их. Это называется замыканием. В то время, как замыкания считаются одним из самых непонятых разработчиками аспектов JavaScript, вы вероятно используете их много раз в день, даже не подозревая об этом!
Послесловие автора: JavaScript состоит из этих концепций, но не исчерпывается ими. Я был сильно озабочен своими знаниями, пока не смог построить корректную ментальную модель языка, и теперь хочу помочь следующему поколению разрабочтиков преодолеть этот путь быстрее.
Если вы хотите присоединиться к глубокому погружению в рассмотренные темы, у меня есть кое-что для вас. Just JavaScript — то моя дистилированная модель того, как работает JavaScript, и она будет наполнена визуальными иллюстрации невероятной Maggie Appleton. В отличии от этого поста, там скорость подачи материала будет существенно меньше, чтобы вы могли вникнуть в каждую деталь.
Jusr JavaScript находится на своей ранней стадии, поэтому он доступен только в качестве серии писем без редактуры и исправлений. Если это звучит для вас интересно, вы можете подписаться на получение бесплатных черновиков по почте. Я буду благодарен за обратную связь. Спасибо!