Перевод не завершен. Пожалуйста, помогите перевести эту статью с английского.
Функции в 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 для получения дополнительной информации по функции как объекту.
Внешние ресурсы: