- PVSM.RU - https://www.pvsm.ru -

JavaScript: многоликие функции

Если вы занимаетесь JavaScript-разработкой, о какой бы платформе ни шла речь, это значит, что вы способны оценить значение функций. То, как они устроены, те возможности, которыми они наделяют программиста, делают их поистине универсальным и незаменимым инструментом. Так думают и разработчики Test262 [1] — официального набора тестов, который предназначен для проверки JavaScript-движков на совместимость со стандартом EcmaScript.

JavaScript: многоликие функции - 1 [2]

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

Традиционные подходы

▍Объявление функции и функциональное выражение

Самые известные и широко используемые способы определения функций в JS, кроме того, являются и самыми старыми. Это — объявление функции (Function Declaration) и функциональное выражение (Function Expression). Первый способ был частью исходного варианта языка с 1995-го года и был отражён в первой редакции [3] спецификации, в 1997-м. Второй представлен в третьей редакции [4], в 1999-м.

Если присмотреться к этим способам определения функций, можно увидеть три варианта их использования:

// Объявление функции
function BindingIdentifier() {}

// Именованное функциональное выражение
// (BindingIdentifier недоступно за пределами этой функции)
(function BindingIdentifier() {}); 

// Анонимное функциональное выражение
(function() {});

Тут, однако, стоит учесть, что у анонимного функционального выражения всё-таки может быть «имя». Вот [5] хороший материал об именах функций.

▍Конструктор Function

Если говорить об «API функций» в JavaScript, то начать такой разговор стоит с конструктора Function. Принимая во внимание изначальный подход к проектированию языка, по аналогии с другими конструкциями, рассмотренное выше объявление функции можно интерпретировать в виде «литерала» к API конструктора Function.

Конструктор Function предоставляет средства для определения функций путём указания параметров и тела функции посредством строковых аргументов. Последний из этих аргументов представляет собой тело функции:

new Function('x', 'y', 'return x ** y;');

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

Новые подходы

В стандарте ES2015 [6] были представлены несколько новых синтаксических форм определения функций. У них огромное количество вариантов.

▍Не такое уж и анонимное объявление функции

Если у вас есть опыт работы с модулями ES, вам должна быть знакома новая форма анонимного объявления функции. Хотя это очень похоже на анонимное функциональное выражение, такая функция, на самом деле, имеет связанное имя [7] "*default*". В результате, функция получается не такой уж и анонимной.

// Не такое уж и анонимное объявление функции
export default function() {}

Кстати, такое «имя» не является корректным идентификатором и привязка тут не создаётся.

▍Способы определения методов объектов

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

let object = {
  propertyName: function() {},
};
let object = {
  // (BindingIdentifier недоступен за пределами этой функции)
  propertyName: function BindingIdentifier() {},
};

Вот объявления свойств-аксессоров, представленные В ES5:

let object = {
  get propertyName() {},
  set propertyName(value) {},
};

В ES2015 появился сокращённый синтаксис для определения методов объектов, который можно использовать как в формате обычного имени свойства, так и с квадратными скобками, в которые заключено строковое представление имени. То же самое касается и свойств-аксессоров:

let object = {
  propertyName() {},
  ["computedName"]() {},
  get ["computedAccessorName"]() {},
  set ["computedAccessorName"](value) {},
};

Похожий подход можно использовать и для определения методов прототипов в объявлениях классов (Class Declarations) и в выражениях классов (Class Expressions):

// Объявление класса
class C {
  methodName() {}
  ["computedName"]() {}
  get ["computedAccessorName"]() {}
  set ["computedAccessorName"](value) {}
}

// Выражение класса
let C = class {
  methodName() {}
  ["computedName"]() {}
  get ["computedAccessorName"]() {}
  set ["computedAccessorName"](value) {}
};

То же самое применимо и к статическим методам классов:

// Объявление класса
class C {
  static methodName() {}
  static ["computedName"]() {}
  static get ["computedAccessorName"]() {}
  static set ["computedAccessorName"](value) {}
}

// Выражение класса
let C = class {
  static methodName() {}
  static ["computedName"]() {}
  static get ["computedAccessorName"]() {}
  static set ["computedAccessorName"](value) {}
};

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

