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

в 13:05, , рубрики: javascript, Блог компании RUVDS.com, Программирование, Разработка веб-сайтов, функции

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Если у вас есть опыт работы с модулями ES, вам должна быть знакома новая форма анонимного объявления функции. Хотя это очень похоже на анонимное функциональное выражение, такая функция, на самом деле, имеет связанное имя "*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.

Асинхронные функции позволяют удобно описывать асинхронные операции. Благодаря их использованию код получается чистым и единообразным. При вызове асинхронной функции будет возвращён промис, который разрешится после того, как асинхронная функция возвратит результат своей работы. Если в асинхронной функции встречается выражение с ключевым словом 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 будет расширено и на генераторы. За ходом работ по реализации этой функциональности можно наблюдать здесь. Как вы, вероятно, догадались, речь идёт о комбинации ключевых слов 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, заключается в том, чтобы, разобравшись со стандартами, подготовить испытания, проверяющие новые языковые конструкции с учётом уже существующих. Такой подход означает огромнейшую работу по созданию тестов, которую нерационально возлагать на человека. Например, проверка аргументов по умолчанию должна быть проведена со всеми формами функций, в подобных вещах нельзя ограничиться, скажем, только простой формой объявления функции. Всё это привело к разработке инструментария для создания тестов, что позволяет говорить о том, что испытаниям подвергается практически всё, что можно проверить.

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

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

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

Автор: RUVDS.com

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js