Функции скрипта. Функциональное программирование

Люди считают, что компьютерные науки – это искусство для гениев. В реальности всё наоборот – просто множество людей делают вещи, которые стоят друг на друге, будто составляя стену из маленьких камушков.

Дональд Кнут

Вы уже видели вызовы функций, таких как alert . Функции – это хлеб с маслом программирования на JavaScript. Идея оборачивания куска программы и вызова её как переменной очень востребована. Это инструмент для структурирования больших программ, уменьшения повторений, назначения имён подпрограммам, и изолирование подпрограмм друг от друга.

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

Средний взрослый русскоговорящий человек знает примерно 10000 слов. Редкий язык программирования содержит 10000 встроенных команд. И словарь языка программирования определён чётче, поэтому он менее гибок, чем человеческий. Поэтому нам обычно приходится добавлять в него свои слова, чтобы избежать излишних повторений.

Определение функции

Определение функции – обычное определение переменной, где значение, которое получает переменная, является функцией. Например, следующий код определяет переменную square, которая ссылается на функцию, подсчитывающую квадрат заданного числа:

Var square = function(x) { return x * x; }; console.log(square(12)); // → 144

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

У функции может быть несколько параметров, или вообще их не быть. В следующем примере makeNoise не имеет списка параметров, а у power их целых два:

Var makeNoise = function() { console.log("Хрясь!"); }; makeNoise(); // → Хрясь! var power = function(base, exponent) { var result = 1; for (var count = 0; count < exponent; count++) result *= base; return result; }; console.log(power(2, 10)); // → 1024

Некоторые функции возвращают значение, как power и square, другие не возвращают, как makeNoise, которая производит только побочный эффект. Инструкция return определяет значение, возвращаемое функцией. Когда обработка программы доходит до этой инструкции, она сразу же выходит из функции, и возвращает это значение в то место кода, откуда была вызвана функция. return без выражения возвращает значение undefined .

Параметры и область видимости

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

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

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

Следующий код иллюстрирует это. Он определяет и вызывает две функции, которые присваивают значение переменной x. Первая объявляет её как локальную, тем самым меняя только локальную переменную. Вторая не объявляет, поэтому работа с x внутри функции относится к глобальной переменной x, заданной в начале примера.

Var x = "outside"; var f1 = function() { var x = "inside f1"; }; f1(); console.log(x); // → outside var f2 = function() { x = "inside f2"; }; f2(); console.log(x); // → inside f2

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

Вложенные области видимости

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

К примеру, следующая довольно бессмысленная функция содержит внутри ещё две:

Var landscape = function() { var result = ""; var flat = function(size) { for (var count = 0; count < size; count++) result += "_"; }; var mountain = function(size) { result += "/"; for (var count = 0; count < size; count++) result += """; result += "\\"; }; flat(3); mountain(4); flat(6); mountain(1); flat(1); return result; }; console.log(landscape()); // → ___/""""\______/"\_

Функции flat и mountain видят переменную result, потому что они находятся внутри функции, в которой она определена. Но они не могут видеть переменные count друг друга, потому что переменные одной функции находятся вне области видимости другой. А окружение снаружи функции landscape не видит ни одной из переменных, определённых внутри этой функции.

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

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

Var something = 1; { var something = 2; // Делаем что-либо с переменной something... } // Вышли из блока...

Но something внутри блока – это та же переменная, что и снаружи. Хотя такие блоки и разрешены, имеет смысл использовать их только для команды if и циклов.

Если это кажется вам странным – так кажется не только вам. В версии JavaScript 1.7 появилось ключевое слово let, которое работает как var, но создаёт переменные, локальные для любого данного блока, а не только для функции.

Функции как значения

Имена функций обычно используют как имя для кусочка программы. Такая переменная однажды задаётся и не меняется. Так что легко перепутать функцию и её имя.

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

Var launchMissiles = function(value) { missileSystem.launch("пли!"); }; if (safeMode) launchMissiles = function(value) {/* отбой */};

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

Объявление функций

Есть более короткая версия выражения “var square = function…”. Ключевое слово function можно использовать в начале инструкции:

Function square(x) { return x * x; }

Это объявление функции. Инструкция определяет переменную square и присваивает ей заданную функцию. Пока всё ок. Есть только один подводный камень в таком определении.

Console.log("The future says:", future()); function future() { return "We STILL have no flying cars."; }

Такой код работает, хотя функция объявляется ниже того кода, который её использует. Это происходит оттого, что объявления функций не являются частью обычного исполнения программ сверху вниз. Они «перемещаются» наверх их области видимости и могут быть вызваны в любом коде в этой области. Иногда это удобно, потому что вы можете писать код в таком порядке, который выглядит наиболее осмысленно, не беспокоясь по поводу необходимости определять все функции выше того места, где они используются.

А что будет, если мы поместим объявление функции внутрь условного блока или цикла? Не надо так делать. Исторически разные платформы для запуска JavaScript обрабатывали такие случаи по разному, а текущий стандарт языка запрещает так делать. Если вы хотите, чтобы ваши программы работали последовательно, используйте объявления функций только внутри других функций или основной программы.

Function example() { function a() {} // Нормуль if (something) { function b() {} // Ай-яй-яй! } }

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

Function greet(who) { console.log("Привет, " + who); } greet("Семён"); console.log("Покеда");

Обрабатывается она примерно так: вызов greet заставляет проход прыгнуть на начало функции. Он вызывает встроенную функцию console.log, которая перехватывает контроль, делает своё дело и возвращает контроль. Потом он доходит до конца greet, и возвращается к месту, откуда его вызвали. Следующая строчка опять вызывает console.log.

Схематично это можно показать так:

Top greet console.log greet top console.log top

Поскольку функция должна вернуться на то место, откуда её вызвали, компьютер должен запомнить контекст, из которого была вызвана функция. В одном случае, console.log должна вернуться обратно в greet. В другом, она возвращается в конец программы.

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

Хранение стека требует места в памяти. Когда стек слишком сильно разрастается, компьютер прекращает выполнение и выдаёт что-то вроде “stack overflow” или “ too much recursion”. Следующий код это демонстрирует – он задаёт компьютеру очень сложный вопрос, который приводит к бесконечным прыжкам между двумя функциями. Точнее, это были бы бесконечные прыжки, если бы у компьютера был бесконечный стек. В реальности стек переполняется.

Function chicken() { return egg(); } function egg() { return chicken(); } console.log(chicken() + " came first."); // → ??

Необязательные аргументы
Следующий код вполне разрешён и выполняется без проблем:

Alert("Здрасьте", "Добрый вечер", "Всем привет!");

Официально функция принимает один аргумент. Однако, при таком вызове она не жалуется. Она игнорирует остальные аргументы и показывает «Здрасьте».

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

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

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

Function power(base, exponent) { if (exponent == undefined) exponent = 2; var result = 1; for (var count = 0; count < exponent; count++) result *= base; return result; } console.log(power(4)); // → 16 console.log(power(4, 3)); // → 64

В следующей главе мы увидим, как в теле функции можно узнать точное число переданных ей аргументов. Это полезно, т.к. позволяет создавать функцию, принимающую любое количество аргументов. К примеру, console.log использует это свойство, и выводит все переданные ему аргументы:

Console.log("R", 2, "D", 2); // → R 2 D 2

Замыкания

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

Следующий пример иллюстрирует этот вопрос. В нём объявляется функция wrapValue, которая создаёт локальную переменную. Затем она возвращает функцию, которая читает эту локальную переменную и возвращает её значение.

Function wrapValue(n) { var localVariable = n; return function() { return localVariable; }; } var wrap1 = wrapValue(1); var wrap2 = wrapValue(2); console.log(wrap1()); // → 1 console.log(wrap2()); // → 2

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

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

С небольшим изменением мы превращаем наш пример в функцию, умножающую числа на любое заданное число.

Function multiplier(factor) { return function(number) { return number * factor; }; } var twice = multiplier(2); console.log(twice(5)); // → 10

Отдельная переменная вроде localVariable из примера с wrapValue уже не нужна. Так как параметр – сам по себе локальная переменная.

Потребуется практика, чтобы начать мыслить подобным образом. Хороший вариант мысленной модели – представлять, что функция замораживает код в своём теле и обёртывает его в упаковку. Когда вы видите return function(...) {...}, представляйте, что это пульт управления куском кода, замороженным для употребления позже.

В нашем примере multiplier возвращает замороженный кусок кода, который мы сохраняем в переменной twice. Последняя строка вызывает функцию, заключённую в переменной, в связи с чем активируется сохранённый код (return number * factor;). У него всё ещё есть доступ к переменной factor, которая определялась при вызове multiplier, к тому же у него есть доступ к аргументу, переданному во время разморозки (5) в качестве числового параметра.

Рекурсия

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

Function power(base, exponent) { if (exponent == 0) return 1; else return base * power(base, exponent - 1); } console.log(power(2, 3)); // → 8

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

Однако, у такой реализации есть проблема – в обычной среде JavaScript она раз в 10 медленнее, чем версия с циклом. Проход по циклу выходит дешевле, чем вызов функции.

Дилемма «скорость против элегантности» довольно интересна. Есть некий промежуток между удобством для человека и удобством для машины. Любую программу можно ускорить, сделав её больше и замысловатее. От программиста требуется находить подходящий баланс.

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

Основное правило, которое уже не раз повторяли, и с которым я полностью согласен – не беспокойтесь насчёт быстродействия, пока вы точно не уверены, что программа тормозит. Если так, найдите те части, которые работают дольше всех, и меняйте там элегантность на эффективность.

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

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

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

Вот вам загадка: можно получить бесконечное количество чисел, начиная с числа 1, и потом либо добавляя 5, либо умножая на 3. Как нам написать функцию, которая, получив число, пытается найти последовательность таких сложений и умножений, которые приводят к заданному числу? К примеру, число 13 можно получить, сначала умножив 1 на 3, а затем добавив 5 два раза. А число 15 вообще нельзя так получить.

Рекурсивное решение:

Function findSolution(target) { function find(start, history) { if (start == target) return history; else if (start > target) return null; else return find(start + 5, "(" + history + " + 5)") || find(start * 3, "(" + history + " * 3)"); } return find(1, "1"); } console.log(findSolution(24)); // → (((1 * 3) + 5) * 3)

Этот пример не обязательно находит самое короткое решение – он удовлетворяется любым. Не ожидаю, что вы сразу поймёте, как программа работает. Но давайте разбираться в этом отличном упражнении на рекурсивное мышление.

Внутренняя функция find занимается рекурсией. Она принимает два аргумента – текущее число и строку, которая содержит запись того, как мы пришли к этому номеру. И возвращает либо строчку, показывающую нашу последовательность шагов, либо null.

Для этого функция выполняет одно из трёх действий. Если заданное число равно цели, то текущая история как раз и является способом её достижения, поэтому она и возвращается. Если заданное число больше цели, продолжать умножения и сложения смысла нет, потому что так оно будет только увеличиваться. А если мы ещё не достигли цели, функция пробует оба возможных пути, начинающихся с заданного числа. Она дважды вызывает себя, один раз с каждым из способов. Если первый вызов возвращает не null, он возвращается. В другом случае возвращается второй.

Чтобы лучше понять, как функция достигает нужного эффекта, давайте просмотрим её вызовы, которые происходят в поисках решения для числа 13.

Find(1, "1") find(6, "(1 + 5)") find(11, "((1 + 5) + 5)") find(16, "(((1 + 5) + 5) + 5)") too big find(33, "(((1 + 5) + 5) * 3)") too big find(18, "((1 + 5) * 3)") too big find(3, "(1 * 3)") find(8, "((1 * 3) + 5)") find(13, "(((1 * 3) + 5) + 5)") found!

Отступ показывает глубину стека вызовов. В первый раз функция find вызывает сама себя дважды, чтобы проверить решения, начинающиеся с (1 + 5) и (1 * 3). Первый вызов ищет решение, начинающееся с (1 + 5), и при помощи рекурсии проверяет все решения, выдающие число, меньшее или равное требуемому. Не находит, и возвращает null. Тогда-то оператор || и переходит к вызову функции, который исследует вариант (1 * 3). Здесь нас ждёт удача, потому что в третьем рекурсивном вызове мы получаем 13. Этот вызов возвращает строку, и каждый из операторов || по пути передаёт эту строку выше, в результате возвращая решение.

Выращиваем функции

Существует два более-менее естественных способа ввода функций в программу.

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

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

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

007 Коров 011 Куриц

Очевидно, что нам понадобится функция с двумя аргументами. Начинаем кодить.
// вывестиИнвентаризациюФермы function printFarmInventory(cows, chickens) { var cowString = String(cows); while (cowString.length < 3) cowString = "0" + cowString; console.log(cowString + " Коров"); var chickenString = String(chickens); while (chickenString.length < 3) chickenString = "0" + chickenString; console.log(chickenString + " Куриц"); } printFarmInventory(7, 11);

Если мы добавим к строке.length, мы получим её длину. Получается, что циклы while добавляют нули спереди к числам, пока не получат строчку в 3 символа.

Готово! Но только мы собрались отправить фермеру код (вместе с изрядным чеком, разумеется), он звонит и говорит нам, что у него в хозяйстве появились свиньи, и не могли бы мы добавить в программу вывод количества свиней?

Можно, конечно. Но когда мы начинаем копировать и вставлять код из этих четырёх строчек, мы понимаем, что надо остановиться и подумать. Должен быть способ лучше. Пытаемся улучшить программу:

// выводСДобавлениемНулейИМеткой function printZeroPaddedWithLabel(number, label) { var numberString = String(number); while (numberString.length < 3) numberString = "0" + numberString; console.log(numberString + " " + label); } // вывестиИнвентаризациюФермы function printFarmInventory(cows, chickens, pigs) { printZeroPaddedWithLabel(cows, "Коров"); printZeroPaddedWithLabel(chickens, "Куриц"); printZeroPaddedWithLabel(pigs, "Свиней"); } printFarmInventory(7, 11, 3);

Работает! Но название printZeroPaddedWithLabel немного странное. Оно объединяет три вещи – вывод, добавление нулей и метку – в одну функцию. Вместо того, чтобы вставлять в функцию весь повторяющийся фрагмент, давайте выделим одну концепцию:

// добавитьНулей function zeroPad(number, width) { var string = String(number); while (string.length < width) string = "0" + string; return string; } // вывестиИнвентаризациюФермы function printFarmInventory(cows, chickens, pigs) { console.log(zeroPad(cows, 3) + " Коров"); console.log(zeroPad(chickens, 3) + " Куриц"); console.log(zeroPad(pigs, 3) + " Свиней"); } printFarmInventory(7, 16, 3);

Функция с хорошим, понятным именем zeroPad облегчает понимание кода. И её можно использовать во многих ситуациях, не только в нашем случае. К примеру, для вывода отформатированных таблиц с числами.

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

Хорошее правило – добавляйте только ту функциональность, которая вам точно пригодится. Иногда появляется искушение создавать фреймворки общего назначения для каждой небольшой потребности. Сопротивляйтесь ему. Вы никогда не закончите работу, а просто напишете кучу кода, который никто не будет использовать.

Функции и побочные эффекты

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

Первая вспомогательная функция в примере с фермой, printZeroPaddedWithLabel, вызывается из-за побочного эффекта: она выводит строку. Вторая, zeroPad, из-за возвращаемого значения. И это не совпадение, что вторая функция пригождается чаще первой. Функции, возвращающие значения, легче комбинировать друг с другом, чем функции, создающие побочные эффекты.

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

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

Итог

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

Ключевой момент в понимании функций – локальные области видимости. Параметры и переменные, объявленные внутри функции, локальны для неё, пересоздаются каждый раз при её вызове, и не видны снаружи. Функции, объявленные внутри другой функции, имеют доступ к её области видимости.

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

Упражнения

Минимум
В предыдущей главе была упомянута функция Math.min, возвращающая самый маленький из аргументов. Теперь мы можем написать такую функцию сами. Напишите функцию min, принимающую два аргумента, и возвращающую минимальный из них.

Console.log(min(0, 10)); // → 0 console.log(min(0, -10)); // → -10

Рекурсия
Мы видели, что оператор % (остаток от деления) может использоваться для определения того, чётное ли число (% 2). А вот ещё один способ определения:

Ноль чётный.
Единица нечётная.
У любого числа N чётность такая же, как у N-2.

Напишите рекурсивную функцию isEven согласно этим правилам. Она должна принимать число и возвращать булевское значение.

Потестируйте её на 50 и 75. Попробуйте задать ей -1. Почему она ведёт себя таким образом? Можно ли её как-то исправить?

Test it on 50 and 75. See how it behaves on -1. Why? Can you think of a way to fix this?

Console.log(isEven(50)); // → true console.log(isEven(75)); // → false console.log(isEven(-1)); // → ??

Считаем бобы.

Символ номер N строки можно получить, добавив к ней.charAt(N) (“строчка”.charAt(5)) – схожим образом с получением длины строки при помощи.length. Возвращаемое значение будет строковым, состоящим из одного символа (к примеру, “к”). У первого символа строки позиция 0, что означает, что у последнего символа позиция будет string.length – 1. Другими словами, у строки из двух символов длина 2, а позиции её символов будут 0 и 1.

Напишите функцию countBs, которая принимает строку в качестве аргумента, и возвращает количество символов “B”, содержащихся в строке.

Затем напишите функцию countChar, которая работает примерно как countBs, только принимает второй параметр - символ, который мы будем искать в строке (вместо того, чтобы просто считать количество символов “B”). Для этого переделайте функцию countBs.

В этой главе:

Функции – это один из основных способов объединения операторов в логически связанные блоки. В языке JavaScript функция представляет собой группу выражений, служащих для выполнения какой-либо определенной задачи, объединенных под общим именем.

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

Определение и вызов функций

Прежде, чем вызывать и использовать функцию, ее надо определить. Определение функций в JavaScript имеет следующий синтаксис:

Function ИмяФункции (аргументы) { блок выражений }

Таким образом, функция состоит из следующих частей, предваряемых ключевым словом function:

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

В простейшем случае аргументы могут отсутствовать, а блок операций моет быть представлен единственным оператором:

Function MyFirstFunc () { var MyMessage="Это – моя функция!"; alert(MyMessage); }

Здесь мы определили функцию, которая будет выдавать окно с сообщением «Это – моя функция!». Следует заметить, что даже если функция не принимает никаких аргументов, она все равно должна иметь пару круглых скобок после своего названия.

ВНИМАНИЕ
Важное замечание следует сделать по поводу переменных, объявляемых в теле функций. Такие переменные видны программе только внутри той функции, в которой они определены. Так, в примере с MyFirstFunc, доступ к переменной MyMessage возможен только внутри этой функции, но не вне нее.

Но чаще всего функции все-таки принимают какие-либо значения в качестве своих аргументов. Возьмем для примера ранее рассмотренный блок, вычисляющий список чисел, на которые 100 делится без остатка. Если этот блок вынести в отдельную функцию, то можно будет использовать его для того, чтобы выводить список делителей для любого числа. Для этого нам потребуется всего один аргумент, который и будет определять число, для которого нам нужно получить такой список:

Function remainder_free(j) { var i=0; while (i

В этом случае значение переменной j функция получает при своем вызове. И как раз тут важно учесть, тот факт, что когда браузер загружает документ и интерпретирует объявление функции, он только считывает ее в память. А для того чтобы функция была выполнена, ее надо вызвать. Вызов функции может производиться из любого места JavaScript программы, и имеет такой вид:

ИмяФункции(параметры)

ИмяФункции означает имя ранее объявленной функции, а параметры – список из одного или нескольких значений, разделенных запятыми и передаваемых функции в качестве аргументов. Количество и порядок параметров, передаваемых функции, должен точно соответствовать списку аргументов, указанных при объявлении функции. Так, для случая с только что рассмотренной функцией поиска целых делителей, где используется 1 аргумент, вызов функции может быть таким:

Remainder_free(100);

Если параметров будет меньше или больше чем надо, то в первом случае недостающим параметрам будет назначено значение NaN, а во втором – лишние параметры будут проигнорированы. Таким образом, следующие варианты вызова этой же функции не являются верными:

Remainder_free; // так функции не вызывают вообще remainder_free(); // недостаточно параметров remainder_free(100,0); // лишний параметр

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

Function cube(value) { return value * value * value; }

Таким образом, чтобы в любом месте программы вычислить значение числа в 3-й степени, достаточно написать такое выражение:

Var x = cube(3);

В результате выполнения этого выражения переменной x будет присвоено значение 27. Примеры вызова функций вы можете посмотреть в JavaScript-калькуляторе (файл calc.html).

Как уже отмечалось, функции могут содержать в себе вызовы других функций. Например, в случае с поиском делителей можно было бы вывести, к примеру, не сами делители, а их 3-ю степень, для чего из функции remainder_free вызывалась бы функция cube:

Function remainder_free(j) { var i=0, ic=0; while (i

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

Вложение функций и рекурсия

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

Function AddCubes(x,y) { function cube(value) { return value * value * value; }; return cube(x) + cube(y); }

В данном случае определена функция AddCubes, внутри которой определена еще одна функция – cube. При этом следует учитывать, что для вложенной функции видны переменные, используемые во внешней («наружной») функции, но не наоборот. В то же время сама вложенная функция доступна только для выражений из внешней функции: в данном случае функцией cube можно будет воспользоваться только из функции AddCubes.

Язык JavaScript поддерживает также и такую весьма полезную возможность, как рекурсивный вызов функций, или просто рекурсию. Под рекурсией понимают вызов функции из самой себя. Рассмотрим этот вопрос на следующем примере:

Function recfunc(x) { x--; if (x>5) recfunc(x); alert(x); }

Здесь мы объявили функцию recfunc, принимающую один аргумент, и вызывающую саму себя до тех пор, пока значение этого аргумента больше 5. Хотя на первый взгляд может показаться, что такое поведение функции похоже на обычный цикл, на самом деле все работает несколько по-иному: если вы вызовите ее со значением 8, то она выдаст вам 3 сообщения в следующей последовательности: 5, 6, 7. Иначе говоря, функция вызывала саму себя до тех пор, пока значение x было больше 5, и собственно вывод сообщений начала 3-я по уровню получившейся вложенности функция, которая и вывела первое сообщение (в данном случае им стало декрементированное 6, т.е. 5).

Массивы аргументов

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

Arguments[i]; ИмяФункции.arguments[i];

Здесь i должно быть целым числом, являющимся порядковым номером аргумента, при этом отсчет аргументов начинается с нуля. В случае, когда обращение к массиву аргументов происходит из самой функции, имя функции можно опустить. Чтобы узнать число аргументов, используйте выражение свойство массива length:

Arguments.length;

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

Function ConcatSeparated(separator) { result=""; // инициализируем возвращаемое значение for (var i=1; i

Эта функция будет возвращать одну строку, состоящую из списка аргументов, начиная со второго, и «склеенную» первым аргументом. Пример ее использования можно посмотреть в файле argarray.html.

Но, на самом деле, применение массива аргументов позволяет обходиться вообще без объявления параметров. Допустим, нам нужна функция, возвращающая среднее значение нескольких чисел. Как известно, для этого достаточно сложить все числа и разделить результат на их количество. Соответственно, нам пригодится свойство массива length, отвечающее за количество аргументов. В результате функция получит примерно такой вид:

Function calcAvg() { var summ=0; // инициализируем переменные for (var i=0; i

Здесь хотелось бы обратить внимание на то, что обход массива начинается с нулевого элемента. Это вполне естественно, поскольку нумерация элементов массивов начинается с 0. Если же у вас возник вопрос, почему в предыдущем примере отсчет начинался с 1, то обратите внимание на то, что первым аргументом там был separator, объявленный явно, и который ни с чем склеивать не требовалось.

Другой момент – это использование сокращенной формы записи для подсчета суммы. Конечно, можно было бы написать это выражение в более привычном виде:

Summ = summ + arguments[i];

Но, поскольку уж в JavaScript предусмотрены сокращенные формы записи, то почему бы их и не использовать? Ну и, наконец, после оператора return записана не переменная, а выражение, что так же вполне допустимо с точки зрения синтаксиса языка.

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

Var avg = calcAvg(1,3,5); // переменной avg будет присвоено значение 3 var avg = calcAvg(2); // переменной avg будет присвоено значение 2 var avg = calcAvg(0,4,8,14); // переменной avg будет присвоено значение 6,5

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

Alert(calcAvg(10,100));

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

Предопределенные функции

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

К предопределенным функциям JavaScript 1.3 относятся: eval, isFinite, isNaN, parseInt, parseFloat, Number, String, escape и unescape. В JavaScript 1.5 вместо функций escape и unescape используются 4 новых функции: encodeURI, encodeURIComponent, decodeURI, и decodeURIComponent. Краткое описание всех этих функций приведено в таблице 4.9.

Таблица 4.9. Предопределенные функции языка JavaScript
Функция Синтаксис Описание
eval eval(выражение) Обрабатывает строку как код JavaScript
isFinite isFinite(значение) Проверяет, что значение является конечным числом
isNaN isNaN(значение) Проверяет, что значение не является числом
parseInt parseInt(строка, система) Преобразует строку в целое
parseFloat parseFloat(строка) Преобразует строку в вещественное число
Number Number(объект) Преобразует объект в число
String String(объект) Преобразует объект в строку
escape escape(строка ASCII) Преобразует строку в последовательность символов
unescape unescape(строка) Преобразует последовательность escape-символов в строку
encodeURI encodeURI(URL в ASCII) Кодирует URI полностью путем замены символов на их коды UTF-8
encodeURIComponent encodeURIComponent(URL в ASCII) Кодирует URI по составным частям путем замены символов на их коды UTF-8
decodeURI decodeURI(URL в UTF-8) Раскодирует значение, созданное при помощи encodeURI
decodeURIComponent decodeURIComponent(URL в UTF-8) Раскодирует значение, созданное при помощи encodeURIComponent

Рассмотрим некоторые функции подробнее. Начнем с функции eval, которая передает интерпретатору строку с кодом JavaScript без ссылки на какой-либо конкретный объект. Способ использования у этой функции следующий:

Eval(выражение);

Здесь «выражение» – строка, которая должна содержать код на языке JavaScript для обработки интерпретатором. Ценность функции eval в том, что с ее помощью можно динамически составлять фрагменты программы и отправлять их на выполнение. При дальнейшем написании сценариев мы не раз воспользуемся этой функцией.

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

Функция isNaN выполняет противоположную задачу – она проверяет, что указанный аргумент не является числом. Здесь следует отметить, что в JavaScript имеется еще одно ключевое слово – NaN. Оно используется, когда требуется указать значение, не являющееся числом. Например, такой вызов функции isNaN возвратит истину:

IsNaN(NaN);

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

Var a = parseFloat("31416e-4");

В этом случае переменной a будет присвоено значение 3,1416. В том случае, если преобразовать строку в число не представляется возможным, функция parseFloat возвращает значение NaN.

Кроме функции parseFloat, для преобразования строк в число имеется функция parseInt. В отличие от parseFloat, она может принимать как один, так и два аргумента:

ParseInt("25"); parseInt("25", 10);

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

Var x = parseInt("15"); /* если не оговорено иначе, подразумевается, что система счисления – десятеричная */ var x = parseInt("15", 10); /* здесь мы явно указываем десятеричную систему счисления */ var x = parseInt(15.99, 10); /* в данном случае исходным значением служит не строка, а число с плавающей точкой */ var x = parseInt("F", 16); // шестнадцатеричное F равно десятеричному 15 var x = parseInt("FXX123", 16); /* если какой-либо знак распознать не удается, следующие за ним значения отбрасываются */ var x = parseInt("17", 8); // Восьмеричная система тоже иногда используется var x = parseInt("1111", 2); // Двоичная система – тоже не редкость

В том случае, когда даже первый символ преобразовать в число не удастся, функция возвращает значение NaN:

ParseInt("8",8); // В восьмеричной системе счисления нет цифры 8!

Для преобразования строк и чисел в JavaScript имеются еще 2 функции – String и Number. Они используются для преобразования объектов в строку и число соответственно. Например, если нам надо что-либо преобразовать в строку, например, объект document, то можно написать так:

Var doc = String(document);

В данном случае в качестве значения переменной doc мы получим строку, описывающую объект как «». Примеры работы этих и других предопределенных функций можно найти в файле predef.html.

И, наконец, осталось рассмотреть функции кодирования. На самом деле, в клиентских сценариях они практически не используются, и предназначены для работы на стороне сервера (для случая, если web-сервером является Netscape Enterprise). Так что мы не будем на них останавливаться, и отметим лишь, что, поскольку функции escape и unescape не работают с национальными символами, то, начиная с JavaScript 1.5, они объявлены устаревшими, а вместо них следует использовать encodeURI, decodeURI, encodeURIComponent и decodeURIComponent.

2011-07-28 // Есть вопросы, предложения, замечания? Вы можете

Встроенная функция описывает какие-либо действия, которые она может совершить при её вызове. Описание действий встроенных функций скрыто от программиста. Примером таких действий может служить вычисление математического выражения и возвращение результата или манипуляция с содержимым html-документа. Функция может принимать параметры или аргументы, с которыми она производит какие-то действия. Аргументами могут служить как , так и .

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

var a = functionName(par1, par2, …, parN);

В js существует множество встроенных функций для подсчёта математических выражений. Например, функция Math.sin, возвращает синус угла (угол задаётся в радианах), функция Math.sqrt вычисляет корень квадратный из числа, переданного ей в качестве параметра и т.д.

Например, вычислим корень квадратный из 256.

var b = Math.sqrt(256);

После выполнения кода одного из двух вышепредставленных примеров в переменной b будет содержаться значение 16.

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

Функция alert

Эта функция принимает в качестве параметра текстовое представление значение, которое она выведет на экран в унылом сером окошке с кнопкой «Ок». Пока пользователь не нажмёт эту кнопку, выполнения скрипта не продолжится.

Функция document.write

Эта функция пишет в html-документ текстовое представление значения переданного ей в качестве параметра. Никогда не используйте этот метод при написании скриптов, для реальных проектов, если, конечно, точно не знаете, что делаете. Если вызывать эту функцию после загрузки страницы, то, скорее всего, вы увидите пустую страницу с текстом, который вывел последний вызов этой функции.

Объект Math

В этом объекте содержатся функции для вычисления математических выражений и некоторые константы. Об объектах мы поговорим попозже. Для того, чтобы использовать объект Math вам надо лишь запомнить, что обращение к его свойствам (функциям и константам) необходимо написать Math.имяФункцииИлиКонстанты.

Свойства, которые содержит объект Math (слово «Math» опущено):

1. Константы

E 2.718281828459045
LN10 2.302585092994046 (логарифм натуральный 10)
LN2 0.6931471805599453 (логарифм натуральный 2)
LOG10E 0.4342944819032518 (логарифм десятичный e)
LOG2E 1.4426950408889634 (логарифм по основанию 2 числа e)
PI 3.141592653589793
SQRT1_2 0.7071067811865476 (корень квадратный из 0.5)
SQRT2 1.4142135623730951 (корень квадратный из двух)

2. Тригонометрические функции

sin – синус
cos – косинус
tan – тангенс

Угол, который в качестве аргумента принимают эти функции задаётся в радианах, а не в градусах. Для того, чтобы перевести значение угла из градусов в радианы необходимо умножить его на Math.PI и разделить на 180. И наоборот для того, чтобы перевести значение угла из радианов в градусы необходимо умножить его на 180 и разделить на Math.PI.

Таким образом, 60 градусов это π / 3 радиан, 30 градусов это π / 6 радиан и 45 градусов π / 4 радиан.

3. Обратные тригонометрические функции

acos – арккосинус от числа т.е. такой угол (в радианах), косинус которого равен аргументу
asin – арксинус от числа т.е. такой угол (в радианах), синус которого равен аргументу
atan – арктангенс от числа т.е. такой угол (в радианах), тангенс которого равен аргументу
atan2 – арктангенс от частного двух аргументов

3. Другие функции

abs – модуль числа
floor – целая часть числа, “пол“ или округление в меньшую сторону. Обратите внимание, что, например Math.floor(-0.9) и Math.floor(-0.1) это -1, а не ноль.
ceil – округление в большую сторону или «потолок»
exp – возвращает значение выражения e x , где x – это аргумент функции
log – возвращает натуральный логарифм числа
pow – принимает два аргумента и возвращает степень основание которой это первый аргумент, а показатель – второй.
max – принимает произвольное количество параметров и возвращает максимальный из них
min – принимает произвольное количество параметров и возвращает минимальный из них
random – возвращает случайное значение от 0 до 1
round – округляет число до единиц
sqrt – вычисляет корень квадратный из числа

Многие другие встроенные функции мы рассмотрим по ходу следующих уроков, а на сегодня всё.

Functions are one of the fundamental building blocks in JavaScript. A function is a JavaScript procedure-a set of statements that performs a task or calculates a value. To use a function, you must define it somewhere in the scope from which you wish to call it.

A method is a function that is a property of an object. Read more about objects and methods in Working with objects .

Calling functions

Defining a function does not execute it. Defining the function simply names the function and specifies what to do when the function is called. Calling the function actually performs the specified actions with the indicated parameters. For example, if you define the function square , you could call it as follows:

Square(5);

The preceding statement calls the function with an argument of 5. The function executes its statements and returns the value 25.

Functions must be in scope when they are called, but the function declaration can be hoisted (appear below the call in the code), as in this example:

Console.log(square(5)); /* ... */ function square(n) { return n * n; }

The scope of a function is the function in which it is declared, or the entire program if it is declared at the top level.

Note: This works only when defining the function using the above syntax (i.e. function funcName(){}). The code below will not work. That means, function hoisting only works with function declaration and not with function expression.

Console.log(square); // square is hoisted with an initial value undefined. console.log(square(5)); // TypeError: square is not a function var square = function(n) { return n * n; }

The arguments of a function are not limited to strings and numbers. You can pass whole objects to a function. The show_props() function (defined in ) is an example of a function that takes an object as an argument.

A function can call itself. For example, here is a function that computes factorials recursively:

Function factorial(n) { if ((n === 0) || (n === 1)) return 1; else return (n * factorial(n - 1)); }

You could then compute the factorials of one through five as follows:

Var a, b, c, d, e; a = factorial(1); // a gets the value 1 b = factorial(2); // b gets the value 2 c = factorial(3); // c gets the value 6 d = factorial(4); // d gets the value 24 e = factorial(5); // e gets the value 120

There are other ways to call functions. There are often cases where a function needs to be called dynamically, or the number of arguments to a function vary, or in which the context of the function call needs to be set to a specific object determined at runtime. It turns out that functions are, themselves, objects, and these objects in turn have methods (see the Function object). One of these, the apply() method, can be used to achieve this goal.

Function scope

Variables defined inside a function cannot be accessed from anywhere outside the function, because the variable is defined only in the scope of the function. However, a function can access all variables and functions defined inside the scope in which it is defined. In other words, a function defined in the global scope can access all variables defined in the global scope. A function defined inside another function can also access all variables defined in its parent function and any other variable to which the parent function has access.

// The following variables are defined in the global scope var num1 = 20, num2 = 3, name = "Chamahk"; // This function is defined in the global scope function multiply() { return num1 * num2; } multiply(); // Returns 60 // A nested function example function getScore() { var num1 = 2, num2 = 3; function add() { return name + " scored " + (num1 + num2); } return add(); } getScore(); // Returns "Chamahk scored 5"

Scope and the function stack

Recursion

A function can refer to and call itself. There are three ways for a function to refer to itself:

  1. the function"s name
  2. an in-scope variable that refers to the function

For example, consider the following function definition:

Var foo = function bar() { // statements go here };

Within the function body, the following are all equivalent:

  1. bar()
  2. arguments.callee()
  3. foo()

A function that calls itself is called a recursive function . In some ways, recursion is analogous to a loop. Both execute the same code multiple times, and both require a condition (to avoid an infinite loop, or rather, infinite recursion in this case). For example, the following loop:

Var x = 0; while (x < 10) { // "x < 10" is the loop condition // do stuff x++; }

can be converted into a recursive function and a call to that function:

Function loop(x) { if (x >= 10) // "x >= 10" is the exit condition (equivalent to "!(x < 10)") return; // do stuff loop(x + 1); // the recursive call } loop(0);

However, some algorithms cannot be simple iterative loops. For example, getting all the nodes of a tree structure (e.g. the DOM) is more easily done using recursion:

Function walkTree(node) { if (node == null) // return; // do something with node for (var i = 0; i < node.childNodes.length; i++) { walkTree(node.childNodes[i]); } }

Compared to the function loop , each recursive call itself makes many recursive calls here.

It is possible to convert any recursive algorithm to a non-recursive one, but often the logic is much more complex and doing so requires the use of a stack. In fact, recursion itself uses a stack: the function stack.

The stack-like behavior can be seen in the following example:

Function foo(i) { if (i < 0) return; console.log("begin: " + i); foo(i - 1); console.log("end: " + i); } foo(3); // Output: // begin: 3 // begin: 2 // begin: 1 // begin: 0 // end: 0 // end: 1 // end: 2 // end: 3

Nested functions and closures

You can nest a function within a function. The nested (inner) function is private to its containing (outer) function. It also forms a closure . A closure is an expression (typically a function) that can have free variables together with an environment that binds those variables (that "closes" the expression).

Since a nested function is a closure, this means that a nested function can "inherit" the arguments and variables of its containing function. In other words, the inner function contains the scope of the outer function.

  • The inner function can be accessed only from statements in the outer function.
  • The inner function forms a closure: the inner function can use the arguments and variables of the outer function, while the outer function cannot use the arguments and variables of the inner function.

The following example shows nested functions:

Function addSquares(a, b) { function square(x) { return x * x; } return square(a) + square(b); } a = addSquares(2, 3); // returns 13 b = addSquares(3, 4); // returns 25 c = addSquares(4, 5); // returns 41

Since the inner function forms a closure, you can call the outer function and specify arguments for both the outer and inner function:

Function outside(x) { function inside(y) { return x + y; } return inside; } fn_inside = outside(3); // Think of it like: give me a function that adds 3 to whatever you give // it result = fn_inside(5); // returns 8 result1 = outside(3)(5); // returns 8

Preservation of variables

Notice how x is preserved when inside is returned. A closure must preserve the arguments and variables in all scopes it references. Since each call provides potentially different arguments, a new closure is created for each call to outside. The memory can be freed only when the returned inside is no longer accessible.

This is not different from storing references in other objects, but is often less obvious because one does not set the references directly and cannot inspect them.

Multiply-nested functions

Functions can be multiply-nested, i.e. a function (A) containing a function (B) containing a function (C). Both functions B and C form closures here, so B can access A and C can access B. In addition, since C can access B which can access A, C can also access A. Thus, the closures can contain multiple scopes; they recursively contain the scope of the functions containing it. This is called scope chaining . (Why it is called "chaining" will be explained later.)

Consider the following example:

Function A(x) { function B(y) { function C(z) { console.log(x + y + z); } C(3); } B(2); } A(1); // logs 6 (1 + 2 + 3)

In this example, C accesses B "s y and A "s x . This can be done because:

  1. B forms a closure including A , i.e. B can access A "s arguments and variables.
  2. C forms a closure including B .
  3. Because B "s closure includes A , C "s closure includes A , C can access both B and A "s arguments and variables. In other words, C chains the scopes of B and A in that order.

The reverse, however, is not true. A cannot access C , because A cannot access any argument or variable of B , which C is a variable of. Thus, C remains private to only B .

Name conflicts

When two arguments or variables in the scopes of a closure have the same name, there is a name conflict . More inner scopes take precedence, so the inner-most scope takes the highest precedence, while the outer-most scope takes the lowest. This is the scope chain. The first on the chain is the inner-most scope, and the last is the outer-most scope. Consider the following:

Function outside() { var x = 5; function inside(x) { return x * 2; } return inside; } outside()(10); // returns 20 instead of 10

The name conflict happens at the statement return x and is between inside "s parameter x and outside "s variable x . The scope chain here is { inside , outside , global object}. Therefore inside "s x takes precedences over outside "s x , and 20 (inside "s x) is returned instead of 10 (outside "s x).

Closures

Closures are one of the most powerful features of JavaScript. JavaScript allows for the nesting of functions and grants the inner function full access to all the variables and functions defined inside the outer function (and all other variables and functions that the outer function has access to). However, the outer function does not have access to the variables and functions defined inside the inner function. This provides a sort of encapsulation for the variables of the inner function. Also, since the inner function has access to the scope of the outer function, the variables and functions defined in the outer function will live longer than the duration of the outer function execution, if the inner function manages to survive beyond the life of the outer function. A closure is created when the inner function is somehow made available to any scope outside the outer function.

Var pet = function(name) { // The outer function defines a variable called "name" var getName = function() { return name; // The inner function has access to the "name" variable of the outer //function } return getName; // Return the inner function, thereby exposing it to outer scopes } myPet = pet("Vivie"); myPet(); // Returns "Vivie"

It can be much more complex than the code above. An object containing methods for manipulating the inner variables of the outer function can be returned.

Var createPet = function(name) { var sex; return { setName: function(newName) { name = newName; }, getName: function() { return name; }, getSex: function() { return sex; }, setSex: function(newSex) { if(typeof newSex === "string" && (newSex.toLowerCase() === "male" || newSex.toLowerCase() === "female")) { sex = newSex; } } } } var pet = createPet("Vivie"); pet.getName(); // Vivie pet.setName("Oliver"); pet.setSex("male"); pet.getSex(); // male pet.getName(); // Oliver

In the code above, the name variable of the outer function is accessible to the inner functions, and there is no other way to access the inner variables except through the inner functions. The inner variables of the inner functions act as safe stores for the outer arguments and variables. They hold "persistent" and "encapsulated" data for the inner functions to work with. The functions do not even have to be assigned to a variable, or have a name.

Var getCode = (function() { var apiCode = "0]Eal(eh&2"; // A code we do not want outsiders to be able to modify... return function() { return apiCode; }; })(); getCode(); // Returns the apiCode

There are, however, a number of pitfalls to watch out for when using closures. If an enclosed function defines a variable with the same name as the name of a variable in the outer scope, there is no way to refer to the variable in the outer scope again.

Var createPet = function(name) { // The outer function defines a variable called "name". return { setName: function(name) { // The enclosed function also defines a variable called "name". name = name; // How do we access the "name" defined by the outer function? } } }

Using the arguments object

The arguments of a function are maintained in an array-like object. Within a function, you can address the arguments passed to it as follows:

Arguments[i]

where i is the ordinal number of the argument, starting at zero. So, the first argument passed to a function would be arguments . The total number of arguments is indicated by arguments.length .

Using the arguments object, you can call a function with more arguments than it is formally declared to accept. This is often useful if you don"t know in advance how many arguments will be passed to the function. You can use arguments.length to determine the number of arguments actually passed to the function, and then access each argument using the arguments object.

For example, consider a function that concatenates several strings. The only formal argument for the function is a string that specifies the characters that separate the items to concatenate. The function is defined as follows:

Function myConcat(separator) { var result = ""; // initialize list var i; // iterate through arguments for (i = 1; i < arguments.length; i++) { result += arguments[i] + separator; } return result; }

You can pass any number of arguments to this function, and it concatenates each argument into a string "list":

// returns "red, orange, blue, " myConcat(", ", "red", "orange", "blue"); // returns "elephant; giraffe; lion; cheetah; " myConcat("; ", "elephant", "giraffe", "lion", "cheetah"); // returns "sage. basil. oregano. pepper. parsley. " myConcat(". ", "sage", "basil", "oregano", "pepper", "parsley");

Note: The arguments variable is "array-like", but not an array. It is array-like in that it has a numbered index and a length property. However, it does not possess all of the array-manipulation methods.

Two factors influenced the introduction of arrow functions: shorter functions and non-binding of this .

Shorter functions

In some functional patterns, shorter functions are welcome. Compare:

Var a = [ "Hydrogen", "Helium", "Lithium", "Beryllium" ]; var a2 = a.map(function(s) { return s.length; }); console.log(a2); // logs var a3 = a.map(s => s.length); console.log(a3); // logs

No separate this

Until arrow functions, every new function defined its own value (a new object in the case of a constructor, undefined in function calls, the base object if the function is called as an "object method", etc.). This proved to be less than ideal with an object-oriented style of programming.

Function Person() { // The Person() constructor defines `this` as itself. this.age = 0; setInterval(function growUp() { // In nonstrict mode, the growUp() function defines `this` // as the global object, which is different from the `this` // defined by the Person() constructor. this.age++; }, 1000); } var p = new Person();

In ECMAScript 3/5, this issue was fixed by assigning the value in this to a variable that could be closed over.

Function Person() { var self = this; // Some choose `that` instead of `self`. // Choose one and be consistent. self.age = 0; setInterval(function growUp() { // The callback refers to the `self` variable of which // the value is the expected object. self.age++; }, 1000); }

В этой статье описаны функции Javascript на уровне языка: создание, параметры, приемы работы, замыкания и многое другое.

Создание функций

Существует 3 способа создать функцию. Основное отличие в результате их работы - в том, что именованная функция видна везде, а анонимная - только после объявления:

Функции - объекты

В javascript функции являются полноценными объектами встроенного класса Function. Именно поэтому их можно присваивать переменным, передавать и, конечно, у них есть свойства:

Function f() { ... } f.test = 6 ... alert(f.test) // 6

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

Например,

Function func() { var funcObj = arguments.callee funcObj.test++ alert(funcObj.test) } func.test = 1 func() func()

В начале работы каждая функция создает внутри себя переменную arguments и присваивает arguments.callee ссылку на себя. Так что arguments.callee.test - свойство func.test , т.е статическая переменная test.

В примере нельзя было сделать присвоение:

Var test = arguments.callee.test test++

так как при этом операция ++ сработала бы на локальной переменной test , а не на свойстве test объекта функции.

Объект arguments также содержит все аргументы и может быть преобразован в массив (хотя им не является), об этом - ниже, в разделе про параметры.

Области видимости

Каждая функция, точнее даже каждый запуск функции задает свою индивидуальную область видимости.

Переменные можно объявлять в любом месте. Ключевое слово var задает переменную в текущей области видимости. Если его забыть, то переменная попадет в глобальный объект window . Возможны неожиданные пересечения с другими переменными окна, конфликты и глюки.

В отличие от ряда языков, блоки не задают отдельную область видимости. Без разницы - определена переменная внутри блока или вне его. Так что эти два фрагмента совершенно эквивалентны:

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

Например:

Function a() { z = 5 // поменяет z локально.. // .. т.к z объявлена через var var z } // тест delete z // очистим на всякий случай глобальную z a() alert(window.z) // => undefined, т.к z была изменена локально

Параметры функции

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

Если функции передано меньше параметров, чем есть в определении, то отсутствующие считаются undefined .

Следующая функция возвращает время time , необходимое на преодоление дистанции distance с равномерной скоростью speed .

При первом запуске функция работает с аргументами distance=10 , speed=undefined . Обычно такая ситуация, если она поддерживается функцией, предусматривает значение по умолчанию:

// если speed - ложное значение(undefined, 0, false...) - подставить 10 speed = speed || 10

Оператор || в яваскрипт возвращает не true/false , а само значение (первое, которое приводится к true).

Поэтому его используют для задания значений по умолчанию. В нашем вызове speed будет вычислено как undefined || 10 = 10 .

Поэтому результат будет 10/10 = 1 .

Второй запуск - стандартный.

Третий запуск задает несколько дополнительных аргументов. В функции не предусмотрена работа с дополнительными аргументами, поэтому они просто игнорируются.

Ну и в последнем случае аргументов вообще нет, поэтому distance = undefined , и имеем результат деления undefined/10 = NaN (Not-A-Number, произошла ошибка).

Работа с неопределенным числом параметров

Непосредственно перед входом в тело функции, автоматически создается объект arguments , который содержит

  1. Аргументы вызова, начиная от нуля
  2. Длину в свойстве length
  3. Ссылку на саму функцию в свойстве callee

Например,

Function func() { for(var i=0;i

Свойство arguments похоже на массив, т.к у него есть длина и числовые индексы. На самом деле arguments не принадлежит классу Array и не содержит его методов, таких как push , pop и других.

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

Var args = Array.prototype.slice.call(arguments) // .. теперь args - настоящий массив аргументов.. args.shift() ...

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

Var func = function(a,b) { alert(a+b) } var arr = func.apply(null, arr) // => alert(3)

Пример передачи функции по ссылке

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

Например, map берет функцию func , применяет ее к каждому элементу массива arr и возвращает получившийся массив:

Var map = function(func, arr) { var result = for(var i=0; i

Пример использования:

Map(run, ) // =

Или можно создать анонимную функцию непосредственно в вызове map:

// анонимная функция утраивает числа map(function (a) { return a*3 } , ) // =

Сворачивание параметров в объект

Бывают функции, аргументы которых сильно варьируются.

Например:

// можно указать только часть аргументов // не указанные - вычисляются или берутся по умолчанию function resize(toWidth, toHeight, saveProportions, animate) { // значения по умолчанию saveProportions = saveProportions || true animate = animate || true toHeight = toHeight || ... }

Вызов с необязательными параметрами приходится делать так:

Resize(100, null, null, true)

Чтобы избежать лишних null и сделать код более понятным, используют нечто вроде "keyword arguments", существующих в Python и Ruby. Для этого много параметров пакуют в единый объект:

Function resize(setup) { // значения по умолчанию var saveProportions = setup.saveProportions || true var animate = setup.animate || true var toHeight = setup.toHeight || ... }

Вызов теперь делается гораздо проще:

Var setup = {toWidth: 100, animate: true} resize(setup) // или resize({toWidth: 100, animate: true})

Так - куда понятнее. А если параметров больше 5, то вообще - единственный нормальный способ.

Кроме того, с объектом можно удобнее делать последовательности вызовов вроде:

Var setup = {toWidth: 100, animate: true, saveProportions: false} resize(setup) setup.toWidth = 200 resize(setup)



Просмотров