Магия JavaScript: arguments

    arguments — очень специфическая штука, о которой новички и даже любители знают только то, что это «вроде массив, но какой-то неправильный». На самом деле, у него есть ряд интересных особенностей. Предлагаю в топике пофантазировать на тему TypeHinting, аргументов по-умолчанию и всякого другого.
    (function (foo, bar) {
    	console.log(typeof arguments); // ?
    	
    	arguments[0] = 42;
    	console.log(foo); // ?
    })(10, 20);
    


    А также покажу интересную идею-библиотеку
    function test (foo, bar) {
        Args(arguments).defaults(100, 100);
    
        return [foo, bar];
    };
    
    test(      ); // 100, 100
    test(15    ); //  15, 100
    test(21, 42); //  21,  42
    



    В первую очередь хотел бы заметить, что множество идей высказанных в топике являются достаточно спорными. Я сам не уверен, что буду ими пользоваться и не советую пользоваться новичкам.

    Что такое arguments


    Это хэш. Обычный хэш, как var object = {}
    (function () { console.log(
    	typeof arguments, // object
    	Object.getPrototypeOf(arguments) == Object.prototype // true
    ) })();
    


    Сделать из него массив просто:
    var array = Array.prototype.slice.call(arguments, 0);
    // или покороче, но менее производительно:
    var array = [].slice.call(arguments, 0);
    


    Мы вызываем метод slice прототипа Array от лица arguments.

    Что есть в arguments


    arguments.length — количество аргументов, переданных в функцию.
    var count = function () {
    	console.log(arguments.length);
    };
    
    count(); // 0
    count(first, second); // 2
    


    Не забывайте, что у каждой функции тоже есть свойство length, которое указывает на то, сколько элементов объявлено в её заголовке:

    function one   (foo) {};
    function three (foo, bar, qux) {};
    
    console.log(  one.length); // 1
    console.log(three.length); // 3
    


    arguments.callee — ссылка на саму функцию.

    function foo () {
    	console.log(arguments.callee === foo); // true
    }
    


    Таким образом можно проверить, передано ли правильное количество элементов, или нет:

    function test (foo, bar, qux) {
    	return arguments.callee.length === arguments.length;
    }
    
    test(1); // false
    test(1,2,3); // true
    


    Аргументы в arguments


    В arguments содержится также список переданных аргументов.
    function test (foo, bar) {
    	console.log(foo, bar); // 'a', 'b'
    	console.log(arguments[0], arguments[1]); // 'a', 'b'
    }
    test('a', 'b');
    


    Теперь к интересному. Многие не знают, что объект arguments — содержит на самом деле ссылки, а не значения, и тесно связан с аргументами:
    (function (foo) {
    	arguments[0] = 42;
    	console.log(foo); // 42!
    	
    	foo = 20;
    	console.log(arguments[0]); // 20
    })(5);
    


    При этом связь достаточно крепкая:

    function foo (qux) {
    	change(arguments);
    	return qux;
    };
    
    function change(a) {
    	a[0] = 42;
    }
    
    foo(10); // 42
    


    Что из этого можно получить?


    Во многих языках программирования есть «переменные по-умолчанию». К примеру, php:
    function ($foo = 30, $bar = 'test') {
    	var_dump($foo);
    	var_dump($bar);
    }
    


    В javascript оно будет выглядеть как-то так:
    function (foo, bar) {
    	if (typeof foo === 'undefined') foo = 30;
    	if (typeof bar === 'undefined') bar = 'test';
    	
    	console.log(foo, bar);
    }
    


    Зная особенности arguments можно создать красивый интерфейс:
    function test(foo, bar) {
    	Args(arguments).defaults(30, 'test');
    	
    	console.log(foo, bar)
    }
    
    test(); // 30, 'test'
    


    С помощью такого кода:
    
    function Args (args) {
    	if (this instanceof Args) {
    		this.args = args;
    	} else {
    		// Если создано не через new, а просто вызвана функция, создаем и возвращаем новый объект
    		return new Args(args);
    	}
    };
    Args.prototype = {
    	defaults: function () {
    		var defaults = arguments;
    		for (var i = defaults.length; i--;) {
    			if (typeof args[i] === 'undefined') args[i] = defaults[i];
    		}
    		return this;
    	}
    };
    


    Аналогично можно сделать автоматическое приведение типов:

    function test(foo) {
    	Args(arguments)
    		.defaults(10)
    		.cast(Number);
    	
    	console.log(foo)
    }
    
    test('0100'); // 100
    


    Или Type Hinting:

    function test(foo, bar) {
    	Args(arguments).types(Foo, Bar);
    	
    	// code
    }
    
    test(new Foo(), new Bar());
    test(1, 2); // Error
    


    Из интересных идей — сообщение, что все аргументы обязательны:

    function test (foo, bar, qux) {
    	Args(arguments).allRequired();
    }
    
    test(1,2,3); // success
    test(1,2); // Error: 3 args required, 2 given
    


    Заключение


    Все эти идеи и возможности (и даже больше) я оформил в библиотеку — Args.js.
    Согласен, что кое-какие вещи (как TypeHinting) не совсем подходят к идеологии языка. В то же время например defaults — очень удобная штука, имхо.
    Пока что это прототип и, перед тем как вы будете его использовать — будьте уверены, что оно вам действительно нужно, а не что вы просто стараетесь из прекрасного языка сделать что-то похожее на C#.
    Предлагаю обсудить, покритиковать код, найти пару багов и закоммитить несколько строк кода)

    Args.js



    К сожалению, из-за бага в трёх популярных браузерах(IE, Fx, Opera) я не смог добиться желаемого эффекта, полноценно самое вкусное заработало только в Chrome (ну по крайней мере в node.js работать будет)). Надеюсь, решим эту проблему вместе.

    UPD: В комментах выяснили, что таки это бага Хрома, но, зато, какая приятная! Спасибо jamayka
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 37
    • –1
      полезная вещь, буду пользоваться. спасибо!
      • +3
        Array.prototype.slice.call можно вызывать и без 0:
        Array.prototype.slice.call(arguments);
        • +6
          Вот этот код дефолтных значений аргументов:
          function (foo, bar) {
          if (typeof foo === 'undefined') foo = 30;
          if (typeof bar === 'undefined') bar = 'test';

          console.log(foo, bar);
          }


          Можно написать гораздо проще (:
          function (foo, bar) {
          foo = foo || 30;
          bar = bar || 'test';

          console.log(foo, bar);
          }
          • +2
            >>foo || 30. В этом случае у вас foo = 0 пролетает.
            • –1
              Да, не учел. Но для str или array подходит (:
              • +2
                тогда короче foo ||=30;
                • 0
                  SyntaxError: syntax error ;)
                  • +1
                    блин не интересно, в ActionScript работает
            • +4
              а если в качестве foo 0 передать он не станет 30?
              • 0
                никогда так не делайте.
                0, '', false приведут к фейлу.
              • +2
                К сожалению, из-за бага в трёх популярных браузерах(IE, Fx, Opera) я не смог добиться желаемого эффекта

                О каком баге идет речь? По-моему, наоборот упомянутые браузеры более строго соответствуют спецификациям ECMA.
                function test(a, b) {
                    arguments[0] = 100;
                    arguments[1] = 101;
                    console.log(a,b);
                }
                test(1,2); // => 100, 101
                test(1); // => 100, undefined
                

                Потому как динамическая связь устанавливается только для реально переданных аругментов:
                (...) named data properties of an arguments object whose numeric name values are less than the number of formal parameters of the corresponding function object initially share their values with the corresponding argument bindings in the function’s execution context. This means that changing the property changes the corresponding value of the argument binding and vice-versa. (...)

                До этой фразы идет подробное описание как строить этот объект «arguments» — формальные аргументы не переданные фактически в «arguments» не попадают.
                • 0
                  Да, увидел в посте ссылку на баг в файерфоксе. Во-первых, он давно уже пофиксан, а во-вторых, он совершенно не имеет отношения к вашему коду.
                  • 0
                    Я рад, что затеяли это обсуждение, очень хотел бы определится, кто таки неправ из браузеров.
                    На самом деле я спецификацию читал и, признаюсь, немного в ней запутался, но позвольте проясню.
                    Если вы не против, будем обсуждать перевод.

                    У функции есть аргументы формальные и актуальные. Если создавать функцию через конструктор:
                    new Function('a', 'b', 'return a+b');
                    


                    То формальные аргументы — это «a» и «b». (и они не зависят от того, как функцию вызывали):
                    Если более чем один параметр передаётся конструктору Function, все параметры кроме последнего преобразовываются в строки и конкатенируются вместе с использованием запятых в качестве разделителя. Результирующая строка интерпретируется как СписокФормальныхАргументов для ТелаФункции, определённого последним параметром.


                    Я сходу не нашел такое же определение для создания функции не-через-конструктор, но, можно логически предположить, что оно не отличается. Значит, формальные аргументы — это аргументы, объявлены в загоовку функции. Теперь идем дальше:

                    Для каждого неотрицательного числа arg, меньшего значения свойства length создаётся свойство с именем ToString(arg) и атрибутом { DontEnum }. Начальным значением этого свойства является реальное значение соответствующего аргумента, переданное при вызове. Первое реальное значение аргумента соответствует arg = 0, второе — arg = 1 и так далее. В том случае, когда arg меньше количества формальных параметров объекта Function, значение свойства является общим с соответствующим свойством объекта активации. Это означает, что изменение данного свойства изменяет соответствующее значение свойства у объекта активации и наоборот.


                    Попробую написать псевдокод:
                    for (arg = 0; arg < func.length; arg++) {
                        link(arguments[i], formalParameters[i]);
                    }
                    


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

                    Так я понял этот момент. Поправьте меня, если где-то ошибся.

                    Ссылку на баг дал не просто так. На mdc написано:
                    Note: The SpiderMonkey JavaScript engine has a bug in which arguments[n] cannot be set if n is greater than the number of formal or actual parameters. This has been fixed in the engine for JavaScript 1.6.

                    Согласен, баг другой, раньше, я так понял не менялось даже значение arguments[i], но в 4 часа ночи они показались очень похожими)
                    • 0
                      Почитайте тут на странице 60. Ваш псевдокод должен быть:
                      for (var i = arguments.length; i--; ) {
                          link(arguments[i], formalParameters[i]);
                      }
                      
                      • 0
                        Согласен. Вроде, согласно ES5 оно должно считаться от аргументов.
                        Просто любопытно, как считаете, согласно ES3 — я все правильно понял?
                        • 0
                          Понял, в чем ваше заблуждение:
                          Для каждого неотрицательного числа arg, меньшего значения свойства length создаётся свойство с именем (...)

                          Здесь имеется ввиду свойство length созданное на предыдущем шаге. Т.е. не свойство функции, а свойство только что созданного оьъекта arguments. В ES3 описание просто немного более расплывчатое, но, по-моему, все равно вполне однозначное.
                          • 0
                            Точно, вы правы:
                            Создаётся свойство с именем length и атрибутами { DontEnum }. Начальным значением этого свойства является число реальных значений аргументов, переданное при вызове.
                            • +3
                              Описание семантики объекта arguments в ES5/non-strict ES5 JS кодом: gist.github.com/539974

                              В Хроме помимо упоминавшегося бага, был еще баг с удалением индексов arguments.

                              Так же, учтите, что в strict-ES5 аксессор для индексов arguments больше не создается (т.е. обычные статические копии формальных параметров).

                              И, касательно Harmony (aka ES6 или ES.next) arguments вообще будет удален и заменен на rest — полноценный массив.
                  • 0
                    Да, вы правы — баг именно в Chrome :)
                    • 0
                      Баг, но какой приятный!
                  • +1
                    По-моему, этот фукнционал уместнее расположить в прототипе фукнции.

                    Чтобы типа
                    var myFunc = function(foor, bar){ console.log(arguments); }.types(Foo, Bar).cast(Foo, Bar).allRequired();
                    
                    • 0
                      А allRequired как сделать, тем более ему передастся функция, которая требует 0 аргументов?)
                      Проблема этого подхода в том, что при относительно больших функциях не видно, что происходит с аргументами. Если изучать верх функции, можно пропустить, скажем, кастинг или дефолтные значения.
                      • 0
                        Function.prototype.allRequired = function(){
                          var f = this, validLength = f.length;
                          return function(){
                            if (arguments.length >= validLength)
                              return f.call(this, arguments);
                            else
                              throw new Error('oh Shit..');
                          }
                        };
                        


                        Или я вопрос не правильно понял?
                        • 0
                          только там не .call, а .apply.
                          • 0
                            (function () {
                            	// code
                            }.cast(Number).allRequired());
                            
                            Function.prototype.cast = function (types) {
                            	var fn = this;
                            	return function () { // cast inner function
                            		// casting arguments
                            		return fn.call(this, arguments);
                            	};
                            };
                            
                            Function.prototype.allRequired = function () {
                            	var fn = this; // оно ссылается не на нужную функцию, а на cast inner function
                            	return function () {
                            		
                            	}
                            };
                            
                            • 0
                              Хотя, возможно, можно делать как-то так:
                              Function.prototype.cast = function (types) {
                              	var fn = this;
                              	var result = function () { // cast inner function
                              		// casting arguments
                              		return fn.call(this, arguments);
                              	};
                              	result.length = fn.length;
                              	return result;
                              };
                              
                              • 0
                                Неа, так не работает.

                                Если только делать
                                result._length = fn._length || fn.length;

                                в каждой обертке.
                                • 0
                                  Вот еще придумался вариант с сохранением нативного length: gist.github.com/934203

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

                          var find= Types
                          ( HTMLElement
                          , [ String ]
                          , fucntion( id ){ return document.getElementById( id ) }
                          )

                          var check= Types
                          ( HTMLElement
                          , [ HTMLElement ]
                          , fucntion( elem ){ elem }
                          )

                          var $= Poly( find, check )
                          • +1
                            Часто, когда необходимы значения по умолчанию — в функцию передается объект.

                            function test(options)
                            {
                            if(options.foo===default)options.foo=30;
                            if(options.bar===default)options.bar='test';
                            console.log(options.foo,options.bar);
                            }


                            Для такого случая тоже можно написать аналог вашей функции, устанавливающей значение по умолчанию — и у такого подхода есть свои очевидные преимущества.
                            • +6
                              В strict mode такое работать уже не будет, т.к. объект arguments не будет иметь связи с формальными параметрами, соответственно даже переданный аргумент нельзя изменить.
                              • +2
                                Чтобы использовать обертку надо привыкнуть к ней, внедрить, научить других — все как правило очень долго. Мне обычный подход милее и он не менее нагляднее, да и если ещё снабдить код хорошим JSdoc'ом с описанными дефалтными значениями, который понимает любой современный IDE, то будет совсем хорошо (улучшу немного код omfg):
                                /**
                                 * Pewpew
                                 * 
                                 * @param {Object}   [foo]
                                 * @param {Mixed}    [foo.smth]
                                 * @param {Boolean}  [bar=true]
                                 * @param {String}   [baz='pewpew']
                                 * @param {Boolean}  [bar=false]
                                 *
                                 * @returns {Mixed}
                                 */
                                function pewpew(foo, bar, baz, qqq) {
                                    foo = foo || {};           // default empty object
                                    bar = bar || bar == null;  // default true (можно не использовать typeof bar)
                                    baz = baz || 'pewpew';     // default some string
                                    qqq = qqq || false;        // default false
                                
                                    if (foo.smth) {
                                        do();
                                    }
                                }
                                

                                Ну и не забываем, что грядет "strict mode"
                                • +2
                                  Добрый день, подскажите где можно прочитать про strict mode на русском и что это вообще?
                                • +1
                                  «strict mode» — это да.
                                  подход такой — не совсем правильный, что делать, если я хочу в качестве строки baz передать пустую строку?

                                  Хотя, конечно, можно писать так:
                                  /**
                                   * Pewpew
                                   * 
                                   * @param {Object}   [foo]
                                   * @param {Mixed}    [foo.smth]
                                   * @param {Boolean}  [bar=true]
                                   * @param {String}   [baz='pewpew']
                                   * @param {Boolean}  [bar=false]
                                   *
                                   * @returns {Mixed}
                                   */
                                  function pewpew(foo, bar, baz, qqq) {
                                      foo = foo != null ? foo : {};        // default empty object
                                      bar = bar != null ? bar : true;      // default true
                                      baz = baz != null ? baz : 'pewpew';  // default some string
                                      qqq = qqq != null ? qqq : false;     // default false
                                  
                                      if (foo.smth) {
                                          do();
                                      }
                                  }
                                  
                                  


                                  А в таком коде уже было бы красиво:
                                  /**
                                   * Pewpew
                                   * 
                                   * @param {Object}   [foo]
                                   * @param {Mixed}    [foo.smth]
                                   * @param {Boolean}  [bar=true]
                                   * @param {String}   [baz='pewpew']
                                   * @param {Boolean}  [bar=false]
                                   *
                                   * @returns {Mixed}
                                   */
                                  function pewpew(foo, bar, baz, qqq) {
                                      Args(arguments).defaults({}, true, 'pewpew', false)
                                  
                                      if (foo.smth) {
                                          do();
                                      }
                                  }
                                  
                                  


                                  Хотя, в целом, с аргументами согласен.

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