Статьи

Idiomatic ES6: полное руководство

There are a billion articles on ES6 at this point. What’s one more? Here we discuss some emerging patterns and issues related to real world use of ES6 as well as how one can go about using it now via Babel. If you aren’t yet familiar with the features and changes of ES6 itself, you’ll probably want to check out the following links first:

  • MDN is invaluable. It provides systematic coverage of all JS, including ES6.
  • ②ality isn’t organized like MDN, but it boasts the finest collection of deep articles covering specific features and edge cases.
  • The online Babel REPL. This is fantastically useful to answer quick questions like ‘does this work?’ and of course, ‘how?’
  • The final draft. Dry reading, but sometimes it’s the only way to get an authoritative answer. Edifying if you stare at it long enough.

    Original author: Darien Maillet Valentine

idiomatic_es6

TABLE OF CONTENTS

Where We’re At

In April, the ES6 spec reached its final draft. Later this month, the Grand Council of Javascript Elders will shuffle into the silver sanctum to seal the document with unicorn wax. A glass bell will ring in the spire of the tallest tower in the City and leprechauns will be dispatched to carry the good news to the farthest corners of the Kingdom. ‘ES5 is dead, long live ES6!’ they shout.

Unfortunately, that last part takes about ten years. Leprechauns aren’t nearly as fast as Hollywood has led you to believe, and they’re easily distracted.

On one hand, progress towards ES6 support in browsers has been rapid. If you’ve followed the Kangax Table for the last few months, that should be clear. Yeah, there’s a lot of red yet, but look at Firefox 40 (66%), Chrome 45 (45%) and holy s– yes, that really is IE Edge, aka Project Spartan, at 63%. ‘Imagine there’s no heaven…’

  • Note, these figures sometimes go down, too, as new tests are added to confirm implementation details. You can only precisely compare them at a given moment rather than over time.

On the other, we’ve all had too many workyears / firstborn stolen by spiteful, undying IE versions to place much stock in the idea that there’s a corner we’ll turn when suddenly it’s totally cool to destructure an array in the browser. Well, you can polyfill Map, you can polyfill Array.from or whatever. But how do you polyfill syntax? ES6 isn’t valid ES5 at a syntactic level. This is a new problem.

Age of Babel

You can’t polyfill syntax – you can transpile it though. Babel has been tearing it up since the tail end of its days as ‘6to5’ (when somebody must have realized it was too npm-big to not have a sexy name). Transpiling JS wasn’t new (in fact we owe a lot of ES6 refinements to Coffeescript), nor was transpiling ES6 to ES5 (Traceur’s been around a while). But npm download stats present a picture of Babel as now being the community’s go-to ES6 transpiler. The line keeps angling upwards.

Babel might owe its popularity to good timing or the fact that they’ve hung onto the highest ‘Kangax Index’ for a while. But that line probably owes its recent incline mostly to the fact that ES6 has been finalized. Suddenly it seems a lot less speculative to jump on board.

Now, many folks feel transpiling is icky:

  1. Tougher to debug
  2. Feels funny

But that said, some of those who were uneasy about Coffeescript seem to have fewer reservations about writing code that, in theory, won’t need to be transpiled… someday. That may help with #2 anyway – but as for #1, even with sourcemaps, there’s no denying that you’re risking an extra maintenance burden by depending on a transpiler. Is it worth it?

Language Shapes Usage

The answer comes down to the ways ES6 could improve the quality of your code. An enumeration of new features / toys may not help much in determining that because the real-world implications of those features aren’t always immediately clear. It’s through use that we develop a shared vocabulary, composed not by the language’s grammar dictates but by the patterns and preferences – the idiom – that makes it easier for us to work together and write reusable code. A language’s features and syntax do shape the development of that idiom, and language designers take that into account: they’re planting seeds. The available features and syntax will encourage or discourage particular habits and solutions.

At this point, a hazy image of how ES6 really gets used has begun to form. I’ve cataloged a handful of patterns I’ve seen emerging in the wild, tried to supply their rationales (as I see them), and supplemented these with some of the conclusions I’ve arrived at from working mainly in ES6 for a few months myself. As always: YMMV.

