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

Функции в JavaScript относятся к категории объектов высшего порядка. Они могут интерпретироваться в JavaScript как любые друге объекты и сосуществовать с ними. На них, как и на обычные типы данных, можно ссылаться с помощью переменных, объявлять в виде литералов и даже передавать в качестве параметров другим функциям. Функция является основным модульным исполняемым блоком.

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

  • создавать с помощью литералов {};
  • присваивать переменным, элементам массива и свойствам других объектов, как показано в следующем примере кода:

      var foo = {}; // присвоить объект переменной
      array.push({}); // ввести новый объект в массив
      foo.data = {}; // присвоить новой объект свойству другого объекта
    
  • передавать в качестве аргументов функциям, как показано в следующем примере кода:

      function fn(foo){
          foo.visibility = false;
      }
      fn({}); // вновь созданные объект передается функции в качестве аргумента
    
  • возвращать в качестве значений из функций, как показано в следующем примере кода:

      function fn(foo){
          return {}; // возвращать новый объект из функции
      }
    
  • наделять свойствами, которые можно динамически создавать и присваивать им значения, как показано в следующем примере кода:

      var foo = {};
      foo.name = "Denis"; // создать новой свойство объекта
    

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

  • создавать с помощью литералов:

      function fn(){}
    
  • присваивать переменным, элементам массива и свойствам других объектов:

      var foo = function() {}; // присвоить новую функцию переменной
      array.push(function(){}); // ввести новую функцию в массив
      foo.data = function(){}; // присвоить новую функцию свойству другого объекта
    
  • передавать в качестве аргументов другим функциям:

      function fn(foo){
          foo();
      }
      fn(function(){}); // вновь созданная функция передается в качестве аргумента вызываемой функции
    
  • возвращать в качестве значений из других функций:

      function fn(){
          return function(){} // возвратить новую функцию
      }
    
  • наделять свойствами, которые можно динамически создавать и присваивать им значения

      var fn = function(){};
      fn.name = 'Denis'; // ввести новое свойство в функцию
    

Определение функций

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

  1. Function Declaration – функция, объявленная в основном потоке кода.

     function fn(a, b){
         return a + b;
     };
    
  2. Function Expression – объявление функции в контексте какого-либо выражения, например присваивания. Основное отличие между ними: функции, объявленные как Function Declaration, создаются интерпретатором до выполнения кода.

     var fn = function(a, b){
         return a + b;
     };
    
  3. Arrow functions (стрелочные функции или лямбда-функции). Этот тип функции лишь недавно появился в стандарте ES6. У стрелочных функций есть две главные задачи: обеспечить более лаконичный синтаксис; обеспечить передачу лексического this с родительским scope.

     var fn = (a, b) => a + b;
    
  4. Конструкторы функций. Это нечасто применяемый способ определения функций, позволяющий динамически конструировать новую функцию из символьной строки.

     new Function ('a', 'b', 'return a + b');
    
  5. Функции-генераторы. Этот тип функций также был введен в стандарт ES6. Он позволяет создать функцию, из которой, можно выйти и снова войти в нее при последующем выполнении приложения, сохраняя значения ее переменных в промежутках между последовательными выходами и возвратами к данной функции.

     function* fn(){
         yield 1;
     }
    

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

(function fn(a,b){
    return a + b
})(1,3);

Есть и другие варианты:

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

      (function fn(a,b){
          return a + b
      }(1,3));
    
  • Четыре последних выражения являются вариациями на ту жже самую тему немедленно вызываемых функциональных выражений. Такие выражения нередко встречаются в библиотеках JavaScript.

      +function(){}();
      -function(){}();
      !function(){}();
      ~function(){}();
    

Аргументы и параметры функций

При обсуждении функций термины аргумент и параметр нередко употребляются попеременно.

  • Параметр - это переменная, которая указывается как часть определения функций.
  • Аргумент - это значение, которое передается функции при ее вызове.

Оператор Rest. Если мы не знаем, сколько аргументов должны прийти в функцию. Можем указать данные оператор и все не обработанные переменные попадут в массив.

Отсортируем массив:

function multiMax(first, ...remainNumbers){
    var sorted = remainNumbers.sort(function(a,b){
        return b - a;
    });
}

multiMax(3, 1, 2, 3, 5);

Неявный параметр функции

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

Параметр arguments

Этот параметр представляет собой коллекцию всех аргументов, передаваемых функции. Но благодаря введению в стандарт ES6 оставшихся параметров(rest), потребность в параметре arguments значительно сократилась.

У объекта arguments имеется свойство length, обозначающее точное количество аргументов. Значения отдельных аргументов могут быть получены по индексу. Обращаться к параметру arguments все же не рекомендуется. Ведь вы можете принять его за массив, поскольку у него имеется свойство length, а его элементы могут быть извлечены, как из массива. Тем не менее это не типичный для JavaScript массив, и если попытаться применить к параметру arguments методы обработки массивов (например, метод sort()), вас постигнет неудача. Поэтому параметр arguments нужно воспринимать как похожую на массив конструкцию с стараться пользоваться ей как можно реже.

function sum(){
    var sum = 0;
    for(var i = 0; i < arguments.length; i++){
        sum += arguments[i];
    }
    return sum;
}

console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4)); // 10

Параметр this, представляющий контекст функции

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

Вызов функции

