Как написать функцию на javascript

Функции

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

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

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

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

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

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

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

Функции вида «function declaration statement»

Объявление функции (function definition, или function declaration, или function statement) состоит из ключевого слова function и следующих частей:

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

Например, следующий код объявляет простую функцию с именем square:

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

Функция square принимает один параметр, названный number. Состоит из одной инструкции, которая означает вернуть параметр этой функции (это number) умноженный на самого себя. Инструкция return указывает на значение, которые будет возвращено функцией.

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

Если вы передадите объект как параметр (не примитив, например, массив или определяемые пользователем объекты), и функция изменит свойство переданного в неё объекта, это изменение будет видно и вне функции, как показано в следующем примере:

function myFunc(theObject) {
  theObject.make = 'Toyota';
}

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

x = mycar.make; // x получает значение "Honda"

myFunc(mycar);
y = mycar.make; // y получает значение "Toyota"
                // (свойство было изменено функцией)

Функции вида «function definition expression»

Функция вида «function declaration statement» по синтаксису является инструкцией (statement), ещё функция может быть вида «function definition expression». Такая функция может быть анонимной (она не имеет имени). Например, функция square может быть вызвана так:

var square = function(number) { return number * number; };
var x = square(4); // x получает значение 16

Однако, имя может быть и присвоено для вызова самой себя внутри самой функции и для отладчика (debugger) для идентифицированные функции в стек-треках (stack traces; «trace» — «след» / «отпечаток»).

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

console.log(factorial(3));

Функции вида «function definition expression» удобны, когда функция передаётся аргументом другой функции. Следующий пример показывает функцию map, которая должна получить функцию первым аргументом и массив вторым.

function map(f, a) {
  var result = [], // Создаём новый массив
      i;
  for (i = 0; i != a.length; i++)
    result[i] = f(a[i]);
  return result;
}

В следующем коде наша функция принимает функцию, которая является function definition expression, и выполняет его для каждого элемента принятого массива вторым аргументом.

function map(f, a) {
  var result = []; // Создаём новый массив
  var i; // Объявляем переменную
  for (i = 0; i != a.length; i++)
    result[i] = f(a[i]);
      return result;
}
var f = function(x) {
   return x * x * x;
}
var numbers = [0, 1, 2, 5, 10];
var cube = map(f,numbers);
console.log(cube);

Функция возвращает: [0, 1, 8, 125, 1000].

В JavaScript функция может быть объявлена с условием. Например, следующая функция будет присвоена переменной myFunc только, если num равно 0:

var myFunc;
if (num === 0) {
  myFunc = function(theObject) {
    theObject.make = 'Toyota';
  }
}

В дополнение к объявлениям функций, описанных здесь, вы также можете использовать конструктор Function для создания функций из строки во время выполнения (runtime), подобно eval().

Метод — это функция, которая является свойством объекта. Узнать больше про объекты и методы можно по ссылке: Работа с объектами.

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

Объявление функции не выполняет её. Объявление функции просто называет функцию и указывает, что делать при вызове функции.

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

Эта инструкция вызывает функцию с аргументом 5. Функция вызывает свои инструкции и возвращает значение 25.

Функции могут быть в области видимости, когда они уже определены, но функции вида «function declaration statement» могут быть подняты (поднятиеhoisting), также как в этом примере:

console.log(square(5));
/* ... */
function square(n) { return n * n; }

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

Примечание: Это работает только тогда, когда объявлении функции использует вышеупомянутый синтаксис (т.е. function funcName(){}). Код ниже не будет работать. Имеется в виду то, что поднятие функции работает только с function declaration и не работает с function expression.

console.log(square); // square поднят со значением undefined.
console.log(square(5)); // TypeError: square is not a function
var square = function(n) {
  return n * n;
}

Аргументы функции не ограничиваются строками и числами. Вы можете передавать целые объекты в функцию. Функция show_props() (объявленная в Работа с объектами) является примером функции, принимающей объекты аргументом.

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

function factorial(n) {
  if ((n === 0) || (n === 1))
    return 1;
  else
    return (n * factorial(n - 1));
}

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

var a, b, c, d, e;
a = factorial(1); // a получает значение 1
b = factorial(2); // b получает значение 2
c = factorial(3); // c получает значение 6
d = factorial(4); // d получает значение 24
e = factorial(5); // e получает значение 120

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

Область видимости функций

(function scope)

Переменные объявленные в функции не могут быть доступными где-нибудь вне этой функции, поэтому переменные (которые нужны именно для функции) объявляют только в scope функции. При этом функция имеет доступ ко всем переменным и функциям, объявленным внутри её scope. Другими словами функция объявленная в глобальном scope имеет доступ ко всем переменным в глобальном scope. Функция объявленная внутри другой функции ещё имеет доступ и ко всем переменным её родительской функции и другим переменным, к которым эта родительская функция имеет доступ.

// Следующие переменные объявленны в глобальном scope
var num1 = 20,
    num2 = 3,
    name = 'Chamahk';

// Эта функция объявленна в глобальном scope
function multiply() {
  return num1 * num2;
}

multiply(); // вернёт 60

// Пример вложенной функции
function getScore() {
  var num1 = 2,
      num2 = 3;

  function add() {
    return name + ' scored ' + (num1 + num2);
  }

  return add();
}

getScore(); // вернёт "Chamahk scored 5"

Scope и стек функции

(function stack)

Рекурсия

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

  1. по имени функции
  2. arguments.callee
  3. по переменной, которая ссылается на функцию

Для примера рассмотрим следующие функцию:

var foo = function bar() {
   // здесь будут выражения
};

Внутри функции (function body) все следующие вызовы эквивалентны:

  1. bar()
  2. arguments.callee()
  3. foo()

Функция, которая вызывает саму себя, называется рекурсивной функцией (recursive function). Получается, что рекурсия аналогична циклу (loop). Оба вызывают некоторый код несколько раз, и оба требуют условия (чтобы избежать бесконечного цикла, вернее бесконечной рекурсии). Например, следующий цикл:

var x = 0;
while (x < 10) { // "x < 10" — это условие для цикла
   // что-то делаем
   x++;
}

можно было изменить на рекурсивную функцию и вызовом этой функции:

function loop(x) {
  if (x >= 10) // "x >= 10" — это условие для конца выполнения (тоже самое, что "!(x < 10)")
    return;
  // делать что-то
  loop(x + 1); // рекурсионный вызов
}
loop(0);

Однако некоторые алгоритмы не могут быть простыми повторяющимися циклами. Например, получение всех элементов структуры дерева (например, DOM (en-US)) проще всего реализуется использованием рекурсии:

function walkTree(node) {
  if (node == null) //
    return;
  // что-то делаем с элементами
  for (var i = 0; i < node.childNodes.length; i++) {
    walkTree(node.childNodes[i]);
  }
}

В сравнении с функцией loop, каждый рекурсивный вызов сам вызывает много рекурсивных вызовов.

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

Поведение стека можно увидеть в следующем примере:

function foo(i) {
  if (i < 0)
    return;
  console.log('begin: ' + i);
  foo(i - 1);
  console.log('end: ' + i);
}
foo(3);

// Output:

// begin: 3
// begin: 2
// begin: 1
// begin: 0
// end: 0
// end: 1
// end: 2
// end: 3

Вложенные функции (nested functions) и замыкания (closures)

Вы можете вложить одну функцию в другую. Вложенная функция (nested function; inner) приватная (private) и она помещена в другую функцию (outer). Так образуется замыкание (closure). Closure — это выражение (обычно функция), которое может иметь свободные переменные вместе со средой, которая связывает эти переменные (что «закрывает» («close») выражение).

Поскольку вложенная функция это closure, это означает, что вложенная функция может «унаследовать» (inherit) аргументы и переменные функции, в которую та вложена. Другими словами, вложенная функция содержит scope внешней («outer») функции.

Подведём итог:

  • Вложенная функция имеет доступ ко всем инструкциям внешней функции.
  • Вложенная функция формирует closure: она может использовать аргументы и переменные внешней функции, в то время как внешняя функция не может использовать аргументы и переменные вложенной функции.

Следующий пример показывает вложенную функцию:

function addSquares(a, b) {
  function square(x) {
    return x * x;
  }
  return square(a) + square(b);
}
a = addSquares(2, 3); // возвращает 13
b = addSquares(3, 4); // возвращает 25
c = addSquares(4, 5); // возвращает 41

Поскольку вложенная функция формирует closure, вы можете вызвать внешнюю функцию и указать аргументы для обоих функций (для outer и innner).

function outside(x) {
  function inside(y) {
    return x + y;
  }
  return inside;
}
fn_inside = outside(3); // Думайте об этом как: дайте мне функцию,
                        // которая добавляет 3 к любому введенному значению

result = fn_inside(5); // возвращает 8

result1 = outside(3)(5); // возвращает 8

Сохранение переменных

Обратите внимание, значение x сохранилось, когда возвращалось inside. Closure должно сохранять аргументы и переменные во всем scope. Поскольку каждый вызов предоставляет потенциально разные аргументы, создаётся новый closure для каждого вызова во вне. Память может быть очищена только тогда, когда inside уже возвратился и больше не доступен.

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

Несколько уровней вложенности функций (Multiply-nested functions)

Функции можно вкладывать несколько раз, т.е. функция (A) хранит в себе функцию (B), которая хранит в себе функцию (C). Обе функции B и C формируют closures, так B имеет доступ к переменным и аргументам A, и C имеет такой же доступ к B. В добавок, поскольку C имеет такой доступ к B, который имеет такой же доступ к A, C ещё имеет такой же доступ к A. Таким образом closures может хранить в себе несколько scope; они рекурсивно хранят scope функций, содержащих его. Это называется chaining (chain — цепь; Почему названо «chaining» будет объяснено позже)

Рассмотрим следующий пример:

function A(x) {
  function B(y) {
    function C(z) {
      console.log(x + y + z);
    }
    C(3);
  }
  B(2);
}
A(1); // в консоле выводится 6 (1 + 2 + 3)

В этом примере C имеет доступ к y функции B и к x функции A. Так получается, потому что:

  1. Функция B формирует closure, включающее A, т.е. B имеет доступ к аргументам и переменным функции A.
  2. Функция C формирует closure, включающее B.
  3. Раз closure функции B включает A, то closure С тоже включает A, C имеет доступ к аргументам и переменным обоих функций B и A. Другими словами, С связывает цепью (chain) scopes функций B и A в таком порядке.

В обратном порядке, однако, это не верно. A не имеет доступ к переменным и аргументам C, потому что A не имеет такой доступ к B. Таким образом, C остаётся приватным только для B.

Конфликты имён (Name conflicts)

Когда два аргумента или переменных в scope у closure имеют одинаковые имена, происходит конфликт имени (name conflict). Более вложенный (more inner) scope имеет приоритет, так самый вложенный scope имеет наивысший приоритет, и наоборот. Это цепочка областей видимости (scope chain). Самым первым звеном является самый глубокий scope, и наоборот. Рассмотрим следующие:

function outside() {
  var x = 5;
  function inside(x) {
    return x * 2;
  }
  return inside;
}

outside()(10); // возвращает 20 вместо 10

Конфликт имени произошёл в инструкции return x * 2 между параметром x функции inside и переменной x функции outside. Scope chain здесь будет таким: {inside ==> outside ==> глобальный объект (global object)}. Следовательно x функции inside имеет больший приоритет по сравнению с outside, и нам вернулось 20 (= 10 * 2), а не 10 (= 5 * 2).

Замыкания

(Closures)

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

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

Также, поскольку вложенная функция имеет доступ к scope внешней функции, переменные и функции, объявленные во внешней функции, будет продолжать существовать и после её выполнения для вложенной функции, если на них и на неё сохранился доступ (имеется ввиду, что переменные, объявленные во внешней функции, сохраняются, только если внутренняя функция обращается к ним).

Closure создаётся, когда вложенная функция как-то стала доступной в неком scope вне внешней функции.

var pet = function(name) {   // Внешняя функция объявила переменную "name"
  var getName = function() {
    return name;             // Вложенная функция имеет доступ к "name" внешней функции
  }
  return getName;            // Возвращаем вложенную функцию, тем самым сохраняя доступ
                             // к ней для другого scope
}
myPet = pet('Vivie');

myPet();                     // Возвращается "Vivie",
                             // т.к. даже после выполнения внешней функции
                             // name сохранился для вложенной функции

Более сложный пример представлен ниже. Объект с методами для манипуляции вложенной функции внешней функцией можно вернуть (return).

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.getName();                  // Vivie

pet.setName('Oliver');
pet.setSex('male');
pet.getSex();                   // male
pet.getName();                  // Oliver

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

var getCode = (function() {
  var apiCode = '0]Eal(eh&2'; // Мы не хотим, чтобы данный код мог быть изменен кем-то извне...

  return function() {
    return apiCode;
  };
}());

getCode();    // Возвращает apiCode

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

var createPet = function(name) {  // Внешняя функция определяет переменную с именем "name".
  return {
    setName: function(name) {    // Внутренняя функция также определяет переменную с именем "name".
      name = name;               // Как мы можем получить доступ к "name", определённой во внешней функции?
    }
  }
}

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

Объект arguments функции является псевдо-массивом. Внутри функции вы можете ссылаться к аргументам следующим образом:

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

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

Для примера рассмотрим функцию, которая конкатенирует несколько строк. Единственным формальным аргументом для функции будет строка, которая указывает символы, которые разделяют элементы для конкатенации. Функция определяется следующим образом:

function myConcat(separator) {
   var result = '';
   var i;

   // iterate through arguments
   for (i = 1; i < arguments.length; i++) {
      result += arguments[i] + separator;
   }
   return result;
}

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

// возвращает "red, orange, blue, "
myConcat(', ', 'red', 'orange', 'blue');

// возвращает "elephant; giraffe; lion; cheetah; "
myConcat('; ', 'elephant', 'giraffe', 'lion', 'cheetah');

// возвращает "sage. basil. oregano. pepper. parsley. "
myConcat('. ', 'sage', 'basil', 'oregano', 'pepper', 'parsley');

Примечание: arguments является псевдо-массивом, но не массивом. Это псевдо-массив, в котором есть пронумерованные индексы и свойство length. Однако он не обладает всеми методами массивов.

Рассмотрите объект Function в JavaScript-справочнике для большей информации.

Параметры функции

Начиная с ECMAScript 2015 появились два новых вида параметров: параметры по умолчанию (default parameters) и остаточные параметры (rest parameters).

Параметры по умолчанию (Default parameters)

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

В прошлом для этого было необходимо в теле функции проверять значения параметров на undefined и в положительном случае менять это значение на дефолтное (default). В следующем примере в случае, если при вызове не предоставили значение для b, то этим значением станет undefined, тогда результатом вычисления a * b в функции multiply будет NaN. Однако во второй строке мы поймаем это значение:

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

  return a * b;
}

multiply(5); // 5

С параметрами по умолчанию проверка наличия значения параметра в теле функции не нужна. Теперь вы можете просто указать значение по умолчанию для параметра b в объявлении функции:

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

multiply(5); // 5

Для более детального рассмотрения ознакомьтесь с параметрами по умолчанию.

Остаточные параметры (Rest parameters)

Остаточные параметры предоставляют нам массив неопределённых аргументов. В примере мы используем остаточные параметры, чтобы собрать аргументы с индексами со 2-го до последнего. Затем мы умножим каждый из них на значение первого аргумента. В этом примере используется стрелочная функция (Arrow functions), о которой будет рассказано в следующей секции.

function multiply(multiplier, ...theArgs) {
  return theArgs.map(x => multiplier * x);
}

var arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]

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

(Arrow functions)

Стрелочные функции — функции вида «arrow function expression» (неверно fat arrow function) — имеют укороченный синтаксис по сравнению с function expression и лексически связывает значение this. Стрелочные функции всегда анонимны. Посмотрите также пост блога hacks.mozilla.org «ES6 In Depth: Arrow functions».

На введение стрелочных функций повлияли два фактора: более короткие функции и лексика this.

Более короткие функции

В некоторых функциональных паттернах приветствуется использование более коротких функций. Сравните:

var a = [
  'Hydrogen',
  'Helium',
  'Lithium',
  'Beryllium'
];

var a2 = a.map(function(s) { return s.length; });

console.log(a2); // выводит [8, 6, 7, 9]

var a3 = a.map(s => s.length);

console.log(a3); // выводит [8, 6, 7, 9]

Лексика this

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

function Person() {
  // Конструктор Person() определяет `this` как самого себя.
  this.age = 0;

  setInterval(function growUp() {
    // Без strict mode функция growUp() определяет `this`
    // как global object, который отличается от `this`
    // определённого конструктором Person().
    this.age++;
  }, 1000);
}

var p = new Person();

В ECMAScript 3/5 эта проблема была исправлена путём присвоения значения this переменной, которую можно было бы замкнуть.

function Person() {
  var self = this; // Некоторые выбирают `that` вместо `self`.
                   // Выберите что-то одно и будьте последовательны.
  self.age = 0;

  setInterval(function growUp() {
    // Колбэк ссылается на переменную `self`,
    // значением которой является ожидаемый объект.
    self.age++;
  }, 1000);
}

Альтернативой может быть связанная функция (bound function), с которой можно правильно вручную определить значение this для функции growUp().

В arrow function значением this является окружающий его контекст, так следующий код работает ожидаемо:

function Person() {
  this.age = 0;

  setInterval(() => {
    this.age++; // |this| должным образом ссылается на объект Person
  }, 1000);
}

var p = new Person();

Далее

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

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

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

  • ECMAScript® 2015 Language Specification
  • Учебник по Javascript — замыкания
  • « Предыдущая статья
  • Следующая статья »

Время на прочтение
17 мин

Количество просмотров 228K

