Остаточные параметры и оператор расширения
Многие встроенные функции JavaScript поддерживают произвольное количество аргументов.
- Math.max(arg1, arg2, . argN) – вычисляет максимальное число из переданных.
- Object.assign(dest, src1, . srcN) – копирует свойства из исходных объектов src1..N в целевой объект dest .
- …и так далее.
В этой главе мы узнаем, как сделать то же самое с нашими собственными функциями и как передавать таким функциям параметры в виде массива.
Остаточные параметры ( . )
Вызывать функцию можно с любым количеством аргументов независимо от того, как она была определена.
function sum(a, b) < return a + b; >alert( sum(1, 2, 3, 4, 5) );
Лишние аргументы не вызовут ошибку. Но, конечно, посчитаются только первые два.
Остаточные параметры могут быть обозначены через три точки . . Буквально это значит: «собери оставшиеся параметры и положи их в массив».
Например, соберём все аргументы в массив args :
function sumAll(. args) < // args — имя массива let sum = 0; for (let arg of args) sum += arg; return sum; >alert( sumAll(1) ); // 1 alert( sumAll(1, 2) ); // 3 alert( sumAll(1, 2, 3) ); // 6
Мы можем положить первые несколько параметров в переменные, а остальные – собрать в массив.
В примере ниже первые два аргумента функции станут именем и фамилией, а третий и последующие превратятся в массив titles :
function showName(firstName, lastName, . titles) < alert( firstName + ' ' + lastName ); // Юлий Цезарь // Оставшиеся параметры пойдут в массив // titles = ["Консул", "Император"] alert( titles[0] ); // Консул alert( titles[1] ); // Император alert( titles.length ); // 2 >showName("Юлий", "Цезарь", "Консул", "Император");
Остаточные параметры собирают все остальные аргументы, поэтому бессмысленно писать что-либо после них. Это вызовет ошибку:
function f(arg1, . rest, arg2) < // arg2 после . rest ?! // Ошибка >
. rest должен всегда быть последним.
Переменная «arguments»
Все аргументы функции находятся в псевдомассиве arguments под своими порядковыми номерами.
function showName() < alert( arguments.length ); alert( arguments[0] ); alert( arguments[1] ); // Объект arguments можно перебирать // for (let arg of arguments) alert(arg); >// Вывод: 2, Юлий, Цезарь showName("Юлий", "Цезарь"); // Вывод: 1, Илья, undefined (второго аргумента нет) showName("Илья");
Раньше в языке не было остаточных параметров, и получить все аргументы функции можно было только с помощью arguments . Этот способ всё ещё работает, мы можем найти его в старом коде.
Но у него есть один недостаток. Хотя arguments похож на массив, и его тоже можно перебирать, это всё же не массив. Он не поддерживает методы массивов, поэтому мы не можем, например, вызвать arguments.map(. ) .
К тому же, arguments всегда содержит все аргументы функции — мы не можем получить их часть. А остаточные параметры позволяют это сделать.
Соответственно, для более удобной работы с аргументами лучше использовать остаточные параметры.
Если мы обратимся к arguments из стрелочной функции, то получим аргументы внешней «нормальной» функции.
function f() < let showArg = () =>alert(arguments[0]); showArg(2); > f(1); // 1
Как мы помним, у стрелочных функций нет собственного this . Теперь мы знаем, что нет и своего объекта arguments .
Оператор расширения
Мы узнали, как получить массив из списка параметров.
Но иногда нужно сделать в точности противоположное.
Например, есть встроенная функция Math.max. Она возвращает наибольшее число из списка:
Допустим, у нас есть массив чисел [3, 5, 1] . Как вызвать для него Math.max ?
Просто так их не вставишь — Math.max ожидает получить список чисел, а не один массив.
let arr = [3, 5, 1]; alert( Math.max(arr) ); // NaN
Конечно, мы можем вводить числа вручную : Math.max(arr[0], arr[1], arr[2]) . Но, во-первых, это плохо выглядит, а, во-вторых, мы не всегда знаем, сколько будет аргументов. Их может быть как очень много, так и не быть совсем.
И тут нам поможет оператор расширения. Он похож на остаточные параметры – тоже использует . , но делает совершенно противоположное.
Когда . arr используется при вызове функции, он «расширяет» перебираемый объект arr в список аргументов.
let arr = [3, 5, 1]; alert( Math.max(. arr) ); // 5 (оператор "раскрывает" массив в список аргументов)
Этим же способом мы можем передать несколько итерируемых объектов:
let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1]; alert( Math.max(. arr1, . arr2) ); // 8
Мы даже можем комбинировать оператор расширения с обычными значениями:
let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1]; alert( Math.max(1, . arr1, 2, . arr2, 25) ); // 25
Оператор расширения можно использовать и для слияния массивов:
let arr = [3, 5, 1]; let arr2 = [8, 9, 15]; let merged = [0, . arr, 2, . arr2]; alert(merged); // 0,3,5,1,2,8,9,15 (0, затем arr, затем 2, в конце arr2)
В примерах выше мы использовали массив, чтобы продемонстрировать свойства оператора расширения, но он работает с любым перебираемым объектом.
Например, оператор расширения подойдёт для того, чтобы превратить строку в массив символов:
let str = "Привет"; alert( [. str] ); // П,р,и,в,е,т
Посмотрим, что происходит. Под капотом оператор расширения использует итераторы, чтобы перебирать элементы. Так же, как это делает for..of .
Цикл for..of перебирает строку как последовательность символов, поэтому из . str получается «П», «р», «и», «в», «е», «т» . Получившиеся символы собираются в массив при помощи стандартного объявления массива: [. str] .
Для этой задачи мы можем использовать и Array.from . Он тоже преобразует перебираемый объект (такой как строка) в массив:
let str = "Привет"; // Array.from преобразует перебираемый объект в массив alert( Array.from(str) ); // П,р,и,в,е,т
Результат аналогичен [. str] .
Но между Array.from(obj) и [. obj] есть разница:
- Array.from работает как с псевдомассивами, так и с итерируемыми объектами
- Оператор расширения работает только с итерируемыми объектами
Выходит, что если нужно сделать из чего угодно массив, то Array.from — более универсальный метод.
Итого
Когда мы видим «. » в коде, это могут быть как остаточные параметры, так и оператор расширения.
Как отличить их друг от друга:
- Если . располагается в конце списка параметров функции, то это «остаточные параметры». Он собирает остальные неуказанные аргументы и делает из них массив.
- Если . встретился в вызове функции или где-либо ещё, то это «оператор расширения». Он извлекает элементы из массива.
- Остаточные параметры используются, чтобы создавать новые функции с неопределённым числом аргументов.
- С помощью оператора расширения можно вставить массив в функцию, которая по умолчанию работает с обычным списком аргументов.
Вместе эти конструкции помогают легко преобразовывать наборы значений в массивы и обратно.
К аргументам функции можно обращаться и по-старому — через псевдомассив arguments .
Переменное количество аргументов: проблемы и решения
Время от времени возникает потребность написать функцию с переменным количеством аргументов. Особенно это актуально для библиотек: есть простой способ вызвать функцию, который подходит в девяноста процентах случаев, и есть сложный, нужный в оставшихся десяти процентах, когда требуется передать какие-нибудь дополнительные настройки или данные.
Пример — jQuery.get, который можно вызвать как $.get(url, callback), а можно и как $.get(url, data, callback).
JavaScript не особенно богат средствами работы с аргументами (в отличие, например, от Python), поэтому для реализации функций с интерфейсом как у jQuery.get приходится писать что-то вроде этого:
function openTheDoor(door, options, callback) < if (typeof options === 'function') < callback = options options = <>> // тут уже код нашей функции var handlePosition = door.getHandlePosition() // . >
Во-первых, начало функции не несет смысловой нагрузки, это просто надстройка над способом передачи аргументов. Чтобы увидеть что функция делает, нужно «промотать» в голове четыре строки кода.
Во-вторых, есть соблазн совместить жонглирование параметрами с присваиванием значений по умолчанию, и это может выйти боком — инициализация в следующий раз будет просто «промотана» как стандартный boilerplate.
Подобный подход можно найти в коде многих библиотек, работающих асинхронно. У этой проблемы должно быть решение.
Плохие решения
В реестре npm есть достаточно много модулей, решающих проблему путем обработки объекта arguments. Код выглядит примерно так:
var somehowParse = require('some-fancy-args') function openTheDoor() < var args = somehowParse(arguments) // тут уже код нашей функции // обратите внимание на отсутствие имен параметров var handlePosition = args.first.getHandlePosition() // . >
Некоторые библиотеки предоставляют маленький «язык определения параметров»:
var parseArgs = require('another-fancy-args') function openTheDoor() < var args = parseArgs(['door|obj', 'options||obj', 'callback||func'], arguments) // тут уже код нашей функции // по крайней мере, у параметров есть имена var handlePosition = args.door.getHandlePosition() // . >
Есть еще магические библиотеки, дважды вызывающие исходную функцию для правильной установки аргументов, и по этой причине заставляющие вас писать код внутри анонимной функции и передавать в вызов библиотеки this:
var magicArgs = require('oh-so-magic-args') function openTheDoor(door, options, callback) < return magicArgs(this, ['obj', '|obj', '|func'], function () < // тут уже код нашей функции // можно использовать объявленные параметры var handlePosition = door.getHandlePosition() // . >) >
Что плохо в этих решениях? Да почти всё.
Начало функции в любом случае остается шаблоном, который нужно «промотать» чтобы добраться до кода. Этот шаблон вводит дополнительный уровень абстракции, а иногда и магии, в котором нужно разбираться отдельно.
«Немагические» библиотеки еще и немножко портят код самой функции — параметры приходят не в виде отдельных переменных, как это обычно бывает, а в виде объекта, и не всегда у параметров есть имена.
На пути к хорошему решению
Во-первых, хорошо написанная функция не должна иметь слишком много параметров. Если функция имеет больше трех параметров, скорее всего ей необходим рефакторинг. Сложную функцию можно разделить на несколько отдельных, можно сгруппировать часть параметров в один параметр-объект, можно еще каким-либо образом её упростить и переписать (хорошая книга по теме — Рефакторинг. Улучшение существующего кода Мартина Фаулера).
Во-вторых, в JavaScript применяется простая и логичная схема работы с отсутствующими параметрами. Если при вызове функции для параметра не передано значение, он принимает значение undefined. Для указания значений параметров по умолчанию удобно применять конструкции вида options = options || <>. [1]
В-третьих, существует соглашение «Callback идет последним», упрощающее асинхронное программирование. В большинстве случаев именно это соглашение и порождает необходимость жонглировать параметрами — раз callback должен всегда идти последним, необязательные параметры вынуждены находиться в середине списка.
Принимая во внимание все три пункта, получаем достаточно простое решение: единственное, что следует сделать — это дополнить список аргументов значениями undefined так, чтобы callback встал на предназначенное ему последнее место. Именно этим и занимается модуль vargs-callback, который я написал для реализации найденного решения.
Модуль vargs-callback
Модуль экспортирует единственную функцию, которую следует использовать как декоратор.
Обычные (именованные) функции: [2]
var vargs = require('vargs-callback') function openTheDoor(door, options, callback) < // тут код нашей функции // options будет иметь значение undefined, если при вызове указаны только door и callback var handlePosition = door.getHandlePosition() // . >openTheDoor = vargs(openTheDoor) // Декорируем именованную функцию
var vargs = require('vargs-callback') var openTheDoor = vargs(function (door, options, callback) < // Декорируем функцию-выражение // тут код нашей функции // options будет иметь значение undefined, если при вызове указаны только door и callback var handlePosition = door.getHandlePosition() // . >)
Декоратор vargs срабатывает в момент вызова декорированной функции и выполняет следующее:
- Если количество переданных аргументов меньше объявленного количества параметров и последний переданный аргумент имеет тип «функция» — поместить перед последним аргументом значения undefined до совпадения количества аргументов и количества параметров.
- Вызвать декорированную функцию с измененными аргументами.
- Если передано достаточно аргументов или последний аргумент не функция — не делать ничего.
Заключение
В найденном решении я могу отметить следующие плюсы:
Не используется дополнительный уровень абстракции для определения параметров функции. Нет нужды в магии или «языке определения параметров», используется только то, что есть в самом JavaScript.
Код становится чище — используются объявленные параметры, не нужно «проматывать» начало функции, а для установки значений параметров по умолчанию можно применять подходящий в каждом конкретном случае способ.
Примечания
- Такой способ небезопасно использовать для параметров, которые могут принимать falsy-значения. Для них можно использовать проверку типа: options = typeof options !== ‘undefined’? options: <>
- Имеется ввиду function declaration.