Представьте себе поселенцев, строящих свой первый маленький городок. В нем всего несколько зданий: пара домов, почта и вокзал. Он такой маленький, что люди могут указывать любое здание по названию: "давай встретимся у почты" или "я живу во втором доме".
С ростом города строилось больше зданий. Вскоре людям пришлось делать выбор:
- давать каждому новому зданию уникальный номер или название
- дробить город на улицы
Конечно, они могли использовать первый способ и просто давать уникальные номера и названия новым строениям, чтобы никогда не было двух зданий с номером 5. Это подходящее, но не самое гениальное решение, особенно для крупного города. Мой адрес: "Нью-Йорк, здание 654 931". Звучит как ерунда.
Большинство выбирает второй путь: делить город на улицы, обычно — прямые линии, и идентификаторы домов — названия или цифры — становятся уникальными в пределах улицы. В городе много зданий с номером 5, но они все расположены на разных улицах, поэтому все в порядке.
Именно поэтому в вашем компьютере есть директории. Без них все ваши файлы хранились бы в одном месте, где вы не смогли бы хранить два файла с одинаковым названием. Когда есть директории, то создавая новый файл, вам нужно заботиться об именах только в текущей директории.
Эта система деления чего угодно на улицы, блоки или директории позволяет нам объединять вещи в значимые модули. Уолл-стрит — это улица банков и финансов. Своеобразный модуль Нью-Йорка с собственной задачей. У вас есть директория "Видео" и вы знаете, что она только для особого типа файлов — видео.
Первые программисты были поселенцами в новом и странном мире компьютеров. Они столкнулись с подобной проблемой и встали перед выбором.
Они могли писать весь код в едином файле, и тогда все — численные и строковые константы, переменные и функции должны были бы иметь уникальные имена. А если бы они захотели переиспользовать какой-то код в другом проекте, им бы потребовалось копировать его из этого гигантского файла и вставлять его в другой гигантский файл.
Или они могли бы пойти вторым путем: разделить код на небольшие модули. Модули могут храниться в отдельных файлах, а имена функций и констант будут уникальными только в пределах файла, но не во всей программе. И модули можно легко переиспользовать в разных проектах, без копирования и вставки.
У разных языков программирования разные подходы к этой задаче. В JavaScript он довольно простой: один файл — один модуль. Все упражнения, которые вы выполняете в этом курсе — это написание модулей.
Иногда нам нужно объединять код из разных файлов. Если вы просто напишете код в разных файлах, интерпретатор JavaScript не поймет, как получить что-то из другого файла.
Экспорт и два способа импорта
Давайте посмотрим на пример, где у нас есть файл math.js
:
const pi = 3.14
const e = 2.718
const square = (x) => {
return x * x
}
const surfaceArea = (r) => {
return 4 * pi * square(r)
}
Это наша крошечная математическая библиотека.
Создадим другой файл под именем planets.js
со следующим содержанием:
const surfaceOfMars = surfaceArea(3390)
const surfaceOfMercury = surfaceArea(2440)
const yearSquared = square(2017)
Код здесь не будет работать, потому что surfaceArea
и square
здесь не существуют. Они в другом файле, о котором JavaScript пока не знает. Нам нужно сказать ему заглянуть в другой файл. Это называется "импортом" — давайте импортируем именно те функции, что нам нужны, по их имени:
import { surfaceArea, square } from './math.js'
const surfaceOfMars = surfaceArea(3390)
const surfaceOfMercury = surfaceArea(2440)
const yearSquared = square(2017)
Ключевое слово import
, затем список того, что мы хотим, в фигурных скобках, а затем название модуля. Файл в той же директории, что и planets.js
, поэтому './math.js'
означает "взять из файла math.js
, расположенного в той же (текущей) директории".
Когда вы импортируете что-то из Китая, что происходит в Китае? Верно, экспорт. Поэтому наша математическая библиотека должна исполнить свою роль — "экспортировать".
Поставьте export
перед тем, что вы хотите экспортировать. Такая операция сделает это импортируемым куда угодно:
export const pi = 3.14
export const e = 2.718
export const square = (x) => {
return x * x
}
export const surfaceArea = (r) => {
return 4 * pi * square(r)
}
Просто укажите export
перед чем угодно, и это можно будет "импортировать" в другой файл. Тут мы экспортируем все. Еще один способ экспортировать несколько значений:
const pi = 3.14
const e = 2.718
const square = (x) => {
return x * x
}
const surfaceArea = (r) => {
return 4 * pi * square(r)
}
export { pi, e, square, surfaceArea }
Возвратимся к planets.js
. Допустим, мы хотим импортировать что-то еще. Мы можем просто добавить названия в список:
import { surfaceArea, square, pi, e } from './math.js'
Или импортировать все сразу:
import * as mathematics from './math.js'
Это значит: "импортировать весь модуль и назвать его mathematics
в этом модуле". Вот почему к импортированным сущностям обращение происходит через mathematics
, например, mathematics.surfaceArea
:
import * as mathematics from './math.js'
const surfaceOfMars = mathematics.surfaceArea(3390)
const surfaceOfMercury = mathematics.surfaceArea(2440)
const yearSquared = mathematics.square(2017)
mathematics + точка + имя функции.
Теперь, если мы добавим функцию square
в этот же файл, никакого конфликта не возникнет:
import * as mathematics from './math.js'
const square = (x) => {
return x * x * x
}
const yearSquared = mathematics.square(2017) // 4068289
const weirdSquare = square(2017) // 8205738913
В этом примере в первом случае была вызвана функция возведения в квадрат из модуля math
, а во втором случае был вызов местной функции неправильного возведения в квадрат.
Экспорт по умолчанию
И последнее: часто вам требуется экспортировать из модуля что-то одно. Существует специальный механизм, который называется "экспорт по умолчанию", и вы можете экспортировать с помощью него только что-то одно. Но экспортированную по умолчанию вещь проще импортировать.
const pi = 3.14
const e = 2.718
const square = (x) => {
return x * x
}
const surfaceArea = (r) => {
return 4 * pi * square(r)
}
export default surfaceArea
Можно также экспортировать функцию или константу без имени:
export default (r) => {
return 4 * pi * square(r)
}
Просто напишите код, как обычно, без специально указанных экспортов, а в конце выполните export default что-нибудь
. В данном случае мы экспортируем функцию surfaceArea
.
Импорт по умолчанию выглядит так:
// Без фигурных скобок
import surfaceArea from './math.js'
const surfaceOfMars = surfaceArea(3390)
При экспорте функции без имени, ее имя в модуле будет определяться в момент импорта, т.е. один и тот же экспорт может иметь разные имена в разных модулях:
math.js
export default () => {
///
}
import1.js:
import something1 from './math.js'
import2.js:
import something2 from './math.js'
Экспорт по умолчанию может сочетаться с обычным экспортом, а экспортировать элементы можно по отдельности:
const pi = 3.14
const e = 2.718
const square = (x) => {
return x * x
}
const surfaceArea = (r) => {
return 4 * pi * square(r)
}
export { e, pi }
export { square }
export default surfaceArea
Импорт может выглядеть так:
import surfaceArea, { square, e, pi } from './math.js'
Ключевое слово as
позволяет задать псевдоним для импортируемой сущности. Благодаря этому появляется возможность импортировать элементы с одинаковыми именами:
import { square, e, pi } from './math.js'
import { square as square1, e as e1, pi as pi1 } from './math1.js'
Расширение файла при импорте модуля
Указание расширения файла гарантирует, что он будет проанализирован как модуль Node.js и d8 и другими средами выполнения, а также Babel и другими инструментами сборки. Если Babel разрешает опустить расширение файла, то официальная документация Node.js устанавливает конкретное требование:
При использовании ключевого слова
import
необходимо указать расширение файла. Пути каталогов (например, './startup/index.js') также должны быть полностью указаны.Этот подход обеспечивает идентичное поведение
import
в среде браузера и на сервере с типовой конфигурацией.
Резюме
Вы можете дробить код на отдельные модули. В JavaScript один модуль — это один файл.
Объединение кода, расположенного в разных модулях, происходит через:
- Экспорт чего-то из модуля
- Импорт в другой модуль
Почти все уроки этого и других курсов на Хекслете используют модули. Такой подход максимально приближает нас к реальной жизни, когда проекты состоят из сотен, тысяч файлов и библиотек, которые пользуются друг другом.
При работе с модулями будет полезным сразу наработать некоторые модели поведения, позволяющие вам с легкостью определять, какой код вам доступен на исполнение, откуда пришел этот код и как его увидеть.
Ниже описан основной алгоритм, по которому нужно анализировать файл с кодом, над которым вы сейчас работаете. Этот алгоритм не является специфичным для работы в среде Хекслета, так нужно делать в принципе:
- Внимательно изучите все импорты, описанные в начале файла. Так вы узнаете, какие модули и функции доступны внутри вашего файла (не считая глобальных функций и модулей, которые доступны и без импорта, например,
Math
) - Попробуйте классифицировать импортируемые функции. Если импорт выглядит так
from './...'
, то есть содержит./
, значит импортируется модуль, содержимое которого находится в текущей файловой системе. Это автоматически означает несколько вещей. Первое: вы всегда можете открыть этот файл и посмотреть, что там написано. Второе: вы не сможете импортировать этот модуль в другой среде (ведь этого файла там нет) - Если
from 'name'
содержит только имя, без./
в начале, значит, модуль подгружается либо из стандартной библиотекиnodejs
, либо из установленных пакетов. Визуально невозможно отличить одно от другого. Попробуйте загуглить имя таким способом: "nodejs name". Если в выдаче будет ссылка на официальную документацию, значит, это модуль nodejs; если на репозиторийnpm
— значит, это обычный пакет, который почти наверняка лежит на гитхабе, что можно проверить таким запросом: "github js name", где "name" это имя пакета