Содержание

  • Введение
  • Величины, типы и операторы
  • Структура программ
  • Функции
  • Структуры данных: объекты и массивы
  • Функции высшего порядка
  • Тайная жизнь объектов
  • Проект: электронная жизнь
  • Поиск и обработка ошибок
  • Регулярные выражения
  • Модули
  • Проект: язык программирования
  • JavaScript и браузер
  • Document Object Model
  • Обработка событий
  • Проект: игра-платформер
  • Рисование на холсте
  • HTTP
  • Формы и поля форм
  • Проект: Paint
  • Node.js
  • Проект: веб-сайт по обмену опытом
  • Песочница для кода

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

Дональд Кнут

Вы уже видели вызовы функций, таких как alert. Функции – это хлеб с маслом программирования на JavaScript. Идея оборачивания куска программы и вызова её как переменной очень востребована. Это инструмент для структурирования больших программ, уменьшения повторений, назначения имён подпрограммам, и изолирование подпрограмм друг от друга.

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

Средний взрослый русскоговорящий человек знает примерно 10000 слов. Редкий язык программирования содержит 10000 встроенных команд. И словарь языка программирования определён чётче, поэтому он менее гибок, чем человеческий. Поэтому нам обычно приходится добавлять в него свои слова, чтобы избежать излишних повторений.

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

Определение функции – обычное определение переменной, где значение, которое получает переменная, является функцией. Например, следующий код определяет переменную square, которая ссылается на функцию, подсчитывающую квадрат заданного числа:

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

console.log(square(12));
// → 144

Функция создаётся выражением, начинающимся с ключевого слова function. У функций есть набор параметров (в данном случае, только x), и тело, содержащее инструкции, которые необходимо выполнить при вызове функции. Тело функции всегда заключают в фигурные скобки, даже если оно состоит из одной инструкции.

У функции может быть несколько параметров, или их вообще может не быть. В следующем примере makeNoise не имеет списка параметров, а у power их целых два:

var makeNoise = function() {
  console.log("Хрясь!");
};

makeNoise();
// → Хрясь!

var power = function(base, exponent) {
  var result = 1;
  for (var count = 0; count < exponent; count++)
    result *= base;
  return result;
};

console.log(power(2, 10));
// → 1024

Некоторые функции возвращают значение, как power и square, другие не возвращают, как makeNoise, которая производит только побочный эффект. Инструкция return определяет значение, возвращаемое функцией. Когда обработка программы доходит до этой инструкции, она сразу же выходит из функции, и возвращает это значение в то место кода, откуда была вызвана функция. return без выражения возвращает значение undefined.

Параметры и область видимости

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

Важное свойство функций в том, что переменные, созданные внутри функции (включая параметры), локальны внутри этой функции. Это означает, что в примере с power переменная result будет создаваться каждый раз при вызове функции, и эти отдельные её инкарнации никак друг с другом не связаны.

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

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

var x = "outside";

var f1 = function() {
  var x = "inside f1";
};
f1();
console.log(x);
// → outside

var f2 = function() {
  x = "inside f2";
};
f2();
console.log(x);
// → inside f2

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

Вложенные области видимости

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

К примеру, следующая довольно бессмысленная функция содержит внутри ещё две:

var landscape = function() {
  var result = "";
  var flat = function(size) {
    for (var count = 0; count < size; count++)
      result += "_";
  };
  var mountain = function(size) {
    result += "/";
    for (var count = 0; count < size; count++)
      result += "'";
    result += "\";
  };

  flat(3);
  mountain(4);
  flat(6);
  mountain(1);
  flat(1);
  return result;
};

console.log(landscape());
// → ___/''''______/'_

Функции flat и mountain видят переменную result, потому что они находятся внутри функции, в которой она определена. Но они не могут видеть переменные count друг друга, потому что переменные одной функции находятся вне области видимости другой. А окружение снаружи функции landscape не видит ни одной из переменных, определённых внутри этой функции.

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

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

var something = 1;
{
  var something = 2;
  // Делаем что-либо с переменной something...
}
// Вышли из блока...

Но something внутри блока – это та же переменная, что и снаружи. Хотя такие блоки и разрешены, имеет смысл использовать их только для команды if и циклов.

Если это кажется вам странным – так кажется не только вам. В версии JavaScript 1.7 появилось ключевое слово let, которое работает как var, но создаёт переменные, локальные для любого данного блока, а не только для функции.

Функции как значения

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

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

var launchMissiles = function(value) {
  missileSystem.launch("пли!");
};
if (safeMode)
  launchMissiles = function(value) {/* отбой */};

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

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

Есть более короткая версия выражения “var square = function…”. Ключевое слово function можно использовать в начале инструкции:

function square(x) {
  return x * x;
}

Это объявление функции. Инструкция определяет переменную square и присваивает ей заданную функцию. Пока всё ок. Есть только один подводный камень в таком определении.

console.log("The future says:", future());

function future() {
  return "We STILL have no flying cars.";
}

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

А что будет, если мы поместим объявление функции внутрь условного блока или цикла? Не надо так делать. Исторически разные платформы для запуска JavaScript обрабатывали такие случаи по разному, а текущий стандарт языка запрещает так делать. Если вы хотите, чтобы ваши программы работали последовательно, используйте объявления функций только внутри других функций или основной программы.

function example() {
  function a() {} // Нормуль
  if (something) {
    function b() {} // Ай-яй-яй!
  }
}
Стек вызовов

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

function greet(who) {
  console.log("Привет, " + who);
}
greet("Семён");
console.log("Покеда");

Обрабатывается она примерно так: вызов greet заставляет проход прыгнуть на начало функции. Он вызывает встроенную функцию console.log, которая перехватывает контроль, делает своё дело и возвращает контроль. Потом он доходит до конца greet, и возвращается к месту, откуда его вызвали. Следующая строчка опять вызывает console.log.

Схематично это можно показать так:

top
   greet
        console.log
   greet
top
   console.log
top

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

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

Хранение стека требует места в памяти. Когда стек слишком сильно разрастается, компьютер прекращает выполнение и выдаёт что-то вроде “out of stack space” или “ too much recursion”. Следующий код это демонстрирует – он задаёт компьютеру очень сложный вопрос, который приводит к бесконечным прыжкам между двумя функциями. Точнее, это были бы бесконечные прыжки, если бы у компьютера был бесконечный стек. В реальности стек переполняется.

function chicken() {
  return egg();
}
function egg() {
  return chicken();
}
console.log(chicken() + " came first.");
// → ??
Необязательные аргументы

Следующий код вполне разрешён и выполняется без проблем:

alert("Здрасьте", "Добрый вечер", "Всем привет!");

Официально функция принимает один аргумент. Однако, при таком вызове она не жалуется. Она игнорирует остальные аргументы и показывает «Здрасьте».

JavaScript очень лоялен по поводу количества аргументов, передаваемых функции. Если вы передадите слишком много, лишние будут проигнорированы. Слишком мало – отсутствующим будет назначено значение undefined.

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

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

function power(base, exponent) {
  if (exponent == undefined)
    exponent = 2;
  var result = 1;
  for (var count = 0; count < exponent; count++)
    result *= base;
  return result;
}

console.log(power(4));
// → 16
console.log(power(4, 3));
// → 64

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

console.log("R", 2, "D", 2);
// → R 2 D 2

Замыкания

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

Следующий пример иллюстрирует этот вопрос. В нём объявляется функция wrapValue, которая создаёт локальную переменную. Затем она возвращает функцию, которая читает эту локальную переменную и возвращает её значение.

function wrapValue(n) {
  var localVariable = n;
  return function() { return localVariable; };
}

var wrap1 = wrapValue(1);
var wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2

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

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

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

function multiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

var twice = multiplier(2);
console.log(twice(5));
// → 10

Отдельная переменная вроде localVariable из примера с wrapValue уже не нужна. Так как параметр – сам по себе локальная переменная.

Потребуется практика, чтобы начать мыслить подобным образом. Хороший вариант мысленной модели – представлять, что функция замораживает код в своём теле и обёртывает его в упаковку. Когда вы видите return function(…) {…}, представляйте, что это пульт управления куском кода, замороженным для употребления позже.

В нашем примере multiplier возвращает замороженный кусок кода, который мы сохраняем в переменной twice. Последняя строка вызывает функцию, заключённую в переменной, в связи с чем активируется сохранённый код (return number * factor;). У него всё ещё есть доступ к переменной factor, которая определялась при вызове multiplier, к тому же у него есть доступ к аргументу, переданному во время разморозки (5) в качестве параметра number.

Рекурсия

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

function power(base, exponent) {
  if (exponent == 0)
    return 1;
  else
    return base * power(base, exponent - 1);
}

console.log(power(2, 3));
// → 8

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

Однако, у такой реализации есть проблема – в обычной среде JavaScript она раз в 10 медленнее, чем версия с циклом. Проход по циклу выходит дешевле, чем вызов функции.

Дилемма «скорость против элегантности» довольно интересна. Есть некий промежуток между удобством для человека и удобством для машины. Любую программу можно ускорить, сделав её больше и замысловатее. От программиста требуется находить подходящий баланс.

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

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

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

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

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

