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


Перед тем как я попытаюсь убедить вас в том, что это хорошая идея, давайте посмотрим, как в JavaScript устроена обработка аргументов.

Объявляя функцию, можно указывать произвольное число аргументов:

function callMe(a, b, c) {}

Когда вы вызываете эту функцию с правильным количеством позиционных аргументов, тогда это имеет смысл:

function callMe(a, b, c) {
    return a + b + c
}

callMe(1, 2, 3)

>>> 6

Во многих других языках, если вы сделаете иначе, то это станет причиной для исключения. Недостаточно аргументов? Исключение. Слишком много аргументов? Исключение. В JavaScript это не так.

Попробуем вызвать ту же функцию с недостаточным количеством аргументов:

callMe(1,2)

>>> NaN

Или, наоборот, слишком много:

callMe(1,2,3,4)

>>> 6

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

Это объясняет почему наша функция выдаёт такие результаты. Мы не передаём значение c значит c undefined, поэтому a + b + c в результате выдаёт NaN. Когда мы передаём лишний аргумент, функция выполняется как будто его там нет.

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

function callMe() {
return Array.prototype.slice.call(arguments)
}

(я использую ES5 для лучшей совместимости в браузерах, но вы можете записать это короче с ES6 Array.from(arguments)).

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

callMe(1, 2, 3, 4)

>>> [1, 2, 3, 4]

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

function callMe(a, b=2, c=4) { return a + b + c }

callMe(1)
>>> 7

callMe(2, 3)
>>> 9

Но вы не можете передавать аргументы в качестве ключей. Я имею ввиду, что подобная запись не сработает, как вы этого ожидаете. Попробуйте сами:

callMe(a=3, c=5);

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

Нередко можно увидеть документацию JavaScript, в которой говорится что-то вроде:

функция doIt(какой-то там аргумент, [какой-то параметр. [другой параметр]])

Вызовите эту функцию с каким-нибудь аргументом в качестве первого аргумента, а затем ваши параметры…

Для меня подобное объявление функций неудобно и почти всегда приводит к череде проб и ошибок, прежде чем я смогу увидеть, как она работает.

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

function doIt(params) {
    var oneThing = params.oneThing;
    var anotherThing = params.anotherThing;
    var someOption = params.someOption;
    var someOtherOption = params.someOtherOption;
    // ....
}

Это не идеально, но мы всё исправим по ходу дела…


Давайте разберёмся почему этот подход хорош:

Мы избавляемся от позиционного упорядочивания аргументов. Тем, кто будет использовать ваш код, не нужно помнить в каком порядке передавать аргументы. Функцию можно вызвать любым способом:

doIt({
    oneThing: "one", anotherThing: "another", 
    someOption: "a", someOtherOption: "b"
});

doIt({
    someOption: "a", someOtherOption: "b",
    oneThing: "one", anotherThing: "another"
});

Такой подход позволяет мне передавать произвольное количество аргументов — я могу пропустить аргумент, не испортив порядок или поведение. Например, если я захочу передать только oneThing или anotherThing :

doIt({
    oneThing: "one", someOption: "a", someOtherOption: "b"
});

doIt({
    anotherThing: "another", someOption: "a", someOtherOption: "b"
});

Я могу легко назначать аргументы по умолчанию, если их не задали при вызове:

function doIt(params) {
    var oneThing = params.hasOwnProperty("oneThing") ? 
                      params.oneThing : "defaultOneThing";

    var anotherThing = params.hasOwnProperty("anotherThing") ? 
                      params.anotherThing : "defaultAnotherThing";
    
    var someOption = params.hasOwnProperty("someOption") ? 
                      params.someOption : "defaultSomeOption";

    var someOtherOption = params.hasOwnProperty("someOther") ? 
                      params.someOther : "defaultSomeOther";
    // ....
}

(Мы не используем старомодный подход var oneThing = params.oneThing || "defaultOneThing" , потому что это сломает нашу функцию.)

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

function doIt(params) {
    var one = params.hasOwnProperty("oneThing") ? 
                      params.oneThing : "defaultOneThing";

    var another = params.hasOwnProperty("anotherThing") ? 
                      params.anotherThing : "defaultAnotherThing";

    var opt1 = params.hasOwnProperty("someOption") ? 
                      params.someOption : "defaultSomeOption";

    var opt2 = params.hasOwnProperty("someOther") ? 
                      params.someOther : "defaultSomeOther";
    // ....
}

Что касается документирования, вид самих функций прекрасно описывает их работу, по крайней мере, так намного проще понять:

doIt({
    oneThing: "one", someOption: "a", someOtherOption: "b"
});

Вместо этого:

doIt("one", "a", "b")

Объявление функции легко документировать, достаточно описать всё что внутри объекта params , а JSDoc поможет вам отформатировать эту информацию. Так выглядит фрагмент документации JSDoc:

/**
 * @param {Object} params
 * @param {string} params.selector - the jQuery selector for the page element into which to render the Edge
 * @param {number} params.top_donor_limit - the number of Donors/Members to include in the Top X Donor/Member chart
 * @param {String} params.funding_progress_header - header text
 * @param {String} params.funding_progress_intro - section intro text
 * @param {String} params.funding_by_country_header - header text
 * @param {String} params.funding_by_country_intro - section intro text
 * @param {String} params.funding_by_continent_header - header text
 * @param {String} params.funding_by_continent_intro - section intro text
 * ...
 */
вывод JSDoc

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

Перевод статьи Richard D Jones: Always pass one argument to your JavaScript function

Предыдущая статьяАлгоритмы машинного обучения простым языком. Часть 2
Следующая статьяАлгоритмы машинного обучения простым языком. Часть 3