April 4th, 2021

Учебник по JavaScript: ч.1: вызов функции с произвольным числом скобок

Начало тут:
1. Учебник по JavaScript: ч.1: var, глобальный объект, объект функции
2. Учебник по JavaScript: ч.1: работа с объектом функции

Вторая задача к подразделу 6.6 «Объект функции, NFE» оказалась для меня очень трудной: «Сумма с произвольным количеством скобок». Но мне удалось ее решить, не подглядывая в решение авторов учебника. Причем моё решение почти дословно совпало с решением авторов учебника.

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

Итак, в задаче требуется написать функцию sum, которая работала бы следующим образом:
alert( sum(1)(2) );             //  3
alert( sum(1)(2)(3) );          //  6
alert( sum(5)(-1)(2) );         //  6
alert( sum(6)(-1)(-2)(-3) );    //  0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15
То есть нужно последовательно суммировать параметры (числа) из скобок и вернуть сумму всех этих параметров. Число пар скобок может быть произвольным (в самом простом случае может быть только одна пара скобок).

Ранее в учебнике уже была похожая задача «Сумма с помощью замыканий» к подразделу 6.3 «Замыкание». Там я ее щелкнул очень быстро. Но в той задаче требовалось лишь написать функцию sum, работавшую с двумя парами скобок. Вот ее решение:
function sum(value1) {
    return function(value2) {
        return value1 + value2;
    };
}

// тестируем
alert( sum(5)(2) ); // 7
Но это решение, очевидно, работает только для двух пар скобок. В случае одной пары скобок это решение возвратит функцию вместо ожидаемого числа. В случае трех и большего числа пар скобок это решение выдаст ошибку (которую можно увидеть в консоли разработчика), так как на третьей паре скобок будет возвращено число, а требуется функция.

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

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

Я решил искать путь к решению задачи маленькими шагами и сначала написать функцию, которая всегда будет возвращать функцию. Вот что у меня получилось:
function sum(value) {
    return sum;
}
Тут функция sum возвращает саму себя.

Надо понимать, что выражение return sum(); запустило бы саму функцию опять и получилась бы бесконечная рекурсия, так как конечное условие не определено (такой код приведет к ошибке превышения максимального количества обращений к стеку (максимальной глубины рекурсии, об этом было в подразделе 6.1 «Рекурсия и стек» учебника), которую можно увидеть в консоли разработчика). А выражение return sum; лишь возвратит объект функции, запуск этого возвращенного объекта функции выполнит движок JavaScript, который ориентируется на следующую пару скобок в выражении. Таким образом, запусков функции будет ровно столько, сколько пар скобок в выражении.

Итак, наша функция теперь будет выполнена столько раз, сколько требуется.

После этого я стал думать, где же здесь можно хранить сумму, которая будет постепенно увеличиваться в процессе работы программы. И тут я вспомнил, что только что в учебнике много прочитал про «замыкания», которые хранят значения переменных при себе, в своем собственном «кармашке» (объекте-лексическом окружении). Однако, для функции sum лексическое окружение находится снаружи, а мне не хотелось выносить какие-либо части своего кода за границы функции sum. Поэтому я решил, что внутри функции sum всё-таки будет нужно создать еще одну функцию, вложенную. В этом случае лексическое окружение вложенной функции окажется в пределах функции sum. Вот что у меня получилось:
function sum(value) {
    let res = 0;

    function foo() {
        return foo;
    }

    return foo;
}
То есть я добавил переменную res, в которой будет копиться сумма. Сама переменная res будет храниться в «кармашке» (объекте-лексическом окружении) функции foo. Функция foo возвращает саму себя, а функция sum теперь возвращает не себя, а сконструированную внутри функцию foo.

Таким образом, при первой паре скобок будет вызвана функция sum, которая вернет функцию foo. Движок JavaScript запустит возвращенную функцию foo со второй парой скобок и эта функция вернет себя же, после чего возвращенная функция foo будет запущена с третьей парой скобок и так далее, пока скобки не закончатся. При этом по ходу всего этого процесса нам будет доступна переменная res.

Очевидно, что начальным значением переменной res должен быть не ноль (как в вышеприведенном коде), а значение value из первой пары скобок. С первой парой скобок запускается функция sum, поэтому ее параметр и будет начальным значением переменной res. Меняем код:
function sum(value) {
    let res = value;

    function foo() {
        return foo;
    }

    return foo;
}

С остальными парами скобок будет запускаться функция foo, поэтому ее параметр будем прибавлять к переменной res. Меняем код:
function sum(value) {
    let res = value;

    function foo(value2) {
        res += value2;
        return foo;
    }

    return foo;
}

Протестировав полученный код пошагово в консоли разработчика, я увидел, что он работает, в переменной res в конце работы программы набирается правильная сумма. Однако, последний вызов функции foo в итоге возвращает себя же, а не требуемое значение переменной res.

Именно в этот момент нужно использовать подсказку из текста обсуждаемой задачи насчет того, что «стоит сделать особый метод преобразования в примитив для функции».

К этому моменту я уже абсолютно забыл про преобразование функции в примитив и полез в подраздел 2.7 «Преобразование типов» учебника. Хорошо, что авторы учебника оставили там ссылку на нужный подраздел 4.8 «Преобразование объектов в примитивы» учебника.

Итак, в конце работы нашего кода возвращается функция, для которой нам нужно прописать преобразование в примитив. В моем тесте функция sum вызывается в качестве параметра для функции alert, выводящей сообщение на экран. Следовательно, нам нужно прописать преобразование нашей возвращаемой функции foo в строку (функция alert требует строку в качестве входного параметра). Меняем код:
function sum(value) {
    let res = value;

    function foo(value2) {
        res += value2;
        return foo;
    }

    foo.toString = function() {
        return res;
    };

    return foo;
}
Само преобразование в строку запускается движком JavaScript, мы только прописали, что движок должен делать при таком преобразовании. А он при этом должен лишь возвратить вычисленную к этому моменту сумму, хранящуюся в переменной res.

Теперь функция работает так, как требуется.