[Перевод] Операторы ?., ?? и |>: будущие возможности JavaScript, которые вам понравятся

Джастин Фуллер, автор материала, перевод которого мы сегодня публикуем, предлагает рассмотреть три новых возможности, появление которых ожидается в JavaScript в обозримом будущем. Сначала он расскажет о процессе развития JS, а после этого представит обзор этих возможностей и покажет примеры их использования. Речь пойдёт об операторах ?., ?? и |>.

О стандарте ECMAScript и развитии JavaScript


405987b152ff04ab9244c47f8bc8e939.png Если вы уже знакомы с особенностями деятельности рабочей группы ECMA TC39, с тем, как она осуществляет отбор и обработку предложений о совершенствовании JavaScript, вы вполне можете пропустить этот раздел. Если же вы из тех, кому интересно об этом узнать — вот краткий обзор того, чем занимается TC39.

JavaScript — это реализация стандарта, называемого ECMAScript, который был создан для стандартизации реализаций языка, которые появились в ранние годы существования веб-браузеров.

Существует восемь редакций стандарта ECMAScript и семь релизов (четвёртая редакция стандарта не выходила, после третьей сразу идёт пятая). Разработчики JavaScript-движков приступают к реализации новшеств языка после выхода стандарта. Здесь можно увидеть, что не каждый движок реализует все возможности, при этом некоторым движкам для введения новшеств требуется больше времени, чем другим. Хотя такое положение дел и не идеально, это, всё же, лучше, чем полное отсутствие стандартов.

Предложения


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

Процесс рассмотрения предложений состоит из пяти шагов, описанных в этом документе. В самом начале предложение находится в состоянии черновика (strawman), это то же самое, что и Stage 0. На этом шаге предложение либо ещё не представлено техническому комитету, либо оно ещё не отвергнуто, но пока ещё не соответствует критериям, позволяющим перейти к следующему этапу согласования. Те возможности, о которых мы будем говорить ниже, уже прошли Stage 0.

Мне хотелось бы порекомендовать читателям избегать использования в продакшне новшеств JS, предложения, описывающие которые, находятся на этапе Stage 0. Лучше дождаться перехода их к более стабильным этапам согласования. Цель этой рекомендации заключается в том, чтобы помочь вам избежать проблем в том случае, если предложение будет отвергнуто или окажется очень сильно изменённым.

Система тестирования


Материалы, в которых рассказывают о новых возможностях языков программирования, часто содержат фрагменты кода, вырванные из контекста. Иногда эти возможности используются для создания неких учебных приложений. Однако, ни того, ни другого мы делать здесь не будем. Так как я — большой поклонник TDD, я полагаю, что лучший способ изучения некоей новой технологии заключается в её тестировании.

Мы будем использовать здесь, для освоения описываемых возможностей JS, то, что Джим Ньюкирк называет обучающими тестами. Такие тесты построены не на утверждениях о коде, написанном на некоем языке. Они построены на анализе утверждений, касающихся самого языка. Тот же подход может оказаться полезным и при изучении API сторонних разработчиков, и при освоении любой возможности языка.

Транспиляторы


Если вы не знакомы с транспиляторами, то у вас, возможно, возникнет вопрос о том, как мы собираемся использовать возможности языка, которые пока не реализованы. Тут надо отметить, что JavaScript постоянно развивается, а для реализации в распространённых движках его новых возможностей, интересных программистам, нужно время. В результате в экосистеме JS существует такое понятие, как транспиляторы.

Они позволяют преобразовывать код, написанный на JS с использованием новейших возможностей, которые, например, ещё не включены в стандарты и не реализованы популярными движками, в JS-код, который понимают существующие среды выполнения JavaScript-программ. Это позволят, например, использовать в коде даже предложения уровня Stage 0, а то, что получится после обработки кода транспилятором, можно будет выполнить, например, в современных браузерах или в среде Node.js. Делается это путём преобразования нового кода таким образом, что он, для среды исполнения, выглядит как код, написанный на одной из поддерживаемых ей версий JS.