Variable Declaration

When first encountering let and const, a common reaction is to think of let as the new var – in fact, I’ve even seen an article called ‘Let Is The New Var.’ The phrase shows up verbatim all over. But it isn’t true: const is the new var.

Alright, that’s not the truth, either. If we’re talking about which is closer in behavior to var, indeed, that’s let. Assuming your code isn’t relying on hoisting and you don’t declare variables in blocks, you could even switch them 1:1 and things would be fine, but that wouldn’t be true with const.

  • You may wonder: Where does var it fit in? It doesn’t. It’s sort of de facto deprecated – like with was for years before they made it official. Hoisting was a language design error, and the benefits of lexical scope are the lure being used to guide us out of this particular problem zone.

The gut feeling for those of us used to var is that ‘vars’ are variables and ‘constants’ are … not. True. But we never had constants, so we used (unsightly but distinctive) case conventions when we wanted to communicate that a particular identifier represented a “pre-supplied” value. It could be an enum value, a “plug in your string here to configure this” slot, etc. To Javascript devs, a ‘constant’ was just a variable whose value was somehow hardcoded.

But the real ES const has nothing to do with ‘hardcodedness’. It just means that a binding is permanent for the duration of the scope in which it was declared. Go open some JS you wrote and review it, looking at variables. How many are ever redefined after they’re declared? And (here’s the crux) – of those which aren’t ever redefined, for how many would such a redefinition, were it to be accidentally introduced, constitute an error? Right now that code doesn’t say so. If it happens, there’s no way to directly trace the problem back to a constraint that was never expressed.

With const, that code will be invalid if a redefinition occurs. The mistake would even be detected by static analysis, so you’ll be told exactly where the offense occurred before the code even has a chance to run.

So a simple pattern has appeared in real-world ES6, the logical consequence of these facts: use const except when let is expressly needed. It’s a kind of defensive coding, which is something we don’t see a lot of in fast and loose JS (or, as the Java dev behind me would say, ‘sloppy and inferior’). Fortunately it’s a simple habit to pick up and it yields immediate benefits.

I suppose I should acknowledge that it’s five characters. And that it therefore will not neatly align with four-space tabs. Once you’ve typed it that first time, it gets easier. I promise.

Lexical Scope, Blocks, and the End of the IIFE

Above we addressed lexical scope briefly. The largest idiomatic impact of lexical scope is that it makes an older idiomatic usage more or less obsolete: the IIFE.

The purpose of an IIFE (immediately-invoked function expression) was to provide a scope-for-hire. Before ES6, aside from a few odd edge cases, function scope was the only scope other than global. Node modules might be argued to afford a different kind of scope, but even the hidden innards of that system involve wrapping modules in IIFEs.

Some background if this is an unfamiliar term: there are function declarations and there are function expressions. A function declaration (hoisted, like var) is a type of statement. Expressions can be statements, but not the other way around. Any statement beginning with function will be a function statement, so it can’t be anonymous and it can’t be invoked in place. Since the object is to avoid polluting the current scope, you need to somehow ‘expressionize’ the function. There are a variety of approaches to this. The most common is to parenthesize it; then it is a function expression inside a parenthesized expression, altogether being an expression statement. Other common choices are prefixing with the logical not operator ! (semantically abusive, aesthetically appealing) or the void operator (arguably more expressive, but relatively obscure).

The fact that there’s no consensus about how to do this tells us a bit about IIFEs. The pattern is unavoidable, but it isn’t really ‘acknowledged’ by the language itself. And although we’re accustomed to it, creating these functions, anonymous or not, is an indirect, unexpressive means to get some scope ‘real estate.’ They’re functions in name but not in, uh, spirit.

So in ES6, (function() { /*...*/ })(); becomes { /* ... */ }. Praise.

  • Block statements are familiar because we use them routinely as the ‘statement’ part of control and loop statements like ‘for’ and ‘if’, but it’s easy to forget they are a type of statement in their own right. This is why a line starting `{` begins a statement, not an object literal.

