Join MDN and developers like you at Mozilla's View Source conference, 12-14 September in Berlin, Germany. Learn more at https://viewsourceconf.org

Перевод не завершен. Пожалуйста, помогите перевести эту статью с английского.

Функции в JavaScript

Функции - ключевая концепция в JavaScript. Важнейшей особенностью языка является первоклассная поддержка функций (functions as first-class citizen). Любая функция это объект, и следовательно ею можно манипулировать как объектом, в частности:

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

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

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

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

  • способы объявления 
  • способы вызова 
  • параметры и аргументы вызова (arguments)
  • область данных (Scope) и замыкания (Closures)
  • объект привязки (this)
  • возращаемое значение (return)
  • исключения (throw)
  • использование в качестве конструктора объектов
  • сборщик мусора (garbage collector)

Объявление функций

Функция (как и всякий объект) должна быть объявлена(определена, define) перед её использованием. 

Объявление(определение) функции - указание сигнатуры и тела функции:

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

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

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

Существует три способа объявить функцию:

В декларативном стиле

Для декларативного объявления функции используется синтаксическая конструкция

function идентификатор(параметры) {

  инструкции

  return выражение

}
  • Ключевое слово function
  • Идентификатор (обязательно).
  • Список имён формальных параметров (и значений по умолчанию) в круглых скобках разделенных запятыми
  • тело функции в фигурных скобках вида {}.

Пример. Следующий код объявляет функцию  с именем square и параметром number; тело состоит из инструкции return и выражения, которое дословно формализуют следующую логику: "вернуть результат произведения аргумента  number на самого себя". :

function square(number) {

  return number * number;
}

Особенностью декларативного объявления является его "всплытие"(hoisting) в начало функции, независимо от того в каком месте контейтера оно находится. 

В примере ниже декларативное объявление функции находится после вызова:

{
  print(square(5));

  // инициализация "всплывает" вместе с декларацией переменной square
  // Аналогичный код в функциональном стиле работать не будет
  function square(n){return n*n}
} 

В функциональном стиле

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

var square = function(number) {
  return number * number;
}

Но могут иметь определённое имя. (это имя удобно использовать для рекурсии, или в отладчике(debugger)):

var factorial = function fac(n) {return n<2 ? 1 : n*fac(n-1)};

print(factorial(3));

Cтрелочные функции

Современный стандарт языка поддерживает аномимные стрелочные функции(fat arrow function).

(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression

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

[0, 1, 2, 5, 10].map((x) => x * x * x); // вернет [0, 1, 8, 125, 1000].

Помимо упрощённого синтаксиса, такие функции всегда неявно привязываются в МОМЕНТ ОБЪЯВЛЕНИЯ к текущему лексическому контексту выполнения:

function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // в данном случае this будет ссылаться на создаваемый объект obj, а не на window
  }, 1000);
}

var obj = new Person();

Полное описание в статье справочника Functions/Arrow_functions

Смотри также статью на hacks.mozilla.org: "ES6 In Depth: Arrow functions".

В стиле ООП

Учитывая то, что функция по сути является объектом, можно использовать оператор new и Function конструктор чтобы создавать функции динамически во время выполнения (подобно тому как это делает eval()). 

Однако такого подхода следует избегать из соображений производительности и безопасности.

var powerOfFive = new Function('x',
                  'return ' + Array(5).map(()=>'x').join('*'));

Методы

Функции очень часто используются как методы объектов, реализующих ООП. 

Метод 
это функция, заданная как значение свойства объекта.

Специальный синтаксис вызова методов позволяет неявно передавать объект в качестве привязки(this).

class Greeting{

  constructor(prefix){
    this.prefix = prefix;
  }

  // это метод:
  hello(name){
    return `${this.prefix}, ${name}`;
  }
}

var obj = new Greeting("Привет");

// вызов метода (obj передаётся в качестве контекста `this`)
obj.hello('Вася');

Больше информации об объектах и методах в Работа с Объектами.

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

