Четыре паттерна вызова функций в JavaScript

в 12:11, , рубрики: javascript, this, объекты, ооп, паттерны, Программирование, функции

Язык 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js