company_banner

Секреты JavaScript-функций

Автор оригинала: bitfish
  • Перевод
Каждый программист знаком с функциями. В JavaScript функции отличаются множеством возможностей, что позволяет называть их «функциями высшего порядка». Но, даже если вы постоянно пользуетесь JavaScript-функциями, возможно, им есть чем вас удивить.



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

Чистые функции


Функция, которая соответствует двум следующим требованиям, называется чистой:

  • Она всегда, при вызове её с одними и теми же аргументами, возвращает один и тот же результат.
  • При выполнении функции не происходит никаких побочных эффектов.

Рассмотрим пример:

function circleArea(radius){
  return radius * radius * 3.14
}

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

Вот ещё один пример:

let counter = (function(){
  let initValue = 0
  return function(){
    initValue++;
    return initValue
  }
})()

Испытаем эту функцию в консоли браузера.


Испытание функции в консоли браузера

Как видно, функция counter, реализующая счётчик, при каждом её вызове возвращает разные результаты. Поэтому её нельзя назвать чистой.

А вот — ещё пример:

let femaleCounter = 0;
let maleCounter = 0;
function isMale(user){
  if(user.sex = 'man'){
    maleCounter++;
    return true
  }
  return false
}

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

▍Зачем нужны чистые функции?


Почему мы проводим границу между обычными и чистыми функциями? Дело в том, что у чистых функций есть множество сильных сторон. Их применение позволяет повысить качество кода. Поговорим о том, что нам даёт использование чистых функций.

1. Код чистых функций понятнее, чем код обычных функций, его легче читать


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

2. Чистые функции лучше поддаются оптимизации при компиляции их кода


Предположим, имеется такой фрагмент кода:

for (int i = 0; i < 1000; i++){
    console.log(fun(10));
}

Если fun — это функция, не являющаяся чистой, то во время выполнения этого кода данную функцию придётся вызвать в виде fun(10) 1000 раз.

А если fun — это чистая функция, то компилятор сможет оптимизировать код. Он может выглядеть примерно так:

let result = fun(10)
for (int i = 0; i < 1000; i++){
    console.log(result);
}

3. Чистые функции легче тестировать


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

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

const incrementNumbers = function(numbers){
  // ...
}

Для проверки такой функции достаточно написать модульный тест, напоминающий следующий:

let list = [1, 2, 3, 4, 5];
assert.equals(incrementNumbers(list), [2, 3, 4, 5, 6])

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

Функции высшего порядка.


Функция высшего порядка — это функция, которая обладает как минимум одной из следующих возможностей:

  • Она способна принимать другие функции в качестве аргументов.
  • Она может возвращать функцию в виде результатов своей работы.

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

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

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

const arr1 = [1, 2, 3];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
    arr2.push(arr1[i] * 2);
}

Если же над задачей поразмыслить, то окажется, что у объектов типа Array в JavaScript есть метод map(). Этот метод вызывают в виде map(callback). Он создаёт новый массив, заполненный элементами массива, для которого его вызывают, обработанными с помощью переданной ему функции callback.

Вот как выглядит решение этой задачи с использованием метода map():

const arr1 = [1, 2, 3];
const arr2 = arr1.map(function(item) {
  return item * 2;
});
console.log(arr2);

Метод map() — это пример функции высшего порядка.

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

Кеширование результатов работы функций


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

function computed(str) {    
    // Представим, что в этой функции проводятся ресурсозатратные вычисления
    console.log('2000s have passed')
      
    // Представим, что тут возвращается результат вычислений
    return 'a result'
}

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

Как оснастить функцию кешем? Для этого можно написать особую функцию, которую можно использовать в качестве обёртки для целевой функции. Этой особой функции мы дадим имя cached. Данная функция принимает целевую функцию в виде аргумента и возвращает новую функцию. В функции cached можно организовать кеширование результатов вызова оборачиваемой ей функции с использованием обычного объекта (Object) или с помощью объекта, представляющего собой структуру данных Map.

Вот как может выглядеть код функции cached:

function cached(fn){
  // Создаёт объект для хранения результатов, возвращаемых после каждого вызова функции fn.
  const cache = Object.create(null);

  // Возвращает функцию fn, обёрнутую в кеширующую функцию.
  return function cachedFn (str) {

    // Если в кеше нет нужного результата - вызывается функция fn
    if ( !cache[str] ) {
        let result = fn(str);

        // Результат, возвращённый функцией fn, сохраняется в кеше
        cache[str] = result;
    }

    return cache[str]
  }
}

Вот результаты экспериментов с этой функцией в консоли браузера.


Эксперименты с функцией, результаты работы которой кешируются

«Ленивые» функции


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

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

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

Её код может выглядеть так:

let fooFirstExecutedDate = null;
function foo() {
    if ( fooFirstExecutedDate != null) {
      return fooFirstExecutedDate;
    } else {
      fooFirstExecutedDate = new Date()
      return fooFirstExecutedDate;
    }
}

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

А именно, функцию мы можем переписать так:

var foo = function() {
    var t = new Date();
    foo = function() {
        return t;
    };
    return foo();
}

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

Это был очень простой условный пример. Давайте теперь рассмотрим нечто, более близкое к реальности.

При подключении к элементам DOM обработчиков событий нужно выполнять проверки, позволяющие обеспечить совместимость решения с современными браузерами и с IE:

function addEvent (type, el, fn) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, false);
    }
    else if(window.attachEvent){
        el.attachEvent('on' + type, fn);
    }
}

Получается, что каждый раз, когда мы вызываем функцию addEvent, в ней проверяется условие, которое достаточно проверить лишь один раз, при её первом вызове. Сделаем эту функцию «ленивой»:

function addEvent (type, el, fn) {
  if (window.addEventListener) {
      addEvent = function (type, el, fn) {
          el.addEventListener(type, fn, false);
      }
  } else if(window.attachEvent){
      addEvent = function (type, el, fn) {
          el.attachEvent('on' + type, fn);
      }
  }
  addEvent(type, el, fn)
}

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

Каррирование функций


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

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

Какая от этого польза?

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

Рассмотрим простую функцию, складывающую передаваемые ей числа. Назовём её add. Она принимает три операнда в виде аргументов и возвращает их сумму:

function add(a,b,c){
 return a + b + c;
}

Такую функцию можно вызвать, передав ей меньшее количество аргументов, чем ей нужно (правда, это приведёт к тому, что возвратит она совсем не то, чего от неё ожидают). Её можно вызывать и с большим количеством аргументов, чем предусмотрено при её создании. В подобной ситуации «лишние» аргументы будут просто проигнорированы. Вот как могут выглядеть эксперименты с подобной функцией:

add(1,2,3) --> 6 
add(1,2) --> NaN
add(1,2,3,4) --> 6 //Дополнительный параметр игнорируется.

Как каррировать такую функцию?

Вот — код функции curry, которая предназначена для каррирования других функций:

function curry(fn) {
    if (fn.length <= 1) return fn;
    const generator = (...args) => {
        if (fn.length === args.length) {

            return fn(...args)
        } else {
            return (...args2) => {

                return generator(...args, ...args2)
            }
        }
    }
    return generator
}

Вот результаты экспериментов с этой функцией в браузерной консоли.


Эксперименты с функцией curry в консоли браузера

Композиция функций


Предположим, надо написать функцию, которая, принимая на вход, например, строку bitfish, возвращает строку HELLO, BITFISH.

Как видно, эта функция выполняет две задачи:

  • Конкатенация строк.
  • Преобразование символов результирующей строки к верхнему регистру.

Вот как может выглядеть код такой функции:

let toUpperCase = function(x) { return x.toUpperCase(); };
let hello = function(x) { return 'HELLO, ' + x; };
let greet = function(x){
    return hello(toUpperCase(x));
};

Поэкспериментируем с ней.


Испытание функции в консоли браузера

Эта задача включает в себя две подзадачи, оформленные в виде отдельных функций. В результате код функции greet получился достаточно простым. Если бы нужно было выполнить больше операций над строками, то функция greet содержала бы в себе конструкцию наподобие fn3(fn2(fn1(fn0(x)))).

Упростим решение задачи и напишем функцию, которая выполняет композицию других функций. Назовём её compose. Вот её код:

let compose = function(f,g) {
    return function(x) {
        return f(g(x));
    };
};

Теперь функцию greet можно создать, прибегнув к помощи функции compose:

let greet = compose(hello, toUpperCase);
greet('kevin');

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

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

Подобная функция, способная принимать любое количество параметров, имеется в широко известной опенсорсной библиотеке underscore.

function compose() {
    var args = arguments;
    var start = args.length - 1;
    return function() {
        var i = start;
        var result = args[start].apply(this, arguments);
        while (i--) result = args[i].call(this, result);
        return result;
    };
};

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

Применяете ли вы в своих JavaScript-проектах какие-то особенные способы работы с функциями?



RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