Помимо манипуляций c ними как с обычными объектами, функции можно вызывать - запустить процесс вычисления выражений над данными

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

В момент вызова фунции могут быть переданы 

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

Кроме этого, выражения в функции могут адресоваться:

  • к константам (строковые литералы, числа и т.д.)
  • к локальным переменным,
  • а также к внешним свободным переменным по цепочке замыкания.

В результате выполнения функция

  • возвращает некоторое значение (явно с помощью return или неявно) 
  • или бросает исключение.

Например, вызвать  square() можно следующим образом:

// Функция выполняет свои инструкции над аргументом 5 
// и возвращает значение-результат 25

var result = square(5);

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

Рекурсия

Рекурсивная функция 
функция, содержащая вызов самой себя непосредственно в своём теле либо через другие функции.
Классический пример рекурсивной функции вычисляющей факториал:
function factorial(n) {
  if ((n == 0) || (n == 1))
    return 1;
  else
    return (n * factorial(n - 1));
}

//вычислить факториал пяти:
var e = factorial(5); // e будет равно 120

Вызов с помощью .apply() и .call()

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

var fn = resolveAction();
fn.apply(context, [number1, number2]);

Смотрите Function объект для более детального понимания.

Параметры и аргументы вызова

Параметры функции
список идентификаторов (и возможно сопоставленных им значений по умолчанию и типов данных), заданный В МОМЕНТ ОБЪЯВЛЕНИЯ.
Параметры удобно рассматривать как объявления локальных переменных, каждая из которых инициализируются в МОМЕНТ ВЫЗОВА соответствующим значением из списка аргументов (или значением по умолчанию).
Аргументы(входные данные) функции
произвольный список значений, передаваемых функции В МОМЕНТ ВЫЗОВА.

Соответствие имён параметров и значений аргументов задано тупо порядком их перечисления при обявлении/вызове.

Параметры в  Javascript никак не ограничивают ни количество, ни содержание аргументов.

 

По значению или по ссылке

Типы аргументов функции могут быть как примитивами (строки, числа, логические(boolean)), так и объектами (включая Array или функции):

  • Значения-примитивы передаются в функции по значению: значение КОПИРУЕТСЯ, так что если функция изменит значение параметра, это изменение не будет иметь внешнего эффекта.
  • объекты передаются в фунцию по ссылке:  переприсваивание самой ссылки также не имеет внешнего эффекта, НО если функция изменяет свойства объекта, то эти изменения будут видимы вне функции (побочный эффект).

пример:  Стрёмный побочный эффект от изменения свойств объекта-аргумента:

function myFunc(patient) {
  patient.gender= "F";
}

var he = {gender: "M"};

myFunc(he);

var y = he.gender;     // y gets the value "F", oops

Назначение параметру новой ссылки на объект не имеет влияния вне функции. Это продемонстровано в примере ниже:

function myFunc(theObject) {
  theObject = {make: "Ford", model: "Focus", year: 2006};
}

var mycar = {make: "Honda", model: "Accord", year: 1998},
    x,
    y;

x = mycar.make;     // x gets the value "Honda"

myFunc(mycar);
y = mycar.make;     // y still gets the value "Honda" 

В первом случае, объект mycar был передан в функцию myFunc, котороя изменила его состояние. Во втором случае, функция не изменяла переданный объект; вместо этого, она создавала новую локальную переменную с тем же именем что и имя параметра функции, так что никак не влияет на глобальный объект передаваемый в функцию.

Использование объекта arguments

Доступ к аргументам может быть получен непосредственно (без обращения по именам объявленных параметров).

Ключевое слово arguments адресует структуру, где содержится список входных аргументов в порядке передачи при вызове. Общее количество аргументов содержится в arguments.length, а значение отдельного аргумента можно получить как arguments[i], где i - порядковый номер аргумента начиная с нуля. Таким образом, например, первый аргумент переданный в функцию будет arguments[0]. .

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

Заметьте, что структура arguments крякает как утка похожа на массив, но массивом не является.