Вот вам загадка: можно получить бесконечное количество чисел, начиная с числа 1, и потом либо добавляя 5, либо умножая на 3. Как нам написать функцию, которая, получив число, пытается найти последовательность таких сложений и умножений, которые приводят к заданному числу? К примеру, число 13 можно получить, сначала умножив 1 на 3, а затем добавив 5 два раза. А число 15 вообще нельзя так получить.

Рекурсивное решение:

function findSolution(target) {
  function find(start, history) {
    if (start == target)
      return history;
    else if (start > target)
      return null;
    else
      return find(start + 5, "(" + history + " + 5)") ||
             find(start * 3, "(" + history + " * 3)");
  }
  return find(1, "1");
}

console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)

Этот пример не обязательно находит самое короткое решение – он удовлетворяется любым.

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

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

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

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

find(1, "1")
  find(6, "(1 + 5)")
    find(11, "((1 + 5) + 5)")
      find(16, "(((1 + 5) + 5) + 5)")
        too big
      find(33, "(((1 + 5) + 5) * 3)")
        too big
    find(18, "((1 + 5) * 3)")
      too big
  find(3, "(1 * 3)")
    find(8, "((1 * 3) + 5)")
      find(13, "(((1 * 3) + 5) + 5)")
        found!

Отступ показывает глубину стека вызовов. В первый раз функция find вызывает сама себя дважды, чтобы проверить решения, начинающиеся с (1 + 5) и (1 * 3). Первый вызов ищет решение, начинающееся с (1 + 5), и при помощи рекурсии проверяет все решения, выдающие число, меньшее или равное требуемому. Не находит, и возвращает null. Тогда-то оператор || и переходит к вызову функции, который исследует вариант (1 * 3). Здесь нас ждёт удача, потому что в третьем рекурсивном вызове мы получаем 13. Этот вызов возвращает строку, и каждый из операторов || по пути передаёт эту строку выше, в результате возвращая решение.

Выращиваем функции

Существует два более-менее естественных способа ввода функций в программу.

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

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

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

007 Коров
011 Куриц

Очевидно, что нам понадобится функция с двумя аргументами. Начинаем кодить.


// вывестиИнвентаризациюФермы
function printFarmInventory(cows, chickens) {
  var cowString = String(cows);
  while (cowString.length < 3)
    cowString = "0" + cowString;
  console.log(cowString + " Коров");
  var chickenString = String(chickens);
  while (chickenString.length < 3)
    chickenString = "0" + chickenString;
  console.log(chickenString + " Куриц");
}
printFarmInventory(7, 11);

Если мы добавим к строке .length, мы получим её длину. Получается, что циклы while добавляют нули спереди к числам, пока не получат строчку по меньшей мере в 3 символа.

Готово! Но только мы собрались отправить фермеру код (вместе с изрядным чеком, разумеется), он звонит и говорит нам, что у него в хозяйстве появились свиньи, и не могли бы мы добавить в программу вывод количества свиней?

Можно, конечно. Но когда мы начинаем копировать и вставлять код из этих четырёх строчек, мы понимаем, что надо остановиться и подумать. Должен быть способ лучше. Пытаемся улучшить программу:

// выводСДобавлениемНулейИМеткой
function printZeroPaddedWithLabel(number, label) {
  var numberString = String(number);
  while (numberString.length < 3)
    numberString = "0" + numberString;
  console.log(numberString + " " + label);
}

// вывестиИнвентаризациюФермы
function printFarmInventory(cows, chickens, pigs) {
  printZeroPaddedWithLabel(cows, "Коров");
  printZeroPaddedWithLabel(chickens, "Куриц");
  printZeroPaddedWithLabel(pigs, "Свиней");
}

printFarmInventory(7, 11, 3);

Работает! Но название printZeroPaddedWithLabel немного странное. Оно объединяет три вещи – вывод, добавление нулей и метку – в одну функцию. Вместо того, чтобы вставлять в функцию весь повторяющийся фрагмент, давайте выделим одну концепцию:

// добавитьНулей
function zeroPad(number, width) {
  var string = String(number);
  while (string.length < width)
    string = "0" + string;
  return string;
}

// вывестиИнвентаризациюФермы
function printFarmInventory(cows, chickens, pigs) {
  console.log(zeroPad(cows, 3) + " Коров");
  console.log(zeroPad(chickens, 3) + " Куриц");
  console.log(zeroPad(pigs, 3) + " Свиней");
}

printFarmInventory(7, 16, 3);

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

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

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

Функции и побочные эффекты

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

Первая вспомогательная функция в примере с фермой, printZeroPaddedWithLabel, вызывается из-за побочного эффекта: она выводит строку. Вторая, zeroPad, из-за возвращаемого значения. И это не совпадение, что вторая функция пригождается чаще первой. Функции, возвращающие значения, легче комбинировать друг с другом, чем функции, создающие побочные эффекты.

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

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

Итог

Эта глава показала вам, как писать собственные функции. Когда ключевое слово `function` используется в виде выражения, можно создавать функцию. Когда оно используется как инструкция, вы можете объявлять переменную, назначая ей функцию.

// Создаём f и присваиваем ей функцию
var f = function(a) {
  console.log(a + 2);
};

// Объявляем функцию g
function g(a, b) {
  return a * b * 3.5;
}

Ключевой момент в понимании функций – локальные области видимости. Параметры и переменные, объявленные внутри функции, локальны для неё, пересоздаются каждый раз при её вызове, и не видны снаружи. Функции, объявленные внутри другой функции, имеют доступ к её области видимости.

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

Упражнения

Минимум

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

console.log(min(0, 10));
// → 0
console.log(min(0, -10));
// → -10
Рекурсия

Мы видели, что оператор % (остаток от деления) может использоваться для определения того, чётное ли число ( % 2). А вот ещё один способ определения:

Ноль чётный.
Единица нечётная.
У любого числа N чётность такая же, как у N-2.

Напишите рекурсивную функцию isEven согласно этим правилам. Она должна принимать параметр number и возвращать булевское значение.

Потестируйте её на 50 и 75. Попробуйте задать ей -1. Почему она ведёт себя таким образом? Можно ли её как-то исправить?

console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → ??
Считаем бобы.

Символ номер N строки можно получить, добавив к ней .charAt(N) ( “строчка”.charAt(5) ) – схожим образом с получением длины строки при помощи .length. Возвращаемое значение будет строковым, состоящим из одного символа (к примеру, “к”). У первого символа строки позиция 0, что означает, что у последнего символа позиция будет string.length – 1. Другими словами, у строки из двух символов длина 2, а позиции её символов будут 0 и 1.

Напишите функцию countBs, которая принимает строку в качестве аргумента, и возвращает количество символов “B”, содержащихся в строке.

Затем напишите функцию countChar, которая работает примерно как countBs, только принимает второй параметр — символ, который мы будем искать в строке (вместо того, чтобы просто считать количество символов “B”). Перепишите countBs, чтобы использовать новую функцию.

Статья, в которой рассмотрим, что такое функция и зачем она нужна. Разберём классический способ её объявления, параметры, аргументы и оператор return.

Что такое функция?

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

В следующем примере имеются повторяющиеся блоки кода, которые можно вынести отдельно в JavaScript функцию:

Повторяющие блоки кода, которые можно вынести в JavaScript функцию

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

let a = 5;
let b = 7;

function sum(a, b) {
  sum = a + b;
  console.log(sum);
}

sum(a, b); // 12

a = 10;
b = 4;

sum(a, b); // 14

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

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

JavaScript позволяет создавать функцию различными способами:

  • Function Declaration;
  • Function Expression;
  • Arrow Function.

В этой статье разберём первый классический способ, который называется Function Declaration.

Объявление и вызов функции

Операции с функцией в JavaScript можно разделить на 2 этапа:

  • объявление (создание) функции;
  • вызов (выполнение) этой функции.

1. Объявление функции. Написание функции посредством Function Declaration начинается с ключевого слова function. После чего указывается имя, круглые скобки, внутрь которых при необходимости помещаются параметры, и тело, заключённое в фигурные скобки. Имя функции ещё очень часто называют названием. В теле пишутся те действия, которые вы хотите выполнить с помощью этой функции.

// nameFn – имя функции, params – параметры
function nameFn (params) {
  // тело функции
}

Например:

// объявляем функцию someName
function someName() {
  console.log('Вы вызвали функцию someName!');
}
// function – ключевое слово, которое означает, что мы создаём функцию
// someName – имя функции
// () – круглые скобки, внутри которых при необходимости описываются параметры
// { ... } – тело функции

При составлении имени функции необходимо руководствоваться такими же правилами, что для переменных. Т.е. можно использовать буквы, цифры (0 – 9), знаки «$» и «_». В качестве букв рекомендуется использовать английский алфавит (a-z, A-Z). Имя функции, также как и имя переменной не может начинаться с цифры.

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

Параметры предназначены для получения значений, переданных в функцию, по имени. Их именование осуществляется также как переменных:

// объявляем функцию с двумя параметрами
  function fullname(firstname, lastname) {
  console.log(`${firstname} ${lastname}`);
}

Параметры ведут себя как переменные и в теле функции мы имеем доступ к ним. Значения этих переменных (в данном случае firstname и lastname) определяются в момент вызова функции. Обратиться к ним вне функции нельзя.

Если параметры не нужны, то круглые скобки всё равно указываются.

2. Вызов функции. Объявленная функция сама по себе не выполняется. Запуск функции выполняется посредством её вызова.

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

// объявляем функцию someName
function someName() {
  console.log('Вы вызвали функцию someName!');
}
// вызываем функцию someName
someName();

Параметры и аргументы

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

Аргументы – это значения, которые мы передаём в функцию в момент её вызова.

// userFirstName и userLastName – параметры (userFirstName будет иметь значение первого аргумента, а userLastName соответственно второго в момент вызова этой функции)
function sayWelcome (userFirstName, userLastName) {
  console.log(`Добро пожаловать, ${userLastName} ${userFirstName}`);
}
// 'Иван' и 'Иванов' – аргументы
sayWelcome('Иван', 'Иванов'); // Добро пожаловать, Иванов Иван
// 'Петр' и 'Петров' – аргументы
sayWelcome('Петр', 'Петров'); // Добро пожаловать, Петров Петр

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

function sayWelcome (userFirstName, userLastName) {
  console.log( `Добро пожаловать, ${userLastName} ${userFirstName} `);
}
// с одним аргументом
sayWelcome('Иван'); // Добро пожаловать, undefined Иван
// без передачи аргументов
sayWelcome(); // Добро пожаловать, undefined undefined

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

let a = 7;
let b = 5;

function sum (a, b) {
  a *= 2; // 14
  console.log(a + b);
}
sum(a, b); // 19
console.log(a); // 7

Передача значения по ссылке

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

// объявим переменную someUser и присвоим ей объект, состоящий из двух свойств
const someUser = {
  firstname: 'Петр',
  lastname: 'Петров'
}
// объявим функцию changeUserName
function changeUserName(user) {
  // изменим значение свойства firstname на новое
  user.firstname = 'Александр';
}
// вызовем функцию changeUserName
changeUserName(someUser);
// выводим значение свойства firstname в консоль
console.log(someUser.firstname); // Александр

В этом примере переменные someUser и user ссылаются на один и тот же объект в памяти. И когда мы изменяем объект внутри функции, то someUser тоже изменится.

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

Чтобы избежать изменение внешнего объекта, который мы передаем в функцию через аргумент, необходимо создать копию этого объекта, например, посредством Object.assign:

const someUser = {
  firstname: 'Петр',
  lastname: 'Петров'
}
function changeUserName(user) {
  // создадим копию объекта user
  const copyUser = Object.assign({}, user);
  // изменим значение свойства firstname на новое
  copyUser.firstname = 'Александр';
  // вернём копию объекта в качестве результата
  return copyUser;
}

// вызовем функцию и сохраним результат вызова функции в переменную updatedUser
const updatedUser = changeUserName(someUser);
// выводим значение свойства firstname в консоль для объекта someUser
console.log(someUser.firstname); // Петр
// выводим значение свойства firstname в консоль для объекта updatedUser
console.log(updatedUser.firstname); // Александр

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

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

Локальные и внешние переменные

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

let a = 7;
// объявление функции sum
function sum(a) {
  // локальная переменная функции
  let b = 8;
  console.log(a + b);
}
// вызов функции sum
sum(a);
console.log(b); // Error: b is not defined

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

// внешние переменные
let a = 7;
let b = 3;
function sum() {
  // локальная переменная
  let a = 8;
  // изменение значения внешней переменной (т.к. b нет внутри функции)
  b = 4;
  console.log(a + b);
}
sum(); // 12

Работа с аргументами через arguments

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

function myFn() {
  // проверим является ли arguments обычным массивом
  console.log(Array.isArray(arguments)); // false
  // количество аргументов
  console.log(arguments.length); // 0
}

myFn(); // false

Доступ к аргументам через arguments выполняется точно также как к элементам обычного массива, т.е. по порядковому номеру:

// объявление функции sum
function sum() {
  const num1 = arguments[0]; // получаем значение 1 аргумента
  const num2 = arguments[1]; // получаем значение 2 аргумента
  console.log(num1 + num2);
}

sum(7, 4); // 11

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

function sum() {
  let sum = 0;
  // arguments.length - число аргументов
  for (let i = 0, length = arguments.length; i < length; i++) {
    if (typeof arguments[i] === 'number') {
      sum += arguments[i];
    }
  }
  console.log(sum);
}

sum(4, 20, 17, -6); // 35
sum('', 3, -5, 32, null); // 30

Через цикл for...of этот пример можно записать так:

function sum() {
  let sum = 0;
  for (argument of arguments) {
    if (typeof argument === 'number') {
      sum += argument;
    }
  }
  console.log(sum);
}

При необходимости arguments можно преобразовать в обычный массив:

function names() {
  // превращаем arguments в полноценный массив
  const args = Array.from(arguments);
  console.log(args);
}
names('Даша', 'Маша', 'Нина'); // ["Даша", "Маша", "Нина"]

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

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

function fnA(a, b) {
  const c = 4;
  function fnB(d) {
    console.log(a + b + c + d);
  }
  fnB(1);
}
fnA(3, 5); // 13
fnA(4, 7); // 16
// переменная a не доступна за пределами функции fnA
console.log(a); // Uncaught ReferenceError: a is not defined

При этом внутри функции мы имеем доступ к внешним переменных, но только к тем, которых с таким же именем нет в текущей функции:

const a = 10;
function fa(b) {
  console.log(a + b);
}

fa(7); // 17

Колбэк функции

Колбэк функция (от английского callback function) – это обычная функция, которая просто вызывается внутри другой функции. Такие функции ещё называются функциями обратного вызова. Они очень часто применяются в асинхронном коде.

Передаётся колбэк функция в другую через аргумент:

// колбэк функция
function cb() {
  console.log('callback');
}
// функция, которая будет принимать на вход колбэк функцию
function fnWithCb(cbFn) {
  console.log('before calling the callback function');
  cbFn();
}
// вызываем функцию fnWithCb() и передаём ей в качестве аргумента колбэк функцию cb
fnWithCb(cb);

В этом примере имеются две функции:

  • cb – её будем использовать в роли колбэк функции, т.е. вызывать в fnWithCb;
  • fnWithCb – эта функция, которая содержит параметр cbFn, он будет при её вызове принимать другую функцию (в данном случае cb).

Таким образом, функция cb вызывается внутри функции fnWithCb. В неё она передаётся как аргумент. Без вызова fnWithCb она не вызовется, т.к. она вызывается только внутри fnWithCb.

Другой пример:

// объявим колбэк функцию
function printToLog(message) {
  console.log(message);
}
// объявим функцию, имеющую 3 параметра
function sum(num1, num2, callback) {
  // вычислим сумму 2 значений и запишем его в result
  const result = num1 + num2;
  // вызовем колбэк функцию
  callback(result);
}
// вызовем функцию sum
sum(5, 11, printToLog();

Ещё один пример:

// колбэк функция
function setColorBody() {
  document.body.style.backgroundColor = '#00ff00';
}
// функция, которая будет вызывать функцию setColorBody через 3 секунды
setTimeout(setColorBody, 3000);

В этом примере у нас имеется функция setColorBody. В теле она содержит код, который устанавливает цвет фона <body>.

Далее вызывается функция setTimeout(). Она в свою очередь вызывает где-то внутри себя функцию, переданную ей в качестве первого аргумента. Причем вызывает не сразу, а через указанное количество миллисекунд. В данном случае через 3000.

Функцию setTimeout мы нигде не объявляли, т.к. она присутствует в JavaScript по умолчанию. Оня является методом глобальное объекта window в браузере и global в Node.js.

Обратите внимание, что в setTimeout мы просто передаём функцию setColorBody. Т.е. сами её не вызываем.

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

В JavaScript всё это возможно, благодаря тому, что функции являются объектами. А так как функция является объектом, её как значение можно передавать в другие функции посредством аргументов.

Функция – это объект

Функция в JavaScript, как уже было отмечено выше – это определённый тип объектов, которые можно вызывать. А если функция является объектом, то у неё как у любого объекта имеются свойства. Убедиться в этом очень просто. Для этого можно воспользоваться методом console.dir() и передать ему в качестве аргумента функцию.

Убеждаемся что функция является объектом и у неё есть свойства

На изображении показана структура функции sum. Используя точечную запись, мы например, можем получить название функции (свойство name) и количество параметров(length):

console.log(sum.name); // sum
console.log(sum.length); // 2

Узнать является ли переменная функцией можно с помощью typeof:

function myFunc() {};

console.log(typeof myFunc); // function

Например, проверим является переменная колбэк функцией перед тем её вызвать:

function sum(num1, num2, callback) {
  const result = num1 + num2;
  if (typeof callback === 'function') {
    callback(result);
  }
}

Возврат значения

Функция всегда возвращает значение, даже если мы не указываем это явно. По умолчанию она возвращает значение undefined.

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

// expression – выражение, результат которого будет возвращен функцией myFn
function myFn() {
  return expression;
}

Если return не указать, то функция всё равно возвратит значение, в данном случае undefined.

function sum(a, b) {
  console.log(a + b);
}

// вызовем функцию и сохраним её результат в константу result
const result = sum(4, 3);
// выведем значение переменной result в консоль
console.log(result); // undefined

С использованием инструкции return:

function sum(a, b) {
  // вернём в качестве результата a + b
  return a + b;
}

// вызовем функцию и сохраним её результат в константу result
const result = sum(4, 3);
// выведем значение переменной result в консоль
console.log(result); // 7

Инструкции, расположенные после return никогда не выполняются:

function sum(a, b) {
  // вернём в качестве результата a + b
  return a + b;
  // код, расположенный после return никогда не выполнится
  console.log('Это сообщение не будет выведено в консоль');
}

sum(4, 90);

В этом примере, функция sum возвращает число 94 и прекращает выполнение дальнейших инструкций после return. А так как работа функции закончилась, то сообщение в консоль выведено не будет.

Функция, которая возвращает функцию

В качестве результата функции мы можем также возвращать функцию.

Например:

function outer(a) {
  return function(b) {
    return a * b;
  }
}

// в one будет находиться функция, которую возвращает outer(3)
const one = outer(3);
// в two будет находиться функция, которую возвращает outer(4)
const two = outer(4);

// выведем в консоль результат вызова функции one(5)
console.log(one(5)); // 15
// выведем в консоль результат вызова функции two(5)
console.log(two(5)); // 20

Вызовы функции outer(3) и outer(4) возвращают одну и туже функцию, но первая запомнила, что a = 3, а вторая — что a = 4. Это происходит из-за того, что функции в JavaScript «запоминают» окружение, в котором они были созданы. Этот приём довольно часто применяется на практике. Так как с помощью него мы можем, например, на основе одной функции создать другие, которые нужны.

В примере, приведённом выше, мы могли также не создавать дополнительные константы one и two. А вызвать сразу после вызова первой функции вторую.

// выведем в консоль результат вызова функции one(5)
console.log(outer(3)(5)); // 15
// выведем в консоль результат вызова функции two(5)
console.log(outer(4)(5)); // 20

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

Функцию, приведённую в коде мы можем также создать и так:

function outer(a) {
  function inner(b) {
    return a * b;
  }
  return inner;
}

Кроме этого в качестве результата мы можем также возвратить внешнюю функцию:

function fa() {
  return 'Привет!';
}

function fb() {
  return fa;
}

fb()(); // Привет!

Рекурсия

Функцию можно также вызвать внутри самой себя. Это действие в программировании называется рекурсией.

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

Например, использование рекурсии для вычисления факториала числа:

function fact(n) {
  // условие выхода из рекурсии
  if (n === 1) {
    return 1;
  }

  // возвращаем вызов функции fact(n - 1) умноженное на n
  return fact(n - 1) * n;
}
console.log(fact(5)); // 120

Пример, в котором используя рекурсию выведем числа от указанного до 10:

function counter(value) {
  // условие выхода из рекурсии
  if (value < 10) {
    console.log(value);

    // возвращаем вызов функции counter(value + 1)
    return counter(value + 1);
  }
}

counter(1); // 1, 2, 3, 4, 5, 6, 7, 8, 9
counter(7); // 7, 8, 9

Перегрузка функций в JavaScript

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

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

Например, того чтобы проверить имеет параметр значение или нет, мы можем проверить его значения на undefined. Узнать количества переданных аргументов функции можно через arguments.length. Определить значения параметра можно используя typeof или instanceof.

Например, создадим функцию bodyBgColor, которая будет иметь 2 режима работы. Если её вызвать без аргументов, то она будет возвращать цвет фона body. А если с текстовым аргументом, то она будет устанавливать цвет фона body.

// объявление функции bodyBgColor
function bodyBgColor(color) {
  // если параметр color имеет в качестве значения строку, то установим цвет фона body
  if (typeof color === 'string') {
    document.body.style.backgroundColor = color;
  }

  // вернём в качестве результата текущий цвет фона body
  return getComputedStyle(document.body).backgroundColor;
}

// получим текущий цвет body и выведем его в консоль
console.log(bodyBgColor());

// установим новый цвет фона body
bodyBgColor('green');

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

function calculateСalories(gender, height) {
  let result = gender === 'man' ? (height - 100) * 20 : (height - 105) * 19;
  if (typeof arguments[2] === 'number') {
    result *= arguments[2];
  }
  return result.toFixed(0);
}

console.log(`Оптимальное кол-во ккал: ${calculateСalories('man', 185)}`);
console.log(`Оптимальное кол-во ккал: ${calculateСalories('woman', 168, 1.2)}`);
console.log(`Оптимальное кол-во ккал: ${calculateСalories('woman', 168)}`);

Значение параметров функции по умолчанию

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

function setBgColor(selector, color = 'green') {
  const el = document.querySelector(selector);
  el.style.backgroundColor = color;
}
setBgColor('body');

При вызове функции с одним аргументом, второму параметру будет автоматически присвоено строка 'green'.

Работу параметра по умолчанию можно представить так:

function setBgColor(selector, color) {
  const el = document.querySelector(selector);
  color = color === undefined ? 'green' : color;
  el.style.backgroundColor = color;
}
setBgColor('body');

Пример функции addNewMessage, в которой один из параметров имеет дефолтное значение. Причем это значение будет меняться в зависимости от текущей даты:

const messages = [];
function addNewMessage(text, date = new Date()) {
  messages.push({
    text,
    date
  })
};
addNewMessage('Как сделать вкусный коктейль в блендере?');
addNewMessage('Какое мороженое лучше для коктейля?');
console.log(messages);

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

Остаточные параметры

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

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

// ...nums – остаточные параметры
function calc(action, ...nums) {
  const initialValue = action === '*' ? 1 : 0;
  return nums.reduce((result, current) => {
    return action === '+' ? result + current : action === '*' ? result * current : 0;
  }, initialValue)
}
console.log(calc('+', 3, 4, 21, -4)); // 24
console.log(calc('*', 1, 4, 3)); // 12

В описании параметров ...nums должен всегда быть последним. Если что-то указать после него, то это вызовет ошибку:


function calc(action, ...nums, cb) {
  // ...
}

calc(); // Uncaught SyntaxError: Rest parameter must be last formal parameter

Что такое встроенные (стандартные) функции

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

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

// вызов функции alert
alert("Некоторый текст");

JavaScript - Вызов функции alert

Функция в JavaScript в результате своего выполнения всегда возвращает результат, даже если он явно не определён с помощью оператора return. Этот результат значение undefined.

// 1. функция, не возвращающая никакого результата
function sayWelcome (userFirstName, userLastName) {
  console.log("Добро пожаловать, " + userLastName + " " + userFirstName);
}
// попробуем получить результат у функции, которая ничего не возвращает
console.log(sayWelcome ("Иван", "Иванов"));
// 2. функция, содержащая оператор return без значения
function sayDay (day) {
  day = "Сегодня, " + day;
  return;
  //эта инструкция не выполнится, т.к. она идёт после оператора return
  console.log(day);
}
// попробуем получить результат у функции, которая содержит оператор return без значения
console.log(sayDay("21 февраля 2016г."));

JavaScript - Получить значение у функции, которая ничего не возвращает

Такой же результат будет получен, если для оператора return не указать возвращаемое значение.

Следующая тема: Функциональные выражения и стрелочные функции в JavaScript.

Что такое функция

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

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

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

Как объявить функцию

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

Синтаксис такой:

Синтаксис функции на JavaScript
        function имя(параметры) {
  тело функции (любые команды)
};
    

Например, функция, которая вызывает предупреждение:

Код функции с предупреждением
        function showMessage() {
  alert( 'Предупреждение!' );
};

    

Схема строения функции

Схема строения функции

Как вызвать функцию

Функцию вызывают по имени. Например, чтобы вызывать функцию из нашего примера, нужно после функции написать showAlert();

Параметры и аргументы

Аргументы

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

Например:

Пример использования параметров и аргументов
        function showMessage(from, text) { // параметра: from, text
  alert(from + ', ' + text);
};

showMessage('Привет', 'мне нравится proglib'); // Привет, Мне нравится proglib

    

В примере мы используем два параметра: from и text. Затем в теле функции соединяем from + ', ' + text. А снаружи функции присваиваем им аргументы и выводим на экран.

Передачи одной функции в другую

В функцию можно передать значение другой функции. Например:

Примеры передачи одной функции в другую
        function getDomNodesBySelector(selector) {
  return Array.from(document.querySelectorAll(selector));
};
document.querySelector('.total__button').addEventListener('click', applyDiscount);
let numDiscount = 15;
function applyDiscount() {
      let items = getDomNodesBySelector('.price-value');
      items.forEach(function(item){
            item.innerText = item.innerText - (item.innerText*numDiscount/100);
      });
};

    

В примере задано две функции:

  1. getDomNodesBySelector — преобразует найденные селекторы на веб странице в массив.
  2. applyDiscount — считает какую-то величину и берет данные для этого из функции getDomNodesBySelector.

Колбэки

Строки кода в языке JavaScript выполняются по очереди. Например:

Пример функций, которые выполняются по порядку
        function one(){
  console.log('один');
}
function two(){
  console.log('два');
}
one();//один
two();//два

    

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

Например:

Пример функций, которые выполняются не по порядку
        function one(){
setTimeout( function(){
    console.log(1);
  }, 500 );

  console.log('один');
}// функция выполнится через 500 миллисекунд

function two(){
  console.log('два');
}
first();//два
second();//один

    

Вызов функции с отсрочкой называется колбэк (callback).

Колбэк передают в виде параметра. Например:

Пример колбэка
        function myHomework(subject, callback) {
  alert(`Starting my ${subject} homework.`);
  callback();
}
function myHomeworkFinished(){
  alert('Finished my homework');
}
myHomework('math', myHomeworkFinished);

    

Функция myHomework имеет два параметра subject и callback.

Затем мы вызываем параметр subject, а параметр callback вызовем функцией myHomeworkFinished.

Когда код выполнится, появятся два предупреждения: Starting my math homework и Finished my homework.

Порядок вызова функций и колбэков

Порядок вызова функций и колбэков

Локальные переменные

Локальные переменные видны только внутри тела функции. За пределами функции эти переменные уже не работают.

Например:

Пример кода  с локальной переменной
        function showMessage() {
  let message = "Я локальная переменная"; // локальная переменная

  alert( message );
}

showMessage(); // Я локальная переменная

alert( message ); // <-- будет ошибка, так как переменная видна только внутри функции

    

В примере мы попытались вывести значение переменной message двумя способами: внутри функции путем вызова самой функции и путем вызова самой переменной.

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

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

Внешние переменные

Давайте возьмем пример выше. Оставим внутри функции значение переменной message. А за пределами функции заведем другую переменную и назовем ее message.

Пример кода с локальной и внешней переменной
        function showMessage() {
  let message = "Я локальная переменная"; // локальная переменная

  alert( message );
}
let message = "Я внешняя переменная"
showMessage(); // Я локальная переменная
alert( message ); //Я внешняя переменная

    

В коде у нас две переменные с одинаковым именем. Но наш сценарий будет считать их разными переменными.

В первом случае переменная будет видна программе только внутри функции showMessage() , во втором случае переменная будет видна всей программе.

Параметры по умолчанию

Когда мы указали параметр и не присвоили ему аргумент, то его значение будет undefined.

Например:

Пример кода с параметром по умолчанию
        function showMessage(from, text) { // параметра: from, text
  alert(from + ', ' + text);
}

showMessage('Привет', ); // Привет, undefined

    

В примере параметру text мы не передали значение аргумента. Поэтому в результате мы видим аргумент параметра from и undefined.

Возврат значения

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

Пример возврата значения функции с помощью директивы return
        function sum(a, b) {
  return a + b;
}

let result = sum(1, 2);
alert( result ); // 3

    

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

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

Функция, которая возвращает функцию

Функцию из предыдущего примера можно записать так:

Пример функции, которая возвращает функцию
        const generateSumFinder = () => {
  const sum = (a, b) => a + b;   
  return sum;                      
};

const sum = generateSumFinder();   
sum(1, 5); 

    

Что мы сделали:

  1. Создали переменную generateSumFinder и присвоили ей значение стрелочной функции. Когда мы видим значок =>, перед нами стрелочная функция.
  2. Внутри функции создали переменную sum. Это локальная переменная и видна только внутри функции и присвоили ей значение стрелочной функции, которая складывает аргументы a и b.
  3. Вернули переменную sum.
  4. За пределами функции создали переменную sum. Это внешняя переменная и она не зависит от переменной внутри функции. Этой переменной мы присвоили значение функции generateSumFinder.

Рекурсия

В рекурсии функция вызывает саму себя. Рекурсия достигается за счет конструкции if…else. Чтобы применить рекурсию нужно определить базовый и рекурсивный случай. Функция будет вызывать саму себя, пока результат не приведет к базовому случаю. В других случаях будет выполняться рекурсивный случай.

Например:

Пример рекурсии
        function countdown (i){
             if (i <=1){
                 return i;
             } else {
                 return i-1
             };
         };

         alert (countdown(1));

    

Функция будет вызываться каждый раз, пока значение ее параметра i больше 1 – это рекурсивный случай. Или вызывается при базовом случае при i<=1.

Перегрузка функций в JavaScript

У перегруженных функций одинаковые имена, но разные параметры. JavaScript не поддерживает перегрузку.

Например:

Пример функции без перегрузки 
        function overload(a){
    console.log(«Параметр»)
}

function overload(a,b){
    console.log(«Два параметра»)
}
overload(1);      // Два параметра
overload(1,2);    // Два параметра

    

Несмотря на то, что в первой функции один параметр, вывод аргументов этой функции выведет два параметра. Как вы помните, второй ее параметр будет undefined.

В JavaScript есть способы, как перегрузить функцию. Для этого используют метод arguments.length.

Например:

Пример функции с перегрузкой 
        function overload () {
  if (arguments.length === 1) {
    console.log(«Параметр»)
  }
  if (arguments.length === 2) {
    console.log(«Два параметра»)
  }
}
overload(1);      // Один параметр
overload(1, 2);  // Два параметра

    

В примере с помощью метода arguments.length мы узнаем количество параметров в функции и выводим столько параметров, сколько аргументов мы задаем.

Выбор имени функции

Чтобы выбрать имя функции, необходимо следовать правилам:

Использовать слова на английском языке

неправильно правильно
pokazatSoobsheniye showMessage

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

неправильно правильно
Message (сообщение) showMessage (показать сообщение)

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

Комментарии нужны, чтобы кто-то кроме вас понимал, что происходит в функции.

Например:

Пример комментария в функции
        function getDomNodesBySelector(selector) {
  return Array.from(document.querySelectorAll(selector));
};// функция возвращает массив, который она возьмет из списка DOM узлов по указанному селектору. 

    

Замыкания

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

Например:

Пример замыкания функции на внешней переменной
        let name = "Nikolay";

function sayHi() {
  alert("Hi, " + name);
}

name = "Irina";

sayHi();//Hi, Irina
    

В примере функция sayHi() будет оперировать со вторым значением переменной name. Таким образом, функция замыкает свое действие на известной переменной, где бы не задали ее значение.

Приведем еще один пример:

Пример замыкания функции на внутренней переменной
        function makeWorker() {
  let name = "Nikolay";

  return function() {
    alert(name);
  };
}

let name = "Irina";

// create a function
let work = makeWorker();

// call it
work(); //Nikolay

    

В примере функция makeWorker() замыкается на внутренней переменной name = "Nikolay". Поэтому при инициализации переменных, следует обращать внимание, какая функция будет ими пользоваться и на какой из них будет замыкаться.

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

Функцию можно задать не только с помощью слова function. Есть способ короче, для этого используют значок =>.

Например:

Пример функции, которая складывает параметры a и b
        let sum = (a, b) => a + b;

    

Если параметр в функции один, то скобки можно не писать.

Например:

Пример записи стрелочной функции с одним параметром n
        let double = n => n * 2;
alert(double(3))//6

    

Если параметров нет, то пишут круглые скобки.

Например:

Пример стрелочной функции без параметров
        let sayHi = () => alert("Hello!");

sayHi();//Hello!

    

Лексика this

В стрелочных функциях нет лексики this. Если происходит обращение к this, его значение берется снаружи.

Например:

Пример использования this в стрелочной функции
        let object = {
  title: "our object",
  students: ["Nikolay", "Irina", "Roma"],

  showList() {
    this.students.forEach(
      student => alert(this.title + ': ' + student)
    );
  }
};

object.showList();

    

***

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

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

Материалы по теме

  • ☕ 40 основных и нестандартных методов для работы со строками в JavaScript
  • ☕ 5+5=? Преобразование значений в строку или число в JavaScript
  • ☕📚 Методы массивов в JavaScript для новичков: советы, рекомендации и примеры

Функции.

Функции в JavaScript. Возврат значения. Параметры.

Объявление и вызов функции

Один из способ объявления функции выглядит так.

function add(x, y) {
    return x + y;
}

Мы объявляем функцию add с двумя параметрами — x и y. Функция возвращает сумму этих параметров.

Вызов функции выполняется следующим образом.

Если явно не указывать возвращаемое значение, как в этом примере, функция вернёт undefined.

function lazy() {
}

lazy(); // undefined

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

Функции можно вызывать с любым количеством аргументов.

Если параметр не передан при вызове, ему будет присвоено значение undefined.

function asIs(x) { 
    return y;
}

asIs(y); // undefined

То же самое справедливо для функции с несколькими аргументами.

function myMin(a, b) {
    return a < b ? a : b;
}

myMin(2, 7); // 2
myMin(13, 7); // 7
myMin(13); // undefined

Мы можем проверить, передан ли параметр, и если нет, задать ему значение по умолчанию.

function myMin(a, b) {
    if (b === undefined) {
        return a;
    }

    return a < b ? a : b;
}

myMin(13); // 13

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

Зачастую для задания значения по умолчанию используют более короткую форму с импользованием оператора ||.

function myMin(a, b) {
    b = b || Infinity;

    return a < b ? a : b;
}

myMin(2, 7); // 2
myMin(13, 7); // 7
myMin(-13); // -13

Этот способ следует использовать с осторожностью. Если параметр может принимать значения, приводимые к false — например, 0, '' или null, они будут перезаписаны значением по умолчанию.

function getSalary(rate, days) {
    days = days || 22;

    return rate * days;
}

getSalary(3, 10); // 30
getSalary(1); // 22
getSalary(2, 0); // 44 ???

В некоторых языках (например, в python), при вызове функции можно передавать именованные аргументы.

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

В JavaScript мы можем имитировать именованные аргументы с помощью литерала объекта.

BMI({ m: 60, h: 1.7 }) // 20.7

Обратите внимание на скобки — на самом деле мы передаём в функцию один аргумент — объект, свойствами которого являются наши как-бы именованные аргументы.

Объявление функции будет выглядеть слудующим образом.

function BMI(params) {
    var h = params.h;

    return params.m / (h * h);
}

Однако, у такого подхода есть несколько недостатков:

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

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

Мы также можем передать больше аргументов, чем определено в функции. Избыточные аргументы будут проигнорированы.

function myMin(a, b) {
    return a < b ? a : b;
}

myMin(7, 5, 1); // 5

Внутри функции доступна специальная переменная arguments, которая позволяет получить доступ ко всем переданным аргументам.

function myMin(a, b) {
    var min = a < b ? a : b;
    var c = arguments[2];

    return c < min ? c : min;
}

myMin(2, 3, 4); // 2
myMin(20, 3, 4); // 3
myMin(20, 30, 4); // 4

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

Например, эта функция суммирует все переданные при вызове числа.

function addMany()
{
    var sum = 0;
    var len = arguments.length;

    for (var i = 0; i < len; i += 1) {
        sum += arguments[i];
    }

    return sum;
}

addMany(2, 3, 4); // 9

По историческим причинам arguments — не массив, а так называемый array-like object. Поэтому у него нет методов массива, таких как forEach или map.

К счастью, есть способ преобразовать его к массиву с помощью следующей конструкции.

var args = [].slice.call(arguments);

Тут нам играет на руку динамическая природа языка JavaSctipt. Нам понадобится метод slice, который может использоваться для копирования массива. Мы берём этот метод у только что созданного массива, и вызываем его на нашем объекте.

Эта конструкция аналогична следующей:

// не надо использовать этот код
// он здесь только для наглядного описания

arguments.slice = [].slice;
var args = arguments.slice();

В следующих лекциях мы более подробно поговорим о методе call.

Методы функции

Рассмотрим следующий пример. Нам нужно найти минимальное значение из элементов, которые лежат в массиве numbers.

Math.min(3, 5, 2, 6); // 2

var numbers = [3, 5, 2, 6];
Math.min(numbers); // NaN

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

На помощь нам приходит метод apply.

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

В качестве второго аргумента метод принимает массив, элементы которого будут переданы в функцию.

var numbers = [3, 5, 2, 6];
Math.min.apply(null, numbers); // 2

Другой полезный метод — bind, позволяет выполнить так называемое частичное применение функции.

Частичное применение — это процесс предачи части аргументов функции, который возвращает другую функцию, принимающую оставшиеся аргументы (функцию меньшей арности).

Рассмотрим функцию возведения в степень — Math.pow. Она принимает два аргумента.

Math.pow(2, 4); // 16
Math.pow(2, 10); // 1024

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

var binPow = Math.pow.bind(null, 2);

binPow(4); // 16
binPow(10); // 1024

Функции — объекты первого класса

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

Функцию можно положить в переменную:

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

var sum = add;
sum(1, 3); // 4

Функуию можно передать в качестве аргумента другой функции:

function multiplyBy2(x) {
    return x * 2;
}

var numbers = [3, 5, 9];

numbers.map(multiplyBy2); // [6, 10, 18] 

Наконец, функция может быть возвращена результате выполнения другой функции:

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

    return add;
}

