JavaScript для чайников. Всё что вы хотели знать о функциях но боялись спросить

    Как-то незаметно для себя, я решил отойти от возни с классами и паттернами, и разобраться с самыми обычными Js функциями. Думал, что будет достаточно скучно, но ошибся — оказалось очень даже интересно.

    В этой статье я расскажу об особенностях объявления функций, и некоторых полезных паттернах (кхе-хе, да, они есть и тут)


    1. Объявление функции


    Если не обращать внимание на всякие извращения, то объявить функцию можно всего двумя способами:

    //создать именованную функцию
    function a() { console.log(1); }
    
    //создать анонимную функцию, а затем присвоить ее переменной
    b = function() { console.log(2); }
    

    Разница между ними кажется небольшой, но это только кажется.

    Что будет если выполнить этот код?

    console.log( "sample b" );
    b();
    
    function b() { console.log(1); }
    

    правильно — в консоли появится сначала текст с названием примера, а потом единичка, потому что все именованные функции переносятся в начало области видимости, и вызывать их можно когда угодно.

    Усложняем задачу

    console.log( "sample c" );
    
    function c() { console.log(1); }
    	
    c();
    
    function c() { console.log(2); }
    

    Но нас ведь так просто не запутать. Функции переносятся в начало ровно в том порядке, в котором они были созданы, то есть в консоли будет 2.

    Ладно, а так?

    console.log( "sample d" );
    d();
    var d = function() { console.log(2); }
    


    Ответ: а тут правила другие — это вообще не заработает, потому что значение переменной на момент вызова — undefined.

    А если так?

    console.log( "sample e" );
    var e = function(){ console.log(1); } 
    	
    e();
    
    e = function() { console.log(2); }
    

    Ну тут уже все ясно, да — на момент вызова е содержит в себе первую функцию, так что в консоли будет единичка.
    А теперь настало время для самого сложного вопроса *барабанная дробь*

    А что будет тут ?

    console.log( "sample f" );
    var f = function() { console.log(1); }
    f();
    
    function f(){ console.log(2); } 
    f();
    

    правильный ответ: два раза единичка. После своего объявления, переменные перекрывают функции.
    Всегда.

    Immediate functions


    За этим названием скрываются «одноразовые» функции — они не имеют имени, и выполняются сразу после своего объявления.

    Зачем это нужно? По большей части за тем, чтобы не засорять глобальное пространство имен. Например, если надо проинициализировать что-нибудь, и при инициализации нам потребуется парочка переменных которые потом вообще совсем не нужны.

    Объявить такую функцию можно двумя, по сути, эквивалентными способами.

    console.log("//immidiate function");
    
    //sample a
    (function(){
    	console.log( "a" );
    })();
    
    //sample b
    (function(){
    	console.log( "b" );
    }());
    

    Отличий между ними нет никаких.

    Init time branching


    Иногда случается, что значение функции зависит от какого-нибудь значения, которое после инциализации не меняется. ну что-нибудь вроде этого:

    // ПЛОХОЙ пример
    function saySomethingClever(){
    	var appleTest = /Apple/i;
    	var googleTest = /Google/i;
    
    	if( appleTest.test(navigator.vendor) ) console.log("I love apples <3")
    	else if( googleTest.test(navigator.vendor) ) console.log("android is everything for me <3")
    	else console.log("i love this unpopular corporation too")
    }
    
    saySomethingClever();
    
    

    Все здорово, кроме того, что каждый раз при вызове мы делаем ставшую уже абсолютно ненужной проверку. Переписать функцию можно так:

    // Хороший пример
    var saySomethingClever = (function(){
        var appleTest = /Apple/i;
        var googleTest = /Google/i;
    
        if( appleTest.test(navigator.vendor) ) 
            return function(){ console.log("I love apples <3"); }
        if( googleTest.test(navigator.vendor) )
            return function(){ console.log("android is everything for me <3"); }
        
        return function(){ console.log("i love this unpopular corporation too"); }
    })();
    
    saySomethingClever();
    

    Как мог заметить внимательный читатель, здесь используется ещё и immidiate function. Теперь проверка выполняется ровно один раз.

    Self defining function


    Иногда бывает так, что при первом вызове функции нужно выполнить какие-нибудь дополнительные действия. Реализовать это можно следующим образом:

    var selfDefining = function()
    {
    	console.log("some really heavy initialization occured");
    	console.log("f*ck yeah!");
    	selfDefining = function(){
    		console.log("job done!");
    	}
    }
    selfDefining();
    selfDefining();
    

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

    Currying


    С помощью этого приема можно создать частный вариант какой-нибудь довольно общей функции. Реализация выглядит так:

    function curry( fn ){
    	var slice = Array.prototype.slice,
    		storedArgs = slice.call( arguments, 1 );
    
    	return function() {
    		var args = storedArgs.concat( slice.call( arguments ) );
    		return fn.apply( this, args );
    	}
    }
    

    Пока конечно нифига непонятно, но это нормально. Сейчас все объясню.
    Допустим у нас есть функция которая печатает сообщение.

    function printMessage( author, message ){
    	console.log( author + " say: " + message )
    } 
    

    Но в данном модуле проекта в качестве значения author всегда используют me, так что нам очень желательна более частная её версия принимающая только строчку с сообщением, а автора вписывающая сама.

    var printMyMessage = curry( printMessage, "me" );
    printMyMessage( "I would like to tell you about birds and bees in js world" );
    

    И теперь она у нас есть.

    UPD. Дополнение к статье от одного очень хорошего человека с ником jamayka

    Объявление рекурсивных функций


    Например, у нас есть тупо факториал, который используется в куче кода:

    var factorial = function (n) {
        return n === 1 ? 1 : factorial(n - 1) * n;
    };
    

    ну или

    function factorial(n) {
        return n === 1 ? 1 : factorial(n - 1) * n;
    };
    

    Потом по каким-то причинам нам нужно

    var realFactorial = factorial;
    factorial = function (n) {
        throw 'please use realFactorial instead';
    };
    

    В итоге получается, что и вызовы factorial(5) и realFactorial(5) вываливают ошибку. Это получается, т.к. рекурсивный вызов factorial использует переменную из родительской области видимости, а там она переопределяется. В этом случае надо определять функцию так:

    var factorial = function factorial (n) {
        return n === 1 ? 1 : factorial(n - 1) * n;
    };
    

    Ну или чтобы не путаться:

    var factorial = function f (n) {
        return n === 1 ? 1 : f(n - 1) * n;
    };
    

    Вроде бы всё — как всегда, исходники примеров скачать можно тут
    Поделиться публикацией

    Похожие публикации

    Комментарии 43
      +2
      Подробное описание работы функций в стандарте ECMA-262-3.
        +2
        спасибо — обязательно прочитаю. сейчас просмотрел где-то до середины — интересно :)
        +6
        Объявить такую функцию можно двумя, по сути, эквивалентными способами.
        На самом деле, способов существенно больше. Правда, использовать их я бы не советовал.
        +function(){console.log(5)}() // подставьте свой любимый оператор
        1,function(){console.log(5)}() // подставьте своё любимое число, строку или ещё какое-нибудь выражение
        [function(){console.log(5)}()]


        Общее же правило таково: если функция является частью выражения, то она становится FunctionExpression и является выражением, т.е. её можно вызвать. Если же она не является частью выражения, то она является FunctionDeclaration и вызову не поддается. Например:
        function fnc(){} // FunctionDeclaration
        fnc = function(){} // FunctionExpression
        Отсюда же получаем, что следующая запись вполне корректна
        fnc = function(){return 5}()

        Но лично я не советую её применять, поскольку для очень длинных функций становится не очевидно, что будет результатом выражения — то ли функция, то ли её значение (по-умолчанию предполагается первое).

        По этой теме советую почитать статью на javascript.ru.
          0
          Ну и да: тут скорее не объявление, а именно использование.
            0
            я может себе неправильно все это представляю но все эти три примера это по сути второй способ. + одновременно изварщение, специально для этогои написал «Если не обращать внимание на всякие извращения» :)

            идея была просто в том чтобы показать какраз Function declaration и Function Expression так чтобы на практике было понятно. может не лучшим образом сделал конечно :)
              0
              Все эти способы ничем между собой не отличаются, т.к. это всё один и тот же случай с FunctionExpression с различными способами получения выражения.
            –1
            > Правда, использовать их я бы не советовал.
            Мне тильда (битовый NOT) нравится в этом плане :)
            0
            Ох, а у меня есть еще столько интересных вопросов, но я… боюсь спросить.
              0
              спрашивайте :) если знаю и где-нибудь не всплывет какойнибудь косяк, то отвечу :)
              +1
              «Этого нельзя понять, это можно только запомнить!»
                +1
                Self defining function — это страшно читаемая вещь, а в обфусцированном коде это вообще как выстрел в голову!
                  +2
                  Он на то и обфусцирован, чтобы стрелять в голову.
                    0
                    Выстрел в голову с рикошетом в выстрелившую ногу.
                    Левую.
                    +1
                    А не будет ли правильней в последнем примере с Currying делать
                    return fn.apply( this, args );

                    ?

                    Чтобы правильно обрабатывался this и можно было использовать currying и для методов
                      0
                      спасибо огромное. так будет лучше. поправил :)
                        0
                        > Чтобы правильно обрабатывался this
                        Боюсь, что невероятные похождения this заканчиваются только с кофескриптом
                        0
                        Self defining function — до этой статьи, если бы я увидел подобный код, врядли бы догадался о намерениях писавшего. Так что… применение под вопросом. Но решение да, элегантное.
                          +5
                          Не коснулись такой интересной темы как объявление рекурсивных функций. Например, у нас есть тупо факториал, который используется в куче кода:
                          var factorial = function (n) {
                              return n === 1 ? 1 : factorial(n - 1) * n;
                          };
                          

                          ну или
                          function factorial(n) {
                              return n === 1 ? 1 : factorial(n - 1) * n;
                          };
                          

                          Потом по каким-то причинам нам нужно
                          var realFactorial = factorial;
                          factorial = function (n) {
                              throw 'please use realFactorial instead';
                          };
                          

                          В итоге получается, что и вызовы factorial(5) и realFactorial(5) вываливают ошибку. Это получается, т.к. рекурсивный вызов factorial использует переменную из родительской области видимости, а там она переопределяется. В этом случае надо определять функцию так:
                          var factorial = function factorial (n) {
                              return n === 1 ? 1 : factorial(n - 1) * n;
                          };
                          

                          Ну или чтобы не путаться:
                          var factorial = function f (n) {
                              return n === 1 ? 1 : f(n - 1) * n;
                          };
                          
                            +1
                            спасибо огромное — добавил в статью :)
                            +2
                            var saySomethingClever;
                            
                            (function(){
                                var appleTest = /Apple/i;
                                var googleTest = /Google/i;
                            
                                if( appleTest.test(navigator.vendor) ) 
                                    saySomethingClever = function(){ console.log("I love apples <3"); }
                                else if( googleTest.test(navigator.vendor) )
                                    saySomethingClever = function(){ console.log("android is everything for me <3"); }
                                else saySomethingClever = function(){ console.log("i love this unpopular corporation too"); }
                            })();
                            
                            saySomethingClever();


                            Пример тоже так себе. Тогда уж так:

                            var saySomethingClever = (function(){
                                var appleTest = /Apple/i;
                                var googleTest = /Google/i;
                            
                                if( appleTest.test(navigator.vendor) ) 
                                    return function(){ console.log("I love apples <3"); }
                                else if( googleTest.test(navigator.vendor) )
                                    return function(){ console.log("android is everything for me <3"); }
                                else
                                    return function(){ console.log("i love this unpopular corporation too"); }
                            })();
                            
                            saySomethingClever();


                            Или, даже так:

                            var saySomethingClever = (function(){
                                var appleTest = /Apple/i,
                                    googleTest = /Google/i;
                            
                                return appleTest.test(navigator.vendor) ? function(){ console.log("I love apples <3")                      } : 
                                      googleTest.test(navigator.vendor) ? function(){ console.log("android is everything for me <3")       } :
                                                                          function(){ console.log("i love this unpopular corporation too") } ;
                            })();
                            
                            saySomethingClever();


                            Раз уж рассказываете про такую тему, то сразу можно было привести пример этого-же без copy-paste:

                            var saySomethingClever = (function(){
                                var appleTest = /Apple/i,
                                    googleTest = /Google/i,
                                    log = function (message) {
                                        return function(){ console.log(message) };
                                    };
                            
                                return appleTest.test(navigator.vendor) ? log("I love apples <3"): 
                                      googleTest.test(navigator.vendor) ? log("android is everything for me <3"):
                                                                          log("i love this unpopular corporation too");
                            })();
                            
                            saySomethingClever();


                            Как-то так, в общем.
                              +1
                              Т.к. третий пример поехал из-за ширины — его можно записать так:

                              var saySomethingClever = (function(){
                                  var appleTest = /Apple/i,
                                      googleTest = /Google/i;
                              
                                  return appleTest.test(navigator.vendor) ?
                                             function(){ console.log("I love apples <3")                      } : 
                                        googleTest.test(navigator.vendor) ?
                                             function(){ console.log("android is everything for me <3")       } :
                                             function(){ console.log("i love this unpopular corporation too") } ;
                              })();
                              
                              saySomethingClever();
                                0
                                первый пример действительно лучше. на мой взгляд с тренарным оператором код становится в данном случае менее читаемым. а вариант с функцией log хорош в этом случае но в качестве общего примера он неподходит, потому что если в ветках есть какиенибудь отличчия — он летит в трубу.
                                  0
                                  Мне всегда была непонятна поголовная боязнь тернарного оператора. Он прекрасен для своих целей.
                                    0
                                    я специально упомянул что в данном случае. возможно это мой пунктик но мне неочень нравится возвращать с его помощью большой кусок кода. тем более что реальных преимуществ при этом никаких
                              0
                              спасибо огромное — заменил пример в саттье на немного исправленный ваш первый.
                                0
                                Спасибо, очень интересно и позновательно. Но использовать их надо аккуратно, а-то потом коллеги по работе будут мучаться, если они не читали этот пост. :)
                                  0
                                  console.log( "sample b" );
                                  b();
                                  
                                  function b() { console.log(1); }

                                  «ну хоть ты тресни», но FF и Chrome показали один и тот же результат
                                  > sample b
                                  > 1
                                  даже если определение функции b() поставить в самом начале
                                    0
                                    ну да. я вроде бы так и написал, незаню как можно лучше было сформулировать. смысл примера b показать что в случае объявления именованной функции к ней можно обратиться даже до появления непосредственно строки с обьявлением.
                                      0
                                      Надо так и формулировать, что вызов функции можно осуществлять до её объявления.
                                      Хотя тот же FF сказал «ай-яй-яй»:
                                      ReferenceError: b is not defined
                                      Chrome скушал.

                                      Лично я понял, что вы примером пытались показать, что «1» выведется раньше, чем «sample b»
                                        0
                                        пс. я с соседом читали статью и поняли одинаково. Может всё-таки дело в формулировке?
                                          0
                                          Спасибо сейчас уточню формулировку :)
                                          по поводу второго коментария — фаерфокс неможет это нескушать, боюсь потому что это ожидаемое поведение от любого браузера :)
                                          впрочем если вы вбивали это непосредственно через консоль фаербага то да могли быть проблемы.
                                            +1
                                            угу в firebug. Даже если выполняется eval() разве должа быть какая-то разница с обычным кодом, запускаемым из загруженного файла?
                                              0
                                              сказать по правде я с этим особенно не разбирался. в ближайшем будушем попробую это исправить :) но чисто эмпирически — мне кажется вполне — всетаки поведение именованных функций не совсем обычное. ну и фаербаг не всегда себя идеально ведет так что он может :)
                                                0
                                                я тут посмотрел на сообщение об ошибке в firebug. скорее всего при прогоне через консоль в фаербаге функция превращается в именованный function expression( это второй вариант в этой статье, только у меня он без имени, думал что так проще будет но вот этот момент упустил из виду конечно. )
                                                ну вот типа этого a = function b() { console.log(2); }. и тогда она ведет себя как положенно для этого случая, но не так как мы ожидаем
                                      +1
                                      Карринг для замыкания строки из двух букв даёт сомнительную экономию текста за счёт производительности.
                                      Вот еслиб там был объект Author и в console.log передавалось author.toString() — жаверы 90ых бы заценили.
                                        +4
                                        А ещё можно соорудить и само-пере-определяемую функцию, если её замкнуть в саму себя…
                                        /** usage: 
                                           * yield(N) -- (re)start yielding song  
                                           * yield() -- continue/loop yielding 
                                           */
                                        var yield = function(more) {
                                            more = more||Math.ceil(Math.random()*3);
                                            (function(jack){
                                                yield = function(nomore) {
                                                    if( nomore || !more--) return (yield = jack)();            
                                                    return "No more!";
                                                    }
                                            })(yield);
                                            return "Hit the road, Jack! And don't you come back!";
                                        }
                                        
                                          0
                                          это супер — мне нравится :))))
                                          0
                                          var f = function() { console.log(1); }
                                          f();

                                          function f(){ console.log(2); }
                                          f();

                                          выдаст 1 и 2, а не две единицы, кстати
                                            +1
                                            а, не, туплю — все верно
                                            тут хук фбага
                                              0
                                              отлично :) выше просто уже были проблемы с этим — надо бы про фаербаг написать наверно.
                                              0
                                              вы, кстати, ошибаетесь. при выполнении через консоль может быть такой косяк потому что функция превращается в фанкшен экспрешен. каждый тест перед постом я проверяю как мнимум в двух браузерах.
                                              0
                                              Очень полезно и познавательно. Спасибо

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое