Мы работаем! Пишите WhatsApp / Viber / Telegram: +7 951 127-23-57, Skype: creograf

Конспект книги 'ES6 и за его пределами', Кайл Симпсон. Часть 1.

Конспект книги 'ES6 и за его пределами', Кайл Симпсон. Часть 1.

15:43:55 15.12.2016

Полизаполения помогают в старых ES5 средах использовать возможность ES6. Есть замечательная коллекция ES6 Shim (https://github.com/paulmillr/es6-shim/), которую стоит включать во все новые JS-проекты.

Синтаксис

Область видимости можно задавать просто блоками {}, а не функцией с вызовом в месте определения:

var a = 2;
{
  let a = 3;
  console.log( a ); // 3
}
console.log( a ); // 2

Аналогично предыдущему блоку:

let (a = 2) {
// ..
}

Объявления let стоит ставить в начале блока. Обращение к переменной, объявленной как let до момента объявления вызвает ошибку ReferenceError, так как переменная видна во всей области видимости, но до объявления она не инициализирована:

{
  console.log( a ); // значение не определено
  console.log( b ); // ReferenceError!
  var a;
  let b;
}

Аналогично:

{
  // переменная 'a' не объявлена
  if (typeof a === "undefined") {
    console.log( "cool" );
  }
  // переменная 'b' объявлена, но находится в мертвой зоне
  if (typeof b === "undefined") { // ReferenceError!
    // ..
  }
  // ..
  let b;
}

let отлично использовать в циклах for, так как объявляет переменную на каждой итерации:

var funcs = [];
for (let i = 0; i < 5; i++) {
  funcs.push( function(){
    console.log( i );
  } );
}
funcs[3](); // 3

Если бы i было объявлено, как var, то результат вызова funcs[3](5) был бы 5, так как замыканию подверглась бы i во внешней области видимости.
const не позволяет менять значение скалярных переменных и указателей на объекты/массивы. Значения свойств объектов и элементы массива, на которые указывает переменная при этом могут меняться.
Область видимости функций внутри блока теперь работает так:

{
  foo(); // работает!
  function foo() {
    // ..
  }
}
foo(); // ReferenceError, а в ES5 функция вызывалась

Spread и rest (...)

Разделяет элементы массива по отдельности:

var a = [2,3,4];
var b = [ 1, ...a, 5 ];
console.log( b ); // [1,2,3,4,5]

function foo(x,y,z) {
  console.log( x, y, z );
}
foo( ...[1,2,3] ); // 1 2 3

Собирает остальные параметры функции в массив:

  function foo(x, y, ...z) {
    console.log( x, y, z );
  }
  foo( 1, 2, 3, 4, 5 ); // 1 2 [3,4,5]

Новый способ работы с arguments:

function foo(...args) {
  args.shift();
  console.log( ...args );
}

Значение параметров функции по-умолчанию:

function foo(x = 11, y = 31) {
  console.log( x + y );
}

//недопустимо совместное использование с ...:
foo(...vals=[1,2,3]) { }

//можно использовать выражения
function foo(x = y + 3, z = bar( x )) {
  console.log( x, z );
}

// каждая переменная определяется, как let
var w = 1, z = 2;
function foo( x = w + 1, y = x + 1, z = z + 1 ) {
  console.log( x, y, z );
}
foo(); // ReferenceError, так let z = z + 1, а z локально не инициализированв

Деструктурирующее присваивание

function foo() {
  return [1,2,3];
}

function bar() {
  return {
    x: 4,
    y: 5,
    z: 6
  };
}
var [ a, b, c ] = foo();
var { x: x, y: y, z: z } = bar();
( { x, y, z } = bar() ); // если деструктурирующее присваивание происходит не в момент определения переменной, то нужны скобки, чтобы отличить их от блока {}
console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 6

//Если переменная создается с тем же именем, что и свойство объекта, можно сократить запись:
var { x, y, z } = bar();

//пример для вычисляемых свойств
var which = "x",
o = {};
( { [which]: o[which] } = bar() );
console.log( o.x ); // 4

Запись o[which] соответствует обычной ссылке на ключ объекта, которая равняется o.x и выступает как цель присваивания. Обмен значений двух переменных:

var x = 10, y = 20;
[ y, x ] = [ x, y ];
console.log( x, y ); // 20 10

Цепочки деструктивных присваиваний:

var o = { a:1, b:2, c:3 },  //аналогично для массива [1,2,3],
a, b, c, p;
p = { a, b, c } = o;
console.log( a, b, c ); // 1 2 3
p === o; // true

var o = { a:1, b:2, c:3 },
p = [4,5,6],
a, b, c, x, y, z;
( {a} = {b,c} = o );
[x,y] = [z] = p;
console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 4

//ненужные значения можно отбросить, пропустив их:
var [,,c,d] = foo();

//работает с ...
var a = [2,3,4];
var [ b, ...c ] = a;
console.log( b, c ); // 2 [3,4]

Можно задавать значения по-умолчанию:

var [ a = 3, b = 6, c = 9, d = 12 ] = foo();
var { x = 5, y = 10, z = 15, w = 20 } = bar();
var { x, y, z, w: WW = 20 } = bar();

Деструктурирующее присваивание возможно при любом уровне вложенности объектов или массивов:

var a1 = [ 1, [2, 3, 4], 5 ];
var o1 = { x: { y: { z: 6 } } };
var [ a, [ b, c, d ], e ] = a1;
var { x: { y: { z: w } } } = o1;
console.log( a, b, c, d, e ); // 1 2 3 4 5
console.log( w ); // 6

Вложенная деструктуризация может оказаться простым способом сведения воедино пространств имен объектов.

var App = {
  model: {
    User: function(){ .. }
  }
};
// вместо:
// var User = App.model.User;
var { model: { User } } = App;

Деструктуризация параметров

Деструктуризация объектов в качестве параметров - это реализация именованных аргументов + необязательные параметры в любой позиции.

function foo( { x, y } ) {
  console.log( x, y );
}
foo( { y: 1, x: 2 } ); // 2 1
foo( { y: 42 } ); // undefined 42
foo( {} ); // undefined undefined

//аналогично для массивов
function foo( [ x, y ] ) {
  console.log( x, y );
}
foo( [ 1, 2 ] ); // 1 2
foo( [ 1 ] ); // 1 undefined
foo( [] ); // undefined undefined

//можно пользоваться вложенностью, значениями по умолчанию, ...
function f1([ x=2, y=3, z ]) { .. }
function f2([ x, y, ...z], w) { .. }
function f3([ x, y, ...z], ...w) { .. }
function f4({ x: X, y }) { .. }
function f5({ x: X = 10, y = 20 }) { .. }
function f6({ x = 10 } = {}, { y } = { y: 10 }) { .. }

Значения по умолчанию для деструктуризации и для параметров

function f6({ x = 10 } = {}, { y } = { y: 10 }) {
  console.log( x, y );
}
f6(); // 10 10
f6( {}, {} ); // 10 undefined
f6( { x: 2 }, { y: 3 } ); // 2 3

{y: 10} означает объект как значение по умолчанию параметра функции, а не как значение по умолчанию деструктуризации и поэтому применимо только в случае, когда второй аргумент вообще не передается или передается как undefined.
{ x = 10 } = {} если аргумент опущен или имеет значение undefined, применяется значение по умолчанию пустого объекта {}. Затем выполняется деструктуризация с учетом того, что { x = 10 }. При этом ищется свойство x. Если обнаружить его не удается (или оно имеет значение undefined), именованный параметр x получает значение по умолчанию 10.
В объекте defaults хранятся настройки по умолчанию, в config часть этих настроек переопределяется. Задача: перенести неопределенные в config настройки из defaults.

// сливаем 'defaults' в 'config' в отдельном коде, чтобы перменные remove, enable, instance, warn, error не захламляли общую область видимости
{
  // деструктуризация (с присваиваниями значений по умолчанию)
  let {
    options: {
      remove = defaults.options.remove,
      enable = defaults.options.enable,
      instance = defaults.options.instance
    } = {},
    log: {
      warn = defaults.log.warn,
      error = defaults.log.error
    } = {}
  } = config;

  // реструктуризация
  config = {
    options: { remove, enable, instance },
    log: { warn, error }
  };
}

Объекты

Краткие свойства и функции

var x = 2, y = 3,
o = {
  x,  //вместо x: x,
  y,  //вместо y: y,
  x() {}, // вместо x: function(){}
  y() {}, // вместо y: function(){}
};

Краткие функции удобны, но пользоваться ими можно, только если вы не собираетесь выполнять рекурсию или передавать функцию в обработчики событий.

runSomething( {
  something: function something(x,y) {
  if (x > y) {
    // рекурсивный вызов с заменой 'x' и 'y'
    return something( y, x ); //работает, а с краткой функцией не будет работать, т.к. не будет имени функции в определении
  }
  return y - x;
  }
} );

Setter'ы/getter'ы из ES5

var o = {
  __id: 10,
  get id() { return this.__id++; },
  set id(v) { this.__id = v; }  // сеттер принимает только один парамерт, который может иметь значение по-умолчанию
}

o.id; // 10
o.id; // 11
o.id = 20;
o.id; // 20
// ...и прямой доступ:
o.__id; // 21
o.__id; // 21 — все еще!

Имена вычисляемых свойств

Имя свойства объекта может вычисляться:

var o = {
  baz: function(..){ .. },
  [ prefix + "foo" ]: function(..){ .. },
  [ prefix + "bar" ]: function(..){ .. }
  [Symbol.toStringTag]: "really cool thing",
  *["b" + "ar"]() { .. } // вычисляемый краткий генератор..
};

[[Prototype]]

Для определения класса:

var o1 = {
  // ..
};
var o2 = {
  __proto__: o1,
  // ..
};

Для существующего объекта:

  Object.setPrototypeOf( o2, o1 );

В JS понятие класса приравнивается к понятию объекта с прототипом. Ключевое слово super допустимо только в сокращенных методах, а не в свойствах регулярных функциональных выражений. Кроме того, оно действительно только в форме super.XXX (для доступа к свойству/методу) и не применяется в форме super(). super, по сути, означает Object.getPrototypeOf(o2).

var o1 = {
  foo() {
    console.log( "o1:foo" );
  }
};
var o2 = {
  foo() {
    super.foo();
    console.log( "o2:foo" );
  }
};
Object.setPrototypeOf( o2, o1 );
o2.foo(); // o1:foo
          // o2:foo

Шаблонные строки

- термином интерполяция (interpolation) или обработка по шаблону (templating).

var name = "Kyle";
//! обратные кавычки
var greeting = 'Hello ${name}!';  // === var greeting = "Hello " + name + "!";
console.log( greeting ); // "Hello Kyle!"
console.log( typeof greeting ); // "string"

Можно использовать любые выражения. Переводы строк тоже считаются:

var text =
'A very ${upper( "warm" )} welcome
to all of you ${upper( '${who}s' )}!';

Тегированные строковые литералы (tagged string literals) - это особый вид вызова функции без использования скобок.
Тег (tag) — часть foo перед строковым литералом '..' — представляет собой значение функции, которую нам нужно вызвать.
В массив values - результаты уже вычисленных интерполированных выражений, обнаруженных в строковом литерале. Массив strings - массив строк в литерале.

function foo(strings, ...values) {
  console.log( strings );
  console.log( values );
}
var desc = "awesome";
foo'Everything is ${desc}!'; 
  // [ "Everything is ", "!"]
  // [ "awesome" ]

function tag(strings, ...values) {
  return strings.reduce( function(s,v,idx){
    return s + (idx > 0 ? values[idx-1] : "") + v;
  }, "" );
}
var desc = "awesome";
var text = tag'Everything is ${desc}!';
console.log( text ); // Everything is awesome!

String.raw - доступ к неформатированным версиям строк со спецсимволами:

function showraw(strings, ...values) {
  console.log( strings );
  console.log( strings.raw );
}
showraw'Hello\nWorld';
// [ "Hello
// World" ]
// [ "Hello\nWorld" ]

Стрелочные функции

function foo(x,y) {
  return x + y;
}
// в сравнении с
var foo = (x,y) => x + y;

Стоит использовать, если
1) При наличии короткого встроенного функционального выражения, состоящего из одного оператора, возвращающего вычисленное значение и не имеющего внутри ссылки с ключевым словом this или с переменной self (рекурсии, связывание/открепление событий), причем только если вы уверены, что эти вещи там никогда не появятся, скорее всего, вы можете переписать код с применением стрелочных функций.
2) При наличии внутреннего функционального выражения, связанного с объявлением var self = this или с вызовом .bind(this) в охватывающей функции, для гарантии корректного связывания с помощью this это выражение, скорее всего, можно без проблем превратить в стрелочную функцию.
3) При наличии внутреннего функционального выражения, связанного, например, с объявлением var args = Array.prototype.slice.call(arguments) в охватывающей функции, для создания лексической копии аргументов это выражение, скорее всего, можно без проблем превратить в стрелочную функцию.

В остальных случаях — таких как обычные объявления функций, более длинные, состоящие из нескольких операторов функциональные выражения, функции, которым требуется ссылка на лексический именной идентификатор self (при рекурсии и т. п.), и все прочие функции, не отвечающие приведенным выше характеристикам, — имеет смысл избегать синтаксиса с =>.

 

Цикл for..of

Значение, которое вы просматриваете с помощью цикла for..of, должно быть итерируемым или же допускающим приведение к итерируемому объекту.

var a = ["a","b","c","d","e"];
for (var idx in a) {
  console.log( idx );
}
// 0 1 2 3 4
for (var val of a) {
  console.log( val );
}
// "a" "b" "c" "d" "e"

Стандартные встроенные значения JavaScript, которые по умолчанию являются итерируемыми: массивы, строки, генераторы, коллекции / типизированные массивы.
С обычными объектами цикл for..of не работает, потому что у них отсутствует итератор.

Регулярные выражения

1) Флаг интерпретации символов Unicode - u (https://mathiasbynens.be/notes/es6-unicode-regex)

/^.-clef/ .test( "-clef" ); // false, т.к. это два символа 0xD834 и 0xDD1E
/^.-clef/u.test( "-clef" ); // true, т.к. это один символ \u{1D11E}

2) Липкое позиционирование - y

Позиция для старта следующего поиска хранится в свойстве lastIndex. Для поиска с флагом y эта позиция обновляется после каждого поиска - указывает на символ, следующий за найденым фрагментом. И новый поиск начинается точно с этой позиции. Подходит для шаблонов с фиксированным количеством символов на каждое поле.

var re = /\d+\.\s(.*?)(?:\s|$)/y
str = "1. foo 2. bar 3. baz";
str.match( re ); // [ "1. foo ", "foo" ]
re.lastIndex; // 7 — корректное положение!
str.match( re ); // [ "2. bar ", "bar" ]
re.lastIndex; // 14 — корректное положение!
str.match( re ); // ["3. baz", "baz"]

От глобального поиска липкое позиционирование отличается тем, что обычный поиск осуществляется путем смещения вперед по строке до первого соотвествия. В липком поиске смещение невозможно.

var re = /o+./g, // <-- обратите внимание на флаг 'g'!
str = "foot book more";
str.match( re ); // ["oot","ook","or"]

3) Флаги регулярных выражений

Флаги всегда перечисляются в порядке “gimuy”, независимо от их последовательности в исходном шаблоне.

var re = /foo/ig;
re.flags; // "gi";

Можно создавать новые рег.выражения с другими флагами:

var re1 = /foo*/y;
re1.source; // "foo*"
re1.flags; // "y"

var re2 = new RegExp( re1, "ig" );
re2.source; // "foo*"
re2.flags; // "gi"

Расширения числовых литералов

var dec = 42,
oct = 0o52, // или '0O52' :(
hex = 0x2a, // или '0X2a' :/
bin = 0b101010; // или '0B101010' :/

// в десятичную форму
Number( "42" ); // 42
Number( "0o52" ); // 42
Number( "0x2a" ); // 42
Number( "0b101010" ); // 42

//в строку 
var a = 42;
a.toString(); // "42" — кроме того, 'a.toString( 10 )'
a.toString( 8 ); // "52"
a.toString( 16 ); // "2a"
a.toString( 2 ); // "101010"

Unicode

1) Нормализация подзволяет сравнивать unicode-строки, заданные значениями в разных форматах. Метод normalize(..) берет последовательность, например "e\u0301", и нормализует ее до "\xE9".

var s1 = "\xE9",
s2 = "e\u0301";
s1.normalize().length; // 1
s2.normalize().length; // 1
s1 === s2; // false
s1 === s2.normalize(); // true

Можно использовать normalize вместо charAt(), т.к. он не работает:

var s1 = "abc\u0301d",
s2 = "ab\u0107d",
s3 = „ab\u{1d49e}d";
[...s1.normalize()][2]; // "ć"
[...s2.normalize()][2]; // "ć"
[...s3.normalize()][2]; // " "
//или
String.fromCodePoint( s1.normalize().codePointAt( 2 ) ); //тоже работает

2) Идентификаторы тоже могут содерать unicode-символы:

var \u{2B400} = 42;
var \u03A9 = 42;

Тип данных Symbol

1) Новый примитивный тип данных. Главным образом он применяется для создания напоминающего строку значения, которое не конфликтует ни с каким другим в рамках приложения.

var sym = Symbol( "необязательное описание" ); // без new, т.к. это не объект
typeof sym; // "symbol"
sym.toString(); // "Symbol(какое-то необязательное описание)"

Рассмотрим модуль, реализующий поведение шаблона-одиночки (singleton), то есть такого, который можно создать всего один раз:

 

const INSTANCE = Symbol( "instance" );
function HappyFace() {
  if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];
  function smile() { .. }
  return HappyFace[INSTANCE] = {
    smile: smile
  };
}
var me = HappyFace(),
you = HappyFace();
me === you; // true

Символ по ключу и по описанию:

var s = Symbol.for( "something cool" );
var desc = Symbol.keyFor( s );
console.log( desc ); // "something cool"

var s2 = Symbol.for( desc );
s2 === s; // true

2) Символы как свойства объектов

var o = {
foo: 42,
[ Symbol( "bar" ) ]: "hello world",
baz: true
};
Object.getOwnPropertyNames( o ); // [ "foo","baz" ]
Object.getOwnPropertySymbols( o ); // [ Symbol(bar) ]

3) Встроенные символы

Для ссылки на эти встроенные символы в спецификации используется префикс @@. Вот наиболее распространенные варианты: @@iterator, @@toStringTag, @@toPrimitive.

 

var a = [1,2,3];
a[Symbol.iterator]; // native function

Структура

Итераторы

1) интерфейс Iterator

Iterator [обязательные параметры]
  next() {метод}: загружает следующий IteratorResult

//Два дополнительных параметра для расширения некоторых итераторов:
Iterator [необязательные параметры]
  return() {метод}: останавливает итератор и возвращает

IteratorResult
  throw() {метод}: сообщает об ошибке и возвращает IteratorResult

После вызова методов return(..) или throw(..) итератор не должен больше генерировать никаких результатов.

Интерфейс IteratorResult:

IteratorResult
  value {свойство}: значение на текущей итерации или окончательное возвращаемое значение (не обязательно, если это 'undefined')
  done {свойство}: тип boolean, показывает состояние выполнения

Интерфейс Iterable, описывающий объект, который должен уметь генерировать итераторы:

Iterable
  @@iterator() {метод}: генерирует Iterator

2) метод next()

var arr = [1,2,3];
var it = arr[Symbol.iterator]();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }

3) пример итератора

var Fib = {
  [Symbol.iterator]() {
    var n1 = 1, n2 = 1;
    return {
      // делаем итератор итерируемым
      [Symbol.iterator]() { return this; },
      next() {
        var current = n2;
        n2 = n1;
        n1 = n1 + current;
        return { value: current, done: false };
      },

      return(v) {
        console.log("Последовательность Фибоначчи завершена.");
        return { value: v, done: true };
      }
    };
  }
};

for (var v of Fib) {
  console.log( v );
  if (v > 50) break;
}
// 1 1 2 3 5 8 13 21 34 55
// Последовательность Фибоначчи завершена

Генераторы

 

1) В ES6 появилась новая, несколько необычная форма функции, названная генератором.
Такая функция может остановиться во время выполнения, а затем продолжить работу с прерванного места.
Более того, каждый цикл остановки и возобновления работы позволяет передавать сообщения в обе стороны.

 

function *foo() { .. } // определение
foo(); // вызов

Внутри генераторов используется ключевое слово, сигнализирующее о прерывании работы: yield.

function *foo() {
  while (true) {
    yield Math.random();
  }
}
// обмен значениями
function *foo() {
  var x = yield 10; // передает 10 в точку остановки, и присваивает передаваемое при повторном запуске значение в x
  console.log( x );
}

2) Выражение yield *

yield-делегирование: выражению yield *.. требуется итерируемый объект; оно вызывает его итератор и передает ему управление собственным генератором. Рассмотрим пример:

 

function *foo() {
  yield *[1,2,3];
}
// это аналогично
function *foo() {
  yield 1;
  yield 2;
  yield 3;
}
function *bar() {
  yield *foo();
}

Рекурсивный генератор:

function *foo(x) {
  if (x < 3) {
    x = yield *foo( x + 1 ); //три рекурсивных вызова foo, результаты которых передаются в х
  }
  return x * 2;
}
foo( 1 ); //24

3) Контроль со стороны итератора

Генераторы управляются итераторами. Для рекурсивного кода выше генератор вообще не останавливает свою работу, так как выражение yield .. отсутствует. Вместо него в коде имеется выражение yield *, которое обеспечивает прохождение каждой итерации путем рекурсивного вызова. Так что работа генератора осуществляется исключительно вызовами функции next() итератора.

var it = foo( 1 );
it.next(); // { value: 24, done: true }

Итератор для yield ..:

function *foo() {
  yield 1;
  yield 2;
  yield 3;
}
for (var v of foo()) {
  console.log( v );
}
// 1 2 3
// или
var it = foo();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }

Для цикла for..of необходим итерируемый объект. Сама по себе ссылка на функцию генератора (например, foo) таковым считаться не может; для получения итератора ее следует выполнить как foo() (при этом, такой итератор сам является итерируемым).
Генераторы можно рассматривать как управляемое поэтапное выполнение кода, во многом напоминающее очередь задач или как удобный синтаксис для конечного автомата.

 

Раннее завершение

Присоединенный к генератору итератор поддерживает необязательные методы return(..) и throw(..) — оба немедленно прерывают работу приостановленного генератора:

function *foo() {
  yield 1;
  yield 2;
  yield 3;
}
var it = foo();
it.next(); // { value: 1, done: false }
it.return( 42 ); // { value: 42, done: true }
it.next(); // { value: undefined, done: true }

Эта функциональная особенность позволяет уведомить генератор, что контролирующий код прекратил выполнять перебор и можно приступить к задачам, связанным с очисткой (освобождением ресурсов, сбросом состояния и т. п.)

function *foo() {
  try {
    yield 1;
    yield 2;
    yield 3;
  }
  finally { //помещать сюда yield нельзя
    console.log( "cleanup!" );
  }
}
for (var v of foo()) {
  console.log( v );
}
// 1 2 3
// очистка!
var it = foo();
it.next(); // { value: 1, done: false }
it.return( 42 ); // очистка!
// { value: 42, done: true }

Исключения:

try {
  it.throw( "Oops!" );
}
catch (err) {
  console.log( err ); // Исключение: Ой!
}
it.next(); // { value: undefined, done: true }

4) Применение генераторов

- Создание набора значений
- Очередь задач для последовательного выполнения

 

Модули

1) Старый способ

function Hello(name) {
  function greeting() {
    console.log( "Hello " + name + "!" );
  }
  // открытый API
  return {
    greeting: greeting
  };
}
var me = Hello( "Kyle" );
me.greeting(); // Hello Kyle!

Singleton на основе него:

var me = (function Hello(name){
  function greeting() {
    console.log( "Hello " + name + "!" );
  }
  // открытый API
  return {
    greeting: greeting
  };
})( "Kyle" );
me.greeting(); // Hello Kyle!

Наиболее распространены асинхронное (AMD — asynchronous module definition) и универсальное определения модуля (UMD — universal module definition).

2) Новый способ

- один модуль содержится в одном файле. В веб-приложение в браузере их потребуется загружать по отдельности.
- статическое API: вы статически определяете все виды экспорта верхнего уровня на открытом API модуля, и изменить это потом уже будет нельзя.
- модули ES6 являются синглтонами: существует только один экземпляр, поддерживающий состояние модуля. Каждый раз, импортируя его в другой модуль, вы получаете ссылку на единственный экземпляр. Для получения набора экземпляров модуль должен предоставить своего рода фабрику.
- свойства и методы, доступные через открытый API модуля, нельзя рассматривать как обычные присваивания значений и ссылки. На самом деле это привязки (почти как указатели) к идентификаторам во внутреннем определении модуля. В ES6 при экспорте локальной закрытой переменной, даже если в ней в этот момент хранится примитивная строка, число или что-нибудь подобное, экспортируется привязка к переменной. Если модуль меняет значение переменной, внешняя привязка импорта получает это новое значение.
- импорт модуля аналогичен статическому запросу на его загрузку (если она еще не сделана). В браузере это предполагает блокирующую загрузку. Если вы находитесь на сервере (например, Node.js), аналогичная загрузка будет осуществляться из файловой системы.

Ключевые слова import и export всегда должны появляться на верхнем уровне области видимости, в которой будут применяться. Например, их нельзя вставлять в условный оператор if; они должны располагаться вне блоков и функций.
Ключевое слово export или помещается перед объявлением, или используется в качестве своего рода оператора со специальным списком привязок, предназначенных для экспорта.

export function foo() {
  // ..
}
export var awesome = 42;
// или
var bar = [1,2,3];
export { bar }; 
// переименовать при экспорте
export { foo as bar }; 

Экспорт по умолчанию (default export) задает конкретную экспортированную привязку как вариант по умолчанию, выбираемый при импорте модуля.

export default function foo(..) {
  // ..
}
// или
function foo(..) {
  // ..
}
export default foo; // экспортируется функционал
// или
export { foo as default }; // экспортируется идентификатор, функционал которого может меняться
function foo() { .. }
function bar() { .. }
function baz() { .. }
export { foo as default, bar, baz, .. }; //foo можно будет переименовать при импорте

Модули должны быть простые с небольшим числом экспортируемых элементов.
Форма export default ..., экспортирует выражение со значением привязки, все остальные экспортируют привязки к локальным идентификаторам. В этом случае изменение значения переменной внутри модуля после экспорта приводит к тому, что внешняя импортированная привязка обращается к обновленному значению. Привязки — это живые ссылки, потому важно только их значение на момент обращения к ним. Двусторонняя привязка недопустима. Если после импорта из модуля переменной foo вы попытаетесь поменять значение импортированной переменной, появится сообщение об ошибке.

export { foo, bar } from "baz";
export { foo as FOO, bar as BAR } from "baz";
export * from "baz";

Импорт

import { foo, bar, baz } from "foo";

Строка "foo" называется спецификатором модуля (module specifier) - это строковый литерал, нельзя использовать переменную. Идентификаторы foo, bar и baz должны совпадать с элементами именованного экспорта API модуля. В текущей области видимости они связаны как идентификаторы верхнего уровня. Их можно переименовывать.

import { foo as theFooFunc } from "foo";
theFooFunc();

Импортировать только default можно без фигурных скобок:

import foo from "foo";
// или:
import { default as foo } from "foo";

Импортировать следует только конкретные привязки. Если модуль предоставляет 10 методов API, а вам требуются только два из них, считается, что перетаскивать весь набор привязок API — пустая трата ресурсов.

import FOOFN, { bar, baz as BAZ } from "foo";
FOOFN(); // default
bar();
BAZ(); // baz()

Импорт пространства имен (namespace import) - импорт всего, что экспортирует модуль:

import * as foo from "foo";
foo.bar();
foo.x; // 42
foo.baz();

Если модуль, который импортируется с помощью выражения * as .., обладает результатом экспорта по умолчанию, этот результат в указанном пространстве имен называется default.

export default function foo() { .. }
export function bar() { .. }
export function baz() { .. }
//...
import foofn, * as hello from "world";
foofn(); //== hello.foo(); лучше не мешать оба стиля в одном импорте 
hello.default();
hello.bar();

API модулей ES6 следует рассматривать и проектировать как статические и неизменные, что сильно мешает введению альтернативных шаблонов проектирования модулей. Эти ограничения можно обойти, экспортировав обычный объект, а затем изменив его по своему желанию. Такое поведение необходимо документировать.
Объявления, возникающие в результате импорта, считаются «приподнятыми», то есть импортрованную функцию можно вызывать до импорта в этой же области видимости.

Циклическая зависимость модулей

A импортирует B. B импортирует A.
Взаимный импорт вкупе со статической проверкой обоих операторов импорта виртуально объединяет области видимости двух модулей (через привязки), так что функция foo(..) может вызывать функцию bar(..) и наоборот.

import bar from "B";
export default function foo(x) {
if (x > 10) return bar( x - 1 );
return x * 2;
}
//..
import foo from "A";
export default function bar(y) {
if (y > 5) return foo( y / 2 );
return y * 3;
}

Загрузка модулей извне

Полизаполнитель для API загрузчика модуля (см. https://github.com/ModuleLoader/es6-moduleloader)

// обычный сценарий в браузере загружается через '',
// оператор 'import' здесь недопустим
Reflect.Loader.import( "foo" ) // возвращает обещание для '"foo"'
  .then( function(foo){
    foo.bar();
  } );

Когда нужно настроить поведение модуля посредством изменения конфигурации или даже переопределения, можно использовать вызов служебной программы Reflect.Loader.import(..) со вторым аргументом, задающим различные варианты настройки задач импорта/загрузки.

Reflect.Loader.import( "foo", { address: "/path/to/foo.js" } )
  .then( function(foo){
   // ..
  } )

Классы

«Классы» JS не имеют ничего общего с обычными классами.
- class Foo влечет за собой создание (специальной) функции с именем Foo
- constructor(..) определяет сигнатуру функции Foo(..) и содержимое ее тела.
- для методов класса применяется тот же самый синтаксис «кратких методов», что и для объектных литералов.
- в отличие от объектных литералов, в теле класса не используются запятые, отделяющие члены друг от друга.

class Foo {
  constructor(a,b) {
    this.x = a;
    this.y = b;
  }
  gimmeXY() {
    return this.x * this.y;
  }
}

var f = new Foo( 5, 15 );
f.x; // 5
f.y; // 15
f.gimmeXY(); // 75

Отличия от классов:
- следует объявить класс, прежде чем создавать его экземпляры
- обращение функции Foo(..) к классу Foo должно осуществляться через оператор new (Foo.call( obj ) уже не работает)
- class Foo наверху глобальной области видимости создает в ней лексический идентификатор Foo, но, в отличие от функции Foo, не создает свойства глобального объекта с таким именем

Ключевое слово class можно рассматривать как макрос, который автоматически заполняет прототип объекта. По вашему желанию он также связывает соотношение [[Prototype]], когда используется с ключевым словом extends.

class может находиться в составе выражения, например var x = class Y { .. }.
В основном это используется для передачи определения класса (с технической точки зрения самого конструктора) как аргумента функции или для присваивания его свойству объекта.

Классы ES6 имеют удобный синтаксис для установления делеги рующей связи [[Prototype]] между двумя прототипами функций - extends, это не наследование.

class Bar extends Foo {
  constructor(a,b,c) {
    super( a, b );
    this.z = c;
  }
  gimmeXYZ() {
    return super.gimmeXY() * this.z;
  }
}
var b = new Bar( 5, 15, 25 );
b.x; // 5
b.y; // 15
b.z; // 25
b.gimmeXYZ(); // 1875

Внутри метода super ссылается на «родительский объект», обеспечивая вам доступ к свойству/методу, например super.gimmeXY(). Запись Bar extends Foo, разумеется, означает связывание [[Prototype]] свойства Bar.prototype со свойством Foo.prototype. Соответственно, ключевое слово super в таком методе, как gimmeXYZ(), означает Foo.prototype, в то время как в конструкторе Bar оно указывает на Foo. Работает super и в объектных литералах.

В конструкторе не работает super, как ссылка ссылка на Foo.prototype для прямого доступа к одному из его свойств/методов, то есть запись super.prototype не будет работать.
Запись super(..) означает вызов новой функции Foo(..), но ссылкой на саму функцию Foo это не является.

Запись new super.constructor(..) вполне корректна, и это единственный способ вызвать функцию super.constructor(..), но в большинстве случаев пользоваться ею нельзя, так как вы не можете заставить вызов ссылаться на являющийся его текущим контекстом объект или использовать его.

super не является динамическим. Когда конструктор или метод с его помощью создает внутри себя ссылку во время объявления (в теле класса), слово super статически связывается с иерархией конкретного класса и не допускает переопределения.

class ParentA {
  constructor() { this.id = "a"; }
  foo() { console.log( "ParentA:", this.id ); }
}
class ParentB {
  constructor() { this.id = "b"; }
  foo() { console.log( "ParentB:", this.id ); }
}
class ChildA extends ParentA {
  foo() {
    super.foo();
    console.log( "ChildA:", this.id );
  }
}
class ChildB extends ParentB {
  foo() {
    super.foo();
    console.log( "ChildB:", this.id );
  }
}
var a = new ChildA();
a.foo(); // ParentA: a
// ChildA: a
var b = new ChildB(); // ParentB: b
b.foo(); // ChildB: b

// заимствуем 'b.foo()', чтобы использовать в контексте 'a'
b.foo.call( a ); // ParentB: a
                // ChildB: a

То есть this меняется динамически, а super остается неизменным. В случае комбинации class и super следует избегать подмены this.

Выхода два:
1. Сузить процесс проектирования объектов до статических иерархий — ключевые слова class, extends и super в этом случае будут прекрасно работать.
2. Отказаться от имитации классов и использовать динамические гибкие бесклассовые объекты и делегирование [[Prototype]].

Конструктор подкласса

Используемый по умолчанию конструктор подкласса автоматически вызывает родительский конструктор и передает любые аргументы туда. Этот конструктор можно представить примерно вот так:

constructor(...args) {
  super(...args);
}

Доступ к ключевому слову this в таком конструкторе появляется только после вызова метода super(..).
До ES6 автоматического вызова родительского конструктора не было. До ES6 это работало в обратную сторону: объект this создавался «конструктором подклассов», а затем вы вызвали родительский конструктор в контексте this этого подкласса.

Расширение встроенных объектов

До ES6 имитация «подкласса» Array путем создания объекта вручную и связывания со свойством Array.prototype работала лишь частично. Не удавалось воспроизвести особые поведения настоящих массивов, например автоматическое обновление свойства length.

class MyCoolArray extends Array {
  first() { return this[0]; }
  last() { return this[this.length - 1]; }
}

var a = new MyCoolArray( 1, 2, 3 );
a.length; // 3
a; // [1,2,3]
a.first(); // 1
a.last(); // 3
class Oops extends Error {
  constructor(reason) {
    this.oops = reason;
  }
}
// позднее:
var ouch = new Oops( "I messed up!" );
throw ouch;

Свойство new.target - метасвойство (meta property) для конструкторов.

Свойство new.target представляет собой новое «магическое» значение, доступное во всех функциях. Если new.target равняется undefined, значит, функция с помощью оператора new не вызывалась.
В любом конструкторе new.target всегда будет указывать на конструктор, непосредственно вызвавший оператор new, даже если тот располагается в параллельном классе и был делегирован через вызов super(..) из дочернего конструктора.

class Foo {
  constructor() {
    console.log( "Foo: ", new.target.name );
  }
}
class Bar extends Foo {
  constructor() {
    super();
    console.log( "Bar: ", new.target.name );
  }
  baz() {
    console.log( "baz: ", new.target );
  }
}
var a = new Foo(); // Foo: Foo
var b = new Bar(); // Foo: Bar <-- учитывает сторону, вызвавшую 'new'
// Bar: Bar
b.baz(); // baz: undefined

Ключевое слово static

Cтатические методы (не только свойства) для класса, так как они добавляются непосредственно в его объект-функцию, а не в объект-прототип этой функции.

class Foo {
  static cool() { console.log( "cool" ); }
  wow() { console.log( "wow" ); }
}
class Bar extends Foo {
  static awesome() {
    super.cool();
    console.log( "awesome" );
  }
  neat() {
    super.wow();
    console.log( "neat" );
  }
}
Foo.cool(); // "cool"
Bar.cool(); // "cool"
Bar.awesome(); // "cool"
// "awesome"
var b = new Bar();
b.neat(); // "wow"
          // "neat"
b.awesome; // undefined
b.cool; // undefined

Метод чтения конструктора в свойстве Symbol.species
Ключевое слово static может пригодиться нам при задании метода чтения в свойстве Symbol.species (в спецификации оно известно как @@species) для производного (дочернего) класса.
Эта возможность позволяет дочернему классу передать в родительский информацию о том, каким конструктором следует пользоваться — когда вы не собираетесь задействовать конструктор самого дочернего класса, — если какой-либо метод родительского класса должен породить новый экземпляр.

Многие методы объекта Array создают и возвращают новые экземпляры Array:

class MyCoolArray extends Array {
  // принудительно превращаем 'species' в родительский конструктор
  static get [Symbol.species]() { return Array; }
}
var a = new MyCoolArray( 1, 2, 3 ),
b = a.map( function(v){ return v * 2; } );
b instanceof MyCoolArray; // false
b instanceof Array; // true

Похожие записи

© 2002-2022 Креограф. Все права защищены законом РФ
 Русский /  English