Одним из самых популярных JavaScript-транспиляторов является Babel, совсем скоро мы поговорим о том, как им пользоваться.

Подготовка рабочей среды


Если вы хотите самостоятельно повторить всё то, о чём мы будем говорить — вы можете это сделать, настроив npm-проект и установив необходимые зависимости. Предполагается, что сейчас у вас уже установлены Node.js и NPM.

Для того чтобы подготовиться к нашим экспериментам, выполните следующую команду, находясь в отведённой для этих экспериментов директории:

npm init -f && npm i ava@1.0.0-beta.3 @babel/preset-env@7.0.0-beta.42 @babel/preset-stage-0@7.0.0-beta.42 @babel/register@7.0.0-beta.42 @babel/polyfill@7.0.0-beta.42 @babel/plugin-transform-runtime@7.0.0-beta.42 @babel/runtime@7.0.0-beta.42 --save-dev


Затем добавьте следующее в файл package.json:

"scripts": {
  "test": "ava"
},
"ava": {    
  "require": [      
    "@babel/register",
    "@babel/polyfill"   
  ]  
}


Далее, создайте файл .babelrc со следующим содержимым:

{  
  "presets": [    
    ["@babel/preset-env", {      
      "targets": {        
        "node": "current"      
      }    
    }],    
    "@babel/preset-stage-0"  
  ],  
  "plugins": [    
    "@babel/plugin-transform-runtime"
  ]
}


Теперь вы готовы к тому, чтобы писать тесты, исследующие новые возможности JS.

1. Оператор ?.


В ходе написания приложений на JavaScript мы постоянно работаем с объектами. Однако иногда эти объекты не имеют той структуры, которой мы от них ожидаем. Вот, например, объект с данными. Подобный объект можно получить, скажем, в результате запроса к базе данных или при обращении к некоему API.

const data = {
  user: {
    address: {
      street: 'Pennsylvania Avenue',
    }, 
  },
};


Этот объект описывает пользователя, прошедшего регистрацию. А вот — похожий объект, но в данном случае пользователь, которого он описывает, регистрацию не завершил.

const data = {
  user: {},
};


Если попытаться получить доступ к свойству street объекта address, вложенного в этот «неполный» объект, рассчитывая на то, что он будет выглядеть так же, как объект, содержащий все необходимые данные, можно столкнуться с такой ошибкой:

console.log(data.user.address.street); // Uncaught TypeError: Cannot read property 'street' of undefined


Для того чтобы этого избежать, в текущих условиях необходимо использовать, для обращения к свойству street, такую конструкцию:

const street = data && data.user && data.user.address && data.user.address.street;
console.log(street); // undefined


Я полагаю, что всё это, во-первых, плохо выглядит, во-вторых, это тяжело писать, а в-третьих — подобные конструкции оказываются слишком длинными.

Именно в таких ситуациях как нельзя кстати оказываются опциональные последовательности или опциональные цепочки (optional chaining), представленные оператором, выглядящим как знак вопроса с точкой (?.).

console.log(data.user?.address?.street); // undefined


Выглядит это лучше, да и строить такие конструкции проще. Уверен, вы с этим утверждением согласитесь. Убедившись в полезности этой новой возможности, исследуем её. Напишем тест, поместив код в файл optional-chaining.test.js. Мы, в этом разделе, будем постепенно дополнять этот файл новыми тестами.

import test from 'ava';

const valid = {
  user: {
    address: {
      street: 'main street',
    },
  },
};

function getAddress(data) {
  return data?.user?.address?.street;
}

test('Optional Chaining returns real values', (t) => {
  const result = getAddress(valid);
  t.is(result, 'main street');
});


Этот тест позволяет нам убедиться в том, что оператор ?., в том случае, если объект выглядит так, как ожидается, работает точно так же, как способ доступа к свойствам объекта через точку. Теперь проверим поведение этого оператора в ситуации, когда объект не является тем, чем мы его считаем.

test('Optional chaining returns undefined for nullish properties.', (t) => {
  t.is(getAddress(), undefined);
  t.is(getAddress(null), undefined);
  t.is(getAddress({}), undefined);
});