Рассмотрим, например, функцию которая объединяет произвольное количество строк. Единственный аргумент параметр в определении функции - это строка-разделитель:

function myConcat(separator) {
   var result = ""; // initialize list
       
   // iterate through arguments
   for (var i = 1; i < arguments.length; i++) {
      result += arguments[i] + separator;
   }
   return result;
}

// returns "red, orange, blue, " 
myConcat(", ", "red", "orange", "blue"); 

// returns "elephant; giraffe; lion; cheetah; " 
myConcat("; ", "elephant", "giraffe", "lion", "cheetah"); 

Аргументы и оператор развёртки

Современный стандарт языка позволяет в элегантной функциональной манере отказаться от arguments, заменив его оператором развёртки(spread) (...) :

const myConcat = (sep, ...strings) => strings.join(sep) // !!!

Значения по умолчанию

В JavaScript, значения параметров по умолчанию устанавливаются в undefined.

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

function multiply(a, b) {
  b = typeof b !== 'undefined' ?  b : 1;

  return a*b;
}

multiply(5); // 5

Современный стандарт языка вводит прекрасную возможность явного указания значения по умолчанию с помощью выражения при ОБЪЯВЛЕНИИ функции:

function multiply(a, b = 1) {
  return a*b;
}

multiply(5); // 5

Заметим что вычисление значения по умолчанию будет происходить В МОМЕНТ ВЫЗОВА функции 

Также будет нелишним заметить что такие выражения могут адресоваться к предыдущим параметрам:

function multiply(a, b = 2*a) { return a*b; } 

multiply(5); // 50

Детальная информация в статье справочника default parameters 

Область данных и замыкания

В момент вычисления выражений имена переменных необходимо подменить соответствующими значениями.

Для этих целей в JavaScript реализован  механизм разрешения имён на основе области данных(Scope) и замыканий(Closure):

var prefix = "Привет";
function sayHi(name) {
  var phrase = prefix + ", " + name;
  alert( phrase );
}
// логика разрешения переменных
RESOLVE = (env, id) => {
 if (env[id]) return env[id];
 if (env.OUTER_SCOPE) return RESOLVE(env.OUTER_SCOPE, id);
 throw new Error('ReferenceError: variable %id is not declared')
}
...
// внешний область данных, актуальный на момент создания функции
SCOPE = { prefix: "Привет" , sayHi: undefined} 
...
// замыкание = функция + внешнее окружение
var sayHi = { 
 OUTER_SCOPE: SCOPE, //захвачен в момент создания функции
 PARAMETERS:['name'] 
 BODY: ()=>{

  // внутренняя область данных, создаваемая неявно в момент вызова
  SCOPE = { 
    name: 'Вася', 
    phrase: undefined,
    OUTER_SCOPE: sayHi.OUTER_SCOPE // образуем цепочку 
  }

  var phrase = 
             RESOLVE(SCOPE,'prefix') // будет извлечён из внешней области
             + ", "
             + RESOLVE(SCOPE,'name'); // будет извлечён из внутренней области

  alert( RESOLVE(SCOPE,'phrase' );

 }
}

// передаётся контекст null и список аргументов
CALL_FUNCTION(sayHi, null, ['Вася']);

1) Каждый раз В МОМЕНТ ВЫЗОВА функции, происходит неявное создание нового экземпляра области данных(Scope) - специального объекта , где ДО НАЧАЛА ВЫПОЛНЕНИЯ(hoisting) регистрируются

  • переменные из списка параметров функции, проинициализированные соответствующими аргументами-значениями.
  • переменные, явно объявленные в теле функции с помощью var
  • вложенные функции, объявленные в теле в декларативном стиле
  • неявная ссылка-замыкание[[OuterScope]] данной функции (образуя цепочку замыканий лексического диапазона)

2) В МОМЕНТ ОБЪЯВЛЕНИЯ объект-функция неявно получает постоянную ссылку [[OuterScope]] на актуальную область переменных  выполняющейся функции-контейнера.