От способа вызова функции зависит ее контекст (this). Рассмотрим 4 способа вызова функции, каждому из которых присущи свои особенности.

  • Как функция. Например, fn() - это функция, вызывается непосредственно.
  • Как метод. Вызов foo.fn() тесно связан с объектом, допуская объектно-ориентированное программирование.
  • Как конструктор. В операции new foo() создается новый объект.
  • Через методы apply() и call() отдельной функции.

Вызов как функции

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

function fn(){};
fn();

(function(){})

Когда функция вызывается таким способом (код выше), ее контекст (т.е. значение параметра this) может быть двояким. В нестрогом режиме контекст может быть глобальным (объект window), а в строгом режиме - неопределенным (undefined).

function fn1(){
    return this;
};
console.log(fn1()); // window

function fn2(){
    "use strict";
    return this;
};
console.log(fn2()); // undefined

Вызов как метода

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

var foo = {};
foo.fn = function(){};
foo.fn();

Объект, к которому относится вызываемый метод, доступен в теле метода по ссылке this.

function fn(){
    return this;
}

var foo = {
    prop: fn
}

console.log(fn()); // window
console.log(foo.prop()); // foo

Вызов как конструктора

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

  1. Создаётся новый пустой объект;
  2. Ключевое слово this получает ссылку на этот объект;
  3. Функция выполняется. Как правило, она модифицирует this (т.е. этот новый объект), добавляет методы, свойства;
  4. Возвращается this.
function Animal(name) {
  // this = {}; <- эту строчку выполняет интерпретатор

  // в this пишем свойства, методы
  this.name = name;
  this.canWalk = true;

  // return this; <- эту строчку выполняет интерпретатор
}

А что будет если конструктор возвратить некоторое значение? Если в кравце.

  • Если конструктор возвращает объект, этот объект возвращается в виде значений всей операции new, а вновь созданный объект, передаваемый конструктору в качестве параметра this, игнорируется.
  • Но если конструктор возвращает не объект, то возвращаемое значение игнорируется, а вместо него возвращается вновь созданный объект.

Вызовы через методы apply() и call()

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

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

<button id="test">Click Me!</button> <!-- Кнопки присваивается обработчик событий -->
<script>
    function Button(){ // функция-конструктор. С ее помощью можно отслеживать нажатие кнопки
        this.clicked = false;
        this.click = function(){ // объявить метод, предназначенный для обработки событий от щелчков.
            this.clicked = true;
            console.log(button.clicked);
        }
    }

    var button = new Button();
    var elem = document.getElementById('test');
    elem.addEventListener('click', button.click);

</script>

Но если запустить данный код и нажать по кнопке. Консоль выведет false. Почему так произошло? Если обратиться к функции через вызов button.click(), то ее контекстом будет сам элемент разметки <button>, а не объект button! Таким образом, состояние click устанавливается не для того объекта.

Применение методов apply(), call() и bind()

В языке JavaScript предоставляются средства для вызова функции и явного указания любого объекта, который должен служить в качестве контекста функции. Это делается с помощью одного из двух методов, существующих для каждой функции: apply() и call(). Да, да именно методов функций. Ведь функции - это объекты высшего порядка, создаваемые с помощью конструктора объектов типа Function. Следовательно, у них могут быть свойства и методы, как и у объектов любого другого типа.

Для вызова apply() ему нужно передать два параметра: объект, назначаемый в качестве контекта функции, и необязательный параметр массив значений.

Аналогичным образом используется метод call(), за исключением того, что аргументы передаются функции через запятую, а не в массиве.

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


    elem.addEventListener('click', button.click.bind(button)); // использовать метод bind() для создания новой функции, привязываемой к объекту button
}

Обращение с контекстом функций с помощью стрелочных функций

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

<button id="test">Click Me!</button>
<script>
    function Button(){ 
        this.clicked = false;
        this.click =() => { // стрелочная функция
            this.clicked = true;
            console.log(button.clicked);
        }
    }

    var button = new Button();
    var elem = document.getElementById('test');
    elem.addEventListener('click', button.click);

</script>

Единственное изменение заключается в использовании стрелочной функции. Как упоминалось ранее, стрелочные функции не получают свой неявный параметр this, когда они вызываются. Вместо этого они запоминают значение параметра this в тот момент, когда создаются. В данном примере кода стрелочная функция click() была создана в теле функции-конструктора, где параметр this обозначает вновь созданный объект. Поэтому всякий раз, когда интерпретатор вызывает стрелочною функция click(), значение параметра this постоянно привязано к вновь созданному объекту button.

И еще чуть чуть

Значения параметра this выбирается в тот момент, когда создается стрелочная функция. Допустим мы пришли к следующему выводу. У нас одно кнопка, поэтому заменим функцию-конструктор простым литералом объекта.

<button id="test">Click Me!</button> <!-- Кнопки присваивается обработчик событий -->
<script>
    console.log(this); // значением параметра this в глобальном коде является глобальный объект window
    var button = {
        clicked: false;
        click: () => { // стрелочная функция является свойством литерала объекта
            this.clicked = true;
            console.log(button.clicked);
            console.log(this); // значение параметра this в стрелочной функции является глобальный объект window
        }
    }

    var elem = document.getElementById('test');
    elem.addEventListener('click', button.click);

</script>

Нужно помнить очень важное правило: стрелочные функции получают значение параметра this в момент их создания. Стрелочная функция click() создается в виде значения свойства объекта, а объект в глобальном коде, и поэтому параметр this этой стрелочной функции приобретает то значение, которое он получает в глобальном кода оказывается глобальный объект window.