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

Original author: Barry Steyn
  • Translation
Язык JavaScript был представлен как язык функционального программирования. Причина заключается в том, что функции в JS не просто разделяют логику на операционные блоки, функции являются объектами первого класса, способными создавать другие объекты. Подобная зависимость от функций одновременно является как сильной стороной, так и настоящим проклятием этого языка. Сильная сторона заключается в том, что язык, обладая подобными особенностями, становится легковесным и быстрым (каким JavaScript изначально и видели его создатели). Однако если вы не знаете что делаете — однозначно ждите беды.

Я предлагаю посмотреть на паттерны вызова функций, а точнее на то, как значительно изменяется результат в зависимости от выбранного паттерна. Также мы рассмотрим как ведет себя this, в зависимости от способа вызова функции.

Итак, существует четыре пути вызова функций:

  • Вызов метода — Method Invocation
  • Вызов функции — Function Invocation
  • Вызов конструктора — Constructor Invocation
  • Вызов apply и call — 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 решили не ходить далеко за примером, и реализовать нечто подобное в паттерне «вызов конструктора»…

Паттерн запускается путем размещения оператора new прямо перед вызовом, например:

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 и call — 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. Надеюсь что этот пост вам поможет.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 51

    +18
    >> вините кривоватый дизайн JavaScript
    Очень хочется попросить Барри не использовать языки программирования, которые он считает кривоватыми и тем более — не писать статьи для новичков о языках, которые он считает кривоватыми.

    Было бы немного лучше, если бы он по крайней мере акцентировал внимание на том, что здесь очень много личного мнения. О вкусах, как говорят, не спорят. Но материал преподносится как борьба с каким-то объективным злом в то время, как имеет место обычное непонимание и это неприятно, это сеет ложные представления о языке среди новичков.
      +1
      Не обижайтесь так на него, судя по блогу этот человек живет JavaScript'ом :)
        +1
        Судя по блогу doctrina.org он только Fri 12 October 2012 написал первую статью про JavaScript, а живет он на Cryptography и Math.
          0
          да, это я не туда посмотрел. :)
        +2
        все языки в чём-то кривоваты. что теперь, не программировать вовсе?) а писать статьи можно лишь об идеальных языках?
        дизайн в этом месте и правда крив — вынуждает использовать хаки на ровном месте. ну вот какого лешего this начинает указывать в стратосферу, когда можно «по умолчанию» подставлять объект в контексте которого эта функция была создана?
          0
          Вы преувеличиваете. Сохранить ссылку на текущий контекст — ну разве «хак»? :) Просто надо понимать, что в JavaScript переменная this выполняет немного другую роль, нежели в других языках. Возьмем, например, небольшой код:
          var MyClass = function(){
              this.a = 0;
              return {
                  add: function(){
                      this.a++;
                  }
              }
          }
          
          var test = new MyClass ();
          test.add();
          


          Работать такой код правильно не будет, думаю ясно почему. Но стоит его немного модифицировать, как встанет на свои места.

          var MyClass = function(){
              var self = this;
              self.a = 0;
              return {
                  add: function(){
                      self.a++;
                  }
              }
          }
          
          var test = new MyClass ();
          test.add();
          


          Я к тому, что гибкость языка позволяет создавать совершенно разные конструкции, в которых поведение this может показаться нелогичным относительно других языков программирования. Так что, я бы не назвал создание промежуточной переменной хаком на ровном месте.
      • UFO just landed and posted this here
          0
          Следующей на очереди обязана быть «JavaScript. The Good Parts»
            +4
            • UFO just landed and posted this here
              0
              У книжки такой плохой перевод, что глава названа «УжасТные вещи в JavaScript»?
              • UFO just landed and posted this here
                  0
                  Тогда ладно :). На будущее — если непонятно как писать, то проверяется элементарно — ужас (а не ужасТ)))
                  • UFO just landed and posted this here
                      0
                      Ну я уж в такие дебри не лезу :). Понятно, читается нормально — и ладно.
              +6
              Бедный, бедный «this». Не видать тебе покоя.
                +24
                Ждём статьи «3 паттерна вывода Hello Wrold» и «5 паттернов инкремента переменной»
                  0
                  И обязательный холиварчик на тему i++ vs i+=1
                    0
                      +4
                      Я знаю. Надо определять движок и запускать самую быструю версию! Как-то так:

                      if (getEngine() == 'webkit') {
                        i += 1;
                      } else if (getEngine() == 'trident') {
                        i++;
                      } else if (getEngine() == 'gecko') {
                        ++i;
                      }
                      

                      Так мы сможем написать РЕАЛЬНО быстрое приложение.
                        0
                        Не не не, операторы сравнения тут не строгие, весь прирост производительности на нуль сводят:( Может, если свичом их перебирать — вообще летать будет. Хотя и это по разным движкам сравнивать нужно и дальше под все оптимизировать.
                          +1
                          А теперь смертельный номер — запихнуть это дело в for (i=0; i<x; ???)
                            +1
                            for(var i = 0, engine = getEngine(); i < x; engine == 'webkit'? i+= 1: engine == 'trident'? i++: engine == 'gecko'? ++i;);
                    +14
                    кривоватый дизайн JavaScript
                    он не кривой, а другой. Функции имеют лексическую область видимости, а this — это спец переменная контекста вызова функции как и arguments, которая просто называется this и путает многих. Достаточно это понять и простить и все встанет на места.

                      +4
                      Используя данный паттерн, this привязывается к global object. Это, несомненно, является ошибкой языка — постоянная привязка this к глобальному объекту может уничтожить его контекст.

                      По-моему главная ошибка автора в понимании языка.
                      И не совсем верно, на мой взгляд, в контексте javascript, называть методом, свойство объекта которому присвоена функция.
                      Заметьте, именно так, а не наоборот: «Функции являющиеся свойствами объекта».
                      Как писалось:
                      функции являются объектами первого класса

                      и их нужно воспринимать как самостоятельную боевую единицу.

                      Identifier resolution как раз и решает все проблемы с this.
                        +1
                        Когда я начал изучать JS после C++, Java, мне тоже показалось не логичным и магическим все эти манипуляции с this, но стоит немного разобраться и это превращается в удобный и мощный инструмент, без которых уже и не привычно и сложно представить как то иначе. С мнением о кривоватости и бажности языка не согласен.

                        А еще из статьи я так и не понял каких особенностей нужно избегать, может просто применять по месту надо и всегда понимать что происходит?
                          0
                          В догонку напишите ешё про function_name.call.apply(… )

                            0
                            Ага) и var binded = func_name.bind(this); :)
                              0
                              о!
                              а это где работает?
                              и нафига тогда jQuery.proxy?
                            0
                            Так можно до бесконечности развивать эту идею:

                            var fn = function() {
                                alert(1);
                            };
                            
                            new fn.prototype.constructor;
                            // fn.prototype.constructor();
                            
                            +2
                            Раз про «кривоватый дизайн» автора статьи в комментах не заикнулся только ленивый, понаезжаю еще и на переводчика:
                            если функция возвращает число, цепочку, логическое выражение (true/false), null или undefined, return не сработает, a мы получим this
                            Строку, надеюсь?
                            К сожалению, существует несколько паттернов для вызова функций. О них не нужно быть в курсе. Их нужно зазубрить и понять...
                            Ломает мозг.

                            Ну и про this в strict mode и про bind в статье ничего.
                            this в strict mode
                            В Strict Mode объект this не будет корректироваться. Это возможно самая интересная часть Strict Mode и самая тяжелая(шокирующая) для разработчиков. Все знают, что если первый аргумент call или apply — null или undefined, то значение this выполняемой функции будет преобразование в глобальный объект (для браузеров это window). отсюда
                              0
                              >JavaScript, как и все современные языки, может модулировать логику внутри функций
                              Что значит «модулировать логику»? Это какой-то термин, или ошибка перевода? Ни разу не встречал этот глагол в контексте логики.
                                0
                                Есть же ссылка на оригинал:)
                                JavaScript (like all languages these days) has the ability to modularise logic in functions

                                тык
                                  0
                                  Хм… Всё-таки ошибка перевода тогда.
                                  modularize — гл.; амер. модЕлировать, собирать из блоков
                                  Но уж никак не модУлировать.
                                  Спасибо, сам что-то не догадался посмотреть в оригинал (или поленился?..)
                                    0
                                    Наверное, имеется в виду «модулировать» — собирать из модулей.
                                    • UFO just landed and posted this here
                                    0
                                    «модулеризировать»
                                    • UFO just landed and posted this here
                                  0
                                  Потрясающе!
                                  Огромнейшее спасибо за статью. Она очень помогла мне с пониманием этих отличий. К 2-м из трёх паттернов я уже и так пришёл методом проб и ошибок. Мануалы которые читал в своё время давали ответы на некоторые вопросы, но они не были настолько просты и одновременно глобальны как эта статья.

                                  З.Ы. про call и aply отдельный респект. До этого времени я вообще не понимал зачем они в принципе нужны в js.
                                    0
                                    Парсер съел сарказм?
                                    0
                                    Здорово. Теперь это называется паттернами
                                      0
                                      Микропаттерны. Функциональные микропаттерны!
                                      0
                                      Я рад сообщить, что начиная с версии 1.8.5 JavaScript, Object.create является вполне работающим инструментом.

                                      А с каких это пор у нас стал JavaScript версионным :)? Что это за реализация такая JavaScript
                                      Может таки стандарт ECMAScript… нет? ECMAScript 5 или ECMAScript 6 (Harmony)
                                      0
                                      Что Вы наделали? Как же мне дальше с этим жить…
                                        0
                                        Спасибо, отличная статья! Помогла ноконец то расставити все точки над И. )

                                        Only users with full accounts can post comments. Log in, please.