...

пятница, 3 января 2020 г.

[Перевод] Из чего сделан JavaScript?

В течение первых нескольких лет использования 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?

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий