07. Функции. Часть 1. Общие вопросы

Настало время побольше узнать о функциях JavaScript. Этой теме посвящена текущая заметка и пара следующих.

Функции в JavaScript — это данные

В таких языках программирования, как С, С++, Паскаль, Java, функция — это просто синтаксическая конструкция, подобно циклу, ветвлению, инструкции присваивания.

В JavaScript функция — это ещё и данные, которые можно записывать как литералы или хранить в переменных (конечно, не сами функции, а ссылки на них).

Запись функции в привычном виде


function f(x) { return x*x; }

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

Вспомним, что функция — это объект, значит, в переменной хранится не он сам, а ссылка на него.

Следующие способы задания функции эквивалентны:


1) function f(x) { return x*x; }  
2) var f = function (x) { return x*x; }
3) var window.f = function (x) { return x*x; }
4) f = function (x) { return x*x; }
5) var f = new Function("x", "return x*x;");

Во всех пяти вариантах создаётся глобальная переменная с именем f, и в неё помещается ссылка на функцию function (x) {return x*x;} О последнем варианте будет рассказано немного позже.

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

Пример 1


function f(x) { return x*x; }  
var a = [];
a[0] = f;
a[0](10); // Равно 100

Пример 2


var f = function (x) { return x*x; }
var ob = {};
a.kv = f;
a.kv(10); // Равно 100

Что возвращает функция

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

Если вычисление функции заканчивается инструкцией return выражение, функция возвращает значение выражения. Если вычисление функции заканчивается при достижении конца её тела или на инструкции return, функция возвращает undefined.

Пример 1


var f1 = function (x) { x*x; }
f1(10); // Равно undefined

Пример 2


var f2 = function (x) { x*x; return; }
f2(10); // Равно undefined

Пример 3


var f3 = function (x) 
{ 
  return function (t) { return x*t*t; }
}
var y = f3(2);
var z = y(10); // Равно 200

Функция — это объект

Функция — это объект. Вернее, каждая функция это экземпляр класса Function, поэтому её можно создавать и при помощи конструктора Function следующим образом:


var f = new Function("x", "return x*x;");

В общем случае аргументы конструктора Function устроены следующим образом:

Способ задания функции при помощи конструктора Function не слишком удобен и практически не используется. Но как бы мы не создавали новую функцию, всегда вызывается конструктор Function, явно или неявно. То есть все функции JavaScript являются экземплярами класса Function.

Функция — это объект, поэтому ничто не мешает работать с функцией, как с объектом.


function f(x) {return x*x;} // Переменной f присвоена ссылка на функцию
f.pi = 3.14; // Задано свойство объекта f с именем pi, и этому свойству 
             // присвоено значение 3.14
var x = f(10)*f.pi; // Равно числу 314

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

При описании функции могут указываться формальные аргументы в виде списка идентификаторов:


function f(x,y)  // указано два формальных аргумента x и y
{
  return x+y;
} 

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


var t = 100;
var x = f(t+1,f(t/20,15)); // Равно 121

Количество фактических аргументов может быть меньше количества формальных, тогда недостающим автоматически присваивается значение undefined.


var x = f("1"); // Равно "1undefined"

Количество фактических аргументов может быть больше количества формальных, тогда лишние игнорируются.


var x = f(1,2,3); // Равно 3

Если недостающие аргументы должны принимать значения по умолчанию, то это можно организовать следующим образом:


// Функция, которая суммирует 2 числа.
// Если второй аргумент отсутствует, он 
// полагается равным 0
function f(x,y)  
{
  y = y || 0;  // Вспомните алгоритм работы оператора ||  
  return x+y;
} 
f(10,20); // Равно 30
f(10);    // Равно 10
f();      // Равно NaN

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


// Функция находит сумму своих аргументов
function add()
{
 ...
}

Как же обратиться к аргументам, если их список пуст? Проблему решает объект arguments, который создаётся автоматически в момент вызова функции (и к которому можно обращаться в теле функции).

Этот объект в отношении к фактическим аргументам функции подобен массиву (смотрите раздел «Объекты, подобные массивам» в заметке 6), а именно, значение свойства arguments.length равно количеству фактических аргументов, а сами аргументы доступны по синтаксису массива: arguments[i], где i — порядковый номер фактического аргумента (нумерация, естественно, с нуля).

Теперь легко написать запланированную функцию add:


// Функция находит сумму своих аргументов
function add()
{
  var sum = 0;
  for(var i=0; i<arguments.length; i++) sum += arguments[i];
  return sum;
}
add();          // Равно 0
add(1);         // Равно 1
add(1,2);       // Равно 3
add(1,2,10,20); // Равно 33

Объект arguments имеет ещё одно интересное свойство — callee. В нём хранится ссылка на саму функцию. Свойство arguments.callee позволяет записывать рекурсивные функциональные литералы.

Ниже записан литерал функции, которая вычисляет n!, и этот литерал вычисляется при значении n=10.


var x = (function (n)
  {
    if(n<2) return 1;
    return n*arguments.callee(n-1);
  })(10); // Равно 3628800

Свойства и методы функции

Любая функция — это экземпляр класса Function. Значит, функция (как объект) может иметь свойства и методы (заданные в классе Function). И, действительно, имеет.

Свойство length

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

Обратиться к свойству length можно как внутри, так и вне функции.


function f(x,y,z)
{
  f.length;                 // Равно 3
  arguments.callee.length;  // Равно 3
  ...
}
f.length; // Равно 3
Метод call

Формат обращения к методу call:

функция.call(объект, список_фактических_аргументов);

Метод call позволяет выполнить функцию в контексте какого либо объекта, то есть ключевое слово this внутри функции будет относиться к этому объекту.

Пример 1


var ob1 = {x:1,y:2};
var ob2 = {x:10,y:20};
function f(t) {return t*(this.x+this.y);}
sum.call(ob1,2); // Равно 6
sum.call(ob2,2); // Равно 60

Пример 2

Зададим объект с двумя свойствами x и y:


var ob = 
{
  x:1,
  y: [function () { this.x++;}, function () { this.x*this.x;}] 
};

Свойство x содержит число, а свойство y — массив функций, каждая из которых выполняет над x некоторую операцию.

Как вы думаете, если мы запишем, например, вызов ob.y[0](), будет ли значение ob.x увеличено на 1?

Нет, не будет. Ибо ключевое слово this в функции относится к объекту, в контексте которого функция вызывается. Таким объектом вызова является массив ob.y (массивы — это объекты), а не объект ob.

Если мы, действительно, хотим изменить значение свойства x в объекте ob, то нужно вызвать функцию в контексте этого объекта, то есть записать вызов так: ob.y[0].call(ob) (используйте эту идею при решении контрольного задания 4).

Метод apply

Формат обращения к методу apply:

функция.apply(объект, массив_фактических_аргументов);

Метод apply работает так же, как и метод call (позволяет выполнить функцию в контексте заданного объекта), но аргументы передаются не списком, а в виде массива.

Например, вызов f.call(ob,12,40) эквивалентен вызову f.apply(ob,[12,40]).

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


// Функция execWithObject возвращает функцию, которая 
// позволяет вызывать функцию f как метод объекта ob
function execWithObject(ob,f) 
{
  return function() { return f.apply(ob,arguments);};
}

Пример использования функции execWithObject


var ob1 = { x:1, y:2 };
var ob2 = { x:10, y:20 };
function sumMulN (n) { return n*(this.x+this.y); }           
var f1 = execWithObject(ob1,sumMulN); // Получили функцию для контекста ob1
var f2 = execWithObject(ob2,sumMulN); // Получили функцию для контекста ob2
var x = f1(10); // Равно 30   --  выполнение sumMulN в контексте объекта ob1
var y = f2(10); // Равно 300  --  выполнение sumMulN в контексте объекта ob2

Имена функций

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

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

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

Примеры


getElement        // получить элемент
setElement        // установить элемент
getdayOfWeek      // получить день недели
setdayOfWeek      // установить день недели
buildSomeWidget   // построить некоторую штучку

get_Element       // получить элемент
set_Element       // установить элемент
get_day_Of_Week   // получить день недели
set_day_Of_Week   // установить день недели
build_Some_Widget // построить некоторую штучку

В разделе задания собраны контрольные задания (с решениями).