Комментарии 21

    +19
    Не увидел никаких «продвинутых возможностей JavaScript-функций».
    Увидел стандартные приёмы использования функций, в равной степени применимые в любом ЯП.
    Ещё и с ошибками в коде (if(user.sex = 'man')), указывающими на то, что автор свой код даже не пытался запускать.
      0
      автор свой код даже не пытался запускать

      Вообще-то мог даже и попытаться ;-): программа с ошибкой не вывалится, а если автор еще и тест сделал на пользователе мужского пола — то даже получил правильный результат.
      PS Лично я по причине таких ошибок ещё с давних времен работы на C, когда компиляторы были не такими дотошными, как сейчас, всегда при проверке на равенство старался слева писать не переменную, а константу или выражение, чтобы ошибка компиляции была гарантирована — и даже в C#, где компилятор такую дичь не пропустит, так по привычке пишу.
        +7

        Как и 99% других постов от ru_vds.

        –7

        В общем вот тебе фунция чтобы ты мог вызвать функцию которая вызовет функцию. Но вызов функии чаще дороже выполнения простых операций на месте. Вот если бы в Javascript таки встроили модификатор "inline" как в C было бы не плохо.

          +8
          Во-первых, модификатор «inline» в современном Си означает не то, что вы думаете:
          Historically, the inline C++ keyword was originally an optimizer hint, but optimizers were given permission to ignore it and make their own decisions about inline substitution during code generation. Nowadays, compilers pretty much ignore the optimization aspect of the inline keyword. The only thing that remains of the inline keyword is the ability to have multiple definitions without violating ODR.

          Во-вторых, JIT вполне может заинлайнить вызов, если решит, что это того стоит.

          В-третьих, претензии по типу «в JS нет моей любимой фичи из Си, вот бы её добавили» — это примерно как «моим шуруповёртом неудобно заколачивать гвозди, вот бы ему приделали боёк как у молотка».
            –4

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


            Это позволяет простым коротким операциям дать понятное имя и использовать его без накладных расходов на вызов функции.


            По мне так такой модификатор гораздо понятнее чем макросы.

              +3
              Как объяснено выше, компилятор Си сам решает, встраивать ли тело функции в место её вызова — не глядя на этот модификатор.
                +1
                Ответ в общем дан. При этом компилятор может поглядеть, что функция используется 1 раз и встроить её независимо от размера. Опять же компилятор может вообще всегда встраивать функции с малым размером, с одним выражением и т.д., это зависит ещё от указанного уровня оптимизации. В C++ при работе с шаблонной магией, когда прям вот compile-time не получится использовать, где-нить к примеру на последнем этапе или работа с runtime инфой, то компилятор без указания inlne почти всегда функции встраивал. В общем да, inline не показатель сейчас, вообще. А так же в догонку, компилятор может и циклы развернуть и рекурсию в цикл превратить и т.д. И ничего для этого говорить ему не надо.
              +1
              Учитывая, что подавляющее число проектов используют минификатор github.com/terser/terser,
              то вполне можно использовать аннотацию /*@__INLINE__*/
              0
              Охренеть, это теперь продвинутые возможности. Видимо, шутка про рекурсию теперь не только про php-шников?
              Давайте в следующий раз про оптимизацию хвостовой рекурсии? А, и ещё можно линзы описать в статье «Ультра-фичи js, для понимания которых нужно три докторских степени»
                +2

                Это ruvds, у них статьи для пиара хостинга, не стоит воспринимать это всерьёз, другое дело что такие статьи всегда в топе из-за того что их плюсуют сотрудники

                  +3
                  Бывает переводят и хорошие материалы. Но тут опять кликбейт с медиума про набор из популярных слов (Pure Function, Higher-Order Function, Function Caching, Lazy Function, Currying и Function Compose).
                  +3
                  А что за шутка про рекурсию и пхпшника?
                  +6

                  Я, наверное, недостаточно продвинутый, но вот такие "ленивые" функции должны превращать отладку в ад, нет? Ты думаешь, что функция делает что-то определенное, а она сама себя подменяет в зависимости от каких-то внешних условий… расскажите, это реально используется или просто "потому что могу"?

                    +1

                    Я ни разу не встречал. Вместо этого к функциям явным образом приделывают мемоизацию. Этот способ решения проблемы подразумевает более простой и понятный код.

                      +1

                      Ну так и синглтон поступает примерно так же. Конечно, это добавляет неочевидности, но не вижу почему вдруг отладка должна превращаться в ад. Отладка — это само по себе нелинейное понятие, при тщательной отладке заходите в тело каждой вызываемой функции и видите что происходит. Да и не заходя, нужно проверять возвращаемый результат (то есть нет никакого "я думал оно всегда возвращает a, а оно возвращает b").
                      Это не по ивентам (которые вызваны другими ивентами, которые вызваны другими ивентами) прыгать, вот уж где можно повеселиться.

                        0

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

                        +5
                        for (int i = 0; i < 1000; i++){

                        Мы точно про Javascript?
                          0
                          Переводчик вкладки попутал )
                            +1
                            Нет, в оригинале вся та же муть.
                              +1
                              Ну тогда к переводчику вопросов нет, только к заказчику )

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое