Мастер-классы по Javascript Екатеринбург Ростов-на-Дону Москва Узнать больше...
Содержание (скрыть) Содержание (показать)

Аргументы функций

  1. Доступ к «лишним» аргументам
    1. arguments — не Array
    2. Использование arguments как Array
    3. Превращение arguments в настоящий Array
    4. Функция copy с переменным числом объектов
  2. Передача вызова при помощи call/apply
  3. Значения по умолчанию
  4. Именованные аргументы
  5. Особые свойства arguments
    1. arguments.callee
    2. arguments.callee.caller
  6. Почему callee и caller устарели?

В JavaScript любая функция может быть вызвана с произвольным количеством аргументов.

Например:

function go(a,b) {
  alert("a="+a+", b="+b);
}

go(1)     // a=1, b=undefined
go(1,2)   // a=1, b=2
go(1,2,3) // a=1, b=2, третий аргумент не вызовет ошибку

Отсутствующие аргументы становятся undefined. Таким образом, мы можем легко проверить, был ли передан аргумент:

function check(x) {
  if(x === undefined) {
    alert('функция вызвана без аргументов');
  }
}

check();

В JavaScript нет «перегрузки» функций

В некоторых языках, программист может создать две функции с одинаковым именем, но разным набором аргументов, а при вызове интерпретатор сам выберет нужную:

function log(a) {
  ...
}

function log(a,b,c) {
  ...
}

*!*
log(a); // вызовется первая функция
log(a,b,c); // вызовется вторая функция
*/!*

Это называется «полиморфизмом функций» или «перегрузкой функций». В JavaScript ничего подобного нет.

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

В примере выше второе объявление log просто переопределит первое.

Доступ к «лишним» аргументам

Как получить значения аргументов, которых нет в списке параметров?

Доступ к ним осуществляется через «псевдо-массив» arguments.

Он содержит список аргументов по номерам: arguments[0], arguments[1]…, а также свойство length.

Например, выведем список всех аргументов:

function sayHi() {
  for (var i=0; i<arguments.length; i++) {
    alert("Привет, " + arguments[i]);
  }
}
 
sayHi("Винни", "Пятачок");  // 'Привет, Винни', 'Привет, Пятачок'

Все параметры находятся в arguments, даже если они есть в списке. Код выше сработал бы также, будь функция объявлена sayHi(a,b,c).

arguments — не Array

Частая ошибка новичков — попытка применить методы Array к arguments. Это невозможно:

function sayHi() {
  var a = arguments.shift(); // error! arguments is not Array
}

sayHi(1);

Дело в том, что arguments - это не массив Array. Но если это не массив, тогда что же это? Попробуем использовать свойство [[Class]] для проверки:

(function() {

  alert( {}.toString.call(arguments) );  // [object Arguments] 

})();

Тип Arguments — это обычный Object, просто называется по-другому. Никаких особых методов у него нет.

Использование arguments как Array

Существует способ вызывать методы Array для arguments. Например, техника одалживания метода (на англ. method borrowing).

Возьмем метод join из массива и вызовем его в контексте arguments при помощи call:

function sayHi() {
  var join = [].join; // одолжили метод

*!*
  // вызовем join с this=arguments, как если бы был метод arguments.join(':')
  var argStr = join.call(arguments, ':'); 
*/!*

  alert(argStr);  // сработает и выведет 1:2:3
}

sayHi(1, 2, 3);

Здесь метод join массива вызван в контексте arguments. Не произойдёт ли что-то плохое от того, что arguments — не массив? Почему он, вообще, сработал?

Ответ на эти вопросы простой. В соответствии со спецификацией, внутри join реализован примерно так:

function join(separator) {
  if (!this.length) return '';

  var str = this[0]; 

  for (var i = 1; i<this.length; i++) {
    str += separator + this[i]; 
  }
  
  return str;
}

Как видно, используется this, числовые индексы и свойство length. Если эти свойства есть, то все в порядке. А больше ничего и не нужно. Подходит даже обычный объект:

var obj = {  // обычный объект с числовыми индексами и length
  0: "А", 
  1: "Б", 
  2: "В",
  length: 3
};

*!*
alert( [].join.call(obj, ";") ); // "A;Б;В"
*/!*

Как в функции отличить отсутствующий аргумент от undefined?

function f(x) { 
  // ..ваш код..
  // выведите 1, если первый аргумент есть, и 0 - если нет
}

f(undefined); // 1
f(); // 0

Решение
Решение

Узнать количество реально переданных аргументов можно по значению arguments.length:

function f(x) {
  alert(arguments.length ? 1 : 0);
}

f(undefined);
f();

Превращение arguments в настоящий Array

Бывает удобно вместо запуска методов в другом контексте - просто создать новый массив из arguments. В JavaScript есть очень простой способ это сделать.

Метод массивов arr.slice(start, end) копирует часть массива arr от start до end в новый массив. Вызовем его в контексте arguments:

function sayHi() {
  // вызов arr.slice() без аргументов копирует все элементы из this в новый массив
  var args = [].slice.call(arguments);
 
  alert( args.join(':') ); // теперь .join работает
}

sayHi(1,2);

Связь между arguments и параметрами

Псевдо-массив arguments и переменные-параметры ссылаются на одни и те же значения.

Обновление arguments[..] меняет параметры и наоборот. Например:

function f(x) {
  arguments[0] = 5; // меняет переменную x
  alert(x); // 5
} 

f(1);

Наоборот:

function f(x) {
  x = 5; 
  alert(arguments[0]); // 5, обновленный x 
} 

f(1);

В современной редакции стандарта это поведение изменено. Аргументы отделены от локальных переменных:

function f(x) {
  "use strict"; // для браузеров с поддержкой строгого режима

  arguments[0] = 5;
  alert(x); // не 5, а 1! Переменная "отвязана" от arguments
} 

f(1);

Если вы не используете строгий режим, то чтобы переменные не менялись «неожиданно», рекомендуется никогда не изменять arguments. Лучше сделать из них массив вызовом [].slice.call(arguments) и менять его.

Функция copy с переменным числом объектов

Иногда встаёт задача — расширить существующий объект, используя один или несколько других.

Напишем для этого функцию copy с переменным числом аргументов.

Синтаксис:

copy(dst, src1, src2…)
Копирует свойства из объектов src1, src2,... в объект dst. Возвращает получившийся объект.

Использование для расширения объекта:

var user = { 
  name: "Вася",
};

// добавить свойства
*!*
copy(user, { 
  age: 25, 
  surname:"Петров" 
}); 
*/!*

Использование для нерекурсивного клонирования объекта:

// скопирует все свойства в пустой объект
var clone = copy({}, user);

Первый аргумент у copy всегда есть, поэтому укажем его в определении. А остальные будем получать из arguments:

function copy(dst) {
  for (var i=1; i<arguments.length; i++) {
    var obj = arguments[i];
    for (var key in obj) {
      dst[key] = obj[key];
    }
  }
  return dst;
}

При желании, такое копирование можно реализовать рекурсивно.

Передача вызова при помощи call/apply

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

Код, который это делает, независимо от аргументов: anotherFunc.apply(this, arguments).

В примере ниже вызов user.log(...) передаёт всю работу logService.add(...):

var user = {
  log: function() {
*!*
    logService.add.apply(logService, arguments);
*/!*
  }
}

var logService = {
  add: function() {
    alert( this.format(arguments) );
  },

  format: function(args) {
    return [].join.call(args, ' ');
  }
}

user.log("Вася", "сказал", "что-то умное..");

Обратите внимание, что вызов logService.add.apply(logService, arguments) передал аргументы в функцию logService.add, но при этом сохранил this.

Иногда this нужно тоже передать текущий. В этом случае можно использовать вызов logService.add.apply(this, arguments).

Значения по умолчанию