Стрелочные функции, которые появились в ES2015, наделали много шума, однако, в итоге приобрели широкую известность и популярность. Существует две формы стрелочных функций. Первая — это краткая форма (Concise Body), которая не предусматривает наличия фигурных скобок после стрелки, там находится лишь выражение присваивания (Assignment Expression). Вторая — блочная форма (Block Body). Здесь, после стрелки, идёт тело функции в фигурных скобках, которые могут быть пустыми, либо содержать некоторое количество выражений.

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

На практике вышесказанное означает наличие множества вариантов определения стрелочных функций:

// Краткая форма Функции без параметров
(() => 2 ** 2);

// Краткая форма функции с одним параметром
(x => x ** 2);

// Функция с одним параметром и телом функции
(x => { return x ** 2; });

// Краткая форма функции со списком параметров в скобках
((x, y) => x ** y);

В последней части примера показан набор параметров стрелочной функции в скобках (covered parameters). Такой подход даёт возможность работы со списком параметров, например, позволяя использовать шаблоны деструктуризации:

({ x }) => x

Параметр без скобок (uncovered parameter), как уже было сказано, позволяет задать стрелочную функцию, имеющую лишь один аргумент. Перед этим единственным аргументом можно использовать ключевые слова await или yield — если стрелочная функция определена внутри асинхронной функции или генератора, но на этом возможности такого синтаксиса заканчиваются.

Стрелочные функции можно использовать в инициализаторах объектов и при задании их свойств. Тут используются стрелочные функциональные выражения (Arrow Function Expression):

let foo = x => x ** 2;

let object = {
  propertyName: x => x ** 2
};

▍Генераторы

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

// Объявление генератора
function *BindingIdentifer() {}

// Ещё одно объявление не слишком анонимного генератора
export default function *() {}

// Выражение генератора
// (BindingIdentifier не доступен за пределами этой функции)
(function *BindingIdentifier() {});

// Анонимное выражение генератора
(function *() {});

// Определение методов
let object = {
  *methodName() {},
  *["computedName"]() {},
};

// Определение методов в объявлении класса
class C {
  *methodName() {}
  *["computedName"]() {}
}

// Определение статических методов в объявлении класса
class C {
  static *methodName() {}
  static *["computedName"]() {}
}

// Определение методов в выражении класса
let C = class {
  *methodName() {}
  *["computedName"]() {}
};

// Определение статических методов в выражении класса
let C = class {
  static *methodName() {}
  static *["computedName"]() {}
};

ES2017

▍Асинхронные функции

В июне 2017-го был опубликован стандарт ES2017, в котором, после нескольких лет разработки, были представлены асинхронные функции (Async Functions). Несмотря на то, что стандарт буквально только что «вышел из типографии», множество разработчиков уже пользуется асинхронными функциями благодаря Babel [8].

Асинхронные функции позволяют удобно описывать асинхронные операции. Благодаря их использованию код получается чистым и единообразным. При вызове асинхронной функции будет возвращён промис, который разрешится после того, как асинхронная функция возвратит результат своей работы. Если в асинхронной функции встречается выражение с ключевым словом await, она может приостановить работу, и, дождавшись выполнения выражения, например, возвратить его результаты.

Синтаксис асинхронных функций не особенно отличается от того, что уже было рассмотрено. Главная их особенность — префикс async:

// Объявление асинхронной функции
async function BindingIdentifier() { /**/ }

// Ещё одно объявление не такой уж анонимной асинхронной функции
export default async function() { /**/ }

// Именованное асинхронное функциональное выражение
// (BindingIdentifier недоступно за пределами этой функции)
(async function BindingIdentifier() {});

// Анонимное асинхронное функциональное выражение
(async function() {});

// Асинхронные методы
let object = {
  async methodName() {},
  async ["computedName"]() {},
};

// Асинхронные методы в объявлении класса
class C {
  async methodName() {}
  async ["computedName"]() {}
}

// Статические асинхронные методы в объявлении класса
class C {
  static async methodName() {}
  static async ["computedName"]() {}
}

// Асинхронные методы в выражении класса
let C = class {
  async methodName() {}
  async ["computedName"]() {}
};

// Статические асинхронные методы в выражении класса
let C = class {
  static async methodName() {}
  static async ["computedName"]() {}
};

▍Асинхронные стрелочные функции

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

// Краткая форма функции с одним параметром
(async x => x ** 2);

// Функция с одним параметром, за которым следует тело функции
(async x => { return x ** 2; });

