Я разберу, что такое модуль, как происходит экспорт сущностей, как происходит импорт сущностей, чем система модулей ES6 отличается от системы модулей в NodeJS.
Модуль
По сути, модуль — это инструкция (statement), которая вызывается неявно — посредством создания файла и выполнения его с помощью интерпретатора ES (прямо, при «запуске» файла программистом, или косвенно, в результате импорта другим модулем). В ES6 есть чёткое соотношение: один файл — один модуль. Каждый модуль имеет отдельную область видимости (Lexical environment) — т. е. все объявления переменных, функций и классов не будут доступны за пределами модуля (файла), если не экспортированы явно. На верхнем уровне модуля (т. е. вне других инструкций и выражений) можно использовать операторы import для импорта других модулей и их экспортируемых сущностей, и export для экспорта собственных сущностей модуля.
Оператор export
Оператор export позволяет экспортировать сущности модуля, чтобы они были доступны из других модулей. У каждого модуля есть неявный объект [[Exports]], в котором хранятся ссылки на все экспортируемые сущности, а ключом является идентификатор сущности (например, имя переменной). Это очень напоминает module.exports из модульной системы NodeJS, но [[Exports]] всегда объект, и его нельзя получить напрямую. Единственный способ его изменить — использовать оператор export.
Этот оператор имеет несколько модификаций, рассмотрим все возможные случаи.
Экспорт объявляемой сущности
По сути, это обычное объявление переменной, функции или класса, с ключевым словом "export" перед ним.
export var variable;
export const CONSTANT = 0;
export let scopedVariable = 20;
export function func(){};
export class Foo {};
В данном случае система экспортов ES6 удобнее, чем в NodeJS, где пришлось бы сначала объявить сущность, а потом добавить её в объект module.exports.
var variable;
exports.variable = variable;
const CONSTANT = 0;
exports.CONSTANT = CONSTANT;
...
Но есть и гораздо более важное различие между этими двумя системами. В NodeJS свойству объекта exports присваивается значение выражения. В ES6 оператор export добавляет в [[Exports]] ссылку (или привязку, binding) на объявленную сущность. Это значит, что [[Exports]].<имя сущности> всегда будет возвращать текущее значение этой сущности.
export var bla = 10; // [[Exports]].bla === 10
bla = 45; // [[Exports]].bla === 45
Экспорт уже объявленной сущности
Здесь всё то же самое, только экспортируем мы сущность, которая уже была объявлена выше. Для этого применяются фигурные скобки после ключевого слова export, в которых через запятую указываются все сущности (ну, их идентификаторы — например, имя переменной :) ), которые необходимо экспортировать.
var bar = 10;
function foo() {}
export { bar, foo };
С помощью ключевого слова "as" можно «переименовать» сущность при экспорте (точнее будет сказать, изменить ключ для [[Exports]]).
var bar = 10;
function foo() {}
export { bar as bla, foo as choo };
Для такого вида экспорта также верно, что [[Exports]] хранит у себя лишь ссылку на сущность, даже в случае «переименования».
var bar = 10;
export { bar as bla }; // [[Exports]].bla === 10
bar = 42; // [[Exports]].bla === 42
Экспорт по умолчанию
Этот вариант использования export отличается от двух описанных выше, и, на мой взгляд, он немного нелогичен. Заключается он в использовании после export ключевого слова default, после которого может идти одно из трех: выражение, объявление функции, объявление класса.
export default 42;
export default function func() {}
export default class Foo {}
Каждый из этих трех вариантов использования добавляет в [[Exports]] свойство с ключом «default». Экспортирование по умолчанию выражения (первый пример, «export default 42;») — единственный случай при использовании export, когда значением свойства [[Exports]] становится не ссылка на какую-либо сущность, а значение выражения. В случае же экспорта по умолчанию функции (не анонимной, естественно) или класса — они будут объявлены в области видимости модуля, а [[Exports]].default будет ссылкой на эту сущность.
Оператор import
Чтобы не разрывать повествование, продолжу сразу об импорте по умолчанию.
Экспортированное по умолчанию свойство считается «главным» в этом модуле. Его импорт осуществляется с помощью оператора import следующей модификации:
import <любое имя> from '<путь к модулю>';
В этом вся польза дефолтного экспорта — при импорте можно назвать его, как угодно.
// sub.js
export default class Sub {};
// main.js
import Base from './sub.js'; // И да, иногда это может сбить столку, поэтому лучше всё же использовать имя модуля
Импорт обычных экспортируемых свойств выглядит немного иначе:
// file1.js
export let bla = 20;
// file2.js
import { bla } from './file1.js'; // нужно точно указать имя сущности
// file3.js
import { bla as scopedVariable } from './file1.js'; // но можно переименовать
Рассмотрим модуль «file2.js». Оператор import получает объект [[Exports]] импортируемого модуля ('file1.js'), находит в нём нужное свойство («bla»), а после создаёт привязку идентификатора "bla" к значению [[Exports]].bla.
Т. е., точно так же, как и [[Exports]].bla, bla в модуле «file2.js» всегда будет возвращать текущее значение переменной «bla» из модуля «file1.js». Равно как и scopedVariable из модуля «file3.js».
// count.js
export let counter = 0;
export function inc() {
++counter;
}
// main.js
import { counter, inc } from './count.js';
console.log(counter); // 0
inc();
console.log(counter); // 1
Импорт всех экспортируемых свойств
import * as sub from './sub.js';
По сути, таким образом мы получаем копию [[Exports]] модуля «sub.js».
Включение модуля без импорта
Иногда бывает нужно, чтобы файл просто запустился.
import './worker';
Реэкспорт
Последняя вещь, которую я здесь рассмотрю — это повторный экспорт модулем свойства, которое он импортирует из другого модуля. Осуществляется это оператором export.
// main.js
export { something } from './another.js';
Два замечания, которые тут стоит сделать: первое — something после реэкспорта НЕ становится доступной внутри модуля main.js, для этого придётся сделать отдельный импорт (уж не знаю, почему так, видимо, чтобы сохранить дух оператора export); и второе — система ссылок работает и тут: модуль, который импортирует из «main.js» something, будет получать актуальное значение переменной something в «another.js»;
Так же можно зареэкспортить все свойства из другого модуля.
export * from './another';
Однако важно помнить, что если вы объявите в своём модуле экспорт с таким же именем, как у реэкспортного, то он затрёт реэскпортное.
// sub.js
export const bla = 3, foo = 4;
// another.js
export const bla = 5;
export * from './sub';
// main.js
import * as another from './another';
console.log(another); // { bla: 5, foo: 4 }
Это решается переименованием конфликтных свойств при реэкспорте.
И, почему-то, синтаксиса для реэкспорта дефолтных свойств у export нет, но можно сделать так:
export { default as sub } from './sub';
Несколько слов о свойствах импортов
Поддержка циклических ссылок
Собственно, вся эта пляска с биндингами вместо присвоения нужна для нормального разрешения циклических ссылок. Т. к. это не значение (которое может быть и undefined), а ссылка на то место, где когда-то что-то будет лежать — ничего не упадёт, даже если цикл.
Импорты всплывают
Импорты «всплывают» наверх модуля.
// sub.js
console.log('sub');
// main.js
console.log('main');
import './sub.js';
Если запустить main.js, то в консоль сначала выведется «sub», и только потом «main» — именно из-за всплытия импортов.
Экспорт по умолчанию — это ещё не конец
Вот такие конструкции вполне допустимы.
// jquery.js
export default function $() {}
export const AUTHOR = "Джон Резиг";
// main.js
import $, { AUTHOR } from 'jquery.js';
И вообще, по сути, default — это просто ещё один именованный экспорт.
import Base from './Base';
То же самое, что и
import { default as Base } from './Base';
Спасибо большое за прочтение статьи, надеюсь, она сэкономит вам время и вообще поможет. Буду рад услышать вопросы и помочь)
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий