Август 2016

Том 31, выпуск 8

Работающий программист - MEAN: исследуем ECMAScriptMEAN: исследуем ECMAScript

Тэд Ньюард | Август 2016

Тэд НьюардС возвращением, дорогие «министы».

Мы в середине 2016 года. Возможно, вы не в курсе, что JavaScript (на самом деле официально называемый ECMAScript) имеет новую версию языка, ECMAScript 2015. Если он еще не используется в вашем JavaScript-коде, сейчас самое время начать с ним работу. К счастью, делается это тривиально, поскольку на то есть веские причины. В этой статье рассматриваются некоторые из более интересных и важных средств ECMAScript 2015, а в будущем мы обсудим, как выглядело бы использование MEAN в более современном стиле (теперь, когда у вас есть твердые знания о том, как функционируют приложения MEAN, я полностью переработаю кодовую базу, но об этом позже).

Простые изменения

Некоторые из простейших изменений в языке относятся к методам программирования, которые стали широко распространенными соглашениями в экосистеме и сообществе.

Одно из таких соглашений — неизменяемость, которая реализуется в ECMAScript 2015 через использование объявления const:

const firstName = "Ted";

Здесь, как и в C++-объявлении того же имени, объявляется, что ссылочное имя (firstName) не может указывать ни на что иное, но это не значит, что объект, на который указывает ссылка, нельзя изменять. ECMAScript 2015 также имеет новую форму объявления изменяемой ссылки — let, которая фактически является упрощенной заменой var; советую пользоваться ею при любой возможности.

Изменение в большей мере относящееся к «удобству», — добавление в язык строковой интерполяции с помощью обратных апострофов (одинарные кавычки с наклоном влево, которые обычно находятся под тильдой на клавиатуре с английской раскладкой) вместо одинарных или двойных кавычек:

const speaker = { name: "Brian Randell", rating: 5 };
console.log(`Speaker ${speaker.name} got a rating of ${speaker.rating}`);

Как и при строковой интерполяции в стиле C#, ECMAScript 2015 будет интерпретировать выражение в скобках, пытаясь преобразовать его в строку и вставить результат в конечную строку.

Одно из более существенных изменений в языке — введение блочной области видимости (block-scoping); ранее в JavaScript область видимости переменных ограничивалась функциями, а не произвольными блоками кода. Это означало, что любая переменная, объявленная где-то в функции, была доступна в любом месте функции, а не только в блоке, где она была объявлена, что вызывало путаницу и служило источником трудноуловимых ошибок.

Интересный побочный эффект от поддержки блочной области видимости заключается в том, что ECMAScript теперь располагает локально объявляемыми функциями, похожими на то, что предлагается для C# 7:

{
  function foo () { return 1 }
  foo() === 1
  {
    function foo () { return 2 }
    foo() === 2
  }
  foo() === 1
}

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

Однако идиоматически ECMAScript не использует локально вложенные функции; вместо этого в нем отдается предпочтение идиоме программирования в более функциональном стиле с определением переменных-ссылок, указывающих на определения анонимных функций, и их использование. В подтверждение этого ECMAScript 2015 теперь поддерживает стрелочные функции (arrow functions), синтаксис которых почти идентичен таковому в C#:

const nums = [1, 2, 3, 4, 5];
nums.forEach(v => {
  if (v % 2 == 0)
    console.log(v);
});

(Вспомните, что вы добавляли функцию forEach к массивам в рамках предыдущего стандарта ECMAScript.) В этом примере, как и следовало ожидать, выводятся четные числа из массива. С другой стороны, если мне нужно конструировать четные числа из массива-источника, можно использовать встроенную функцию map и стрелочную функцию для выполнения, по большому счету, той же работы:

const nums = [1, 2, 3, 4, 5];
const evens = nums.map(v => v * 2);

Стрелочные функции являются долгожданным изменением в языке, и логично предположить, что большинство кода на основе JavaScript будет агрессивно использовать их.

Обещания

Одним из многих решений, витавших в сообществе ECMAScript и нацеленных на уменьшение сложности вокруг программирования под Node.js с применением обратных вызовов, было то, которое относилось к обещаниям (promises); в основном это подход на основе библиотеки, трансформирующий обратные вызовы в нечто казавшееся более последовательным по своей природе. В сообществе было весьма популярно несколько разных библиотек на базе обещаний, и комитет по ECMAScript в конечном счете выбрал стандартизацию одной из них, которая теперь называется просто Promises. (Обратите на заглавную букву в названии; это еще и имя главного объекта, используемого в реализации обещаний.) Поначалу использование ECMAScript 2015 Promises может показаться несколько странным по сравнению со стандартным синхронным программированием, но по большей части это имеет смысл.

Представьте на минутку код приложения, которому нужно вызвать библиотечную подпрограмму, использующую Promise для выполнения каких-то задач в фоне:

msgAfterTimeout("Foo", 100).then(() =>
  msgAfterTimeout("Bar", 200);
).then(() => {
  console.log(`done after 300ms`);
});

Здесь идея в том, что по истечении 100 мс msgAfterTimeout выведет «Hello, Foo», а потом сделает то же самое («Hello, Bar» через 200 мс) и, наконец, просто покажет сообщение в консоли. Обратите внимание на то, как связаны этапы вызовами функции then: из msgAfterTimeout возвращается объект Promise, а then определяет функцию, вызываемую по окончании выполнения изначального Promise. Это объясняет происхождение имени — объект Promise, по сути, обещает вызвать функцию then, когда будет завершен начальный код. (Кроме того, что происходит, если в функции генерируется исключение? Promise позволяет с помощью метода catch указать функцию, выполняемую в этом случае.)

В ситуации, где нужно выполнять несколько функций одновременно, а по завершении работы всех функций вызывать некую функцию, можно использовать Promise.all:

const fetchPromised = (url, timeout) => { /* ... */ }
Promise.all([
  fetchPromised("http://backend/foo.txt", 500),
  fetchPromised("http://backend/bar.txt", 500),
  fetchPromised("http://backend/baz.txt", 500)
]).then((data) => {
  let [ foo, bar, baz ] = data;
  console.log(`success: foo=${foo} bar=${bar} baz=${baz}`);
}, (err) => {
  console.log(`error: ${err}`);
});

Как видите, результаты каждой асинхронно выполняемой функции будут собираться в массив, который потом передается как первый параметр (первой) функции, подключенной к then. (Выражение let — пример деструктурирующего присваивания [destructuring assignment] в ECMAScript 2015, еще одного нового средства; фактически каждый элемент массива data присваивается каждой из трех объявленных переменных, а остаток, если таковой имеется, отбрасывается.) Также обратите внимание на то, что then передается второй функции, которая используется, если при выполнении любой из асинхронных функций произошла какая-то ошибка.

Это определенной другой стиль программирования, но не необычный для любого, кто имеет опыт работы с различными механизмами асинхронного выполнения в C# наподобие PLINQ или TPL.

Библиотека

В ECMAScript 2015, помимо Promise, добавлено несколько важных средств в стандартную библиотеку (предполагается, что ее предоставляет любая среда ECMAScript, будь она предназначена для браузера или сервера). В частности, добавлены Map (хранилище по типу «ключ-значение», аналог .NET-типа Dictionary<K,V>) и Set («контейнер» значений без дубликатов), которые оба доступны безо всякого импорта:

const m = new Map();
m.set("Brian", 5);
m.set("Rachel", 5);
console.log(`Brian scored a ${m.get("Brian")} on his talk`);
const s = new Set();
s.add("one");
s.add("one"); // дубликат
s.add("one"); // дубликат
console.log(`s holds ${s.size} elements`);
  // Выводит "1"

Кроме того, добавляется несколько более стандартных функций в различные прототипы объектов, уже имеющиеся в среде ECMAScript, такие как find в Arrays и некоторые методы численной оценки (вроде isNAN или isFinite) в прототип Number. По большей части, эти и ранее упомянутые типы Map и Set уже присутствовали в сообществе как сторонние пакеты, но введение их в стандартную библиотеку ECMAScript будет способствовать сокращению количества пакетов-зависимостей, засоряющих ландшафт ECMAScript.

Модули

По-видимому, одно из самых важных изменений, по крайней мере с точки зрения создания MEAN-приложения, — принятие формальной системы модулей. Ранее, как я рассказывал почти год назад, MEAN-приложение вызывало функцию require из Node.js для «импорта» (интерпретации/выполнения) другого JavaScript-файла и возврата объекта для дальнейшего использования. То есть, когда MEAN-разработчик писал следующую строку, при оценке JavaScript-файла из подкаталога в node_modules, который был установлен через npm, возвращался объект express:

var express = require('express');

Чтобы это работало, нужно было соблюдать несколько соглашений, но это работало, хотя отсутствие формализации определенно мешало дальнейшему развитию языка и связанной с ним экосистемы. В ECMAScript 2015 для формализации большей части всего этого были введены новые ключевые слова.

Это в некоторой степени закольцованное объяснение, поэтому давайте начнем с простого примера. У вас есть два файла: app.js и необходимая ему библиотека math. Библиотека math не является npm-пакетом (для простоты) экспортирует значение пи (3.14), называемое PI, и функцию для суммирования массива чисел с именем sum:

//  lib/math.js
export function sum (x, y) { return x + y };
export var pi = 3.141593;
//  app.js
import * as math from "lib/math";
console.log("2PI = " + math.sum(math.pi, math.pi));

Заметьте, что в этой библиотеке символы, сделанные доступными клиентам, объявляются ключевым словом export, а при ссылке на библиотеку используется ключевое слово import. Но чаще требуется импортировать символы из библиотеки как элементы верхнего уровня (а не члены), поэтому более распространенный шаблон использования скорее всего будет выглядеть так:

//  app.js
import {sum, pi} from "lib/math"
console.log("2π = " + sum(pi, pi));

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

Заключение

В стандарте ECMAScript 2015 кроется гораздо больше; если вы еще не видели эти средства, то сейчас определенно должны проверить какой-либо ресурс в Интернете из постоянно растущего списка, где описывается новый стандарт. Официальный стандарт ECMA доступен по ссылке bit.ly/1xxQKpl (текущая спецификация, обозначенная как 7-е издание, названа ECMAScript 2016 в основном потому, что в ECMA решили ежегодно стандартизовать новые изменения в языке). Однако менее формальное описание языка можно найти на сайте es6-features.org, который предоставляет список новых языковых средств и их сравнение с тем, что было в языке до этого.

Кроме того, хотя Node.js почти полностью совместима с функциональным набором ECMAScript 2015, другие среды — не совместимы или поддерживают, но на разных уровнях. Для сред с неполной совместимостью предлагается две утилиты для компиляции из одного исходного кода в другой (transpiler tools): компилятор Traceur, спонсируемый Google, и компилятор Babel. (Обе утилиты, конечно же, доступны через npm.) Разумеется, собственный TypeScript от Microsoft удивительно близок к ECMAScript 2015, а это означает, что можно было бы адаптировать TypeScript прямо сегодня, и он был бы почти полностью совместим с ECMAScript 2015, если и когда вам понадобится преобразование TypeScript в ECMAScript 2015.

Все эти средства станут более очевидными, когда вы начнете работать со средствами MEAN, использующими их, так что пока не напрягайтесь, если для вас они еще не имеют смысла. Ну а тем временем… удачи в кодировании!


Тэд Ньюард (Ted Neward) — глава фирмы Neward & Associates, предоставляющей консалтинговые услуги по самым разнообразным технологиям. Автор и соавтор многочисленных книг, в том числе «Professional F# 2.0» (Wrox, 2010), более сотни статей, часто выступает на многих конференциях по всему миру; кроме того, имеет звание Microsoft MVP в области F#. С ним можно связаться по адресу ted@tedneward.com или почитать его блог blogs.tedneward.com.

Выражаю благодарность за рецензирование статьи эксперту Шону Уайлдермуту (Shawn Wildermuth).