Perhaps this means that we’ll see a return of the long-maligned (but harmless) statement label. A block statement (unless it belongs to a loop) can only use break with a label. I haven’t actually seen this as a pattern in practice – just speculating.

A discussion of real usage should address common errors, too. As far as lexical scope goes, there’s one I’ve seen a few times now. Sometimes people who follow the const-unless-let principle abandon it as soon as they get to for/of loops, apparently thinking the identifiers in the loop will be ‘reused’ and are therefore let vars. This isn’t the case – for/of|in loop scopes

  1. are unique per iteration and
  2. include their initializers! Thus,

for (const char of str) console.log(char); is valid and, if char should be immutable (per-iteration), preferred. Note that that isn’t true of the for ;; loop, however.

Arrow As Default

Here’s a snippet from a developer issue thread for V8. The title of the issue is ‘Implement arrow functions’ and it dates from 2013, which is around 1838 AD in Javascript years:

  • What’s wrong with the actual language construct? What benefit does it have to be able to write foo(x => x + 1); instead of foo(function(x) { return x + 1; });, other than saving a few bytes and losing verbosity (i.e. clarity) in the code?

The writer sort of had a good point. It just wasn’t obvious to most folks what arrows were supposed to bring to the table. And they looked weird, which is what he or she is actually saying there. At this point, we’re used to seeing them, and the argument now seems comically backwards (wait, which one has greater clarity?). But at the time, I’d have agreed.

Now, I consider arrow functions to be the “default” that one diverges from only as situationally required. I’ll come clean here – the argument for this that I’m about to present is the product of my own experience, not an observed outside trend (which is what I’ve tried to stick to so far). Take it with a grain of salt.

The core behavioral difference between arrow-functions (AF) and function-functions (FF) concerns this. Many JS devs avoid using this because it’s a pain. It was a pain – arrows fixed it.

Their utility as event handlers is pretty obvious – but it doesn’t say much about why one might treat them as the norm. Well, I’d said that AFs fixed the this problem, but in truth, the choice between AFs and FFs is what’s fixed it. We didn’t just gain a way to express lexical this, we effectively gained a way to express variable this. Afterall, previously we would have used `function` for both, with lexical this approximated with aliases like `self`. Yet in the majority of cases, it simply doesn’t matter: most functions in any given (average) project probably will make use of neither lexical nor variable this.

class SomethingParser extends Writable {
  constructor() {
      super();

      this.on('finish', () => {
          if (this.validate(this.result))
              this.emit('result', this.result);
          else
              this.emit('error', new ParseError('Oh no!'));
      });
  }

  // ...
}

For functions where it doesn’t matter, I find it reasonable to say one or the other should be ‘default’ – otherwise you’re choosing at random, and missing an opportunity to make your code clearer.

const says, “I am not redefinable”. AF says, “my this is lexical” – but that’s also a way of saying “my this is not redefinable.” Since a contextual this is the special case, the thing you need to take care with and draw attention to, it stands to reason that the AF should be used for functions that don’t make any use of this at all. Then function means a good deal more:

const speak = function() {
  console.log(`I, ${ this.name }, have a variable "this".`);
};

const say = () => console.log(`I don’t. Not really my thing.`);

To be fair, this isn’t really that analogous to const/let. You won’t get any benefit from static analysis or early errors; it’s merely a convention. So you can just as readily argue that the reverse should be true – that lexical this should be considered the ‘special’ case. Taking care to be consistent in this regard is more important than which way one chooses to be consistent.

Classes, Symbols and Object Literals

ES6 classes aren’t really the totally new construct that they may appear to be (if they were, Babel wouldn’t be able to transpile them). But it seems a bit much to just call them sugar. Afterall, they’re doing a lot of (obnoxious) work for you and finally provide a singular consistent approach to defining constructors and their prototypes, along with inheritance, all at once and in a very clear way.

The usage trend of note – other than the fact that it’s being used at all – is the use of symbolic property names to get something very close to private methods and properties. I think the jury is still out on whether this is something to be gung ho about. The benefit is encapsulation without having to create new scopes, but it can be argued that an overt concern with hiding things is best left to the sorts of languages where that’s like, a thing.

The best use case for privacy-via-symbols, though, is to shadow accessors:

const $str = Symbol();

class ASCIIString {
  constructor(str='') {
    this.content = str;
  }

  get content() {
    return this[$str];
  }

  set content(str) {
    str = String(str);

    for (const char of str) {
      if (!this.isValidChar(char))
        throw new Error(`Char "${ char }" is not valid.`);

      this[$str] = str;
    }
 }

 isValidChar(char) {
   return char.codePointAt(0) <= 0x7F;
 }
}
  • Unicode <3: In the above example, when we iterate over the characters and when we use codePointAt, astral plane characters work correctly.

Not the most realistic example, but you get the idea. There are a lot of great things you can do with accessors. I find them invaluable when writing libraries that benefit from a greater degree of opacity and need more aggressive guarding. However, you probably don’t want to make getters too elaborate; accessors tend to hide the fact that there may be a higher cost associated with not caching their values than the user of your API may realize.

What class really delivers is slick, useful prototype inheritance. Making use of constructor inheritance is far more common in Node than in the browser, and that’s partly because Node provided a consistent way to do it (util.inherits). You still had to expressly call the parent constructor by name and configure the prototype with Object.defineProperties, but it works and people used it. I believe that class and extends will have a similar effect, and that they also invite deeper inheritance patterns than we’ve been accustomed to, in particular because of the utility and clarity of super:

class ASCIIStringNoControlChars extends ASCIIString {
  constructor(str) {
    super(str);
  }

  isValidChar(char) {
    return super.isValidChar(char) && char.codePointAt(0) > 0x1F;
  }
}

I’ve found myself on occasion creating classes with inheritance chains three or four deep – something I never did before, mainly because the amount of boilerplate involved made it seem awkward, especially when a class only represented a small change from its parent. Now the syntax matches up with the reality of what we’re doing, and it’s turned out to be one of my favorite improvements.

Except for static, object literals now allow methods and accessors using the same syntax as class. It has a nice symmetry, and drives home the point that class is really nothing more than a special sort of object in JS. If you want a ‘singleton’ (without inheritance), the object literal remains a more direct means to implement that pattern than class.

Function Signatures and Binding Patterns

Destructuring has led to a number of new patterns. The first is the reimagining of the traditional ‘options object’ argument:

constructor({ name, age, species='cat' }={}) { ... }

Default assignment in the options argument lets us drop a ton of awkward ‘this or this or this’ variable assignments at the head of a function. Notice that the object has its own default there – you’ll need to do this if you want the options argument itself to be optional.

Rest gets heavy use in method signatures when a child class method exists as a decorator of its super’s same-name method – and is often paired with spread:

class Me extends Human {
  eat(...args) {
    if (args.some(isJello)) 
      throw up;
    else
      super.eat(...args);
  }
}

In any situation where one would have addressed a member by a predetermined index, destructured assignment proves to be more readable and direct. For regex pattern matching with multiple match groups, it’s invaluable. Even for simple matches, I think it’s clearer:

const getTagName = str => {
  const [ , tagName ] = str.match(/^<\s*([^\s\/>]+)/) || [];
  return tagName;
};

Функции с «множественными» возвращаемыми значениями были шаблоном, ранее зарезервированным для случаев, когда нет альтернативы. Функция может возвращать объект, в котором «основным» результатом было одно свойство и было одно или несколько свойств с важными метаданными или что-то в этом роде. Вы избегали этого, потому что это означало, что вызывающему абоненту нужно будет выбирать биты результата для использования — дополнительные шаги. Разрушение делает этот вид возвращаемой стоимости настолько естественным, хотя старые оговорки начинают исчезать. Он даже поддается работе с (нетипизированными, но) «подобными кортежу» значениями.

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

const refill = ([ dispenser, pez ]) => {

  const [ onePack, ...allTheRest ] = pez;

  dispenser.fill(onePack);

  quietlyEat(allTheRest);
};

