Язык JavaScript был представлен как язык функционального программирования. Причина заключается в том, что функции в JS не просто разделяют логику на операционные блоки, функции являются объектами первого класса, способными создавать другие объекты. Подобная зависимость от функций одновременно является как сильной стороной, так и настоящим проклятием этого языка. Сильная сторона заключается в том, что язык, обладая подобными особенностями, становится легковесным и быстрым (каким JavaScript изначально и видели его создатели). Однако если вы не знаете что делаете — однозначно ждите беды.
Я предлагаю посмотреть на паттерны вызова функций, а точнее на то, как значительно изменяется результат в зависимости от выбранного паттерна. Также мы рассмотрим как ведет себя this
, в зависимости от способа вызова функции.
Итак, существует четыре пути вызова функций:
- Вызов метода — Method Invocation
- Вызов функции — Function Invocation
- Вызов конструктора — Constructor Invocation
- Применить и вызвать — Apply And Call Invocation
Выполнение функции
JavaScript, как и все современные языки, может модулировать логику внутри функций, и эти функции могут быть вызваны в любой момент, посреди уже запущенного процесса. Вызвав функцию, мы передаем ей необходимые параметры и управление процессами, останавливая текущую операцию. Оператор вызова — круглые скобки (), которые могу заключать в себе параметры, разделенные через запятую.
К сожалению, существует несколько паттернов для вызова функций. О них не нужно быть в курсе. Их нужно зазубрить и понять, потому как, в зависимости от выбранного паттерна, вы получите разные результаты. На мой взгляд, данная особенность является ошибкой в проектировании самого языка, и если бы JavaScript создавался в меньшей спешке и с большим вниманием, различных проблем подобного характера удалось бы избежать.
Четыре паттерна
Как уже говорилось, оператор для вызова функции один, а способов вызова — четыре.
Вызов метода — Method Invocation
Когда функция является частью объекта, она называется методом. «Вызов метода» представляет из себя вызов функции, принадлежащей объекту. Пример:
var obj = {
value: 0,
increment: function() {
this.value+=1;
}
};
obj.increment();
В «вызове метода» значение this
будет ссылаться на объект, которому принадлежит функция, в нашем случае на obj, причем данная связь будет установлена после запуска функции, что носит термин позднего привязывания (late binding).
Вызов функции — Function Invocation
Вызов функции выполняется с помощью оператора ():
add(2,3); //5
Используя данный паттерн, this
привязывается к global object. Это, несомненно, является ошибкой языка — постоянная привязка this к глобальному объекту может уничтожить его контекст. Это особенно заметно, если использовать функцию внутри метода. Давайте посмотрим на пример:
var value = 500; //Global variable
var obj = {
value: 0,
increment: function() {
this.value++;
var innerFunction = function() {
alert(this.value);
}
innerFunction(); //Function invocation pattern
}
}
obj.increment(); //Method invocation pattern
Как думаете, что будет выведено на экран? Если вы решили, что 1 — вы ошибаетесь (однако не стоит винить себя — вините кривоватый дизайн JavaScript). Правильный ответ — 500. Обратите внимание, innerFunction
вызывается с использованием вышеупомянутого паттерна «вызова функции», соответственно this
привязывается к global object. В результате мы и получаем 500.
Можно легко обойти эту проблему путем создания переменной this
, но это, по моему мнению, является хаком.
var value = 500; //Global variable
var obj = {
value: 0,
increment: function() {
var that = this;
that.value++;
var innerFunction = function() {
alert(that.value);
}
innerFunction(); //Function invocation pattern
}
}
obj.increment();
Таким образом мы привязали this
к объекту, внутри которого вызывается функция.
Вызов конструктора — Constructor Invocation
Предупреждение: это еще одна особенность JavaScript, который сильно отличается от классических языков ООП! Это прототипно-ориентированный язык программирования, однако его создателям показалось, что люди «классической школы» (коих большинство) будут некомфортно себя чувствовать. В результате в прототипный JavaScript добавились принципы классического ООП и получилось что получилось — бардак.
В классическом ООП объект является реализацией класса. В С++ и Java для такой реализации используется оператор new
. Судя по всему, создатели JS решили не ходить далеко за примером, и реализовать нечто подобное в паттерне «вызов конструктора»…
Паттерн запускается путем размещения нового оператора прямо перед вызовом.
The constructor invocation pattern involves putting the new operator just before the function is invoked. For example:
var Cheese = function(type) {
cheeseType = type;
return cheeseType;
}
cheddar = new Cheese("cheddar"); //Возвращается объект, а не тип
Несмотря на то, что Cheese
является функциональным объектом (а значит умеет переваривать код), мы создали новый объект путем вызова функции с new
. this
в данном случае будет относиться к свежесозданному объекту, и поведение return
будет изменено. К слову о return. Его использования в «вызове конструктора» имеет две особенности:
- если функция возвращает число, цепочку, логическое выражение (true/false), null или undefined,
return
не сработает, a мы получимthis
- если функция возвращает реализацию объекта (то есть все, кроме простых переменных), мы увидим данный объект, а не
this
var obj = {
data : "Hello World"
}
var Func1 = function() {
return obj;
}
var Func2 = function() {
return "I am a simple type";
}
var f1 = new Func1(); //f1 назначается объекту
var f2 = new Func2(); //f2 назначается новому объекту
Мы могли бы игнорировать использование this
, и назначать объектам литералы, если бы не одно но: создатели JavaScript связали с данным паттерном одну из ключевых возможностей языка — создание объектов с произвольной ссылкой на прототип (подробнее здесь — англ.). Данный паттерн неинтуитивен, более того, с ним часто возникают проблемы. Решение проблемы предлагал Douglas Crockford: можно использовать augment object с методом create. Я рад сообщить, что начиная с версии 1.8.5 JavaScript, Object.create
является вполне работающим инструментом.
Применить и вызвать — Apply And Call Invocation
Этот паттерн продуман гораздо лучше остальных. Он позволяет вручную запустить функцию, попутно снабдив ее параметрами и обозначив this
. Из-за того, что функции у нас являются полноправными объектами, каждая функция в JavaScript связана с Function.prototype, а значит мы можем легко добавлять к ним методы.
Данный паттерн использует два параметра: первый — это объект, к которому привязывается this
, второй — это массив, связанный с параметрами:
var add = function(num1, num2) {
return num1+num2;
}
array = [3,4];
add.apply(null,array); //7
В примере выше this
относится к null
(функция не является объектом), а массив привязан к num1
и num2
. Но продолжим эксперементировать с первым параметром:
var obj = {
data:'Hello World'
}
var displayData = function() {
alert(this.data);
}
displayData(); //undefined
displayData.apply(obj); //Hello World
Этот пример использует apply
для привязки this
к obj
. В результате мы в состоянии получить значение this.data
. Настоящая ценность apply заключается именно в привязывании this
.
В JavaScript также существует оператор call
, похожий на apply
всем, за исключением того что получает не параметры, а список аргументов.
Заключение
Хорошо это или не очень, JavaScript вот-вот захватит мир. А потому просто необходимо знать о его особенностях, особенно о тех, которых следует избегать. Понимание четырех паттернов вызова функций — обязаетельное условие для изучающих JavaScript. Надеюсь что этот пост вам поможет.
Автор: modernstyle