А вот как опциональные последовательности работают при применении их для доступа к элементам массивов:

const valid = {
  user: {
    address: {
      street: 'main street',
      neighbors: [
        'john doe',
        'jane doe',
      ],
    },
  },
};

function getNeighbor(data, number) {
  return data?.user?.address?.neighbors?.[number];
}

test('Optional chaining works for array properties', (t) => {
  t.is(getNeighbor(valid, 0), 'john doe');
});

test('Optional chaining returns undefined for invalid array properties', (t) => {
  t.is(getNeighbor({}, 0), undefined);
});


Иногда случает так, что нам неизвестно, реализована ли в объекте какая-то функция. Например, такая ситуация распространена при работе с браузерами. Устаревшие браузеры могут не содержать реализаций неких функций. Благодаря оператору ?. мы можем узнать, реализована ли в объекте интересующая нас функция или нет. Вот как это сделать:

const data = {
  user: {
    address: {
      street: 'main street',
      neighbors: [
        'john doe',
        'jane doe',
      ],
    },
    getNeighbors() {
      return data.user.address.neighbors;
    }
  },
};

function getNeighbors(data) {
  return data?.user?.getNeighbors?.();
}
  
test('Optional chaining also works with functions', (t) => {
  const neighbors = getNeighbors(data);
  t.is(neighbors.length, 2);
  t.is(neighbors[0], 'john doe');
});

test('Optional chaining returns undefined if a function does not exist', (t) => {
  const neighbors = getNeighbors({});
  t.is(neighbors, undefined);
});


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

value == null ? value[some expression here]: undefined;


В результате после оператора ?. не будет выполнено ничего в том случае, если значение представлено в виде undefined или null. Взглянуть на это правило в действии можно с помощью следующего теста:

let neighborCount = 0;

function getNextNeighbor(neighbors) {
  return neighbors?.[++neighborCount];
}
  
test('It short circuits expressions', (t) => {
  const neighbors = getNeighbors(data);
  t.is(getNextNeighbor(neighbors), 'jane doe');
  t.is(getNextNeighbor(undefined), undefined);
  t.is(neighborCount, 1);
});


Как видите, опциональные последовательности позволяют уменьшить потребность в конструкциях if, в сторонних библиотеках вроде lodash, и в использовании неуклюжих конструкций, в которых применяется &&.

▍Оператор?… и производительность


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

Я посоветовал бы вам использовать эту возможность вместе с некоей системой проверки, которая позволяет анализировать объекты при их получении откуда-либо или при их создании. Это сократит необходимость в использовании конструкции ?. и ограничит её воздействие на производительность.

2. Оператор ?


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

  1. Проверка значения на undefined и null.
  2. Указание значения, используемого по умолчанию.
  3. Обеспечение того, что появление значений 0, false, и '' не приводит к использованию значения, применяемого по умолчанию.


Вот пример подобного выражения:

value != null ? value : 'default value';


Можно встретить и неграмотно написанный вариант такого выражения:

value || 'default value'


Проблема второго примера заключается в том, что третий пункт вышеприведённого списка здесь не выполняется. Появление тут числа 0, значения false или пустой строки будет распознано как значение false, а это — не то, что нам нужно. Именно поэтому проверку на null и undefined нужно проводить в явном виде:

value != null


Это выражение аналогично такому:

value !== null && value !== undefined


В подобных ситуациях очень кстати окажется новый оператор, называемый «объединение со значением null» (nullish coalescence), который выглядит как два знака вопроса (??). В подобной ситуации теперь можно будет воспользоваться следующей конструкцией:

value ?? 'default value';


Это защищает нас от случайного использования значения по умолчанию при появлении в выражениях значений, распознаваемых как false, но при этом позволяет выявлять значения null и undefined, не прибегая к тернарному оператору и к проверке вида != null.

Теперь, познакомившись с этим оператором, мы можем написать тесты для того, чтобы проверить его в деле. Эти тесты поместим в файл nullish-coalescing.test.js.

import test from 'ava';

test('Nullish coalescing defaults null', (t) => {
  t.is(null ?? 'default', 'default');
});

