Источник: http://bonsaiden.github.io/JavaScript-Garden/ru/#function
Функции в JavaScript тоже являются объектами (шок, сенсация) — следовательно, их можно передавать и присваивать точно так же, как и любой другой объект. Одним из вариантов использования такой возможности является передача анонимной функции как функции обратного вызова в другую функцию — к примеру, для асинхронных вызовов.
Объявление function
:
// всё просто и привычно
function foo() {}
В следующем примере описанная функция резервируется перед запуском всего скрипта; за счёт этого она доступна в любом месте кода, вне зависимости от того, где она определена — даже если функция вызывается до её фактического объявления в коде.
foo(); // сработает, т.к. функция будет создана до выполнения кода
function foo() {}
function
как выражениеvar foo = function() {};
В этом примере безымянная и анонимная функция присваивается переменной foo
.
foo; // 'undefined'
foo(); // вызовет TypeError
var foo = function() {};
Так как в данном примере выражение var
— это определение функции, переменная с именем foo
будет заранее зарезервирована перед запуском скрипта (таким образом, foo
уже будет определена во время его работы).
Но поскольку присвоения исполняются непосредственно во время работы кода, foo
по умолчанию будет присвоено значение undefined` (до обработки строки с определением функции):
var foo; // переменная неявно резервируется
foo; // 'undefined'
foo(); // вызовет TypeError
foo = function() {};
Существует еще нюанс, касающийся именованных функций создающихся через присваивание:
var foo = function bar() {
bar(); // работает
}
bar(); // получим ReferenceError
Здесь объект bar
не доступен во внешней области, так как имя bar
используется только для присвоения переменной foo
; однако bar можно вызвать внутри функции. Такое поведение связано с особенностью работы JavaScript с пространствами имен - имя функции всегда доступно в локальной области видимости самой функции.
this
В JavaScript область ответственности специальной переменной this концептуально отличается от того, за что отвечает this в других языках программирования. Различают ровно пять вариантов того, к чему привязывается this в языке.
Глобальная область видимости
this;
Когда мы используем this
в глобальной области, она будет просто ссылаться на глобальный объект.
Вызов функции
foo();
Тут this
также ссылается на глобальный объект.
ES5 Замечание: В strict-режиме теряется понятие глобальности, поэтому в этом случае this
будет иметь значение undefined
.
Вызов метода
test.foo();
В данном примере this
ссылается на test
.
Вызов конструктора
new foo();
Если перед вызовом функции присутствует ключевое слово new, то данная функция будет действовать как конструктор. Внутри такой функции this
будет указывать на новосозданный Object
.
Переопределение this
function foo(a, b, c) {}
var bar = {};
foo.apply(bar, [1, 2, 3]); // массив развернётся в a = 1, b = 2, c = 3
foo.call(bar, 1, 2, 3); // аналогично
Когда мы используем методы call
или apply
из Function.prototype
, то внутри вызываемой функции this
явным образом будет присвоено значение первого передаваемого параметра.
Исходя из этого, в предыдущем примере (строка с apply
) правило #3 вызов метода не будет применено, и this
внутри foo
будет присвоено bar
.
Хотя большинство из примеров ниже наполнены глубоким смыслом, первый из них можно считать ещё одним упущением в самом языке, поскольку он вообще не имеет практического применения.
Foo.method = function() {
function test() {
// this ссылается на глобальный объект
}
test();
}
Распространенным заблуждением будет то, что this
внутри test
ссылается на Foo
, но это не так.
Для того, чтобы получить доступ к Foo
внутри функции test
, необходимо создать локальную переменную внутри method
, которая и будет ссылаться на Foo
.
Foo.method = function() {
var that = this;
function test() {
// Здесь используем that вместо this
}
test();
}
Подходящее имя для переменной - that
, его часто используют для ссылки на внешний this
. В комбинации с замыканиями this
можно пробрасывать в глобальную область или в любой другой объект.
Еще одной фичей, которая не работает в JavaScript, является создание псевдонимов для методов, т.е. присвоение метода объекта переменной.
var test = someObject.methodTest;
test();
Следуя первому правилу test
вызывается как обычная функция; следовательно this
внутри него больше не ссылается на someObject
.
Хотя позднее связывание this
на первый взгляд может показаться плохой идеей, но на самом деле именно благодаря этому работает наследование прототипов.
function Foo() {}
Foo.prototype.method = function() {};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();
В момент, когда будет вызван method
нового экземпляра Bar
, this
будет ссылаться на этот самый экземпляр.
Одним из самых мощных инструментов JavaScript'а считаются возможность создавать замыкания — это такой приём, когда наша область видимости всегда имеет доступ к внешней области, в которой она была объявлена. Собственно, единственный механизм работы с областями видимости в JavaScript — это функции: т.о. объявляя функцию, вы автоматически реализуете замыкания.
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
В данном примере Counter
возвращает два замыкания: функции increment
и get
. Обе эти функции сохраняют ссылку на область видимости Counter
и, соответственно, имеют доступ к переменной count
из этой самой области.
Поскольку в JavaScript нельзя присваивать или ссылаться на области видимости, заполучить count
извне не представляется возможным. Единственным способом взаимодействовать с ним остается использование двух замыканий.
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
В приведенном примере мы не изменяем переменную count в области видимости Counter
, т.к. foo.hack
не объявлен в данной области. Вместо этого будет создана или перезаписана глобальная переменная count
;
Часто встречается ошибка, когда замыкания используют внутри циклов, передавая переменную индекса внутрь.
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
Данный код не будет выводить числа с 0 до 9, вместо этого число 10 будет выведено десять раз.
Анонимная функция сохраняет ссылку на i
и, когда будет вызвана функция console.log
, цикл for
уже закончит свою работу, а в i
будет содержаться 10.
Для получения желаемого результата необходимо создать копию переменной i
.
Для того, чтобы скопировать значение индекса из цикла, лучше всего использовать анонимную функцию как обёртку.
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
Анонимная функция-обертка будет вызвана сразу же, и в качестве первого аргумента получит i
, значение которой будет скопировано в параметр e
.
Анонимная функция, которая передается в setTimeout
, теперь содержит ссылку на e
, значение которой не изменяется циклом.
Еще одним способом реализации является возврат функции из анонимной функции-обертки, поведение этого кода будет таким же, как и в коде из предыдущего примера.
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
arguments
В области видимости любой функции в JavaScript есть доступ к специальной переменной arguments
. Эта переменная содержит в себе список всех аргументов, переданных данной функции.
Объект arguments
не является наследником Array
. Он, конечно же, очень похож на массив и даже содержит свойство length
— но он не наследует Array.prototype
, а представляет собой Object
.
По этой причине, у объекта arguments отсутствуют стандартные методы массивов, такие как push
, pop
или slice
. Хотя итерация с использованием обычного цикла for по аргументам работает вполне корректно, вам придётся конвертировать этот объект в настоящий массив типа Array
, чтобы применять к нему стандартные методы массивов.
Указанный код вернёт новый массив типа Array
, содержащий все элементы объекта arguments
.
Array.prototype.slice.call(arguments);
Эта конвертация занимает много времени и использовать её в критических частях кода не рекомендуется.
Ниже представлен рекомендуемый способ передачи аргументов из одной функции в другую.
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// делаем здесь что-нибудь
}
Другой трюк — использовать и call
и apply
вместе, чтобы быстро создать несвязанную обёртку:
function Foo() {}
Foo.prototype.method = function(a, b, c) {
console.log(this, a, b, c);
};
// Создаём несвязанную версию "method"
// Она принимает параметры: this, arg1, arg2...argN
Foo.method = function() {
// Результат: Foo.prototype.method.call(this, arg1, arg2... argN)
Function.call.apply(Foo.prototype.method, arguments);
};
Объект arguments создаёт
по геттеру и сеттеру и для всех своих свойств и для формальных параметров функции.
В результате, изменение формального параметра также изменит значение соответствующего свойства объекта arguments
и наоборот.
function foo(a, b, c) {
arguments[0] = 2;
a; // 2
b = 4;
arguments[1]; // 4
var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);
Создание конструкторов в JavaScript также отличается от большинства других языков. Любая функция, вызванная с использованием ключевого слова new, будет конструктором.
Внутри конструктора (вызываемой функции) this
будет указывать на новосозданный Object
. Прототипом этого нового объекта будет prototype
функции, которая была вызвана в качестве конструктора.
Если вызываемая функция не имеет явного возврата посредством return
, то вернётся this
— этот новый объект.
function Foo() {
this.bla = 1;
}
Foo.prototype.test = function() {
console.log(this.bla);
};
var test = new Foo();
В этом примере Foo
вызывается в виде конструктора, следовательно прототип созданного объекта будет привязан к Foo.prototype
.
В случае, когда функция в явном виде возвращает некое значение используя return
, то в результате выполнения конструктора мы получим именно его, но только если возвращаемое значение представляет собой Object
.
function Bar() {
return 2;
}
new Bar(); // новый объект
function Test() {
this.value = 2;
return {
foo: 1
};
}
new Test(); // возвращённый объект
Если же опустить ключевое слово new
, то функция не будет возвращать никаких объектов.
function Foo() {
this.bla = 1; // устанавливается глобальному объекту
}
Foo(); // undefined
Этот пример в некоторых случаях всё-таки может сработать: это связано с поведением this
в JavaScript — он будет восприниматься парсером как глобальный объект.
Если хотите избавится от необходимости использования new
, напишите конструктор, возвращающий значение посредством return
.
function Bar() {
var value = 1;
return {
method: function() {
return value;
}
}
}
Bar.prototype = {
foo: function() {}
};
new Bar();
Bar();
В обоих случаях при вызове Bar
мы получим один и тот же результат — новый объект со свойством method
(спасибо замыканию за это).
Также следует заметить, что вызов new Bar()
никак не связан с прототипом возвращаемого объекта. Хоть прототип и назначается всем новосозданным объектам, но Bar
никогда не возвращает этот новый объект.
В предыдущем примере нет функциональных отличий между вызовом конструктора с оператором new или без него.
Создание объектов с использованием фабрик
Часто рекомендуют не использовать new
, поскольку если вы его забудете, это может привести к ошибкам.
Чтобы создать новый объект, лучше использовать фабрику и создать новый объект внутри этой фабрики.
function Foo() {
var obj = {};
obj.value = 'blub';
var private = 2;
obj.someMethod = function(value) {
this.value = value;
}
obj.getPrivate = function() {
return private;
}
return obj;
}
Хотя данный пример и сработает, если вы забыли ключевое слово new
и благодаря ему легче работать с приватными переменными, у него есть несколько недостатков
Хотя забытое ключевое слово new
и может привести к багам, это точно не причина отказываться от использования прототипов. В конце концов, полезнее решить какой из способов лучше совпадает с требованиями приложения: очень важно выбрать один из стилей создания объектов и после этого не изменять ему.
Хотя JavaScript нормально понимает синтаксис двух фигурных скобок, окружающих блок, он не поддерживает блочную область видимости; всё что остаётся на этот случай в языке — область видимости функций.
function test() { // область видимости
for(var i = 0; i < 10; i++) { // не область видимости
// считаем
}
console.log(i); // 10
}
Также JavaScript не знает ничего о различиях в пространствах имён: всё определяется в глобально доступном пространстве имён.
Каждый раз, когда JavaScript обнаруживает ссылку на переменную, он будет искать её всё выше и выше по областям видимости, пока не найдёт её. В случае, если он достигнет глобальной области видимости и не найдет запрошенное имя и там тоже, он ругнётся ReferenceError
.
// скрипт A
foo = '42';
// скрипт B
var foo = '42'
Вышеприведённые два скрипта не приводят к одному результату. Скрипт A определяет переменную по имени foo
в глобальной области видимости, а скрипт B определяет foo
в текущей области видимости.
Повторимся, это вообще не тот же самый эффект. Если вы не используете var
— то вы в большой опасности.
// глобальная область видимости
var foo = 42;
function test() {
// локальная область видимости
foo = 21;
}
test();
foo; // 21
Из-за того что оператор var
опущен внутри функции, функция test
перезапишет значение foo
. Это поначалу может показаться не такой уж и большой проблемой, но если у вас имеется тысяча строк JavaScript-кода и вы не используете var, то вам на пути встретятся страшные и трудноотлаживаемые ошибки — и это не шутка.
// глобальная область видимости
var items = [/* какой-то список */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// область видимости subLoop
for(i = 0; i < 10; i++) { // пропущенный оператор var
// делаем волшебные вещи!
}
}
Внешний цикл прекратит работу сразу после первого вызова subLoop
, поскольку subLoop
перезаписывает глобальное значение переменной i
. Использование var во втором цикле for
могло бы вас легко избавить от этой ошибки. Никогда не забывайте использовать var
, если только влияние на внешнюю область видимости не является тем, что вы намерены получить.
Единственный источник локальных переменных в JavaScript - это параметры функций и переменные, объявленные с использованием оператора var
.
// глобальная область видимости
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// локальная область видимости для функции test
i = 5;
var foo = 3;
bar = 4;
}
test(10);
Единственный источник локальных переменных в JavaScript - это параметры функций и переменные, объявленные с использованием оператора var
.
// глобальная область видимости
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// локальная область видимости для функции test
i = 5;
var foo = 3;
bar = 4;
}
test(10);
В то время как foo
и i
— локальные переменные в области видимости функции test
, присвоение bar
переопределит значение одноимённой глобальной переменной.
JavaScript высасывает определения. Это значит, что оба определения с использованием var
и определение function
будут перенесены наверх заключающей их области видимости.
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
Этот код трансформируется ещё перед исполнением. JavaScript перемещает операторы var
и определение function
наверх ближайшей оборачивающей области видимости.
// выражения с var переместились сюда
var bar, someValue; // по умолчанию - 'undefined'
// определение функции тоже переместилось
function test(data) {
var goo, i, e; // потерянная блочная область видимости
// переместилась сюда
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // вылетает с ошибкой TypeError,
// поскольку bar всё ещё 'undefined'
someValue = 42; // присвоения не подвержены высасыванию
bar = function() {};
test();
Потерянная область видимости блока не только переместит операторы var
вовне циклов и их тел, но и сделает результаты некоторых конструкций с if
неинтуитивными.
В исходном коде оператор if
изменял глобальную переменную goo
, когда, как оказалось, он изменяет локальную переменную — в результате работы высасывания.
Если вы не знакомы с высасываниями, то можете посчитать, что нижеприведённый код должен породить ReferenceError
.
// проверить, проинициализована ли SomeImportantThing
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
Но, конечно же, этот код работает: из-за того, что оператор var
был перемещён наверх глобальной области видимости.
var SomeImportantThing;
// другой код может инициализировать здесь переменную SomeImportantThing,
// а может и нет
// убедиться, что она всё ещё здесь
if (!SomeImportantThing) {
SomeImportantThing = {};
}
Все области видимости в JavaScript, включая глобальную области видимости, содержат специальную, определённую внутри них, переменную this
, которая ссылается на текущий объект.
Области видимости функций также содержат внутри себя переменную arguments
, которая содержит аргументы, переданные в функцию.
Например, когда JavaScript пытается получить доступ к переменной foo
в области видимости функции, он будет искать её по имени в такой последовательности:
var foo
, использовать его.foo
, использовать его.foo
, использовать её.Нередкое последствие наличия только одного глобального пространства имён — проблемы с перекрытием имён переменных. В JavaScript эту проблему легко избежать, используя анонимные обёртки.
(function() {
// самостоятельно созданное "пространство имён"
window.foo = function() {
// открытое замыкание
};
})(); // сразу же выполнить функцию
Безымянные функции являются выражениями; поэтому, чтобы вы имели возможность их выполнить, они сперва должны быть разобраны.
( // разобрать функцию внутри скобок
function() {}
) // и вернуть объект функции
() // вызвать результат разбора
Есть другие способы разбора и последующего вызова выражения с функцией; они, хоть и различаются в синтаксисе, но действуют одинаково.
// Два других способа
+function(){}();
(function(){}());
Рекомендуется всегда использовать анонимную обёртку для заключения кода в его собственное пространство имён. Это не только защищает код от совпадений имён, но и позволяет создавать более модульные программы.
Важно добавить, что использование глобальных переменных считается плохой практикой. Любое их использование демонстрирует плохое качество кода и может привести к трудноуловимым ошибкам.