Значения по умолчанию нужны там, где аргумент может отсутствовать. Такие аргументы желательно располагать после обязательных. Например, функция показа сообщения showMessage может быть вызвана как showMessage(text, title) или showMessage(text).

Если title не передан, то по умолчанию title = "Сообщение".

  1. Первый способ указать значение по умолчанию для title - явно проверить на undefined и переназначить:
    function showMessage(text, title) {
      if (title === undefined) title = 'Сообщение'; 
        
      ...
    }
    
  2. Второй - использовать оператор ||:

    function showMessage(text, title) {
      title = title || 'Сообщение'; 
    
      ... 
    }
    
    Если title передан и является true в булевом контексте — он не изменится. В противном случае будет присвоено значение по умолчанию.

    При этом параметр будет считаться непереданным, если он в булевом контексте дает false. Например при передаче пустой строки или нуля будет использовано значение по умолчанию.

Переменные-параметры лучше не менять

За исключением присвоения значений по умолчанию, переменные, в которых находятся входные параметры функции, лучше не менять.

Это в первую очередь относится к «повторному использованию», когда программист, вместо того чтобы объявить новую переменную, использует существующую.

Получается — как коробка, в которую кидают то одно, то другое, то третье, при этом не меняя название. Что в ней лежит сейчас? А кто его знает.. Нужно подойти, проверить. Сэкономили время на объявлении переменной — потеряли на отладке.

Именованные аргументы

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

Например:

function showWarning(width, height, title, contents, showYesNo) {
  width = width || 200; // почти все значения - по умолчанию
  height = height || 100;
  
  var title = title || "Предупреждение";

  ...
}

Функция showWarning позволяет указать ширину и высоту width, height, заголовок title, содержание contents и создает дополнительную кнопку если showYesNo == true. Большинство этих параметров имеют значение по умолчанию.

В примере выше значения по умолчанию:

  • width = 200,
  • height = 100,
  • title = "Предупреждение".

Если необязательный параметр находится в середине списка аргументов, то для передачи «значения по умолчанию» обычно используют null:

// width, height, title - по умолчанию
showWarning(null, null, null, "Warning text", true);

Неудобство такого вызова заключается в том, что порядок аргументов легко забыть или перепутать. Кроме того, «дырки» в списке аргументов - это не красиво.

Обычно необязательные параметры переносятся в конец списка, но если таких большинство, то это невозможно.

Для решения этой проблемы в Python, Ruby и многих языках существуют именованные аргументы (keyword arguments, named arguments).

В JavaScript именованные параметры реализуются при помощи объекта. Вместо списка аргумента передается объект с параметрами, вот так:

function showWarning(options) {
  var width = options.width || 200;  // по умолчанию
  var height = options.height || 100;
  
  var title = options.title || "Предупреждение";

  // ...
}

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

showWarning({ 
  contents: "Вы вызвали функцию", 
  showYesNo: true
});

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

var opts = {
  width: 400,
  height: 200, 
  contents: "Текст", 
  showYesNo: true
};

showWarning(opts);

opts.contents = "Другой текст"; 

*!*
showWarning(opts); // не нужно копировать остальные аргументы в вызов
*/!*

Именованные аргументы применяются в большинстве JavaScript-фреймворков.

Особые свойства arguments

Объект arguments не только хранит список аргументов, но и обеспечивает доступ к ряду интересных свойств.

arguments.callee

Свойство arguments.callee содержит ссылку на функцию, которая выполняется в данный момент.

Это свойство устарело. Современная спецификация рекомендует использовать именованные функциональные выражения (NFE).

Браузеры могут более эффективно оптимизировать код, если arguments.callee не используется.

Тем не менее, свойство arguments.callee зачастую удобнее, так как функцию с ним можно переименовывать как угодно и не надо менять ничего внутри. Кроме того, NFE некорректно работают в IE<9.

Например:

function f() {
  alert( arguments.callee === f); // true
}

f();

Зачем нужно делать какое-то свойство callee, если можно использовать просто f? Чтобы это понять, рассмотрим несколько другой пример.

В JavaScript есть встроенная функция setTimeout(func, ms), которая вызывает func через ms миллисекунд, например:

// выведет 1 через 1000 ms (1 секунда)
setTimeout( function() { alert(1) }, 1000);

Функция, которую вызывает setTimeout, объявлена как Function Expression, без имени.

А что если хочется из самой функции вызвать себя еще раз? По имени-то обратиться нельзя. Как раз для таких случаев и придуман arguments.callee, который гарантирует обращение изнутри функции к самой себе.

То есть, рекурсивный вызов будет выглядеть так:

setTimeout(  
  function() { 
    alert(1); 
*!*
    arguments.callee(); // вызвать себя
*/!*
  }, 
  1000
)

Аргументы можно передавать в arguments.callee() так же, как в обычную функцию.

Пример с факториалом:

// factorial(n) = n*factorial(n-1)
var factorial = function(n) {  
  return n==1 ? 1 : n**!*arguments.callee(n-1)*/!*;
}
Функция factorial не использует свое имя внутри , поэтому рекурсивные вызовы будут идти правильно, даже если функция «уехала» в другую переменную.

// factorial(n) = n*factorial(n-1)
var factorial = function(n) {  
  return n==1 ? 1 : n**!*arguments.callee(n-1)*/!*;
}

var g = factorial;
factorial = 0; // функция переместилась в переменную g

alert( g(5) ); // 120, работает!

Рекомендованной альтернативой arguments.callee являются именованные функциональные выражения.

arguments.callee.caller

Свойство arguments.callee.caller хранит ссылку на функцию, которая вызвала данную.

Это свойство устарело, аналогично arguments.callee.

Также существует похожее свойство arguments.caller (без callee). Оно не кросс-браузерное, не используйте его. Свойство arguments.callee.caller поддерживается везде.

Например:

f1();

function f1() {
  alert(arguments.callee.caller); // null
  f2();
}

function f2() {
  alert(arguments.callee.caller); // f1, функция вызвавшая меня
}

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

Почему callee и caller устарели?

В новом стандарте эти свойства объявлены устаревшими, и использовать их не рекомендуется. Хотя де-факто они используются хотя бы потому, что IE до 9 версии не поддерживает Named Function Expression так, как должен.

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

Инлайнинг функций

Инлайнинг - оптимизация, которая заключается в том, что вызов функции заменяется на ее тело.

Она особенно эффективна для простых функций. Например:

function sayHi(name) {
  alert("Привет, " + name);
}

function enterChat(peopleArr) {
  for (var i=0; i<peopleArr.length; i++) {
*!*
    sayHi( peopleArr[i] ); // было
*/!*
  }
}

При обработке этого кода вызов enterChat будет автоматически оптимизирован:

function enterChat(peopleArr) {
  for (var i=0; i<peopleArr.length; i++) {
*!*
    alert("Привет, " + peopleArr[i] ); // станет после оптимизации
*/!*
  }
}
Вызов sayHi заменен на тело функции. В результате исчезают накладные расходы. Это особенно эффективно, если функция очень простая и вызывается часто.

Современные движки JavaScript стараются делать эту оптимизацию автоматически там, где возможно.

Если разработчик не имеет средств доступа к стеку (caller/callee), то интерпретатор чувствует себя совершенно свободно и может инлайнить везде где считает нужным. В этом основная причина отказа от этих, в общем-то, полезных свойств. Тем более, что для arguments.callee есть замена - NFE.


Комментарии

  1. Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  2. Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
  3. Комментарии без смысла, с рекламой или не о статье вообще - удаляются.
Наверх

Содержание

Реклама

Нашли опечатку?

Нашли опечатку на сайте? Что-то кажется странным?
Выделите соответствующий текст и нажмите Ctrl+Enter!

Последние Комментарии

Помоги другим!

Помоги другим узнать о хорошей статье!