// Краткая форма функции со списком параметров в скобках
(async (x, y) => x ** y);

// Список параметров в скобках, за которым следует тело функции
(async (x, y) => { return x ** y; });

Заглянем в будущее

▍Асинхронные генераторы

В будущих версиях спецификации JavaScript применение ключевых слов async и await будет расширено и на генераторы. За ходом работ по реализации этой функциональности можно наблюдать здесь [9]. Как вы, вероятно, догадались, речь идёт о комбинации ключевых слов async/await и существующих форм определения генераторов — через объявления и выражения.

Асинхронный генератор, при вызове, возвращает итератор, метод которого next() возвращает промис, который будет разрешён объектом, являющимся тем, что обычно возвращает итератор. В обычных условиях при вызове этого метода выполняется прямой возврат результата.

Асинхронные генераторы можно найти там, где уже имеются обычные функции-генераторы.

// Объявление асинхронного генератора
async function *BindingIdentifier() { /**/ }

// Объявление не такого уж и анонимного асинхронного генератора
export default async function *() {}

// Асинхронное выражение генератора
// (BindingIdentifier не доступен за пределами этой функции)
(async function *BindingIdentifier() {});

// Анонимное выражение генератора
(async function *() {});

// Определение методов
let object = {
  async *propertyName() {},
  async *["computedName"]() {},
};

// Определение методов прототипа в объявлении класса
class C {
  async *propertyName() {}
  async *["computedName"]() {}
}

// Определение методов прототипа в выражении класса
let C = class {
  async *propertyName() {}
  async *["computedName"]() {}
};
// Определение статических методов в объявлении класса
class C {
  static async *propertyName() {}
  static async *["computedName"]() {}
}

// Определение статических методов в выражении класса
let C = class {
  static async *propertyName() {}
  static async *["computedName"]() {}
};

Итоги. О JavaScript-движках, тестах и функциях

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

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

Сейчас проект содержит набор файлов с исходным кодом [10], которые состоят из разных тестовых сценариев и шаблонов [11].

Например, тут [12] можно посмотреть, как проверяется свойство функции arguments, тут [12] — тесты различных форм функций. Конечно, в Test262 есть ещё много всего. Скажем, вот [13] и вот [14] — тесты, связанные с деструктурированием. В процессе работы над тестами получаются немаленькие pull-запросы, обнаруживаются и исправляются ошибки [15]. Всё это ведёт к постоянному росту качества Test262, а значит, к улучшению проверок JS-движков на соответствие спецификации EcmaScript. Это имеет прямое воздействие на JavaScript-индустрию. Чем больше программных конструкций будет идентифицировано и покрыто тестами, тем легче разработчикам движков будет внедрять новые возможности, тем стабильнее и надёжнее, в итоге, будут работать программы на JavaScript.

Уважаемые читатели! Какими способами определения функций в JavaScript вы пользуетесь чаще всего?

Автор: RUVDS.com

Источник [16]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/259271

Ссылки в тексте:

[1] Test262: https://github.com/tc39/test262

[2] Image: https://habrahabr.ru/company/ruvds/blog/332020/

[3] первой редакции: https://www.ecma-international.org/publications/files/ECMA-ST-ARCH/ECMA-262,%201st%20edition,%20June%201997.pdf

[4] третьей редакции: https://www.ecma-international.org/publications/files/ECMA-ST-ARCH/ECMA-262,%203rd%20edition,%20December%201999.pdf

[5] Вот: https://bocoup.com/blog/whats-in-a-function-name

[6] ES2015: https://www.ecma-international.org/ecma-262/6.0/index.html

[7] связанное имя: https://tc39.github.io/ecma262/#sec-function-definitions-static-semantics-boundnames

[8] Babel: https://babeljs.io/

[9] здесь: https://github.com/tc39/proposal-async-iteration

[10] файлов с исходным кодом: https://github.com/tc39/test262/tree/master/src

[11] шаблонов: https://github.com/tc39/test262/tree/master/src/function-forms/default

[12] тут: https://github.com/tc39/test262/tree/master/src/arguments

[13] вот: https://github.com/tc39/test262/tree/master/src/dstr-binding

[14] вот: https://github.com/tc39/test262/tree/master/src/dstr-assignment

[15] ошибки: https://github.com/tc39/test262/pull/651

[16] Источник: https://habrahabr.ru/post/332020/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best