Тонкости модульной системы ECMAScript 2015 (ES6)

от автора

Уже около полугода я пишу на ES6 (который в итоге назвали ES-2015) и ES7, с использованием бабеля в качестве транслятора. Писать мне приходилось в основном серверную часть, соответственно, использование модулей было само собой разумеющимся: до ES6 — с помощью модульной системы самой ноды, а теперь — с помощью стандартизированной семантики самого языка. И мне захотелось написать статью, в которой расписать тонкости, плюсы, подводные камни и необычности новообретенной модульной системы языка: отчасти — чтобы другим было проще, отчасти — чтобы разобраться во всём окончательно самому 🙂

Я разберу, что такое модуль, как происходит экспорт сущностей, как происходит импорт сущностей, чем система модулей 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'; 

Спасибо большое за прочтение статьи, надеюсь, она сэкономит вам время и вообще поможет. Буду рад услышать вопросы и помочь)

ссылка на оригинал статьи http://habrahabr.ru/post/267639/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *