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 можно как внутри, так и вне функции.
function f(x,y,z)
{
f.length; // Равно 3
arguments.callee.length; // Равно 3
...
}
f.length; // Равно 3
Формат обращения к методу 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 работает так же, как и метод 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 // построить некоторую штучку
В разделе задания собраны контрольные задания (с решениями).