Promise.all([ getPezDispenser(), getPez(5) ]).then(refill);

Деструктуризация объекта плюс значения по умолчанию и вычисленные свойства либо слишком странные, либо удивительно выразительные — ваш вызов:

const charCount = str => {
  const register = {};

  for (const char of str) {
    register[char] = (register[char] || 0) + 1;
  }

  return register;
};

const countCharInWord = (word, char) => {
  const { [char]: count=0 } = charCount(word); // wheeeee

  const plural = count != 1;
  const verb   = plural ? 'are' : 'is';
  const suffix = plural ? '’s' : ''; 

  return `There ${ verb } ${ count } ${ char }${ suffix } in ${ word }.`
};

countCharInWord('mississippi', 'i'); // "There are 4 i’s in mississippi."
countCharInWord('mississippi', 'p'); // "There are 2 p’s in mississippi."
countCharInWord('mississippi', 'x'); // "There are 0 x’s in mississippi."

итерация

Если можно сказать, что у ES6 есть тема, это может быть «итерация». Это также может быть «разоблачить все» (см. Прокси и Отражение ). Нам дают инструменты для работы с поведением низкого уровня — больше нет ничего волшебного. В случае итерации это достигается с помощью свойства Symbol.iterator.

Возможно, вы хотите создать подкласс Array для создания структуры стека. Вероятно, он должен повторяться от последнего к первому:

class Stack extends Array {
  /* ... */

  * [Symbol.iterator]() {
    for (let i = this.length - 1; i >= 0; i--) {
      yield this[i];
    }
  }
}

const stack = new Stack();

stack.push(1, 2, 3);

console.log(...stack); // 3 2 1
  • Обратите внимание, что действительно создание подклассов Array остается невозможным; это не то, что транспортер может полностью эмулировать или заполнять. Методы будут работать, но дела пойдут странно, если вы назначите индексы напрямую, и вам нужно будет предоставить явное toString. Честно говоря, я привожу здесь ужасный пример.

  • Генераторы — это специальные функции, которые возвращают «итерируемое» (как вышеописанный метод). Как и в случае с обещаниями, итерируемые элементы должны соответствовать только определенному шаблону; Вы можете создать свои собственные и использовать их везде, где ожидается «повторяемость». MDN имеет хорошее освещение этого.

Генераторы служат не для итерации, а для других целей. Наиболее значимой тенденцией в использовании генераторов является создание таких библиотек, как co, для создания сопрограмм, управляемых Promise. Это много слов. Что ж, сейчас это важная вещь для узла, но вместо того, чтобы обращаться к ней в этой форме, мы рассмотрим asyncи awaitформальное предложение ES7 по добавлению этого типа функциональности на уровне языка ниже.

Есть два способа часто использовать итерации: for (const x of iterable) {}и [ ...iterable ]. Последний эффективно преобразует любую итерацию в массив, поэтому вы не захотите использовать ее с бесконечно прибыльным генератором.

Это действительно было не так давно, что мы впервые получили forEach()и другие Array.prototypeитерационные методы. Все еще можно увидеть классические for ;;петли в стиле C в тех местах, где что-то еще имеет смысл. Сравнивая for ofциклы с forEachметодом, я думаю, что это обычно сводится к вопросу повторного использования кода. Имеет больше смысла использовать, forEachкогда задействована именованная функция; но если бы это была лямбда, я бы поддержал это утверждение. В частности, учтите, что возврат в in forEachэквивалентен continueциклу in, но forEachне имеет эквивалента для break; чтобы достичь его эффекта, вам нужно использовать everyили не someпо прямому назначению.

Также обратите внимание на неясный, но потенциально запутывающий gotcha: for ofитерация включает неопределенные индексы в разреженном массиве, в то время как forEachи другие Array.prototypeметоды этого не делают.

ES7

ES6 — или ES2015, как его теперь называют, — это только первая волна (и почти наверняка самая крупная) из тех, которые должны быть постепенными, возможно, ежегодными, обновлениями EcmaScript. ES [YEAR], если я правильно понимаю, должен стать своего рода подвижной целью, которая является способом признания реальности того, как двигатели в конечном итоге сами постепенно внедряют новые стандарты. У обновлений есть крайние сроки, но они будут происходить достаточно часто, так что не будет никакого давления для окончательной доработки любых спецификаций, которые не получили требуемый уровень суеты, который держит наш язык (мы надеемся) свободным от кучи и новых слов — потому что это просто означает жду год, чтобы понять это правильно, а не пять.

Babel пришел, чтобы выполнить второстепенную роль в качестве своего рода живого испытательного полигона для предполагаемых изменений языка, взятых из спецификаций стомана. Хотя они существуют в разной степени зрелости, и нельзя ожидать, что они обязательно введут язык в своих текущих формах (или вообще), с ними стоит экспериментировать. Некоторые из них не представляют особой сложности (оператор возведения в степень), некоторые более сложны и сомнительны. Я хочу обратиться к двум из них: во-первых, потому что это может быть и в ES6, поскольку это касается пользователей Babel; и второе, потому что я думаю, что это хорошо создает интересную дискуссию.

Святой Грааль: Асинк / Ожидание

Спецификация async / await уже давно существует; это был претендент на ES6. Он обеспечивает своего рода асинхронный святой Грааль — это для Javascript то, что Flexbox был для CSS. Технически это может быть экспериментальной функцией, но как только вы ее активируете, пути назад уже нет.

Хотя asyncфункции основаны на генераторах, и синтаксис отражает их, они более фундаментально упакованы Promise. Там, где генераторы возвращают итераторы, асинхронные функции возвращают обещания.

Этапы принятия Обещания JavaScript: Невежество ➪ Отказ ➪ Скептицизм ➪ Принятие ➪ Энтузиазм ➪ Чрезмерное использование ism Профилирование ➪ Скептицизм

— Райан Гроув (@yaypie)
28 марта 2015 г.

Обещания хороши — иногда — но даже сейчас, когда у нас есть «Единственное истинное обещание» для работы по всем направлениям, может быть немного сложно избавиться от ощущения, что мы обменяли только один набор проблем на другой. Мы можем throwв обещаниях, но .catch()это не так catch. И код, насыщенный обратным вызовом, может быть почти таким же неловким, если его переписать с обещаниями, которые, в конце концов, все еще по существу принимают обратные вызовы. Это те вещи, которые asyncнаправлены на решение.

Это особенно увлекательно в браузере. Следующий пример, скажем, не безопасен для IE8, но его можно сделать довольно простым; Я просто хочу сохранить ясность предпосылки:

const $domReady = new Promise(resolve => {
  const state = document.readyState;

  if (state == 'interactive' || state == 'complete')
    resolve();
  else
    window.addEventListener('DOMContentLoaded', resolve);
});

const getXHR = url => {
  const req = new XMLHttpRequest();

  req.open('GET', url);

  return new Promise((resolve, reject) => {
    req.onerror   = () => reject(new Error('Connection failure'));
    req.ontimeout = () => reject(new Error('Connection timeout'));

    req.onreadystatechange = () => {
      if (req.readyState == 4) resolve(req.responseText);
    };

    req.send();
  });
};

const insertPigeon = async () => {
  await $domReady;

  const pigeonHole = document.getElementById('pigeon-hole');
  const pigeonURL  = 'http://www.pigeons.com/carrier.html';

  try {
    pigeonHole.innerHTML = await getXHR(pigeonURL);
  } catch (err) {
    console.error('Unable to retrieve pigeon page :(');
  }
};

insertPigeon();

Это очень мило

Когда вы awaitзначение, если значение является обещанием, есть yieldзакулисные. Когда выполнение возобновится, доход от этого скрытого дохода будет значением разрешения обещания. Или, если обещание было отклонено, оно фактически выбрасывает.

Я полагаю, что использование Babel на стороне клиента неизбежно будет увеличиваться, и это принесет асинхронное ожидание. А поскольку обещания включают в себя объекты, похожие на обещания, async / await уже совместима с любыми библиотеками, которые возвращают обещания для асинхронных операций, такими как jQuery и Angular.

Вопросы Оператора Связывания

Одним из наиболее интересных кандидатов на ES7 — также уже реализованным в качестве дополнительной функции в Babel — является оператор привязки. Как и async / await, оператор привязки обсуждался для ES6, но он не был готов; Есть еще неопределенные детали. К его чести, он имеет сладкий, однозначный символ , который не воняет grawlix: ::. Это трудно найти.

Я не уверен, что вы бы назвали его действием в техническом плане — поиск в Google приводит меня к мультиметодам, динамической рассылке или позднему связыванию. Последние два, вероятно, совсем не точны в контексте JS, где все методы имеют позднюю привязку из-за природы цепочки прототипов, а динамическая диспетчеризация является неприменимой концепцией из-за того, как работают свойства JS. «Мультиметод», возможно, имеет немного больше смысла, но в нем также есть куча неприменимого классического ОО багажа.

Что это делает? Связывает вещи, на лету.

const { filter, map, reduce } = Array.prototype;

const h1s = document.querySelectorAll('h1')::map(h1 => h1.innerText);

Другими словами, он call()перестроен так, чтобы разрешать синтаксис, похожий на вызов метода.

Его можно использовать и другим способом. В promise.then(::object.method)аргумент, переданный в thenэквивалентно object.method.bind(object). Грамматики могут заметить, что это представляет уникальный случай — своего рода префиксный бинарный оператор, за исключением того, что его операнды представляют собой последовательность, которая обычно разрешается в одно значение. Я подозреваю, что это может иметь какое-то отношение к тому, почему спецификации все еще в воздухе.

Утилита довольно очевидна — особенно при работе с объектами, похожими на массивы, как в первом примере — но оператор привязки все еще аккуратно попадает в экспериментальную область. Нельзя сказать, что это плохая идея использовать его, но он не видел ничего похожего на хищное внимание async. С ним еще не связано идиоматическое использование, кроме, возможно, его использования как способа заставить извращения DOM вести себя так, как они уже должны.

В таком случае зачем это упоминать? Потому что это имеет … философские последствия. Кроме того, EcmaScript всегда был языком с множеством парадигм. Я не знаю, если это началось случайно или по замыслу, но теперь это считается краеугольным камнем его идентичности. В последние годы наблюдается рост пламенной функционализма в JS (и в других местах) , что часто захватывающим и заманчиво, увлекая нас немного дальше от корней JS как своего рода объект бутлег ориентированной МОГ захватить сумку.

Введение синтаксиса класса укрепило аргумент в пользу — или, по крайней мере, простоты реализации — программного обеспечения, которое следует модели, более или менее объектно-ориентированной. В этом было какое-то сопротивление, и я подозреваю, что в какой-то степени вышеупомянутая группа чувствовала, что их работа по преобразованию дурных голов уже достаточно сложна. (Другие возражения заключались в том, что это может сделать работу наследования прототипов более мрачной, и беспокойство по поводу всего нового мира вещей, которые Java-разработчики могут в конечном итоге делать, когда они касаются JS: «ах, класс — пора!»).

The bind operator fits into this ongoing tug of war about what JS ought to move towards because it can be seen as ‘anti-functional.’ It places emphasis on this and invites us to create whole libraries of plug-and-play methods for use on objects and values without modifying built-ins, while taking advantage of coercion or duck typing. Contrast this with the equally valid functional approach that would prefer to see those objects and values as arguments subservient to the almighty function.

const seconds = function() { return this * 1000; };

3::seconds(); // 3000
'3'::seconds(); // 3000

All other concerns aside, they do scan nicely. If one were dedicated enough to the premise, it’s a short jump to using these free floating methods anywhere that a given function could be said to have a core argument that would make sense as this. Preexisting functions that fit the bill can be converted easily:

const toMulti = method => function() { return method(this, ...arguments); };

const round = toMulti(Math.round);

3.5::round(); // 4

const toJSON  = toMulti(JSON.stringify);

({ a: true })::toJSON(); // '{"a":true}'

const forEach = toMulti(_.forEach);

