JS. Начало #4. Функции

Автор: Коптикова Лилия, Манюшкина Дарья, Рябкова Анна

JS. Функции (4 раздел)

Введение

Что такое функции и зачем они нужны

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

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

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

Основные преимущества использования функций:

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

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

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

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

  1. Функции вида “function declaration statement”. Стандартный способ объявления function

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

function name([param[, param[, ... param]]]) {

    statements

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

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

function square(number) { 

    return number * number; 

}
  1. Функции вида “function definition expression” или функциональные выражения

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

function ([name]([param] [, param] [..., param]) {

    statements

}
  • name - Имя функции. Может быть не указано, в таком случае функция становится анонимной.
  • param - Имя аргумента, передаваемого в функцию. У функции может быть не более 255 аргументов.
  • statements - Инструкции, из которых состоит тело функции

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

var square = function (number) {

    return number * number;

};

var x = square(4); // x получает значение 16

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

  1. **Стрелочные функции-выражения “arrow function expression” **

Стрелочные функции — функции вида “arrow function expression” — имеют укороченный синтаксис по сравнению с function expression и лексически связывают значение своего this. Стрелочные функции всегда анонимны.

([param] [, param]) => {

   statements

}

param => expression
  • param - Имя параметра. Если параметров нет, вместо них нужно поставить (). Если параметров больше одного, их также нужно заключить в ().

  • statements or expression - Если инструкций несколько, их нужно заключить в {}. Для одного выражения фигурных скобок не требуется, а результат этого выражения будет возвращен функцией.

    Если тело функции состоит из одного выражения, {} и return можно опустить:

const square = (number) => number * number; 

console.log(square(4)); // 16

Если функция не принимает аргументы, ставятся пустые ():

const sayHello = () => console.log("Привет!"); sayHello();

Особенности стрелочных функций:

  • Лексическое связывание this – стрелочные функции не содержат собственный контекст this, а используют значение this окружающего контекста.
  • Нет собственного объекта arguments – стрелочные функции не имеют собственного объекта arguments, поэтому в теле стрелочных функций arguments будет ссылаться на переменную в окружающей области. Вместо него можно использовать остаточные параметры (…args).
  • Нельзя использовать с new – стрелочные функции не могут быть использованы как конструктор и вызовут ошибку при использовании с new.
  • Возврат значений – если стрелочная функция состоит из одного выражения, то return можно опустить, результат этого выражения автоматически будет возвращен.

Анонимные и стрелочные функции

Разница между function и =>

Очень простой и лаконичный синтаксис для создания функций, который часто лучше, чем Function Expression. Он называется «функции-стрелки» или «стрелочные функции» (arrow functions), т.к. выглядит следующим образом:

let func = (arg1, arg2, ...argN) => expression;

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

Другими словами, это сокращённая версия:

let func = function(arg1, arg2, ...argN) {

  return expression;

};

Стрелочные функции можно использовать так же, как и Function Expression.

Например, для динамического создания функции:

let age = prompt("Сколько Вам лет?", 18);

let welcome = (age < 18) ?
 () => alert('Привет!') :
 () => alert("Здравствуйте!");

welcome();

Когда что использовать?

Используйте function:

  • Когда нужен доступ к this в методах объектов.

  • Когда нужен доступ к arguments .

  • Когда функция используется как конструктор.

Используйте стрелочную функцию:

  • Для коротких функций, особенно колбеков.

  • Когда нужно сохранить контекст this из внешней области.

  • Для улучшения читаемости кода в функциональном стиле.

Контекст (this):

  • У обычных функций this зависит от того, как функция вызвана.
  • У стрелочных функций this сохраняется из внешней области.

arguments:

  • В обычных функциях доступен объект arguments.
  • В стрелочных функциях нет arguments, используется …args.

Конструктор:

  • Обычные функции могут быть конструкторами (с new).
  • Стрелочные функции не могут быть конструкторами.

Использование:

  • Обычные функции универсальны.
  • Стрелочные функции удобны для колбэков и сохранения контекста.

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

Передача аргументов

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

Пример функции, принимающей один параметр:

function square(number) { 

    return number * number; 

}

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

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

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"

// (свойство было изменено функцией)

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

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

В прошлом для этого было необходимо в теле функции проверять значения параметров на 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)

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

Существует три основных отличия остаточных параметров от объекта arguments:

  • остаточные параметры включают только те, которым не задано отдельное имя, в то время как объект arguments содержит все аргументы, передаваемые в функцию;
  • объект arguments не является массивом, в то время как остаточные параметры являются экземпляром Array и методы sort, map, forEach или pop могут непосредственно у них использоваться;
  • объект arguments имеет дополнительную функциональность, специфичную только для него (например, свойство callee).

Вот пример кода:

function myFunc(...someArgs) { 

    for (var i = 0; i &lt; rest.length; i++) { 

        console.log(`Argument ${ i + 1 }: ${ rest[i] }`); 

        } 

    }

Остаточные параметры здесь обозначаются символом троеточия (…). Так JavaScript понимает, что оставшиеся параметры нужно собрать в массив.

Теперь вызовем функцию:


myFunc('Paul', 'John', 'Ringo');

Полученный в консоли результат:


Argument 1: Paul 

Argument 2: John 

Argument 3: Ringo

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

Также при помощи rest-параметров можно присвоить несколько параметров переменным, а оставшиеся передать в массив.

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


function myFunc(...[name1, name2, name3]) { 


    console.log(name1, name2, name3); 

} 

var names = ['Paul', 'John', 'Ringo']; 

myFunc(names);

Возвращаемое значение функции

Оператор return и его использование

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

Простейшим примером может служить функция сложения двух чисел:

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

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

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

Вызовов return может быть несколько, например:

function checkAge(age) {

  if (age >= 18) {
    return true;

  } else {
    return confirm('А родители разрешили?');

  }
}

let age = prompt('Сколько вам лет?', 18);


if ( checkAge(age) ) {
  alert( 'Доступ получен' );

} else {
  alert( 'Доступ закрыт' );

}

Функции без возвращаемого значения

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

Например:

function showMovie(age) {

  if ( !checkAge(age) ) {
    return;

  }

  alert( "Вам показывается кино" ); // (*)
  // ...

}

В коде выше, если checkAge(age) вернёт false, showMovie не выполнит alert.

🛈 Результат функции с пустым return или без него – undefined

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


function doNothing() { /* пусто */ }

alert( doNothing() === undefined ); // true

Пустой return аналогичен return undefined:

function doNothing() {
    
  return;

}

alert( doNothing() === undefined ); // true

⚠️ Никогда не добавляйте перевод строки между return и его значением

Если мы хотим, чтобы возвращаемое выражение занимало несколько строк, нужно начать его на той же строке, что и return. Или, хотя бы, поставить там открывающую скобку, вот так:

return (`

  some + long + expression
  + or +
  whatever * f(a) + f(b)

  )

И тогда все сработает, как задумано.

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

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

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

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

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


const a = 42

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


const a = 42

console.log(a)

// 42

function wrap() {

  const b = a

  // Без проблем, переменная a доступна в этой функции

}

const c = {

  d: a,

  // Хорошо, переменная a доступна и здесь

}

function secondWrap() {

  const e = {

    f: a,

    // И тут ок, переменная a всё ещё доступна

  }

}

Переменные в глобальной области видимости называются глобальными переменными и доступны всем.

Самый известный пример глобальной переменной — это console.


console.log(console)

// Console {debug: function, error: function,

// log: function, info: function, warn: function, …}

Модульная область видимости

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


// module1.js

const a = 42

function wrap() {

  const b = a

  // Переменная `a` доступна в функции

}

let c = 0

if (a &lt; 100) {

  c = a

  // Переменная `a` доступна в блоке

}

// module2.js

console.log(a)

// ReferenceError: a is not defined

Чтобы предоставить доступ к определённым данным модуля, их нужно экспортировать.

Разделение на модули упрощает задачу структурирования кода. Это особенно важно для больших проектов.

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

Блочная область видимости ограничена программным блоком, обозначенным при помощи { и }. Простейший пример такой области — это выражение внутри скобок:


const a = 42

console.log(a)

// 42

if (true) {

  const b = 43

  console.log(a)

  // 42

  console.log(b)

  // 43

}

console.log(b)

// ReferenceError: Can't find variable: b

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

Скобки могут, однако, не только отделять тело условия. Ими можно обрамлять и другие части кода. Это, например, бывает очень полезно в сложных switch-конструкциях. Например:


switch (animalType) {

  case 'dog': {

    const legs = 4

    const species = 'mammal'

    break

  }

  case 'fish': {

    const legs = 0

    const swims = true

    break

  }

}

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

Функциональная область видимости

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


const a = 42

function scoped() {

  const b = 43

}

console.log(a)

// 42

console.log(b)

// Reference error

К переменной b есть доступ только внутри функции scoped.

Функциональная область видимости — очень мощный инструмент для разделения кода. Во-первых, используя её, мы можем не опасаться за «пересечение имен» переменных.

В одной области видимости объявить дважды let или const нельзя:


const a = 42

const a = 43

// SyntaxError: Cannot declare a const variable twice: 'a'

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


function scope1() {

  const a = 42

}

function scope2() {

  const a = 43

}

«Поднятие» переменных (hoisting)

Запись:


var hi = 'Hello world!'

console.log(window.hi)

// Hello world!

…сработает только с var, но не с let или const. Дело здесь в «поднятии» переменных (hoisting).

Для начала посмотрим на такой код:


function scope() {

  a = 42

  var b = 43

}

scope()

console.log(a)

// 42

console.log(b)

// Reference error

Что произошло?

Чтобы понять, почему доступ к переменной a не вызывал ошибки, разберёмся, как работает var и объявление переменных.

Так как переменная a не была объявлена, то JavaScript сам решил, где объявлять переменную, и «поднял» объявление наверх. Получился вот такой код:


var a

function scope() {

  a = 42

  var b = 43

}

scope()

console.log(a)

// 42

Более того, переменные «поднимаются» и внутри блоков и функций:


console.log(hello)

// undefined

var hello = 'Hello'

console.log(hello)

// Hello

Потому что на самом деле код превращается вот в это:


var hello

console.log(hello)

// undefined

hello = 'Hello'

console.log(hello)

// Hello

В браузере глобальные переменные находятся в области window, поэтому трюк с window.hi сработал — JS «поднял» переменную до глобальной области, а мы потом установили ей значение.

Проблема

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


a = 42

function shouldNotAffectOuterScopeButDoes() {

  a = 43

  console.log(a)

  // 43

}

shouldNotAffectOuterScopeButDoes()

console.log(a)

// 43 (хотя должно было быть 42)

Как бороться?

Первым оружием против этой проблемы стал строгий режим.

Он запрещает объявлять переменные без var, что не позволит случайно перезаписать необъявленную переменную или переменную из внешней области.

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

И именно это делают let и const. Они никогда не выходят из области видимости, где были определены и всегда инициализируются там, где указано.

С var сработает:


console.log(hello)

// undefined

var hello = 'Hello'

С let и const — нет:


console.log(hello)

// Reference error

let hello = 'Hello'

console.log(bye)

// Reference error

const bye = 'Bye'

Лексическое окружение и концепция замыкания

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

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

Как это работает?

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

Пример:


function makeCounter() {

  let count = 0; // Переменная в лексическом окружении

  return function() {

    return count++; // Доступ к переменной count, даже после завершения makeCounter

  };

}

const counter = makeCounter();

console.log(counter()); // 0

console.log(counter()); // 1

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

Замыкания

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

То есть они автоматически запоминают, где были созданы, с помощью скрытого свойства [[Environment]], и все они могут получить доступ к внешним переменным.

Колбэки и функции высшего порядка

Что такое колбэк функции и зачем они нужны

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

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


const first = () => console.log(1);

const second = () => console.log(2);

first();  // 1

second(); // 2

Когда выполняется код с задержкой, например с setTimeout, выполнение функций не происходит строго по порядку:


const first = () => {

  setTimeout(() => console.log(1), 500);

};

const second = () => console.log(2);

first();  // 2

second(); // 1

Здесь second выполняется раньше, потому что JavaScript не ждёт завершения first.

Чтобы гарантировать порядок выполнения, используем колбэк:


const first = (callback) => {

  setTimeout(() => {

    console.log(1);
    callback();

  }, 500);

};

const second = () => console.log(2);

first(second); // 1

Это гарантирует, что функция second выполнится только после завершения first.

Функции высшего порядка как передавать функции в качестве аргументов

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

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


function greaterThan(n) {

  return function(m) { return m > n; };

}

var greaterThan10 = greaterThan(10);

console.log(greaterThan10(11));

// true

Можно сделать функцию, меняющую другие функции.


function noisy(f) {

  return function(arg) {

    console.log("calling with", arg);
    var val = f(arg);
    console.log("called with", arg, "- got", val);
    return val;

  };

}

noisy(Boolean)(0);

// → calling with 0

// → called with 0 - got false

Передача аргументов

Функция noisy, объявленная ранее, которая передаёт свой аргумент в другую функцию, не совсем удобна.


function noisy(f) {

  return function(arg) {

    console.log("calling with", arg);
    var val = f(arg);
    console.log("called with", arg, "- got", val);
    return val;

  };

}

Если f принимает больше одного параметра, она получит только первый. Можно было бы добавить кучу аргументов к внутренней функции (arg1, arg2 и т.д.) и передать все их в f, но ведь неизвестно, какого количества нам хватит. Кроме того, функция f не могла бы корректно работать с arguments.length. Так как мы всё время передавали бы одинаковое число аргументов, было бы неизвестно, сколько аргументов нам было задано изначально.

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

function transparentWrapping(f) {
    
  return function() {

    return f.apply(null, arguments);

  };
}

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

Рекурсия

Определение и примеры использования рекурсивных функций

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

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

В качестве первого примера напишем функцию pow(x, n), которая возводит x в натуральную степень n. Иначе говоря, умножает x на само себя n раз.


pow(2, 2) = 4

pow(2, 3) = 8

pow(2, 4) = 16

Рассмотрим два способа её реализации.

  1. Итеративный способ: цикл for:
function pow(x, n) {

 let result = 1;
 // умножаем result на x n раз в цикле

 for (let i = 0; i < n; i++) {
   result *= x;

 }
 return result;

}

alert( pow(2, 3) ); // 8
  1. Рекурсивный способ: упрощение задачи и вызов функцией самой себя:
function pow(x, n) {

 let result = 1;

 // умножаем result на x n раз в цикле

 for (let i = 0; i < n; i++) {
   result *= x;

 }

 return result;
}

alert( pow(2, 3) ); // 8

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

Сравнение рекурсии и циклов

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

  • Когда глубина рекурсии гарантированно небольшая
  • Когда читаемость и простота кода важнее производительности
  • Задачи, которые естественно описываются рекурсией - рекурсивные алгоритмы

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

Когда лучше использовать циклы?

  • Когда важна производительность и эффективное использование памяти
  • Для простых итеративных задач (например, перебор массива)
  • Когда глубина рекурсии может быть большой (чтобы избежать переполнения стека).
Коптикова Лилия, Манюшкина Дарья, Рябкова Анна

Коптикова Лилия, Манюшкина Дарья, Рябкова Анна