Функция — это блок кода JavaScript, который определяется один раз, но может выполняться или вызываться любое количество раз. Возможно, вы уже знакомы с концепцией функции под таким названием, как подпрограмма, или процедура. Функции JavaScript параметризуются: определение функции может включать список идентификаторов, известных как параметры, которые работают как локальные переменные для тела функции. Вызовы функций предоставляют значения или аргументы для параметров функции. Функции часто используют значения своих аргументов для вычисления возвращаемого
значения, которое становится значением выражения вызова функции. В дополнение к аргументам у каждого вызова есть еще одно значение — контекст вызова, то есть значение ключевого слова this
.
this
.
Функции, предназначенные для инициализации вновь созданных объектов, называются
конструкторами. Конструкторы были описаны в разделе
6.1, и мы вернемся к ним
в главе 9. Функции в языке JavaScript являются объектами и могут использоваться разными способами. Например, функции могут присваиваться переменным и передаваться другим функциям. Поскольку функции являются объектами, имеется возможность присваивать значения их свойствам и даже вызывать их методы.
В JavaScript допускается создавать определения функций, вложенные в другие функции, и такие функции будут иметь доступ ко всем переменным, присутствующим в области видимости определения. То есть функции в языке JavaScript являются замыканиями, что позволяет использовать разнообразные мощные приемы программирования.
Функции определяются с ключевым словом function
,
которое может использоваться в выражениях определения функций
(раздел 4.3) или
в инструкциях объявления функций (раздел 5.3.2).
В любой форме определения функций начинаются с ключевого слова function
,
за которым следуют следующие компоненты:
function
.
В выражениях определения функций имя может отсутствовать: при его наличии
имя будет ссылаться на объект function
только в теле самой функции. Пример 8.1. Определения JavaScript-функций (Defining JavaScript functions)
// Выводит имена и значения всех свойств объекта o. // Возвращает undefined. function printprops(o) { for(var p in o) console.log(p + ": " + o[p] + "\n"); } // Вычисляет Декартово расстояние между точками (x1,y1) и (x2,y2). function distance(x1, y1, x2, y2) { var dx = x2 - x1; var dy = y2 - y1; return Math.sqrt(dx*dx + dy*dy); } // Рекурсивная функция (вызывающая сама себя), // вычисляющая факториал Напомню, что x! - это произведение x // и всех положительных целых чисел, меньше x. function factorial(x) { if (x <= 1) return 1; return x * factorial(x-1); } // Следующее выражение определяет функцию, // вычисляющую квадрат аргумента. // Обратите внимание, что она присваивается переменной var square = function(x) { return x*x; } // Рекурсивная функция (вызывающая сама себя), вычисляющая // факториалы. Вспомним, что x! является произведением x // и всех положительных целых чисел, меньших его. var f = function fact(x) { if (x <= 1) return 1; else return x*fact(x-1); }; // Выражения определения функций могут также использоваться // в качестве аргументов других выражений: data.sort(function(a,b) { return a-b; }); var data = [7, 3, 5, 2]; data.sort(function(a,b) { return a-b; }); => С другой стороны data.sort(function(a,b) { return b-a; }); => // Выражения определения функций иногда могут сразу же вызываться: var tensquared = (function(x) {return x*x;}(10)); // =>
Обратите внимание, что имя функции является необязательным для функций, определенных как выражения.
Инструкция объявления функции фактически объявляет
переменную и присваивает ей объект function
.
С другой стороны, выражение определения функции не объявляет переменную.
Имя разрешено для функций, которые должны ссылаться сами на себя,
таких как функция вычисления факториала, приведенная выше.
Если выражение определения функции включает имя, это имя будет ссылаться на
объект function
в области видимости данной функции. Фактически имя функции
становится локальной переменной внутри тела функции. Большинство функций,
определяемых выражениями, не нуждаются в именах, и их определение становится более компактным.
Выражения определения функции особенно удобно использовать для определения однократно используемых функций,
как в последних двух примерах.
В качестве имени функции может использоваться любой допустимый идентификатор. Старайтесь выбирать функциям достаточно описательные, но не длинные имена. Искусство сохранения баланса между краткостью и информативностью приходит с опытом. Правильно подобранные имена функций могут существенно повысить удобочитаемость (а значит, и простоту сопровождения) ваших программ.
Чаще всего в качестве имен функций выбираются глаголы или фразы,
начинающиеся с глаголов. По общепринятому соглашению имена функций
начинаются со строчной буквы. Если имя состоит из нескольких слов, в
соответствии с одним из соглашений они отделяются друг от друга символом
подчеркивания, примерно так: like_this()
, по другому соглашению все
слова, кроме первого, начинаются с прописной буквы, примерно так: likeThis()
.
Имена функций, которые, как предполагается, реализуют внутреннюю,
скрытую от посторонних глаз, функциональность, иногда начинаются с
символа подчеркивания.
В некоторых стилях программирования или в четко определенных программных платформах бывает полезно давать наиболее часто используемым функциям очень короткие имена. Примером может служить библиотека jQuery клиентского JavaScript (описываемая в главе 19), в public API которой широко используется функция с именем $() (да–да, просто знак доллара). (В разделе 2.4 уже говорилось, что в идентификаторах JavaScript помимо алфавитно-цифровых символов допускается использовать знаки доллара и подчеркивания.)
Как описывалось в разделе 5.3.2, инструкции объявления функций «поднимаются» в начало содержащих их сценария или функции, благодаря чему объявленные таким способом функции могут вызываться в программном коде выше объявления. Это не относится к функциям, которые определяются в виде выражений: чтобы вызвать функцию, необходимо иметь возможность сослаться на нее, однако нельзя сослаться на функцию, которая определяется с помощью выражения, пока она не будет присвоена переменной. Объявления переменных также поднимаются вверх (раздел 3.10.1), но операции присваивания значений этим переменным не поднимаются, поэтому функции, определяемые в виде выражений, не могут вызываться до того, как они будут определены.
Обратите внимание, что большинство (но не все) функций в примере 8.1 содержат
инструкцию return
(раздел 5.6.4). Инструкция return
завершает выполнение
функции и выполняет возврат значения своего выражения (если указано)
вызывающей программе. Если выражение в инструкции return
отсутствует, она
возвращает значение undefined
. Если инструкция return отсутствует в функции,
интерпретатор просто выполнит все инструкции в теле функции и вернет вызывающей
программе значение undefined.
Большинство функций в примере 8.1 вычисляют некоторое значение, и в них
инструкция return
используется для возврата этого значения вызывающей
программе. Функция printprops()
несколько отличается в этом смысле: ее работа
заключается в том, чтобы вывести имена свойств объекта. Ей не нужно возвращать
какое-либо значение, поэтому в функции отсутствует инструкция return
.
Функция printprops()
всегда будет возвращать значение undefined
. (Функции, не
имеющие возвращаемого значения, иногда называются процедурами.)
В JavaScript допускается вложение функций в другие функции. Например:
function hypotenuse(a, b) { function square(x) { return x*x; } return Math.sqrt(square(a) + square(b)); }Особый интерес во вложенных функциях представляют правила видимости переменных: они могут обращаться к параметрам и переменным, объявленным во вмещающей функции (или функциях). Например, в определении выше внутренняя функция
square()
может читать и изменять параметры a
и b
, объявленные во
внешней функции hypotenuse()
. Эти правила видимости, действующие для
вложенных функций, играют важную роль, и мы еще вернемся к ним в разделе 8.6.
Как отмечалось в разделе 5.3.2, инструкции объявления функций в
действительности не являются настоящими инструкциями, и спецификация ECMAScript
допускает использовать их только в программном коде верхнего уровня. Они могут
появляться в глобальном программном коде или внутри других функций, но они
не могут находиться внутри циклов, условных инструкций, инструкций try/catch/finally
или with
(некоторые реализации JavaScript могут иметь менее строгие требования. Например,
в браузере Firefox допускается наличие «условных определений функций» внутри
инструкций if
).
Обратите внимание, что эти ограничения
распространяются только на объявления функций в виде инструкции function
. Выражения
определения функций могут присутствовать в любом месте в программе на языке
JavaScript.
Программный код, образующий тело функции, выполняется не в момент определения функции, а в момент ее вызова. Функции в языке JavaScript могут вызываться четырьмя способами:
• как функции,
• как методы,
• как конструкторы и
• косвенно, с помощью их методов call()
и apply()
.
Вызов функций как функций или как методов выполняется с помощью
выражения вызова (раздел 4.5).
Выражение вызова состоит из выражения обращения
к функции, которое возвращает объект function
, и следующими за ним
круглыми скобками со списком из нуля или более выражений–аргументов, разделенных
запятыми, внутри. Если выражение обращения к функции является
выражением обращения к свойству (если функция – свойство объекта или
элемент массива) то выражение вызова – это выражение вызова метода.
Такой случай будет описан ниже. В следующем фрагменте демонстрируется
несколько примеров выражений вызова обычных функций:
printprops({x:1}); var total = distance(0,0,2,1) + distance(2,1,3,5); var probability = factorial(5)/factorial(13);
При вызове функции вычисляются все выражения–аргументы (указанные в скобках), и полученные значения используются в качестве аргументов функции. Эти значения присваиваются параметрам, имена которых перечислены в определении функции. В теле функции выражения обращений к параметрам возвращают значения соответствующих аргументов.
При вызове обычной функции возвращаемое функцией значение становится
значением выражения вызова. Если возврат из функции происходит по достижении
ее конца интерпретатором, возвращается значение undefined
. Если возврат из
функции происходит в результате выполнения инструкции return
, возвращается
значение выражения, следующего за инструкцией return
, или undefined
, если
инструкция return
не возвращает значение.
При вызове функции в ECMAScript 3 и в нестрогом режиме ECMAScript 5
контекстом вызова (значением this
) является глобальный объект. Однако в строгом
режиме контекстом вызова является значение undefined
.
Функции, написанные для вызовов именно как функции, обычно вовсе не используют ключевое слово this
. Впрочем, его можно
использовать, чтобы определить, не выполняется ли функция в строгом режиме:
// Определение и вызов функции, которая выясняет действующий режим работы: var strict = (function() { return !this; }());
Метод – это не что иное, как JavaScript-функция, которая хранится как свойство
объекта. Если имеется функция f
и объект o
, то можно определить метод объекта o
с именем m
, как показано ниже:
o.m = f;После этого можно вызвать метод
m()
объекта o
:
o.m();Или, если метод
m()
принимает два аргумента, его можно вызвать так:
o.m(x, y);
Этот код является выражением вызова: он включает выражение обращения
к функции o.m
и два выражения–аргумента, x
и y
.
Выражение обращения к функции в свою очередь является выражением обращения к свойству
(раздел 4.4),
а это означает, что функция вызывается как метод, а не как обычная функция.
Аргументы и возвращаемое значение при вызове метода обрабатываются точно
так же, как при вызове обычной функции. Однако вызов метода имеет одно
важное отличие: контекст вызова. Выражение обращения к свойству состоит из двух
частей: объекта (в данном случае o
) и имени свойства (m
). В подобных выражениях
вызова методов объект o
становится контекстом вызова, и тело функции
получает возможность ссылаться на этот объект с помощью ключевого слова this
.
Например:
var calculator = { // Литерал объекта operand1: 1, operand2: 1, add: function() { // Обратите внимание, что для ссылки на этот объект // используется ключевое слово this, this.result = this.operand1 + this.operand2; } }; calculator.add(); // Вызвать метод, чтобы вычислить calculator.result // =>
Чаще всего при вызове методов используется форма с оператором точки, однако точно так же можно обращаться к свойствам–методам с помощью квадратных скобок. Например, оба следующих выражения являются выражениями вызова методов:
o.m(x,y); // Используется форма с оператором точки o.m(3,4); // Пусть var o = { // u: 5, v: 6; // m: function (x,y) {return Math.pow(x, y)} // add: function() {this.result = this.u + this.u;}}; // o.m(3,4) => . o["m"](x,y); // Другой способ записать выражение o.m(x,y); o["m"](3,4) == . o.add(); // Вызвать метод, чтобы вычислить o.u +o.v. o["result"]; // => . a[0](z); // Тоже вызов метода (предполагается, что a[0] - это функция. // Пусть var a = [function (x) {return Math.pow(2, x)},...]; // a[0](3) == .
Выражения вызова методов могут включать более сложные выражения обращения к свойствам:
customer.surname.toUpperCase(); // Вызвать метод объекта customer.surname f().m(); // Вызвать метод m() объекта, возвращаемого // функцей f() var f = function (x,y) { //Пример var q = { u: 5, v: 6, m: function (x,y) {return Math.pow(x, y)}, add: function() {return this.u + this.v;}}; return q }; f().add(); // Вызвать метод, чтобы вычислить u + v => f().m(4,3); // =>
Методы и ключевое слово this
занимают центральное место в парадигме
объектно-ориентированного программирования. Любая функция, используемая как
метод, фактически получает неявный аргумент – объект, относительно которого
она была вызвана. Как правило, методы выполняют некоторые действия с
объектом, и синтаксис вызова метода наглядно отражает тот факт, что функция
оперирует объектом. Сравните следующие две строки:
rect.setSize(width, height): setRectSize(rect, width, height);Гипотетически функции, вызывающиеся в этих двух строках, могут производить абсолютно идентичные действия над объектом
rect
(гипотетическим), но
синтаксис вызова метода в первой строке более наглядно демонстрирует, что в центре
внимания находится объект rect
.
Обратите внимание: this
– это именно ключевое слово, а не имя переменной или
свойства. Синтаксис JavaScript не допускает возможность присваивания
значений элементу this
.
В отличие от переменных, ключевое слово this
не имеет области видимости,
и вложенные функции не наследуют значение this
от вызывающей функции.
Если вложенная функция вызывается как метод, значением this
является объект,
относительно которого был сделан вызов. Если вложенная функция вызывается
как функция, то значением this
будет либо глобальный объект (в нестрогом
режиме), либо undefined
(в строгом режиме). Распространенная ошибка полагать,
что во вложенной функции, которая вызывается как функция, можно
использовать this
для получения доступа к контексту внешней функции. Если во
вложенной функции необходимо иметь доступ к значению this
внешней функции, это
значение следует сохранить в переменной, находящейся в области видимости
внутренней функции. Для этой цели часто используется переменная с именем
self
. Например:
var o = { // Объект o. m: function() { // Метод m объекта. var self = this; // Сохранить значение this в переменной. console.log(this === o); // Выведет "true": this - это объект o. f(); // Вызвать вспомогательную ф-цию f(). function f() { // Вложенная функция f console.log(this === o); // "false": this - глобальный объект // или undefined console.log(self === o); // "true": self - значение this // внешней функции. } } }; o.m(); // Вызвать метод m объекта o.
В примере 8.5 (раздел 8.7.4) демонстрируется более практичный способ
использования идиомы var self=this
.
Когда методы возвращают объекты, появляется возможность использовать значение, возвращаемое одним методом, как часть последующих вызовов. Это позволяет создавать последовательности («цепочки», или «каскады») вызовов методов в одном выражении. При работе с библиотекой jQuery (глава 19), например, часто можно встретить такие инструкции:
// Отыскать все заголовки, отобразить их в значения атрибутов id, // преобразовать в массив и отсортировать $(":header").map(function() { return this.id }).get().sort();Если вы пишете метод, не имеющий собственного возвращаемого значения, подумайте о возможности возвращать из него значение
this
. Если
неуклонно следовать этому правилу при разработке своего API, появится
возможность использовать стиль программирования, известный как
составление цепочек из методов¹, когда обращение к имени метода выполняется
один раз, а затем может следовать множество вызовов его методов:
shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();
He путайте цепочки вызовов методов с цепочками конструкторов, которые описываются в разделе 9.7.2.
Если вызову функции или метода предшествует ключевое слово new
,
то это вызов конструктора (вызовы конструкторов предварительно обсуждались в разделах
4.6
и 6.1.2, а более
подробно конструкторы будут рассматриваться в главе 9). Вызов
конструктора отличается от вызова обычной функции или метода особенностями
обработки аргументов, контекстом вызова и возвращаемым значением.
Если вызов конструктора включает список аргументов в скобках, эти выражения–аргументы вычисляются и передаются конструктору точно так же, как любой другой функции или методу. Но если конструктор не имеет параметров, синтаксис вызова конструктора в языке JavaScript позволяет вообще опустить скобки и список аргументов. При вызове конструктора всегда можно опустить пару пустых скобок. Например, следующие две строки полностью эквивалентны:
var o = new Object(); var o = new Object;
Вызов конструктора создает новый пустой объект, который наследуется
от свойства prototype
конструктора. Функции конструктора предназначены для инициализации
объектов, и этот вновь созданный объект используется в качестве контекста
вызова, поэтому функция–конструктор может обращаться к нему с помощью
ключевого слова this
. Обратите внимание, что новый объект
используется в качестве контекста вызова, даже если вызов конструктора выглядит как вызов
метода. То есть в выражении new o.m()
контекстом вызова будет вновь созданный
объект, а не объект o
.
return
.
Обычно они выполняют инициализацию нового объекта и неявно возвращают
его по достижении своего конца. В этом случае значением выражения вызова
конструктора становится новый объект. Однако если конструктор явно использует инструкцию
return
, чтобы вернуть объект, то этот объект становится значением выражения
вызова. Если конструктор использует инструкцию return
и она не возвращает
значения или вернет простое значение, это возвращаемое значение
будет проигнорировано, а результатом вызова будет новый объект.
Функции в языке JavaScript являются объектами и подобно другим объектам
имеют свои методы. В их числе есть два метода, call() и apply()
, выполняющие
косвенный вызов функции. Оба метода позволяют явно определить значение this
для вызываемой функции, что дает возможность вызывать любую функцию как
метод любого объекта, даже если фактически она не является методом этого
объекта. Кроме того, обоим методам можно передать аргументы вызова. Метод call()
позволяет передавать аргументы для вызываемой функции в своем собственном
списке аргументов, а метод apply()
принимает массив значений, которые будут
использованы как аргументы. Подробнее о методах call() и apply()
рассказывается в разделе 8.7.3.
В определениях функций в языке JavaScript не указываются типы ожидаемых параметров, а при вызове функций не выполняется никаких проверок типов передаваемых значений аргументов. Фактически при вызове функций в языке JavaScript не проверяется даже количество аргументов. Ниже в подразделах описывается, что происходит, если число аргументов в вызове функции меньше или больше числа объявленных параметров. В них также демонстрируется, как можно явно проверить типы аргументов функции, если необходимо гарантировать, что функция не будет вызвана с некорректными аргументами.
Когда число аргументов в вызове функции меньше числа объявленных
параметров, недостающие аргументы получают значение undefined
. Часто бывает
удобным писать функции так, чтобы некоторые аргументы были необязательными
и могли опускаться при вызове функции. В этом случае желательно
предусмотреть возможность присваивания достаточно разумных значений по умолчанию
параметрам, которые могут быть опущены. Например:
// Добавить в массив a перечислимые имена свойств объекта о и вернуть его. // Если аргумент a не не был передан, создать и вернуть новый массив, function getPropertyNames(o, /* optional */ a) { if (a === undefined) a = []; // Если массив не определен, создать новый for(var property in o) a.push(property); return a; } // Эта функция может вызываться с 1 или 2 аргументами: var a = getPropertyNames(o); // Получить свойства объекта o в новом массиве getPropertyNames(p,a); // добавить свойства объекта p в этот массивВместо инструкции
if
в первой строке этой функции можно использовать
оператор ||
следующим образом:
a = a || [];
В разделе 4.10.2
говорилось, что оператор ||
возвращает первый аргумент, если он
имеет истинное значение, и в противном случае возвращает второй аргумент.
В данном примере, если во втором аргументе будет передан какой-либо объект,
функция будет использовать его. Но если второй аргумент отсутствует (или в нем
будет передано значение null
), будет использоваться вновь созданный массив.
Обратите внимание, что при объявлении функций необязательные аргументы
должны завершать список аргументов, чтобы их можно было опустить.
Программист, который будет писать обращение к вашей функции, не сможет передать
второй аргумент и при этом опустить первый: он будет вынужден явно передать
в первом аргументе значение undefined
. Обратите также внимание на
комментарий /* необязательный */ в определении функции, который подчеркивает тот факт,
что параметр является необязательным.
Если число аргументов в вызове функции превышает число имен параметров,
функция лишается возможности напрямую обращаться к неименованным
значениям. Решение этой проблемы предоставляет объект Arguments
. В теле функции
идентификатор arguments
ссылается на объект Arguments
, присутствующий в
вызове. Объект Arguments
– это объект, подобный массиву (раздел 7.11), позволяющий
извлекать переданные функции значения по их номерам, а не по именам.
Предположим, что была определена функция f
, которая требует один аргумент, x
.
Если вызвать эту функцию с двумя аргументами, то первый будет доступен
внутри функции по имени параметра х или как arguments[0]
. Второй аргумент будет
доступен только как arguments[1]
. Кроме того, подобно настоящим массивам,
arguments имеет свойство length
, определяющее количество содержащихся элементов.
То есть в теле функции f
, вызываемой с двумя аргументами, arguments.length
имеет значение 2.
Объект Arguments
может использоваться с самыми разными целями. Следующий
пример показывает, как с его помощью проверить, была ли функция вызвана
с правильным числом аргументов, – ведь JavaScript этого за вас не сделает:
function f(x, y, z) { // Сначала проверяется, правильное ли количество аргументов передано if (arguments.length != 3) { throw new Error("function f called with " + arguments.length "arguments, but it expects 3 arguments."); } // А теперь сам код функции... ... }Обратите внимание, что зачастую нет необходимости проверять количество аргументов, как в данном примере. Поведение по умолчанию интерпретатора Java-Script отлично подходит для большинства случаев: отсутствующие аргументы замещаются значением
undefined
, а лишние аргументы просто игнорируются.
Объект Arguments
иллюстрирует важную возможность JavaScript-функций: они
могут быть написаны таким образом, чтобы работать с любым количеством
аргументов. Следующая функция принимает любое число аргументов и возвращает
значение самого большого из них (аналогично ведет себя встроенная функция
Math.max()
:
function max(/* ... */) { var max = Number.NEGATIVE_INFINITY; // Цикл по всем аргументам, поиск и сохранение наибольшего из них . for(var i = 0; i < arguments.length; i++) if (arguments[i] > max) max = arguments[i]; // Return the biggest return max; } var largest = max(1, 10, 100, 2, 3, 1000, 4, 5, 10000, 6); // =>Функции, подобные этой и способные принимать произвольное число аргументов, называются функциями с переменным числом аргументов (
variadic
functions, variable arity functions, или varargs functions
). Этот термин возник вместе
с появлением языка программирования C.
Обратите внимание, что функции с переменным числом аргументов не должны
допускать возможность вызова с пустым списком аргументов. Будет вполне
разумным использовать объект arguments[]
при написании функции, ожидающей
получить фиксированное число обязательных именованных аргументов, за
которыми может следовать произвольное число необязательных неименованных
аргументов.
Не следует забывать, что arguments
фактически не является массивом – это объект
Arguments
. В каждом объекте Arguments
имеются пронумерованные элементы
массива и свойство length
, но с технической точки зрения это не массив. Лучше
рассматривать его как объект, имеющий некоторые пронумерованные свойства.
Подробнее об объектах, подобных массивам, рассказывается в разделе
7.11.
У объекта Arguments
есть одна очень необычная особенность. Когда у функции
имеются именованные параметры, элементы массива объекта Arguments
при
выполнении в нестрогом режиме являются синонимами параметров, содержащих
аргументы функции. Массив arguments[]
и имена параметров – это два разных
средства обращения к одним и тем же переменным. Изменение значения
аргумента через имя аргумента меняет значение, извлекаемое через массив arguments[]
.
Изменение значения аргумента через массив arguments[]
меняет значение,
извлекаемое по имени аргумента. Например:
function f(x) { console.log(x); // Выведет начальное значение аргумента arguments[0] = null; // При изменении элемента массива изменяется x! console.log(x); // Теперь выведет "null" } Пример: var x = Math.PI; // Выведет начальное значение аргумента // // В следующей строке в консоли выведет "null"Определенно, это не совсем то поведение, которое можно было бы ожидать от настоящего массива. В этом случае
arguments[0]
и
x
могли бы изначально ссылаться на одно и то же значение, но
изменение одного не должно оказывать влияния на другое. Эта особенность в
поведении объекта Arguments
была ликвидирована в строгом
режиме, предусматриваемом стандартом ECMAScript 5. Кроме того, в строгом
режиме имеется еще несколько отличий. В нестрогом режиме arguments
– это всего лишь обычный JavaScript-идентификатор, а не зарезервированное
слово. В строгом режиме не допускается использовать имя arguments
в качестве имени параметра или локальной переменной функции и отсутствует
возможность присваивать значения элементам arguments
.
Помимо элементов своего массива объект Arguments
определяет свойства
callee
и caller
(в пер. с англ. "вызывающая программа") .
При попытке изменить значения этих свойств в строгом режиме
ECMAScript 5 гарантированно возбуждается исключение TypeError
. Однако в нестрогом
режиме стандарт ECMAScript утверждает, что свойство callee
ссылается на
выполняемую в данный момент функцию. Свойство caller
не является
стандартным, но оно присутствует во многих реализациях и ссылается на функцию,
вызвавшую текущую. Свойство caller
можно использовать для доступа к стеку
вызовов, а свойство callee
особенно удобно использовать для рекурсивного вызова
неименованных функций:
var factorial = function(x) { if (x <= 1) return 1; return x * arguments.callee(x-1); };
Когда функция имеет более трех параметров, становится трудно запоминать правильный порядок их следования. Чтобы предотвратить ошибки и избавить программиста от необходимости заглядывать в документацию всякий раз, когда он намеревается вставить в программу вызов такой функции, можно предусмотреть возможность передачи аргументов в виде пар имя/значение в произвольном порядке. Чтобы реализовать такую возможность, при определении функции следует предусмотреть передачу объекта в качестве единственного аргумента. Благодаря такому стилю пользователи функции смогут передавать функции объект, в котором будут определяться необходимые пары имя/значение. В следующем фрагменте приводится пример такой функции, а также демонстрируется возможность определения значений по умолчанию для опущенных аргументов:
// Скопировать length элементов из массива from в массив to. // Копирование начинается с элемента from_start в массиве from // и выполняется в элементы, начиная с to_start в массиве to. // Запомнить порядок следования аргументов такой функции довольно сложно. function arraycopy(/* array */ from, /* index */ from_start, /* array */ to, /* index */ to_start, /* integer */ length) { // здесь находится реализация функции, например, такая: for (var i = 0; i < length; i++) to[to_start + i] = from[from_start + i]; } // Эта версия функции чуть менее эффективная, но не требует запоминать // порядок следования аргументов, а аргументы from_start и to_start // по умолчанию принимают значение 0 function easycopy(args) { arraycopy(args.from, args.from_start || 0, // Обратите внимание, как назначаются args.to, // значения по умолчанию args.to_start || 0, args.length); } // Далее следует пример вызова функции easycopy(): var a = [1,2,3,4], b = [];
В языке JavaScript параметры функций объявляются без указания их типов, а во
время передачи значений функциям не производится никакой проверки их
типов. Вы можете сделать свой программный код самодокументируемым, выбирая
описательные имена для параметров функций и включая описание типов
аргументов в комментарии, как это сделано в только что рассмотренном примере
функции arraycopy()
. Для необязательных аргументов в комментарий можно
добавлять слово «необязательный» («optional»). А если функция может принимать
произвольное число аргументов, можно использовать многоточие:
function max(/* number... */) { /* code here */ }
Как отмечалось в разделе 3.8, при необходимости JavaScript выполняет
преобразование типов. Таким образом, если определить функцию, которая ожидает
получить строковый аргумент, а затем вызвать ее с аргументом какого-нибудь
другого типа, значение аргумента просто будет преобразовано в строку, когда
функция пытается обратиться к нему как к строке. В строку может быть
преобразовано любое простое значение, и все объекты имеют методы toString()
(правда, не
всегда полезные); тем самым устраняется вероятность появления ошибки.
Однако такой подход может использоваться не всегда. Вернемся к методу
arraycopy()
, продемонстрированному выше. Он ожидает получить массив в первом
аргументе. Любое обращение к функции окажется неудачным, если первым
аргументом будет не массив (или, возможно, объект, подобный массиву). Если
функция должна вызываться чаще, чем один-два раза, следует добавить в нее проверку
соответствия типов аргументов. Гораздо лучше сразу же прервать вызов функции
в случае передачи аргументов ошибочных типов, чем продолжать выполнение,
которое потерпит неудачу с сообщением об ошибке, запутывающим ситуацию.
Ниже приводится пример функции, выполняющей проверку типов. Обратите
внимание, что она использует функцию isArrayLike()
из раздела
7.11:
// Возвращает сумму элементов массива (или объекта, подобного массиву) a. // Все элементы массива должны быть числовыми, при этом значения null // и undefined игнорируются. function sum(a) { if (isArrayLike(a)) { var total = 0; for(var i = 0; i < a.length; i++) {// Цикл по всем элементам var element = a[i]; if (element == null) continue;// Пропустить null и undefined if (isFinite(element)) total += element; else throw new Error("sum(): все элементы должны быть числами"); } return total; } else throw new Error("sum(): аргумент должен быть подобным массиву"); } var a = [2,7,1828,1724,45,90,45,23,236];// - истинный массив var o = {"0":2,"1":7,"2":1828,"3":1724,"4":45,"5":90,"6":45,"7":23,"8":236}; // - объект, подобный массиву var b = sum(o); // b =
Приведенный в этом примере метод sum()
весьма строго относится к проверке типов входных аргументов
и генерирует исключения с достаточно информативными сообщениями, если
типы входных аргументов не соответствуют ожидаемым. Тем не менее он остается
достаточно гибким, обслуживая наряду с настоящими массивами объекты,
подобные массивам, и игнорируя элементы, имеющие значения null
и undefined
.
JavaScript - чрезвычайно гибкий и к тому же слабо типизированный язык,
благодаря чему можно писать функции, которые достаточно терпимо относятся к
количеству и типам входных аргументов. Следующий пример – метод flexisum()
–
реализует такой подход (и, вероятно, является примером другой крайности).
Например, он принимает любое число входных аргументов и рекурсивно
обрабатывает те из них, которые являются массивами. Вследствие этого он может
принимать переменное число аргументов или массив аргументов. Кроме того, он
прилагает максимум усилий, чтобы преобразовать нечисловые аргументы в числа,
прежде чем сгенерировать исключение:
function flexisum(a) { var total = 0; for(var i = 0; i < arguments.length; i++) { var element = arguments[i], n; if (element == null) continue;/ // Игнорировать null и undefined if (a.isArray(element)) // Если аргумент - массив n = flexisum.apply(this, element); // вычислить сумму рекурсивно else if (typeof element === "function") // Иначе, если это функция... n = Number(element()); // вызвать и преобразовать, else n = Number(element); // Иначе попробовать преобразовать if (isNaN(n)) // Если не удалось преобразовать // в число, возбудить исключение. throw Error("flexisum(): невозможно преобразовать \" + element + \" в число"); total += n; // Иначе прибавить n в total } return total;//return total; } flexisum([a,sum(o)]); // => flexisum([2,7,1828,1724,45,90,a,45,23,236]); // =>
Важнейшими особенностями функций являются возможности их определять и вызывать. Определение и вызов функции – это синтаксические средства JavaScript и большинства других языков программирования. Однако в JavaScript функции - это не только синтаксические конструкции, но и значения, а это означает, что они могут присваиваться переменным, храниться в свойствах объектов или элементах массивов, передаваться в качестве аргументов функциям и т. д.
(Это может показаться не столь интересным, если вы не знакомы с такими языками, как Java, в которых функции являются частью программы, но не могут управляться программой.)
Чтобы понять, как функции в JavaScript могут быть одновременно синтаксическими конструкциями и данными, рассмотрим следующее определение функции:
function square(x) { return x*x; }
Это определение создает новый объект Function
и присваивает его переменной
square
. Имя функции на самом деле нематериально - это просто имя
переменной, которая ссылается на объект Function
. Функция может быть присвоена
другой переменной и при этом работать так же:
var s = square; // Теперь s ссылается на ту же функцию, что и square square(4); // => s(4); // =>
Функции могут быть присвоены не только глобальным переменным, но и свойствам объектов. В этом случае их называют методами:
var o = {square: function(x) { return x*x; }}; // Литерал объекта var y = o.square(16); // y ==
Функции могут быть даже безымянными, например, в случае присваивания их элементам массива:
var a = [function(x) { return x*x; }, 20]; // An array literal a[0](4); // => a[0](a[1]); // =>Синтаксис вызова функции в последней строчке этого примера выглядит необычно, но это вполне допустимый вариант применения выражения вызова!
В примере 8.2 демонстрируется, что можно делать, когда функции выступают в качестве данных. Хотя пример может показаться вам несколько сложным, комментарии объясняют, что происходит.
Пример 8.2. Использование функций как данных <
// Compute the value ("hello" + " " + "world") like this: // Определения нескольких простых функций function add(x,y) { return x + y; } function subtract(x,y) { return x - y; } function multiply(x,y) { return x * y; } function divide(x,y) { return x / y; } // Следующая функция принимает одну из предыдущих функций // в качестве аргумента и вызывает ее с двумя операндами function operate(operator, operand1, operand2) { return operator(operand1, operand2); } // Так можно вызвать эту функцию для вычисления выражения (2+3) + (4*5): var i = operate(add, operate(add, 2, 3), operate(multiply, 4, 5)); // Ради примера реализуем эти функции снова, на этот раз // с помощью литералов функций внутри литерала объекта. var operators = { add: function(x,y) { return x+y; }, subtract: function(x,y) { return x-y; }, multiply: function(x,y) { return x*y; }, divide: function(x,y) { return x/y; }, pow: Math.pow // Можно использовать даже предопределенные функции }; // Эта функция принимает имя оператора, отыскивает оператор в объекте, // а затем вызывает его с указанными операндами. // Обратите внимание на синтаксис вызова функции оператора. function operate2(operation, operand1, operand2) { if (typeof operators[operation] === "function") return operators[operation](operand1, operand2); else throw "unknown operator"; } // Вычислить значение ("hello" + " " + "world"): var j = operate2("add", "hello", operate2("add", " ", "world")); // => // Использовать предопределенную функцию Math.pow() function: var k = operate2("pow", 10, 2); // =>
Рассмотрим метод Array.sort()
– еще один пример использования функций как значений.
Он сортирует элементы массива. Существует много
возможных порядков сортировки (числовой, алфавитный, по датам, по возрастанию,
по убыванию и т. д.), поэтому метод sort()
принимает в качестве необязательного
аргумента функцию, которая сообщает о том, как выполнять сортировку. Эта
функция делает простую работу: получает два значения, сравнивает их и
возвращает результат, указывающий, какой из элементов должен быть впереди. Она
делает метод Array.sort()
совершенно универсальным и
бесконечно гибким – с ее использованием метод Array.sort()
может сортировать любой тип данных в любом мыслимом
порядке. Примеры использования функции Array.sort()
представлены в разделе
7.8.3.
Функции в языке JavaScript являются не простыми значениями, а особой
разновидностью объектов, поэтому функции могут иметь свойства. Когда
функции требуется «статическая» переменная, значение которой должно сохраняться
между ее вызовами, часто оказывается удобным использовать свойство объекта Function
,
позволяющее не занимать пространство имен определениями
глобальных переменных. Пусть нужно написать функцию,
возвращающую уникальное целое число при каждом своем вызове. Функция никогда
не должна возвращать одно и то же значение дважды. Чтобы обеспечить это,
функция должна запоминать последнее возвращенное значение и сохранять его
между ее вызовами. Можно было бы хранить последнее возвращенное значение в глобальной
переменной, но это нежелательно, потому что оно используется только самой функцией. Лучше сохранять
его в свойстве объекта Function
. Вот
пример функции, возвращающей уникальное целое значение при каждом вызове:
// Инициализировать свойство counter объекта функции. // Объявления функций поднимаются вверх, поэтому мы можем // выполнить следующее присваивание до объявления функции: uniqueInteger.counter = 0; // Предлагаемая функция возвращает разные (увеличенные на 1) целые // числа при каждом вызове. Для сохранения следующего возвращаемого // значения она использует собственное свойство. function uniqueInteger() { return ++uniqueInteger.counter; // Увеличить и вернуть свойство counter. } uniqueInteger; // => uniqueInteger; // => uniqueInteger; // =>
Еще один пример, взгляните на следующую функцию factorial()
, которая
использует собственные свойства (интерпретируя себя как массив) для сохранения
результатов предыдущих вычислений:
// Вычисляет факториалы и сохраняет результаты в собственных свойствах. function factorial(n) {//function factorial(n) { // Только конечные положительные целые: if (isFinite(n) && n>0 && n==Math.round(n)) { if (!(n in factorial))// Если не сохранялось ранее factorial[n] = n * factorial(n-1);// Вычислить и сохранить return factorial[n];// Вернуть сохраненный результат } else return NaN;// Для ошибочного аргумента } factorial[1] = 1;// Инициализировать кэш базовым случаем. factorial(5); // => factorial(8); // => factorial(7); // =>
В разделе 3.10.1 говорилось, что в языке JavaScript существует такое понятие, как область видимости функции: переменные, объявленные внутри функции, видимы в любой точке функции (включая и вложенные функции), но они существуют только в пределах функции. Переменные, объявленные за пределами функции, являются глобальными переменными и видимы в любой точке JavaScript-программы. В языке JavaScript отсутствует возможность объявлять переменные, доступные только внутри отдельно расположенного блока программного кода, и по этой причине иногда бывает удобно определять функции, которые будут играть роль временного пространства имен, в котором можно объявлять переменные, не засоряя глобальное.
Пусть, например, у вас есть модуль JavaScript, который вы хотите использовать в различных JavaScript-программах (или, если говорить на языке клиентского JavaScript, на различных веб-страницах). Допустим, что в программном коде этого модуля, как и в практически любом другом программном коде, объявляются переменные для хранения промежуточных результатов вычислений. Проблема состоит в том, что модуль будет использоваться множеством различных программ, и поэтому заранее неизвестно, будут ли возникать конфликты между переменными, создаваемыми модулем, и переменными, используемыми в программах, импортирующих этот модуль. Решение состоит в том, чтобы поместить программный код модуля в функцию, и затем вызывать эту функцию. При таком подходе переменные модуля из глобальных превратятся в локальные переменные функции:
function mymodule() { // Здесь находится реализация модуля. Любые переменные, // используемые модулем, превратятся в локальные переменные // этой функции и не будут засорять глобальное пространство имен. } mymodule(); // Но не забудьте вызвать функцию!Данный программный код объявляет единственную глобальную переменную: имя функции
«mymodule
». Если даже единственное имя – это слишком много,
можно определить и вызвать анонимную функцию в одном выражении:
(function() {// функция mymodule переписана как неименованное выражение // Здесь находится реализация модуля. }()); // конец литерала функции, теперь вызывайте ее.Такой способ определения и вызова функции в одном выражении настолько часто используется в практике, что превратился в типичный прием. Обратите внимание на использование в этом примере круглых скобок. Открывающая скобка перед ключевым словом
function
является обязательной, потому что без нее ключевое
слово function
будет интерпретироваться как инструкция объявления функции.
Скобки позволят интерпретатору распознать этот фрагмент как выражение
определения функции. Это обычный способ – заключение в скобки, даже когда они не
требуются, определения функции, которая должна быть вызвана сразу же после
ее определения.
Практическое применение приема создания пространства имен демонстрируется
в примере 8.3. Здесь определяется анонимная функция, возвращающая функцию
extend()
, подобную той, что была представлена в примере
6.2. Анонимная функция
проверяет наличие хорошо известной ошибки в Internet Explorer и возвращает
исправленную версию функции, если это необходимо. Помимо этого, анонимная
функция играет роль пространства имен, скрывающего массив с именами свойств.
Пример 8.3. Функция extend(), исправленная, если это необходимо
// Определяет функцию extend, которая копирует свойства второго // и последующих аргументов в первый аргумент. Здесь реализован // обход ошибки в IE: во многих версиях IE цикл for/in не перечисляет // перечислимые свойства объекта o, если одноименное свойство его // прототипа является неперечислимым. Это означает, что такие свойства, // как toString, обрабатываются некорректно, если явно не проверять их. var extend = (function() { // Присвоить значение, возвращаемое этой // функцией. Сначала проверить наличие ошибки, прежде чем исправлять ее. for(var p in {toString:null}) { // Если мы оказались здесь, значит, цикл for/in работает корректно // и можно вернуть простую версию функции extend(). return function extend(o) { for(var i = 1; i < arguments.length; i++) { var source = arguments[i]; for(var prop in source) o[prop] = source[prop]; } return o; }; } // Если мы оказались здесь, следовательно, цикл for/in не перечислил // свойство toString тестового объекта. Поэтому необходимо вернуть // версию extend(), которая явно проверяет неперечислимость // свойств прототипа Object.prototype. // Список свойств, которые необходимо проверить: var protoprops = ["toString", "valueOf", "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable","toLocaleString"]; return function patched_extend(o) { for(var i = 1; i < arguments.length; i++) { var source = arguments[i]; // Скопировать все перечислимые свойства for(var prop in source) o[prop] = source[prop]; // А теперь проверить специальные случаи свойств for(var j = 0; j < protoprops.length; j++) { prop = protoprops[j]; if (source.hasOwnProperty(prop)) o[prop] = source[prop]; } } return o; }; // Это список особых свойств, которые мы проверяем var protoprops = ["toString", "valueOf", "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable","toLocaleString"]; }());
Как и в большинстве языков программирования, в JavaScript используются лексические области видимости. Это означает, что при выполнении функций действуют области видимости переменных, которые имелись на момент их определения, а не на момент вызова. Для реализации лексической области видимости внутренняя информация о состоянии объекта функции в языке JavaScript должна включать не только программный код функции, но еще и ссылку на текущую цепочку областей видимости. (Прежде чем продолжить чтение этого раздела, вам, возможно, следует повторно прочитать сведения об областях видимости переменных и цепочках областей видимости в разделах 3.10 и 3.10.3) Такая комбинация объекта функции и области видимости (множества связанных переменных), в которой находятся переменные, используемые вызываемой функцией, в литературе по информационным технологиям называется замыканием¹.
Технически все функции в языке JavaScript образуют замыкания: они являются объектами и имеют ассоциированные с ними цепочки областей видимости. Большинство функций вызываются внутри той же цепочки областей видимости, которая действовала на момент определения функции, и в этой ситуации факт образования замыкания не имеет никакого значения. Интересные особенности замыканий начинают проявляться, когда их вызов производится в другой цепочке областей видимости, отличной от той, что действовала на момент определения. Чаще всего это происходит, когда вложенный объект– функция возвращается функцией, вмещающей ее определение. . Существует множество мощных приемов программирования, вовлекающих такого рода вложенные функции-замыкания, и их использование довольно широко распространено в программировании на языке JavaScript. Замыкания могут выглядеть малопонятными при первом знакомстве, однако вам необходимо хорошо понимать их, чтобы чувствовать себя уверенно при их использовании.
Первый шаг к пониманию замыканий - знакомство с лексическими правилами области видимости, действующими для вложенных функций. Взгляните на следующий пример (который напоминает пример в разделе 3.10):
var scope = "global scope"; // Глобальная переменная function checkscope() { var scope = "local scope"; // Локальная переменная function f() { return scope; } // вернет значение локальной переменной scope. return f(); } checkscope() // =>Функция
checkscope()
объявляет локальную переменную и вызывает функцию,
возвращающую значение этой переменной. Должно быть совершенно понятно,
почему вызов checkscope()
возвращает строку «local scope
». Теперь немного
изменим пример. Сможете ли вы сказать, какое значение вернет этот фрагмент?
var scope = "global scope"; // Глобальная переменная function checkscope() { var scope = "local scope"; // Локальная переменная function f() { return scope; } // вернет значение локальной переменной scope return f; } checkscope()() // Какое значение вернет этот вызов? =>В этой версии пара круглых скобок была перемещена из тела функции
checkscope()
за ее пределы. Вместо вызова вложенной функции и возврата ее результата функция
checkscope()
теперь просто возвращает сам объект вложенной функции. Что
произойдет, если вызвать вложенную функцию (добавив вторую пару скобок в последней
строке примера) из-за пределов функции, в которой она определена?
Напомню главное правило лексической области видимости: при выполнении
функции в языке JavaScript используется цепочка областей видимости,
действовавшая на момент ее определения. Вложенная функция f()
была определена в
цепочке видимости, где переменная scope
связана со значением «local scope
». Эта
связь остается действовать и при выполнении функции f
, независимо от того,
откуда был произведен ее вызов. Поэтому последняя строка в примере выше вернет
«local scope
», а не «global scope
». Проще говоря, эта особенность является самой
удивительной и мощной чертой замыканий: они сохраняют связь с локальными
переменными (и параметрами) внешней функции, где они были определены.
Понять суть замыканий будет совсем несложно, если усвоить правило
лексической области видимости: во время выполнения функции используется
цепочка областей видимости, которая действовала в момент ее
определения. Однако некоторые программисты испытывают сложности при
освоении замыканий, потому что не до конца понимают особенности
реализации. Известно, думают они, что локальные переменные, объявленные во
внешней функции, прекращают свое существование после выхода из
внешней функции, но тогда как вложенная функция может использовать
цепочку областей видимости, которая больше не существует? Если вы задавали
себе такой вопрос, значит, у вас наверняка есть опыт работы с
низкоуровневыми языками программирования, такими как C, и аппаратными
архитектурами, использующими стек: если локальные переменные размещать
на стеке, они действительно прекращают свое существование после
завершения функции.
Но вспомните определение цепочки областей видимости из раздела
3.10.3.
Там она описывалась как список объектов, а не стек. Каждый раз, когда
интерпретатор JavaScript вызывает функцию, он создает новый объект для
хранения локальных переменных этой функции, и этот объект
добавляется в цепочку областей видимости. Когда функция возвращает управление,
этот объект удаляется из цепочки. Если в программе нет вложенных
функций и нет ссылок на этот объект, он будет утилизирован сборщиком
мусора. Если в программе имеются вложенные функции, тогда каждая из этих
функций будет владеть ссылкой на свою цепочку областей видимости, а
цепочка будет ссылаться на объекты с локальными переменными. Если
объекты вложенных функций существуют только в пределах своих внешних
функций, они сами будут утилизированы сборщиком мусора, а вместе
с ними будут утилизированы и объекты с локальными переменными, на
которые они ссылались. Но если функция определяет вложенную
функцию и возвращает ее или сохраняет в свойстве какого-либо объекта, то
образуется внешняя ссылка на вложенную функцию. Такой объект
вложенной функции не будет утилизирован сборщиком мусора, и точно так же не
будет утилизирован объект с локальными переменными, на который она ссылается.
В разделе 8.4.1
был приведен пример функции uniqueInteger()
, в которой
используется свойство самой функции для сохранения следующего возвращаемого
значения. Недостаток такого решения состоит в том, что ошибочный или
злонамеренный программный код может сбросить значение счетчика или записать в него
нечисловое значение, вынудив функцию uniqueInteger()
нарушить обязательство
возвращать «уникальное» или «целочисленное» значение:
uniqueInteger.counter = 8; uniqueInteger(); // => uniqueInteger.counter = 8; uniqueInteger(); // => - значение уже не уникальноЗамыкания запирают локальные переменные в объекте вызова функции и могут использовать эти переменные для хранения частной информации. Ниже показано, как можно реализовать функцию
uniqueInteger()
с использованием замыкания:
var uniqueInteger = (function() { // Определение и вызов var counter = 0; // Приватное состояние // следующей функции return function() { return counter++; }; }());(Сравним действие функции с предыдущим примером.)
uniqueInteger.counter = 8; uniqueInteger(); //=> uniqueInteger.counter = 8; uniqueInteger(); //=> - значение уникально uniqueInteger.counter = 8; uniqueInteger(); //=> - значение уникально uniqueInteger.counter; //=>
Внимательно изучите этот пример, чтобы понять, как он действует. На первый
взгляд код начинается со строки с инструкцией присваивания функции
переменной uniqueInteger
. Фактически же это определение и вызов функции (как
подсказывает открывающая круглая скобка в первой строке), поэтому в
действительности переменной uniqueInteger
присваивается значение, возвращаемое
функцией. Если теперь обратить внимание на тело функции, можно увидеть, что она
возвращает другую функцию. Именно этот объект вложенной функции
и присваивается переменной uniqueInteger
. Вложенная функция имеет доступ
к переменным в ее области видимости и может использовать переменную counter
,
объявленную во внешней функции. После возврата из внешней функции
никакой другой программный код не будет иметь доступа к переменной counter
:
вложенная функция будет обладать исключительным правом доступа к ней.
Скрытые переменные, такие как counter
, не являются исключительной
собственностью единственного замыкания: в одной и той же внешней функции вполне
возможно определить две или более вложенных функций, которые будут
совместно использовать одну цепочку областей видимости. Рассмотрим следующий
пример:
function counter() {//function counter() { var n = 0; return { count: function() { return n++; }, reset: function() { n = 0; } }; } var c = counter(), d = counter(); // Создать два счетчика c.count() // => d.count() // => они ведут счет независимо c.reset() // методы reset() и count() совместно // используют одну переменную c.count() // => Здесь мы сбросили счетчик c, d.count() // => : но это не повлияло на счетчик dФункция
counter()
возвращает объект «счетчик». Этот объект имеет два метода:
count()
, возвращающий следующее целое число, и reset()
, сбрасывающий
счетчик в начальное состояние. В первую очередь следует понимать, что два метода
совместно используют одну и ту же приватную переменную n
. Во-вторых, каждый
вызов функции counter()
создает новую цепочку областей видимости и новую
скрытую переменную. То есть если вызвать функцию counter()
дважды, она
вернет два объекта-счетчика (c
и d
) с различными скрытыми переменными. Вызов методов
count()
и reset()
одного объекта-счетчика не оказывает влияния на другой.
counter()
является вариацией примера,
представленного в разделе 6.6,
но здесь для приватной информации вместо обычного свойства объекта используются замыкания:
function counter(n) {// Аргумент n функции - приватная переменная return { // Метод чтения свойства возвращает и увеличивает переменную счетчика. get count() { return n++; }, // Метод записи в свойство не позволяет уменьшать значение n set count(m) { if (m >= n) n = m; else throw Error("count can only be set to a larger value"); } }; } var c = counter(1000); c.count // => c.count // => c.count = 2000 // => c.count // => c.count = 2000 // => Error! // => в консоли: Uncaught Error: count can only be set to a larger valueОбратите внимание, что эта версия функции
counter()
не объявляет локальную
переменную. Для сохранения информации она просто использует параметр n,
доступный обоим методам доступа к свойству. Это позволяет программе,
вызывающей counter()
, определять начальное значение скрытой переменной.
В примере 8.4 демонстрируется обобщение приема совместного использования
скрытой информации в замыканиях. Этот пример определяет функцию
addPrivateProperty()
,
которая в свою очередь определяет скрытую переменную и две
вложенные функции для чтения и записи значения этой переменной. Она добавляет
эти вложенные функции как методы указанного вами объекта:
Пример 8.4. Реализация методов доступа к частному свойству с использованием замыканий
// Эта функция добавляет объекту o именованное свойство // с методами доступа. Методы получают имена вида get<name> и set<name>. // Если дополнительно предоставляется функция проверки, метод записи // будет использовать ее для проверки значения перед сохранением. Если // функция проверки возвращает false, метод записи генерирует исключение. // // Необычность этой функции в том, что значение свойства, доступного // методам getter и setter, сохраняется не в виде свойства объекта o, // а в локальной переменной в этой функции. Кроме того, методы доступа // также определяются внутри этой функции и потому получают доступ // к этой локальной переменной. Это означает, что значение доступно // только этим двум методам и не может быть установлено или изменено // иначе, чем методом setter. function addPrivateProperty(o, name, predicate) { var value; // Это значение свойства // Метод чтения просто возвращает значение. o["get" + name] = function() { return value; }; // Метод записи сохраняет значение или возбуждает исключение, // если функция проверки отвергает это значение. o["set" + name] = function(v) { if (predicate && !predicate(v)) throw Error("set" + name + ": invalid value " + v); else value = v; }; } // Следующий фрагмент демонстрирует работу метода addPrivateProperty() var o = {}; // Пустой объект // Добавляет свойство с методами доступа getName и setName(). // Обеспечивает допустимость только строковых значений addPrivateProperty(o, "Name", function(x) { return typeof x == "string"; }); o.setName("Frank"); // Установить значение свойства console.log(o.getName()); // Получить значение свойства // Попробовать установить свойству значение неверного типа: o.setName(0); // => Uncaught Error: setName: invalid value 0Мы увидели несколько примеров, когда замыкания определяются в одной и той же цепочке областей видимости и совместно используют одну локальную переменную или переменные. Важно знать и уметь пользоваться этим приемом, но не менее важно уметь распознавать ситуации, когда замыкания получают переменную в совместное использование по ошибке. Рассмотрим следующий пример:
// Эта функция возвращает функцию, которая всегда возвращает v function constfunc(v) { return function() { return v; }; } // Создать массив функций-констант: var funcs = []; for(var i = 0; i < 10; i++) funcs[i] = constfunc(i); // Функция в элементе массива с индексом 5 возвращает 5. funcs[5]() // =>При создании подобного программного кода, который создает множество замыканий в цикле, часто допускают ошибку, помещая цикл внутрь функции, которая определяет замыкания. Например, взгляните на следующий фрагмент:
// Возвращает массив функций, возвращающих значения 0-9 function constfuncs() { var funcs = []; for(var i = 0; i < 10; i++) funcs[i] = function() { return i; }; return funcs; } var funcs = constfuncs(); funcs[5]() // Что вернет этот вызов?Функция в приведенном коде создает 10 замыканий и сохраняет их в массиве. Замыкания образуются в одном и том же вызове функции, поэтому все они получат доступ к переменной
i
. Когда constfuncs()
вернет управление, переменная i
будет иметь
значение 10, и все 10 замыканий будут совместно использовать это значение. Таким
образом, все функции в возвращаемом массиве будут возвращать одно и то же
значение, что совсем не то, чего мы пытались добиться. Важно помнить, что
цепочка областей видимости, связанная с замыканием, "живет".
Вложенные функции не создают приватные копии области видимости и не фиксируют
значения переменных.
Кроме того, при создании замыканий следует помнить, что this
- это ключевое
слово, а не переменная. Как отмечалось выше, каждый вызов функции получает
значение this
, и замыкание не имеет доступа к значению this
внешней
функции, если внешняя функция не сохранит его в переменной:
var self = this; // Сохранить значение this в переменной для использования // во вложенной функции.То же относится и к объекту
arguments
. Это не ключевое слово, но он
автоматически объявляется при каждом вызове функции. Поскольку замыкания при вызове
получают собственный объект arguments
, они не могут обращаться к массиву
аргументов внешней функции, если внешняя функция не сохранит этот массив
в переменной с другим именем:
>
// Сохранить для использования во вложенных функциях: var outerArguments = arguments;Далее в этой главе в примере 8.5 определяется замыкание, использующее эти приемы для получения доступа к значениям
this
и arguments
внешней функции.
Мы видели, что в JavaScript-программах функции могут использоваться как
значения. Оператор typeof возвращает для функций строку «function
», однако в
действительности функции в языке JavaScript – это особого рода объекты. А раз
функции являются объектами, то они имеют свойства и методы, как любые
другие объекты. Существует даже конструктор Function()
, который создает новые
объекты функций. В следующих подразделах описываются свойства и методы
функций, а также конструктор Function()
. Кроме того, информация обо всем этом
приводится в справочном разделе.
В теле функции свойство arguments.length
определяет количество аргументов,
переданных функции. Однако свойство length
самой функции имеет иной смысл.
Это свойство, доступное только для чтения, возвращает число параметров функции –
число аргументов, указанное при объявлении функции, которое обычно представляет собой
число аргументов, которое функция ожидает получить.
В следующем фрагменте определяется функция с именем check()
, получающая
массив аргументов arguments от другой функции. Она сравнивает свойство
arguments.length
(число фактически переданных аргументов) со свойством arguments.callee.length
,
(число ожидаемых аргументов), чтобы определить, передано ли
функции столько аргументов, сколько она ожидает. Если значения не совпадают,
генерируется исключение. За функцией check()
следует тестовая функция f()
,
демонстрирующая порядок использования функции check()
:
// Эта функция использует arguments.callee, поэтому // она не будет работать в строгом режиме. function check(args) { var actual = args.length; // Фактическое число аргументов var expected = args.callee.length; // Ожидаемое число аргументов if (actual !== expected) // Если не совпадают, генерируется исключение. throw Error("Expected " + expected + "args; got " + actual); } function f(x, y, z) { check(arguments); // Проверить число ожидаемых # // и фактически переданных аргументов #. return x + y + z; // Теперь выполнить оставшуюся часть функции } // как обычно.
Любая функция имеет свойство prototype
, ссылающееся на объект, известный
как Object.prototype
. Каждая функция имеет свой Object.prototype
. Когда
функция используется в роли конструктора, вновь созданный объект наследует
свойства прототипа. Прототипы и свойство prototype
обсуждались
в разделе 6.1.3, и мы еще раз
вернемся к этим понятиям в главе 9.
Методы call()
и apply()
позволяют выполнять косвенный вызов функции
(раздел 8.2.4), как если бы она была методом некоторого другого объекта. (Мы уже
использовали метод call()
в примере 6.4 для вызова Object.prototype.toString
относительно объекта, класс которого необходимо было определить.) Первым аргументом
обоим методам, и call()
, и apply()
, передается объект, чьим методом как бы становится вызываемая функция;
этот аргумент определяет контекст вызова и становится значением
ключевого слова this в теле функции. Чтобы вызвать функцию f()
(без аргументов)
как метод объекта о, можно использовать любой из методов call()
или apply()
:
f.call(o); f.apply(o);Любая из этих строк кода эквивалентна следующему фрагменту (где предполагается, что объект o не имеет свойства с именем m):
o.m = f; // Временно сделать f методом o. o.m(); // Вызывать его без аргументов. delete o.m; // Удалить временный метод.В строгом режиме ECMAScript 5 первый аргумент методов
call()
и apply()
становится значением this
, даже если это простое значение, null
или undefined
. В
ECMAScript 3 и в нестрогом режиме значения null
и undefined
замещаются глобальным
объектом, а простое значение – соответствующим объектом-оберткой.
Все остальные аргументы метода call()
, следующие за первым аргументом,
определяющим контекст вызова, передаются вызываемой функции. Например, ниже
показано, как можно передать функции f()
два числа и вызвать ее, как если бы
она была методом объекта o:
f.call(o, 1, 2);Метод
apply()
действует подобно методу call()
, за исключением того, что
аргументы для функции передаются в виде массива:
f.apply(o, [1,2]);Если функция способна обрабатывать произвольное число аргументов, метод
apply()
может использоваться для вызова такой функции в контексте массива
произвольной длины. Например, чтобы отыскать наибольшее число в массиве
чисел, для передачи элементов массива функции Math.max()
можно было бы
использовать метод apply()
:
var biggest = Math.max.apply(Math, array_of_numbers);Обратите внимание, что метод
apply()
может работать не только с настоящими
массивами, но и с объектами, подобными массивам. В частности, вы можете
вызвать функцию с теми же аргументами, что и текущую функцию, передав массив
с аргументами непосредственно методу apply()
. Этот прием демонстрируется ниже:
// Замещает метод m объекта o версией метода, которая регистрирует // сообщения до и после вызова оригинального метода. function trace(o, m) { var original = o[m]; // Сохранить оригинальный метод в замыкании. o[m] = function() { // Определить новый метод. console.log(new Date(), "Entering:", m); // Записать сообщение. var result = original.apply(this, arguments); // Вызвать оригинал. console.log(new Date(), "Exiting:", m); // Записать сообщение. return result; }; }Эта функция
trace()
принимает объект и имя метода. Она замещает указанный
метод новым методом, который «обертывает» оригинальный метод
дополнительной функциональностью. Такой прием динамического изменения
существующих методов иногда называется «обезьяньей заплатой»
(“monkey-patching”).
Метод bind()
впервые появился в ECMAScript 5, но его легко имитировать в
ECMAScript 3. Как следует из его имени, основное назначение метода bind()
состоит
в том, чтобы связать (bind
) функцию с объектом. Если вызвать метод bind()
функции f
и передать ему объект o
, он вернет новую функцию. Вызов новой функции
(как обычной функции) выполнит вызов оригинальной функции f
как метода
объекта o
. Любые аргументы, переданные новой функции, будут переданы
оригинальной функции. Например:
function f(y) { return this.x + y; } // Функция, которую требуется привязать var o = { x : 1 }; // Объект, к которому выполняется привязка var g = f.bind(o); // Вызов g(x) вызовет o.f(x) g(2) // =>Такой способ связывания легко реализовать в ECMAScript 3, как показано ниже:
// Возвращает функцию, которая вызывает f как метод объекта o // и передает ей все свои аргументы. function bind(f, o) { if (f.bind) return f.bind(o); // Использовать метод bind, если имеется else return function() { // Иначе связать, как здесь показано return f.apply(o, arguments); }; }Метод
bind()
в ECMAScript 5 не просто связывает функцию с объектом. Он также
выполняет частичное применение: помимо значения this
связаны будут все
аргументы, переданные методу bind()
после первого его аргумента. Частичное
применение – распространенный прием в функциональном программировании и
иногда называется каррингом
(currying
). Ниже приводится несколько примеров
использования метода bind()
для частичного применения:
var sum = function(x,y) { return x + y };// Возвращает сумму 2 аргументов // Создать новую функцию, подобную sum, но со связанным значением null // ключевого слова this и со связанным значением первого аргумента, // равным 1 Новая функция принимает всего один аргумент. var succ = sum.bind(null, 1); succ(2) // => аргумент x связан со значением 1, // а 2 передается в аргумент y function f(y,z) { return this.x + y + z }; // Еще одна функция сложения var g = f.bind({x:1}, 2); // Связать this и y g(3) // => : this.x связан с 1, y связан с 2, а z имеет значение 3В ECMAScript 3 также возможно связывать значение
this
и выполнять частичное
применение. Стандартный метод bind()
можно имитировать программным
кодом, который приводится в примере 8.5. Обратите внимание, что этот метод
сохраняется как Function.prototype.bind()
, благодаря чему все функции наследуют
его. Данный прием подробно рассматривается в разделе 9.4.
Пример 8.5. Метод Function.bindf) для ECMAScript 3
if (!Function.prototype.bind) { Function.prototype.bind = function(o /*, args */) { // Сохранить this и arguments в переменных, чтобы их можно было // использовать ниже во вложенной функции. var self = this, boundArgs = arguments; // Возвращаемое значение метода bind() - функция return function() { // Скомпоновать список аргументов, начиная со второго аргумента // метода bind, и передать все эти аргументы указанной функции var args = [], i; for(i = 1; i < boundArgs.length; i++) args.push(boundArgs[i]); for(i = 0; i < arguments.length; i++) args.push(arguments[i]); // Теперь вызвать self как метод объекта o со всеми аргументами return self.apply(o, args); }; }; }Обратите внимание, что функция, возвращаемая этим методом
bind()
, является
замыканием, использующим объявленные во внешней функции переменные self
и
boundArgs
, которые остаются доступными вложенной функции даже хотя внутренняя
функция возвращена из внешней функции и вызывается после того, как внешняя функция завершена.
Метод bind()
, определяемый стандартом ECMAScript 5, имеет некоторые
особенности, которые невозможно реализовать в ECMAScript 3. Прежде всего,
настоящий метод bind()
возвращает объект function
, свойство length которого
установлено в соответствии с количеством параметров связываемой функции минус
количество связанных аргументов (но не меньше нуля). Во-вторых, метод bind()
в ECMAScript 5 может использоваться для частичного применения
функций-конструкторов. Если функцию, возвращаемую методом bind()
, использовать как
конструктор, значение this
, переданное методу bind()
, игнорируется, и оригинальная
функция будет вызвана как конструктор с уже связанными аргументами, если
они были определены. Функции, возвращаемые методом bind()
, не имеют
свойства prototype
(свойство prototype
обычных функций нельзя удалить), и объекты,
созданные связанными функциями-конструкторами, наследуют свойство
prototype
оригинального, несвязанного конструктора.
Кроме того, для целей оператора instanceof
связанные конструкторы действуют
точно так же, как несвязанные.
Подобно другим объектам в языке JavaScript, функции имеют метод toString()
.
Спецификация ECMAScript требует, чтобы этот метод возвращал строку,
следующую синтаксису инструкции объявления функции. На практике
большинство реализаций (но не все) метода toString()
возвращают полный исходный текст
функции. Для встроенных функций обычно возвращается строка, содержащая
вместо тела функции текст «[native code]
» или аналогичный.
Функции обычно определяются с помощью ключевого слова function
либо в
форме инструкции объявления функции, либо в форме выражения-литерала.
Однако функции могут также определяться с помощью конструктора Function()
.
Например:
var f = new Function("x", "y", "return x*y;");Эта строка кода создает новую функцию, которая более или менее эквивалентна функции, определенной со знакомым синтаксисом:
var f = function(x, y) { return x*y; }Конструктор
Function()
принимает произвольное число строковых аргументов.
Последний аргумент должен содержать текст с телом функции; он может
включать произвольное число инструкций на языке JavaScript, разделенных точкой
с запятой. Все остальные аргументы конструктора интерпретируются как имена
параметров функции. Чтобы создать функцию, не имеющую аргументов,
достаточно передать конструктору всего одну строку – тело функции.
Примечательно, что конструктору Function()
не передается никаких аргументов,
определяющих имя создаваемой функции. Подобно литералам функций,
конструктор Function()
создает анонимные функции.
Есть несколько моментов, связанных с конструктором Function()
, о которых
следует упомянуть особо:
• Конструктор Function()
позволяет динамически создавать и компилировать
функции в процессе выполнения программы.
• При каждом вызове конструктор Function()
выполняет синтаксический
анализ тела функции и создает новый объект функции. Если вызов конструктора
производится в теле цикла или часто вызываемой функции, это может
отрицательно сказаться на производительности программы. Напротив,
вложенные функции и выражения определения функций внутри циклов не
компилируются повторно.
• И последний, очень важный момент: когда функция создается с помощью
конструктора Function()
, не учитывается лексическая область видимости –
функции всегда компилируются как глобальные функции, что наглядно
демонстрирует следующий фрагмент:
var scope = "global"; function constructFunction() { var scope = "local"; return new Function("return scope"); // Здесь не используется } // локальная область видимости! // Следующая строка вернет "global", потому что функция, возвращаемая // конструктором Function(), является глобальной. constructFunction()(); // =>Точнее всего конструктор
Function()
соответствует глобальной версии eval()
(раздел 4.12.2),
которая определяет новые переменные и функции в своей
собственной области видимости. Вам редко придется использовать этот конструктор в
своих программах.
В разделе 7.11 мы узнали, что существуют объекты, «подобные массивам», которые не являются настоящими массивами, но во многих случаях могут интерпретироваться как массивы. Аналогичная ситуация складывается с функциями. Вызываемый объект – это любой объект, который может быть вызван в выражении вызова функции. Все функции являются вызываемыми объектами, но не все вызываемые объекты являются функциями.
Вызываемые объекты, не являющиеся функциями, встречаются в современных
реализациях JavaScript в двух ситуациях. Во-первых, веб-броузер IE (версии 8
и ниже) реализует клиентские методы, такие как Window.alert()
и Document.getElementsById()
,
используя вызываемые объекты, а не объекты класса Function()
. Эти
методы действуют в IE точно так же, как в других браузерах, но они не являются
объектами Function()
. В IE9 был выполнен переход на использование настоящих
функций, поэтому со временем эта разновидность вызываемых объектов будет
использоваться все меньше и меньше.
Другой типичной разновидностью вызываемых объектов являются объекты
RegExp
– во многих браузерах предоставляется возможность напрямую
вызывать объект RegExp
, как более краткий способ вызова его метода exec()
. Эта
возможность не предусматривается стандартом JavaScript. В свое время она была
реализована компанией Netscape и подхвачена другими производителями для
обеспечения совместимости. Старайтесь не писать программы, опирающиеся на
возможность вызова объектов RegExp
: данная особенность, скорее всего, будет
объявлена нерекомендуемой и будет ликвидирована в будущем. Оператор typeof
не во всех браузерах одинаково распознает вызываемые объекты RegExp
. В одних
браузерах он возвращает строку «function
», а в других – «object
».
Если в программе потребуется определить, является ли объект настоящим
объектом function
(и обладает методами функций), сделать это можно, определив
значение атрибута class
(раздел 6.8.2),
использовав прием, продемонстрированный в примере 6.4:
function isFunction(x) { return Object.prototype.toString.call(x) === "[object Function]"; }Обратите внимание, насколько эта функция
isFunction()
похожа на функцию
isArray()
, представленную в разделе 7.10.
JavaScript не является языком функционального программирования, таким как Lisp
или Haskell
, но тот факт, что программы на языке JavaScript могут
манипулировать функциями как объектами означает, что в JavaScript можно использовать
приемы функционального программирования. Масса методов в ECMAScript 5,
таких как map()
и reduce()
, сами по себе способствуют использованию
функционального стиля программирования. В следующих разделах демонстрируются
приемы функционального программирования на языке JavaScript. Их цель – не
подтолкнуть вас к использованию этого замечательного стиля
программирования, а показать широту возможностей функций в языке JavaScript.
()Если эта тема вам любопытна, вероятно, вас заинтересует возможность использования (или хотя бы знакомства) библиотеки Functional JavaScript Оливера Стила (Oliver Steel), которую можно найти по адресу: http://osteele.com/sources/javascript/functional/.
Представим, что у нас имеется массив чисел и нам необходимо найти среднее значение и стандартное отклонение для этих значений. Эту задачу можно было бы решить без использования приемов функционального программирования, как показано ниже:
var data = [1,1,3,5,5]; // Массив чисел // Среднее - это сумма значений элементов, деленная на их количество var total = 0; for(var i = 0; i < data.length; i++) total += data[i]; var mean = total/data.length; // Среднее значение равно // Чтобы найти стандартное отклонение, необходимо вычислить // сумму квадратов отклонений элементов от среднего. total = 0; for(var i = 0; i < data.length; i++) { var deviation = data[i] - mean; total += deviation * deviation; } var stddev = Math.sqrt(total/(data.length-1)); // Стандартное отклонение =Те же вычисления можно выполнить в более кратком функциональном стиле, задействовав методы массивов
map()
и reduce()
, как показано ниже (краткое
описание этих методов приводится в разделе 7.9):
// Для начала необходимо определить две простые функции var sum = function(x,y) { return x+y; }; var square = function(x) { return x*x; }; // Затем использовать их совместно с методами класса Array // для вычисления среднего и стандартного отклонения var data = [1,1,3,5,5]; var mean = data.reduce(sum)/data.length; var deviations = data.map(function(x) {return x-mean;}); var stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1));А как быть, если в нашем распоряжении имеется только реализация БСМА- Script 3, где отсутствуют эти новейшие методы массивов? Можно определить собственные функции
map()
и reduce()
, которые будут
использовать встроенные методы при их наличии:
// Вызывает функцию f для каждого элемента массива и возвращает массив // результатов Использует метод Array.prototype.map, если он определен. var map = Array.prototype.map ? function(a, f) { return a.map(f); } // Если метод map() доступен : function(a,f) { // Иначе реализовать свою версию var results = []; for(var i = 0, len = a.length; i < len; i++) { if (i in a) results[i] = f.call(null, a[i], i, a); } return results; }; // Выполняет свертку массива в единственное значение, используя функцию f // и необязательное начальное значение. Использует метод // Array.prototype.reduce, если он определен. var reduce = Array.prototype.reduce ? function(a, f, initial) { // Если метод reduce() доступен. if (arguments.length > 2) return a.reduce(f, initial); // Если указано начальное значение else return a.reduce(f); // Иначе без начального значения. } : function(a, f, initial) { // Этот алгоритм взят из спецификации ES5 var i = 0, len = a.length, accumulator; // Использовать указанное начальное значение или первый элемент a if (arguments.length > 2) accumulator = initial; else { // Найти первый элемент массива с определенным значением if (len == 0) throw TypeError(); while(i < len) { if (i in a) { accumulator = a[i++]; break; } else i++; } if (i == len) throw TypeError(); } // Теперь вызвать f для каждого оставшегося элемента массива while(i < len) { if (i in a) accumulator = f.call(undefined, accumulator, a[i], i, a); i++; } return accumulator; };
После определения этих функций map()
и reduce()
вычисление среднего и стандартного отклонения будет выглядеть так:
var data = [1,1,3,5,5]; var sum = function(x,y) { return x+y; }; var square = function(x) { return x*x; }; var mean = reduce(data, sum)/data.length; var deviations = map(data, function(x) {return x-mean;}); var stddev = Math.sqrt(reduce(map(deviations, square), sum)/(data.length-1));
Функции высшего порядка - это функции, которые оперируют функциями, принимая одну или более функций и возвращая новую функцию. Например:
// Эта функция высшего порядка возвращает новую функцию, которая передает // свои аргументы функции f и возвращает логическое отрицание значения, // свои аргументы возвращаемого функцией f function not(f) { return function() { // Возвращает новую функцию var result = f.apply(this, arguments); // вызов f return !result; // и инверсия результата. }; } var even = function(x) { // Функция, определяющая четность числа return x % 2 === 0; //return x % 2 === 0; }; var odd = not(even); // Новая функция, выполняющая противоположную // операцию [1,1,3,5,5].every(odd); // => : все элементы массива нечетныеФункция
not()
в приведенном примере является функцией высшего порядка, потому что
она принимает функцию в виде аргумента и возвращает новую функцию. В
качестве еще одного примера рассмотрим функцию mapper()
, представленную ниже.
Она принимает функцию в виде аргумента и возвращает новую функцию,
которая отображает один массив в другой, применяя указанную функцию. Данная
функция использует функцию map()
, которая была определена ранее, и важно
понимать, чем отличаются эти две функции:
// Возвращает функцию, которая принимает массив в виде аргумента, // применяет функцию f к каждому элементу и возвращает массив возвращаемых // значений. Эта функция отличается от функции map(), представленной выше. function mapper(f) { return function(a) { return map(a, f); }; } var increment = function(x) { return x+1; }; var incrementer = mapper(increment); incrementer([1,2,3]) // =>Ниже приводится пример еще одной, более универсальной функции, которая принимает две функции, f и g, и возвращает новую функцию, которая возвращает результат f(g()):
// Возвращает новую функцию, которая вычисляет f(g(...)). Возвращаемая // функция h передает все свои аргументы функции g, затем передает // значение, полученное от g, функции f и возвращает результат вызова f. // Обе функции, f и g, вызываются с тем же значением this, что и h. function compose(f,g) { return function() { // Для вызова f используется call, потому что ей передается // единственное значение, а для вызова g используется apply, // потому что ей передается массив значений. return f.call(this, g.apply(this, arguments)); }; } var square = function(x) { return x*x; }; var sum = function(x,y) { return x+y; }; var squareofsum = compose(square, sum); squareofsum(2,3) // =>Функции
partial()
и memoize()
, которые определяются в следующем разделе,
представляют собой еще две важные функции высшего порядка.
Метод bind()
функции f
(раздел 8.7.4) возвращает новую функцию, которая
вызывает f
в указанном контексте и с заданным набором аргументов. Можно сказать,
что он связывает функцию с объектом контекста и частично применяет аргументы.
Метод bind()
применяет аргументы слева, т. е. аргументы, которые передаются
методу bind()
, помещаются в начало списка аргументов, передаваемых оригинальной
функции. Однако есть возможность частичного применения аргументов справа:
// Вспомогательная функция преобразования объекта (или его части), // для преобразования объекта arguments в настоящий массив. function array(a, n) { return Array.prototype.slice.call(a, n || 0); } // Аргументы этой функции помещаются в начало списка function partialLeft(f /*, ...*/) { var args = arguments; // Сохранить внешний массив аргументов return function() {// и вернуть эту функцию var a = array(args, 1); // начиная с элемента 1 во внешнем массиве. a = a.concat(array(arguments)); // Добавить внутренний массив аргумент. return f.apply(this, a); // Вызвать f с этим списком аргументов. }; } //Аргументы этой функции помещаются в конец списка function partialRight(f /*, ...*/) { var args = arguments; // Сохранить внешний массив аргументов return function() { // и вернуть эту функцию. var a = array(arguments); // Начинать с внутреннего массива аргументов. a = a.concat(array(args,1)); // Добавить внешние аргументы, начиная с 1. return f.apply(this, a); // Вызвать f с этим списком аргументов. }; } // Аргументы этой функции играют роль шаблона. Неопределенные значения // в списке аргументов заполняются значениями из внутреннего набора function partial(f /*, ... */) { var args = arguments; // Сохранить внешний массив аргументов return function() { var a = array(args, 1); // Начинать с внешнего массива аргументов var i=0, j=0; // Цикл по этим аргументам, заменить значения undefined значениями // из внутреннего списка аргументов for(; i < a.length; i++) if (a[i] === undefined) a[i] = arguments[j++]; // Добавить оставшиеся внутренние аргументы в конец списка. a = a.concat(array(arguments, j)) return f.apply(this, a); }; } // Здесь приводится функция, принимающая три аргумента var f = function(x,y,z) { return x * (y - z); }; // Обратите внимание на отличия между следующими тремя // частичными применениями partialLeft(f, 2)(3,4)// => : Свяжет первый аргумент: 2 * (3 - 4) partialRight(f, 2)(3,4)// => : Свяжет последний аргумент: 3 * (4 - 2) partial(f, undefined, 2)(3,4)// =>: Свяжет средний аргумент: 3 * (2 - 4)Эти функции частичного применения позволяют легко объявлять новые функции на основе уже имеющихся функций. Например:
var increment = partialLeft(sum, 1); var cuberoot = partialRight(Math.pow, 1/3); String.prototype.first = partial(String.prototype.charAt, 0); String.prototype.last = partial(String.prototype.substr, -1, 1);Прием частичного применения становится еще более интересным, когда он используется в комбинации с функциями высшего порядка. Например, ниже демонстрируется еще один способ определения функции not(), представленной выше, за счет совместного использования приемов композиции и частичного применения:
var not = partialLeft(compose, function(x) { return !x; }); var even = function(x) { return x % 2 === 0; }; var odd = not(even); var isNumber = not(isNaN)Прием композиции и частичного применения можно также использовать для вычисления среднего значения и стандартного отклонения в крайне функциональном стиле:
var data = [1,1,3,5,5];// Исходные данные var sum = function(x,y) { return x+y; };// Две элементарные функции var product = function(x,y) { return x*y; }; var neg = partial(product, -1);// Определения других функций var square = partial(Math.pow, undefined, 2); var sqrt = partial(Math.pow, undefined, .5); var reciprocal = partial(Math.pow, undefined, -1); // Вычислить среднее и стандартное отклонение. Далее используются только // функции без каких либо операторов, отчего программный код начинает // напоминать программный код на языке Lisp! var mean = product(reduce(data, sum), reciprocal(data.length)); var stddev = sqrt(product(reduce(map(data, compose(square, partial(sum, neg(mean)))), sum), reciprocal(sum(data.length,-1))));
В разделе 8.4.1
определена функция нахождения факториала,
сохраняющая ранее вычисленные результаты. В функциональном программировании
такого рода кэширование называется мемоизацией (memoization). В следующем
примере функция memoize()
высшего порядка
принимает функцию в виде аргумента и возвращает ее мемоизованную версию:
// Возвращает мемоизованную версию функции f. Работает, только если все // возможные аргументы f имеют отличающиеся строковые представления. function memoize(f) { var cache = {}; // Кэш значений сохраняется в замыкании. return function() { // Создать строковую версию массива arguments для использования // в качестве ключа кэша. var key = arguments.length + Array.prototype.join.call(arguments,","); if (key in cache) return cache[key]; else return cache[key] = f.apply(this, arguments); }; }Функция
memoize()
создает новый объект для использования в качестве кэша
и присваивает его локальной переменной, оставаясь доступным
(через замыкание) только для возвращаемой функции. Возвращаемая функция
преобразует свой массив arguments
в строку и использует ее как имя свойства
объекта-кэша. Если значение присутствует в кэше, оно просто возвращается в
качестве результата. В противном случае вызывается оригинальная функция,
вычисляющая значение для заданной комбинации значений аргументов; полученное
значение помещается в кэш и возвращается. Следующий фрагмент
демонстрирует, как можно использовать функцию memoize()
:
// Возвращает наибольший общий делитель двух целых чисел, // используя алгоритм Эвклида: function gcd(a,b) { // Проверка типов a и b опущена var t; // Временная переменная для обмена if (a < b) t=b, b=a, a=t; // Убедиться, что a >= b while(b != 0) t=b, b = a%b, a=t; // Это алгоритм Эвклида поиска НОД return a; } var gcdmemo = memoize(gcd); gcdmemo(85, 187) // => // Обратите внимание, что при мемоизации рекурсивных функций желательно, // чтобы рекурсия выполнялась в мемоизованной версии, а не в оригинале. var factorial = memoize(function(n) { return (n <= 1) ? 1 : n * factorial(n-1); }); factorial(5) // => . Также поместит в кэш факториалы чисел 4, 3, 2 и 1.