Эта ссылка (или в более общем смысле - связка функция + ссылка) называется замыканием. Она позволяет адресоваться из тела функции  к внешним(свободным) переменным.

3) Если искомое имя переменной не обнаружено в собственной области переменных, то поиск продолжается последовательно вдоль по цепочке замыканий(scope chain) пока имя не будет найдено (в противном случае сработает исключение  ReferenceError).

В спецификации ECMA-262,  присутствует гораздо больше деталей, в частности речь идёт о двух объектах: VariableEnvironment и LexicalEnvironment. Но мы абстрагируемся и используем везде термин Scope, это позволяет достаточно точно описать происходящее.

Более формальное описание находится в спецификации ECMA-262, секции 10.2-10.5 и 13.

Таким образом, общее правило видимости и доступности переменных таково

 Локальные переменные и параметры функции, видны ТОЛЬКО в пределах этой функции и внутри всех вложенных функций на всю глубину.

Они доступны на протяжении всего времени существования функций, которые к ним обращаются.

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

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

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

пример:  Вложенная функция incrementor обращается к параметру base внешней функции factory (замыкание продолжает существовать после завершения внешней функции):

function factory (base) {
  
  // returning function has access to external `base` parameter  
  return function incrementor(inc) { return base + inc; };
}

var sum2 = factory(2);

sum2(5); // Returns 7

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

var createPet = function(name) {
  var sex;
  
  return {
    setName: function(newName) {
      name = newName;
    },
    
    getName: function() {
      return name;
    },
    
    getSex: function() {
      return sex;
    },
    
    setSex: function(newSex) {
      if(typeof newSex == "string" && (newSex.toLowerCase() == "male" || newSex.toLowerCase() == "female")) {
        sex = newSex;
      }
    }
  }
}

var pet = createPet("Vivie"); 
pet.setSex("female"); 
pet.getName(); // Vivie
pet.getSex(); // female 

var pet2 = createPet("Oliver"); 
pet2.setSex("male"); 
pet2.getName(); // Oliver
pet2.getSex();  // male
                  

Переменные внутренней функции могут использоваться в качестве безопасных хранилищ для закрытых(private) данных:

var getCode = (function(){
  // никто не попробует сахарку
  var secureCode = "сахарок";
​ 
  return function (token) {
    return mix(secureCode, token);
  };
})();

getCode();    // Returns the secret code

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

Пример со перекрытием имён переменных внешней и внутренней функций

var createPet = function(name) {  // Outer function defines a variable called "name"
  return {
    setName: function(name) {    // Enclosed function also defines a variable called "name"
      name = name;               // ??? How do we access the "name" defined by the outer function ???
    }
  }
}

Объект привязки (this)

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

Основное назначение объекта привязки состоит в поддержке вызовов методов объектов в стиле ООП (см. выше).

// для методов объекта 
obj.method(param); // ->  obj.method.call(obj, param)

// или для внешних функций:
var obj = {}, fn = function(){};
obj::fn(param) // -> fn.call(obj, param)

Ссылка на объект привязки this передаётся

  • для обычных функций - каждый раз В МОМЕНТ ВЫЗОВА.
  • для стрелочных функций - связывается в МОМЕНТ ОБЪЯВЛЕНИЯ

Стандарт языка предоставляет средства явного связывания 

var obj = { some : function (){...} }

var boundSome = some.bind(obj);

// или ещё проще:

var boundSome2 = ::obj.some;

Далее

Подробное техническое описание функций в статье справочника Функции

Смотрите также Function в Справочнике JavaScript для получения дополнительной информации по функции как объекту.

Внешние ресурсы:

Метки документа и участники

 Внесли вклад в эту страницу: Grumvol, DeekHalden, alitskevich, keffidesign, JuGeer, serhiyv, pashutk, roma-derski, fscholz, andrcmdr, dixon2002, teoli, uleming
 Обновлялась последний раз: Grumvol,