Границы моего языка олицетворяют границы моего мира.
— Людвиг Витгенштейн
Последние несколько месяцев я пишу только ECMAScript 6 код, воспользовавшись трансформацией [1] в поддерживаемые в настоящее время версии JavaScript.
ECMAScript 6, далее ES6 и ранее ES.next, является последней версией спецификации. По состоянию на август 2014 новые возможности не обсуждаются, но детали и крайние случаи до сих пор уточняются. Ожидается, что стандарт будет завершен и опубликован в середине 2015 года.
Принятие ES6 одновременно привело к повышению производительности (что делает мой код более лаконичным) и ликвидации целого класса ошибок путем устранения распространённых подводных камней JavaScript.
Более того, это подтвердило мою веру в эволюционный подход к языку и проектирования программного обеспечения, в противоположность clean-slate recreation .
Это должно быть достаточно очевидным для вас, если вы использовали CoffeeScript, который сосредотачивается на хороших частях JS и скрывает плохие. ES6 смог принять на столько много инноваций из CoffeeScript, что некоторые даже ставят под сомнение дальнейшее развитие последнего.
For all intents and purposes, JavaScript has merged CoffeeScript into master.
I call that a victory for making things and trying them out.
— Reginald Braithwaite (@raganwald) 14 января 2015
Вместо того, чтобы сделать тщательный анализ новых возможностей я расскажу о наиболее интересных из них. Чтобы стимулировать разработчиков обновляться, новые языки и фреймворки должны (1) иметь убедительную историю совместимости и (2) предлагать вам достаточно большую морковку.
# Синтаксис модулей
ES6 знакомит с синтаксисом для определения модулей и объявления зависимостей. Я подчеркиваю слово синтаксис потому что ES6 не имеет отношение к фактической реализации того, как модули будут выбраны или загружены.
Это еще более укрепляет взаимодействие между различными контекстами, в которых может выполняться JavaScript.
Рассмотрим в качестве примера простую задачу написания многоразового использования CRC32 в JavaScript.
До сих пор, не существовало никаких рекомендаций о том, как на самом деле решить эту задачу. Общий подход это объявить функцию:
function crc32(){
// …
}
С оговоркой, конечно же, что она вводит единое фиксированное глобальное имя, на которое другие части кода будут должны ссылаться. И с точки зрения кода, который использует crc32 функцию, нет способа объявить зависимость. Как только функция была объявлена, она будет существовать до тех пор, пока код не будет интерпретирован.
В этой ситуации, Node.JS выбрал путь введения функции require и объектов module.exports и exports . Несмотря на успех в создании преуспевающей экосистемы модулей, возможности взаимодействия по-прежнему были несколько ограничены.
Типичный сценарий, чтобы проиллюстрировать эти недостатки это генерация связки модулей для браузера, с помощью таких инструментов, как browserify или webpack. Они еще находятся в зачаточном состоянии, потому что они воспринимают require() как синтаксис, эффективно избавляя себя от свойственного им динамизма.
Приведенный пример не подлежит статическому анализу, поэтому если вы попытаетесь транспортировать этот код в браузер, то он сломается:
require(woot() + ‘_module.js’);
Другими словами, алгоритм упаковщика не может знать заранее, что означает woot().
ES6 ввел правильный набор ограничений, учитывая большинство существующих вариантов использования, черпая вдохновение из самых неформально-существующих специальных модульных систем, как jQuery $.
Синтаксис требует некоторого привыкания. Наиболее распространенный шаблон для определения зависимостей удивительно непрактичен.
Следующий код:
import crc32 from ‘crc32’;
работает для
export default function crc32(){}
но не для
export function crc32(){}
последний считается именованным экспортом и требует синтаксис { } в конструкции import:
import { crc32 } from ‘crc32’;
Другими словами, самая простая (и, пожалуй, наиболее желательная) форма определения модуля требует дополнительное ключевое слово default. Или в случае его отсутствия, использование { } при импорте.
# Деструктуризация
Одним из наиболее распространенных шаблонов, возникших в современном JavaScript коде является использование вариантных объектов.
Такая практика широко используется в новых браузерных API, например в WHATWG fetch (современная замена XMLHttpRequest):
fetch(‘/users’, {
method: ‘POST’,
headers: {
Accept: ‘application/json’,
‘Content-Type’: ‘application/json’
},
body: JSON.stringify({
first: ‘Guillermo’,
last: ‘Rauch’
})
});
Повсеместное принятие этой модели эффективно препятствует падению экосистемы JavaScript в логическую ловушку.
Если принять, что API принимает обычные аргументы, а не объект с параметрами, то вызов fetch превращается в задачу запоминания порядка аргументов и ввода ключевого слова null в нужное место.
// пример ночного кошмара из альтернативного мира
fetch(‘/users’, ‘POST’, null, null, {
Accept: ‘application/json’,
‘Content-Type’: ‘application/json’
}, null, JSON.stringify({
first: ‘Guillermo’,
last: ‘Rauch’
}));
Со стороны реализации, однако, это не выглядит так же красиво. Глядя на объявление функции, ее сигнатура больше не описывает входные возможности:
function fetch(url, opts){
// …
}
Обычно это сопровождается ручной установкой значений по-умолчанию локальным переменным:
opts = opts || {};
var body = opts.body || '';
var headers = opts.headers || {};
var method = opts.method || 'GET';
И к сожалению для нас, несмотря на свою распространенность, практика использования || фактически привносит трудно выявляемые ошибки. Например, в этом случае мы не допускаем того, что opts.body может быть 0, поэтому надежный код скорее всего будет выглядеть так:
var body = opts.body === undefined ? '' : opts.body;
Благодаря деструктуризации мы можем сразу четко определить параметры, правильно задать значения по умолчанию и выставить их в локальной области видимости:
fetch(url, { body='', method='GET', headers={} }){
console.log(method); // нету opts.
}
Собственно, значение по умолчанию можно применить и ко всему объекту с параметрами:
fetch(url, { method='GET' } = {}){
// значение по умолчанию для второго параметра - {}
// выведет "GET":
console.log(method);
}
Вы также можете деструктурировать оператор присваивания:
var { method, body } = opts;
Это напоминает мне о выразительности, предоставленные with, но без магии или негативных последствий.
# Новые соглашения
Некоторые части языка были полностью заменены лучшими альтернативами, что быстро станет новым стандартом того, как вы пишете JavaScript.
Я расскажу о некоторых из них.
# let/const вместо var
Вместо того, чтобы писать var x = y скорее всего вы будете писать let x = y. let позволяет объявлять переменные с блочной областью видимости:
if (foo) {
let x = 5;
setTimeout(function(){
// тут x равен `5`
}, 500);
}
// тут x равен `undefined`
Это особенно полезно для for или while циклов:
for (let i = 0; i < 10; i++) {}
// `i` здесь не существует.
Используйте const, если вы хотите обеспечить неизменяемость с той же семантикой, как и let.
# строковые шаблоны вместо конкатенации
В связи с отсутствием sprintf или подобными утилитами в стандартной библиотеки JavaScript, составление строк всегда было более болезненным, чем следовало бы.
Строковые шаблоны сделали встраивание выражений в строки тривиальной операцией, также как и поддержку нескольких линий. Просто замените ‘ на `
let str = `
Здравствуйте ${first}.
Мы в ${new Date().getFullYear()} году
`;
# классы вместо прототипов
Определение класса было громоздкой операцией и требовало глубокого знания внутреннего устройства языка. Даже несмотря на то, что, польза понимания внутреннего устройства очевидна, порог входа для новичков был неоправданно высоким.
class предлагает синтаксический сахар для определения функции конструктора, методов прототипа и геттеров / сеттеров. Он также реализует прототипное наследование со встроенным синтаксисом (без дополнительных библиотек или модулей).
class A extends B {
constructor(){}
method(){}
get prop(){}
set prop(){}
}
Я изначально был удивлен, узнав, классы не всплывают (hoisted) (объяснение тут). Поэтому вы должны думать о них, переводя в var A = function(){} в противоположность function A(){}.
# ()=> вместо function
Не только потому что (x, y) => {} короче написать, чем function (x,y) {}, но поведение this в теле функции, скорее всего, будет ссылаться на то, что вы хотите.
Так называемые функции “толстые стрелки” лексически связанны. Рассмотрим пример метода внутри класса, который запускает два таймера:
class Person {
constructor(name){
this.name = name;
}
timers(){
setTimeout(function(){
console.log(this.name);
}, 100);
setTimeout(() => {
console.log(this.name);
}, 100);
}
}
К ужасу новичков, первый таймер (с использованием function) выведет «undefined». А вот второй правильно выведет name.
# Первоклассная поддержка async I/O
Асинхронное выполнение кода сопровождало нас в течение почти всей истории языка. setTimeout, в конце концов, был введен примерно в то время, когда вышел JavaScript 1.0.
Но, пожалуй, язык не поддерживает асинхронность на самом деле. Возвращаемое значение вызовов функций, которые запланированы “выполниться в будущем” обычно равны undefined или в случае с setTimeout — Number.
Введение Promise позволило заполнить очень большую пропасть в совместимости и композиции.
С одной стороны, вы найдете API более предсказуемым. В качестве теста, рассмотрим новое fetch API. Как это работает за сигнатурой, которую мы только что описали? Вы угадали. Оно возвращает Promise.
Если Вы использовали Node.JS в прошлом, вы знаете, что есть неформальная договоренность о том, что обратные вызовы следуют сигнатуре:
function (err, result){}
Также неофициально указана идея о том, что обратные вызовы будут вызываться только один раз. И null будет значение в случае отсутствия ошибок (а не undefined или false). За исключением, возможно, это не всегда так.
# Вперед к будущему
ES6 набирает немалые обороты в экосистеме. Chrome и io.js уже добавили некоторый функционал из ES6. Много уже было написано об этом.
Но стоит отметить то, что эта популярность была во многом обусловлена наличием утилит для трансформации, а не фактической поддержкой. Отличные инструменты появились, для того чтобы включить трансформацию и эмуляцию ES6, и браузеры со временем добавили поддержку отладки кода и отлова ошибок (с помощью карт кода).
Эволюция языка и его предполагаемый функционал, опережают реализацию. Как говорилось выше, Promise — по-настоящему интересен как самостоятельный блок, который предлагает решение проблемы callback hell раз и навсегда.
Стандарт ES7 предлагает сделать это путем введения возможности ожидания (async) объекта Promise:
async function uploadAvatar(){
let user = await getUser();
user.avatar = await getAvatar();
return await user.save();
}
Хотя эта спецификация обсуждается уже давно, тот же инструмент, который компилирует ES6 в ES5 уже реализовал это.
Предстоит еще много работы для того, чтобы убедиться, что процесс принятия нового синтаксиса языка и API становится еще более лишенным странностей для тех, кто только приступает к работе.
Но одно можно сказать точно: мы должны принять это будущее.
Сноски:
1. ^ я использую слово “трасформация” в статье, чтобы объяснить компиляцию исходного кода в исходный код в JavaScript. Но значение этого термина технически спорно.