var myAdd = getAdd();
myAdd(1, 3); // 4

Способы объявления функции

В начале лекции мы говорили об одном способе объявления функции. Он называется function declaration.

function add(x, y) {
    return x + y;
}

Функция, определённая таким образом, создаётся на этапе интерпретации программы, то есть ещё до начала выполнения кода.

Это приводит к эффекту, называемому hoisting, то есть всплытие или подём. Проявляется он в том, что функцию можно вызывать до её объявления.

add(2, 3); // 5

function add(x, y) {
    return x + y;
}

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

function add(x, y) {
    return x + y;
}

add(2, 3);

Второй способ объявления функции называется function expression и выглядит так:

var add = function (x, y) {
    return x + y;
};

В этом примере мы создаём функцию без имени — так называемую «анонимную функцию» и кладём её в переменную add.

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

add(2, 3); // TypeError: add is not a function

var add = function (x, y) {
    return x + y;
};

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

var add = function hidden() {
    return typeof hidden;
}

add(); // function

В отличие от function declaration, такой идентификатор будет доступен только внутри функции, но не виден снаружи.

var add = function hidden() {
    return typeof hidden;
}

hidden(); // ReferenceError: hidden is not defined

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

Для примера рассмотрим функцию вычисления факториала.

var fac = function me(n) {
    if (n > 0) {
        return n * me(n-1);
    } else {
        return 1;
    }
};
console.log(fac(3)); // 6

Мы рассмотрели два варианта объявления функции. Какой из них лучше?

function add(x, y) {
    return x + y;
}

var add = function (x, y) {
    return x + y;
};

Оба варианта полностью допустимы. В общем случае можно использовать первый спобоб — function declaration. Этот способ обладает более простым синтаксисом, а также позволяет использовать эффект всплытия.

Есть и ещё один способ объявить функцию, но он встречается достаточно редко.

> var add = new Function('x', 'y', 'return x + y');
> add(2, 3)
5

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

Область видимости функции

Область видимости переменной — это часть кода, в пределах которой эта переменная доступна.

В Javascript область видимости переменной ограничивается функцией.

function greet() {
    var text = 'Привет';

    console.log(text);
}

greet(); // 'Привет'
console.log(text); // Uncaught ReferenceError: text is not defined

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

function greet() {
    var text = 'Привет';

    function nested() {
        console.log(text);
    }

    nested();
}

greet(); // 'Привет'

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

function greet() {
    var text = 'Привет';

    function nested() {
        var text = 'Добрый день';

        console.log(text);
    }

    nested();
}

greet(); // 'Добрый день'

В отличие от других языков, блок не создаёт область видимости:

function greet() {
    if (true) {
        var text = 'Привет';
    }

    console.log(text); // 'Привет'
}

В этом примере срабатывает механизм всплытия, о котором мы говорили ранее.

Интерпретатор поднимает объявление переменной к началу области видимости, то есть к началу функции:

function greet() {
    var text;

    if (true) {
        text = 'Привет';
    }

    console.log(text); // 'Привет'
}

Иногда возникает необходимость искуственно создать область видимости. Например, если мы хотим ограничить доступ к некторым переменным.

Для этого можно использовать способ, называемый «немедленно вызываемой функцией» или IIFE (immediately-invoked function expression).

function foo() {
    (function () {
        var bar = 4;
    }());

    console.log(bar); // Reference Error
}

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

Скобки фокруг функции нужны для того, чтобы интерпретатор понял, что мы хотим использовать function expression.

function () {
    
}()

// SyntaxError: Unexpected token (

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

Чтобы исправить ситуацию, нам нужно поместить любой символ перед ключевым словом function. Сделать это мы можем любым способом:

!function () {
}();

void function () {
}();

Новые возможности языка

Рассмотрим возможности, которы вносит новый стандарт языка ECMAScript 2015.

Блочная область видимости

Для создания области видимости нужно было использовать IIFE.

function foo() {
    (function () {
        var bar = 4; 
    }());
}

Теперь можно объявить переменную с блочной областью видимости с помощью ключевого слова let.

function foo() {
    if (true) {
        let bar = 4; 
    }
}

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

Для того, чтобы установить значение параметра по умолчанию, нужно было проверять его значение.

function add(x, y) {
    if (y === undefined) {
        y = 0;
    }

    return x + y;
}

Теперь можно задать значение по умолчанию в сигнатуре функции.

function add(x, y = 0) {
    return x + y;
}

Функции с произвольным числом аргументов

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

function add() {
    var sum = 0;

    for (var i, l = arguments.length; i < l; i += 1) {
        sum += arguments[i];
    }

    return sum;
}

Теперь можно использовать оператор ... чтобы получить массив переданных аргументов.

function add(...args) {
    var sum = 0;

    args.forEach(function (arg) {
        sum += arg;
    });

    return sum;
}

Destructuring

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

function add(options) {
    return options.x + options.y;
}

add({ x: 1, y: 2});

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

function add({ x, y }) {
    return x + y;
}

add({ x: 1, y: 2});

Понравилась статья? Поделить с друзьями:
  • Как написать функциональные требования
  • Как написать функциональное резюме
  • Как написать фугу для чайников
  • Как написать фолк метал
  • Как написать фнаф