Функции в JavaScript - это наиболее используемые конструкции кода. Их можно вызывать столько раз, сколько вам необходимо, назначать в качестве обработчиков события для разных элементов, использовать в виде методов класса. Однако есть ряд особенностей использования функций, которые стоит знать, т.к. иначе можно натолкнуться на необъяснимое поведение некоторых переменных и самих функций.
Подъем функций (hoisting)
В JavaScipt есть понятие hoisting - подъема определений функций и переменных, объявленных с помощью ключевого слова var
. Заключается этот процесс в том,что интерпретатор JavaScript, получая код, просматривает его на предмет наличия в нем ключевых слов var
и function()
, а затем как бы отправляет их в самое начало кода. В этом случае любая функция доступна для вызова до того, как она объявлена:
1 2 3 4 5 6 7 8 9 10 11 |
let alex = info('Alex', 23, 'alex_dream@yahoo.com'); let mary = info('Mary', 28, 'mary_k1988@gmail.com'); function info(name, age, email){ let person = { name, age, email } console.log(person); } |
Несмотря на то, что функция info()
описана в строке 4, вызов функции выполняется без проблем в строках 1 и 2.
Поднятие также существует для переменных: все переменные, объявленные с помощью ключевого слова var, поднимаются в начало кода и инициализируются сначала значением undefined
.
1 2 3 |
console.log(temp); // undefined var temp='Некая переменная'; console.log(temp); // Некая переменная |
Приоритет поднятия переменных и функций
Есть ряд моментов при объявлении и поднятии функций и переменных в JavaScript:
- Инициализация переменных имеет приоритет перед объявлением функции.
- Объявление функции имеет приоритет перед объявлением переменной.
- Объявления функций "поднимаются" над объявлением переменных, но не над их инициализацией.
Это значит, что переменная, объявленная с тем же именем, что и функция, не всегда будет перезаписана функцией. В примере ниже мы видим, что переменная sq
не только объявляется, но и инициализируется значением 12. После объявления переменной была объявлена функция с тем же именем sq
. В консоли мы видим, что значение sq
- это число 12.
1 2 3 4 5 |
var sq = 12; function sq(x) { return x**2; } console.log(sq, typeof sq); // Вывод: 12 "number" |
Второй вариант - когда мы объявляем переменную, но не задаем ей значение:
1 2 3 4 5 |
var sq; function sq(x) { return x**2; } console.log(sq, typeof sq); // Вывод: ƒ sq(x) { return **2;} "function" |
Теперь в консоли мы видим код функции и тип function
, а не значение переменной. Это следует учитывать, т.к. при написании кода вы можете случайно использовать одинаковые имена для переменных и функций, хотя этого делать, конечно, не стоит.
Немедленно вызываемые функции (IIFE)
Обычно немедленно вызываемые функции (IIFE - Immediately Invoked Function Expression) используют для того, чтобы локализовать внутри функции область видимости переменных. Очень часто такой подход используют для оборачивания кода во всем скрипте, чтобы объявленные переменные не переопределяли другие переменные из сторонних библиотек.
Для того чтобы функция превратилась в немедленно вызываемую, необходимо обернуть ее в скобки и вызвать, используя ()
:
1 2 3 4 |
(function (){ document.write(`<p>Изменения в природе происходят год от года</p> <p>Непогода нынче в моде, непогода, непогода...</p>`); })() |
Пример такой функции вы увидите сразу же при открытии этой страницы:
Немедленно вызываемая функция - это функция, которая по сути, превращается в функциональное выражение (Function Expression), поэтому можно изменить вызов функции, поставив перед ее вызовом восклицательный знак, унарный плюс или минус или тильду, а также ключевое слово void
. Кроме того, можно сразу вызвать функцию с каким-то параметром(-ами).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<style> .red {padding: 10px; background-color: #f00; color: #fff;} .black {padding: 10px; background-color: #000; color: #fff;} </style> <script> !function() { document.write("<p>Вызов функции IIFE</p>"); }(); +function(n) { document.write(`<p class="${n>0 ? 'red': 'black'}">Второй вызов функции IIFE</p>`); }(5); </script> |
Результат работы функций представлен ниже. Попробуйте изменить параметр в скобках вызова второй функции на отрицательное число - и фон абзаца поменяется.
В ES6 появился способ обойти создание немедленно вызываемой функции. Теперь можно создавать блочные области видимости и использовать для этого объявление переменных с помощью ключевого слова let
. Переменная, объявленная таким образом, не существует вне пределов своей области видимости, ограниченной фигурными скобками.
1 2 3 4 5 |
{ let myStr = 'Some string'; console.log(myStr); } console.log(myStr); // Uncaught ReferenceError: myStr is not defined |
Особенности передачи параметров в javascript-функции
В Javascript переменные, которые передаются в функцию, могут иметь сложный тип (Object, Array) или простой (String, Number, Boolean). Когда в качестве аргумента передаётся сложный тип (объект), передача происходит по ссылке. Вместо отправки копии переменной, Javascript указывает ссылку на место в оперативной памяти компьютера, где расположен данный объект. Это и называется передачей аргумента по ссылке. Если используется переменная простого типа (строка или число, или true или false, например), то передача происходит по значению. Этот нюанс может привести к скрытым ошибкам и непониманию того, что происходит. Ниже рассмотрим передачу по ссылке и по значению.
В функцию changeNum()
мы передаем значение переменной num
, которая равна 48. Это видно в console.log()
в строке. Затем функция добавляет к этому значению 12 и выводит 60 в строке. Однако при выводе в console.log()
значения переменной num
оказывается, что она снова равна изначальному значению, т.е. 48.
1 2 3 4 5 6 7 8 9 10 |
function changeNum(num){ num+=12; console.log('num='+num+' в функции'); } var num = 48; console.log('Перед вызовом функции num='+num); //Перед вызовом функции num=48 changeNum(num);// num=60 в функции console.log('После вызова функции num='+num); // После вызова функции num=48 |
Смысл этого примера - в том, чтобы показать, что само число не изменяется в функции, когда мы его передаем в качестве параметра в функцию, т.к. в ней работает своя локальная переменная с тем же именем, которая получает значение извне и меняет его, не затрагивая внешней переменной.
Есть вторая сторона этого примера: когда мы не передаем параметр в функцию, а просто используем внешнюю переменную в этой функции. Тогда значение этой переменной будет изменено:
1 2 3 4 5 6 7 |
function changeNum2() { num += 12; console.log('num=' + num + ' в функции'); // 60 } var num = 48; changeNum2(); console.log('После вызова функции num=' + num); // 60 |
Переменная num поменяет значение и в функции, и извне. Произойдет это потому, что значение переменной в функции берется либо из параметров, либо из локальных переменных, либо из ближайшего окружения, т.е. из глобальной области.
В том случае, если у нас передается объект (массив, как частный случай объекта с нумерованными полями), в функцию передается ссылка на этот объект. В результате меняются свойства объекта, как в примере ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var obj = { name: 'John Doe' }; console.log(obj); // {name: "John Doe"} function changeObjName(object) { object.name = 'Mila Born'; } changeObjName(obj); console.log(obj); //{name: "Mila Born"} function changeObj(object) { return object = { name: 'Vasya Pupkin' }; } console.log(changeObj(obj)); // {name: "Vasya Pupkin"} console.log(obj); //{name: "Mila Born"} |
В начале кода объявлен объект со свойством name: "John Doe"
. Затем в функцию changeObjName(obj)
передается этот объект, а в функции меняется только имя объекта. И, действительно, при выводе в консоль данных об объекте в строке 9 выводится новое имя name: "Mila Born"
. Вторая функция changeObj(object)
получает тот же объект и возвращает новый с измененным именем. Если вывести в консоль результат работы функции, то мы увидим {name: "Vasya Pupkin"}
, однако при выводе самого объекта obj
в свойстве name
будет предыдущее значение "Mila Born"
. Это говорит о том, что передаваемый в функцию объект и возвращаемый из нее - это разные объекты.
Callback-функции, или функции обратного вызова
В JS функции могут принимать другие функции в качестве аргументов и возвращать функции. Callback-функция, или функция обратного вызова - это такая функция, которая передается внутрь другой функции, как аргумент. Такие функции сплошь и рядом используются в JavaScript для обработки событий, например, внутри метода addEventListener()
:
1 |
element.addEventListener('click', function(){this.style.color = "blue"}); |
Проверьте сами:
Второй очень распространенный пример - это колбеки внутри методов массивов, таких, как forEach()
, reduce()
, map()
, filter()
:
1 2 3 4 |
const arr = [12, 48, 34, 15, 144]; const arr12 = arr.filter( elem => elem%12==0 ); console.log(arr); console.log(arr12); |
Сам пример:
Также функции обратного вызова используются в Ajax-запросах, при вызове setInterval()
или setTimeout()
. На практике функции-колбеки вызываются при асинхронных операциях, когда результат работы функции-колбека будет получен через некоторое время и только после этого может быть обработан.
Если рассматривать процесс формирования функций обратного вызова, то получается. что фактически мы передаем в виде одного аргумента функции другую функцию, которая может быть анонимной. В свою очередь, внутри этой функции может быть вызов еще одой или нескольких функций.
В примере ниже мы с помощью функции showResult()
запускаем передаваемую ей функцию f()
, в которой вызваны 2 других функции, определяющие произведение и сумму 2-х чисел:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function showResult(f) { f(); } function multiple(x, y) { console.log(`Произведение ${x}*${y} =${x * y}`); } function summa(x, y) { console.log(`Сумма ${x}+${y} =${x + y}`); } showResult(function(a = 15, b = 12) { multiple(a, b); summa(a, b); }); |
В результате в консоли мы увидим 2 строки:
Еще один пример callback-функции вы найдете в статье "Создание и использование таблиц CSS-стилей в JS", где после загрузки таблицы стилей выдается диалоговое окно alert() с информацией, что стилевая таблица загружена. В код на самом деле можно передать любую функцию, которая сработает именно после загрузки таблицы стилей.
Замыкание
Замыкание — это функция, использующая независимые переменные. Иными словами, это функция в функции, которая знает и помнит значения переменных в окружении, в котором она была создана.
В замыканиях используется внутренняя функция, которая получает доступ к переменным своей внешней функции и использует их для каких-либо манипуляций. При этом сами переменные чаще всего обрабатываются во внутренней функции, которая либо возвращает результат, либо выводит его на экран. Доступ к этой внутренней функции существует только внутри основной функции, поэтому переменные как бы замыкаются в области видимости этой функции и существуют обособленно для каждого вызова основной функции.
Классический пример замыкания - это создание счетчиков. В функции counter
задается начальное значение переменной count=1
. Это локальная переменная, находящаяся в области видимости функции counter
. Она не видна извне, из глобальной области видимости, зато к ней может получить доступ вложенная анонимная функция, которая возвращается в из основной (внешней) функции counter()
. При создании переменной myNum
мы фактически получаем код функции, который увеличивает невидимую извне переменную count
на 1 (8-я строка кода). Именно возможность работать со ссылкой на экземпляр локальной переменной называется замыканием. Функция, замыкающая локальные переменные, называется замыкающей.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let counter = function(){ let count = 1; return function() { return count++; }; } let myNum = counter(); console.log(myNum); // ƒ () {return count++;} console.log(myNum()); // 1 console.log(myNum()); // 2 console.log(myNum()); //3 let counter2 = counter(); console.log(counter2()); //1 console.log(counter2()); //2 |
Поскольку myNum
- это функция, а не числовая или строковая переменная, мы можем вызвать ее, используя круглые скобки в конце, как и любую другую функцию. Однако в момент такого вызова мы увеличиваем значение переменной count
на 1 и возвращаем его в место вызова. Само значение переменной count
было "запомнено" при создании переменной myNum
, поэтому оно последовательно увеличивается с каждым новым вызовом myNum()
.
Самое интересное заключается в том, что при создании другой переменной (counter2
в 12-й строке в примере выше) мы получаем другой, отдельный счетчик, который стартует с 1 и увеличивается при вызове counter2()
.
Это выглядит каким-то вывертом. Зачем, спрашивается, нам нужна функция в функции, которая запоминает значения переменных своей родительской функции и потом что-то с ними делает при следующем вызове? Однако, если присмотреться к объектам, особенно созданным на основе функции-конструктора или класса, то окажется, что некоторые методы - это, по сути, те же замыкания, реализованные в виде функций для определенных объектов.
Пример использования замыкания внутри функции-конструктора объекта
Например, у нас есть функция-конструктор Animal
, которая при создании экземпляра cat
требует указания имени, возраста и длины животного. Внутри функции у нас есть переменная toy
, которой присвоено значение 'Моток веревки', и вторая переменная prirost
со случайным значением от 2 до 12. Также в функции-конструкторе Animal
описан метод grow()
, который при вызове увеличивает длину животного на величину, рассчитанную в переменной prirost
. Эта переменная как раз и есть та, которую "запоминает" экземпляр Animal с именем cat
, а затем использует при повторном вызове метода grow()
. В приведенном примере прирост составил 6 см, и именно на столько каждый раз при вызове метода grow()
увеличивалась длина кота. То есть метод grow()
- это как раз вариант использования замыкания.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function Animal(name, age, length) { let toy = 'Моток веревки', prirost = Math.round(Math.random()*10+2); this.name = name; this.age = age; this.length = length; this.grow = function() { this.length +=prirost; return this.name +' длиной '+ this.length+ 'см'; } this.getToy = function(){return toy;} this.setToy = function(newToy){toy = newToy;} } let cat = new Animal('Мурзик', 1, 20); console.log(cat); //Animal {name: "Мурзик", age: 1, length: 20, grow: ƒ, getToy: ƒ, ...} console.log(cat.prirost);//undefined console.log(cat.grow()); //Мурзик длиной 26см console.log(cat.grow()); //Мурзик длиной 32см console.log(cat.grow()); //Мурзик длиной 38см console.log(cat.toy); //undefined console.log(cat.getToy());//Моток веревки cat.setToy('белый мячик'); console.log(cat.getToy());//белый мячик cat.toy = 'заводная мышка'; console.log(cat.getToy());//белый мячик console.log(cat.toy);//заводная мышка |
Если мы попытаемся вывести в консоль значение переменных toy
и prirost
, то получим в строках 16 и 21 значение undefined
, т.к. извне доступа к ним нет. Зато можно создать метод getToy()
, который получает эначение переменной toy
, и метод setToy()
, который устанавливает новое значение.
Если же задать свойство toy
явно строкой cat.toy = 'заводная мышка'
, то все равно мы не получим доступа к внутренней переменной toy
объекта Animal. Это будет просто еще одно дополнительное свойство объекта cat
. Методы getToy()
и setToy()
никак не будут им управлять. Они замыкаются на свойсте toy
внутри Animal и управляют только им. По сути, toy
и prirost
- это приватные переменные объекта Animal
, т.к. добраться до них извне без использования методов нельзя.
Пример использованиея замыкания для создания нескольких абзацев подобных цветов
Возможно, вам встречались несколько подряд идущих элементов подобного цвета, причем каждый следующий был чуть темнее, чем предыдущий. Мы сейчас реализуем такой подход с помощью замыкания.
В функции changeColor()
объявим 3 переменные, которые будут формировать 3 составляющие цвета в системе rgb()
. Затем во вложенной функции darker()
мы отнимаем от полученных значений 20 единиц, проверяя значение каждой из 3 переменных на то, чтобы оно было больше 20.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function changeColor() { let r = Math.round(Math.random() * 256); let g = Math.round(Math.random() * 256); let b = Math.round(Math.random() * 256); function darker() { r = r > 20 ? r - 20 : 0; g = g > 20 ? g - 20 : 0; b = b > 20 ? b - 20 : 0; let color = `rgb(${r}, ${g}, ${b})`; console.log(color); return color; } return darker; } let color = changeColor(); document.write(`<p style="color: #fff; background-color: ${color()}">Абзац с разным цветом фона</p>`); //rgb(122, 176, 132) document.write(`<p style="color: #fff; background-color: ${color()}">Абзац с разным цветом фона</p>`); //rgb(102, 156, 112) document.write(`<p style="color: #fff; background-color: ${color()}">Абзац с разным цветом фона</p>`); //rgb(82, 136, 92) |
Затем создаем переменную color, связанную с внешней функцией и обращаемся к вложенной функции при формировании абзацев методом document.write()
в стилях. В комментариях рядом со строками формирующими абзацы, записан один из вариантов сгенерированных цветов. Там видно, что каждая из единиц цвета уменьшается на 20.
Результат вы можете увидеть ниже. При нажатии на кнопку "Обновить страницу" цвет абзацев будет меняться, но каждый следующий будет темнее предыдущего. И все это происходит только потому, что мы использовали эффект запоминания значения переменной внешней функции в замыкании и формируем следующие вызовы функции color()
на основе сохраненных данных.