'abc'::forEach(::console.log);
// a 0 abc
// b 1 abc
// c 2 abc

So it could get out of hand, but I think it’ll be alright. At this point, on the back end at least, functional techniques have become idiomatic JS themselves. Avoiding mutation and side effects, thinking in terms of higher order functions, and taking joy in writing small, abstract and single-minded components are all recognized as ‘good.’. This is only a tiny portion of what ‘functional’ might mean, though. Where’s the rest? Perhaps it’s still a matter of time, but it’s just as likely that this is a case of plundering the parts we can use … and ignoring the parts that we believe we already have superior — or at least, equally adequate — solutions for.

One of the coolest things about JS is how freely you can mix paradigms without creating discord. Even lodash/underscore, the warhorse of functional programming in JS, is really a hybrid creature – compare it to Ramda and that’ll be clear. Multi-paradigm is our paradigm. It has its own flavor. Since ES6 has seen us make peace with this, the pendulum may swing back a little towards something more OO, but ultimately I expect the popular writing style will continue walking a line right down the center.

Using ES6 Now

Using Babel with node or io.js is pretty straight forward. You’ll want your /src to be in .npmignore and your /lib (or whatever) to be in .gitignore. You can use package.json script hooks to make it build using the Babel CLI, or you can use a build tool or task runner. Personally, I usually use Gobble and tie it in at the “test” script, something like gobble build lib --force && node test/test.js.

There are several options for polyfilling. Babel is only directly responsible for transpiling; concerns like making sure Symbol exists fall on CoreJS, and generator / async support falls on Facebook’s regenerator. You can include the “runtime” transform to get both. Depending on what you’ve written, you may be able to leave regenerator out.

I always transpile with sourcemaps. It’s pretty critical if you want to debug or test without completely losing your mind. At your entry point, you might have something like this before any other code:

import 'babel/polyfill';
import 'source-map-support/register';

That will transform stack trace output so it shows the error position in the original code. It works, which seems like amazing spooky magic to me.

I mentioned earlier that I thought client-side use of Babeled code would increase. But that means including the polyfill (CoreJS and regenerator) which is quite large. The tradeoff between size and utility is still something that needs to be considered case-by-case. That said, I was able to get a Browserify bundle of Babel-transpiled code with CoreJS and the regenerator runtime down to 47kB after mangling and – this is important because of the incredible number of modules in CommonJS – converting all require paths to numeric identifiers using bundle-collapser. And the result? ES6 – ES7, even – works in IE8. Eight.

Here’s the build script that got me there. In this case, I include the polyfill by importing it at the entry point (import 'babel/polyfill';); when building for node it will probably make more sense to polyfill with the ‘runtime’ option. Using loose mode and dead code removal options help, but you should check out the extra caveats that using these options may entail before using them.

#!/bin/sh

babel \
    --loose "all" \
    --optional "\
es7.asyncFunctions,\
es7.functionBind,\
minification.deadCodeElimination,\
minification.inlineExpressions,\
minification.memberExpressionLiterals,\
minification.propertyLiterals,\
validation.undeclaredVariableCheck" \
    --out-dir .tmp \
    src && \
browserify .tmp/client.js -p bundle-collapser/plugin |
uglifyjs -m -c -- - > lib/client.js && \
rm -r .tmp

(If you manage to get it smaller … let me know!)

If you’re looking to learn more about ES6, in addition to the links at the start of this article, I should note that 2ality’s Axel Rauschmayer is about to publish the first comprehensive book dedicated to ES6. Given the quality of the material on his site, it seems like a good bet.

If you’re working in ES6, you’ll probably want an appropriate syntax definition in your editor so highlighting doesn’t turn into a mess with the new syntax. For .tmLanguage, there’s Babel-Sublime and JSNext. That format is supported by many editors, including Sublime. On the off chance that you’re a Sublime Text 3 user who keeps up to date with the dev-channel releases, you can also use .sublime-syntax definitions, in which case you might want to check out my own ES6+ sublime-syntax def (available via Package Control as “Ecmascript Syntax”).