test('Nullish coalescing defaults undefined', (t) => {
  t.is(undefined ?? 'default', 'default');
});

test('Nullish coalescing defaults void 0', (t) => {
  t.is(void 0 ?? 'default', 'default');
});

test('Nullish coalescing does not default 0', (t) => {
  t.is(0 ?? 'default', 0);
});

test('Nullish coalescing does not default empty strings', (t) => {
  t.is('' ?? 'default', '');
});

test('Nullish coalescing does not default false', (t) => {
  t.is(false ?? 'default', false);
});


Из этих тестов можно понять, что значения по умолчанию используются для null, undefined и void 0 (оно преобразуется в undefined). При этом значения по умолчанию не применяются в тех случаях, когда в выражении появляются значения, воспринимаемые как ложные, вроде 0, пустой строки и false.

3. Оператор |>


В функциональном программировании есть такое понятие, как композиция. Это — действие, представляющее собой объединение в цепочку нескольких вызовов функций. Каждая функция принимает, в качестве входных данных, выходные данные предыдущей функции. Вот пример композиции, подготовленный средствами обычного JavaScript:

function doubleSay (str) {
  return str + ", " + str;
}
function capitalize (str) {
  return str[0].toUpperCase() + str.substring(1);
}
function exclaim (str) {
  return str + '!';
}
let result = exclaim(capitalize(doubleSay("hello")));
result //=> "Hello, hello!"


Подобное распространено настолько широко, что средства композиции функций существуют во множестве библиотек, поддерживающих функциональное программирование, например, в таких, как lodash и ramda.

Благодаря новому конвейерному оператору (pipeline operator), который выглядит как комбинация вертикальной черты и знака «больше» (|>), можно отказаться от использования сторонних библиотек и переписать вышеприведённый пример следующим образом:

let result = "hello"
  |> doubleSay
  |> capitalize
  |> exclaim;

result //=> "Hello, hello!"


Цель появления этого оператора заключается в том, чтобы улучшить читаемость цепочек вызовов функций. Кроме того, в будущем, этот оператор найдёт применение в конструкциях частичного применения функций. Сейчас это можно сделать следующим образом:

let result = 1
  |> (_ => Math.max(0, _));

result //=> 1
let result = -5
  |> (_ => Math.max(0, _));

result //=> 0


Разобравшись с основами, напишем тесты, разместив их в файле pipeline-operator.test.js:

import test from 'ava';

function doubleSay (str) {
  return str + ", " + str;
}

function capitalize (str) {
  return str[0].toUpperCase() + str.substring(1);
}

function exclaim (str) {
  return str + '!';
}

test('Simple pipeline usage', (t) => {
  let result = "hello"
    |> doubleSay
    |> capitalize
    |> exclaim;

  t.is(result, 'Hello, hello!');
});

test('Partial application pipeline', (t) => {
  let result = -5
    |> (_ => Math.max(0, _));

  t.is(result, 0);
});

test('Async pipeline', async (t) => {
  const asyncAdd = (number) => Promise.resolve(number + 5);
  const subtractOne = (num1) => num1 - 1;
  const result = 10
    |> asyncAdd
    |> (async (num) => subtractOne(await num));
  
  t.is(await result, 14);
});


Анализируя эти тесты, можно заметить, что при применении в конвейере асинхронной функции, объявленной с использованием ключевого слова async, нужно дождаться появления значения, воспользовавшись ключевым словом await. Дело тут в том, что значение становится объектом Promise. Имеются несколько предложений изменений, направленных на поддержку конструкций вида |> await asyncFunction, но они ещё не реализованы и решение об их дальнейшей судьбе пока не принято.

Итоги


Надеемся, вам понравились ожидаемые возможности JS, которым посвящён этот материал. Вот репозиторий с тестами, которыми мы занимались в этом материале. А вот, для удобства — ссылки на рассмотренные здесь новшества: оператор ?., оператор ?, оператор |>.

Уважаемые читатели! Как вы относитесь к появлению новых возможностей JS, о которых мы рассказали?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru