Как мы уже знаем, функция в JavaScript — это значение.
Каждое значение в JavaScript имеет тип. Какой тип функции?
В JavaScript функции являются объектами.
Хороший способ представить функции — это вызываемые «объекты действий». Мы можем не только вызывать их, но и обращаться с ними как с объектами: добавлять/удалять свойства, передавать по ссылке и т. д.
Объекты-функции содержат некоторые полезные свойства.
Например, имя функции доступно как свойство «name»:
функция SayHi() { Оповещение("Привет"); } оповещение(sayHi.name); // сказать привет
Что забавно, логика присвоения имен умна. Он также присваивает правильное имя функции, даже если она была создана без него, а затем сразу же присваивается:
пусть говорятПривет = функция () { Оповещение("Привет"); }; оповещение(sayHi.name); // сказать Привет (есть имя!)
Это также работает, если назначение выполняется через значение по умолчанию:
функция f(sayHi = function() {}) { оповещение(sayHi.name); // сказать Привет (работает!) } е();
В спецификации эта функция называется «контекстным именем». Если функция его не предоставляет, то в присваивании это определяется из контекста.
Методы объекта тоже имеют имена:
пусть пользователь = { сказатьПривет() { // ... }, скажем пока: функция() { // ... } } оповещение(user.sayHi.name); // сказать привет оповещение(user.sayBye.name); // говорим пока
Хотя никакой магии нет. Бывают случаи, когда нет возможности подобрать правильное имя. В этом случае свойство name пусто, как здесь:
// функция, созданная внутри массива пусть arr = [function() {}]; оповещение(arr[0].имя); // <пустая строка> // у движка нет возможности установить правильное имя, поэтому его нет
Однако на практике большинство функций имеют имя.
Есть еще одно встроенное свойство «длина», которое возвращает количество параметров функции, например:
функция f1(a) {} функция f2(a, b) {} функция многих(a, b, ...подробнее) {} предупреждение (f1.длина); // 1 предупреждение (f2.length); // 2 оповещение(многие.длина); // 2
Здесь мы видим, что остальные параметры не учитываются.
Свойство length
иногда используется для самоанализа в функциях, которые работают с другими функциями.
Например, в приведенном ниже коде функция ask
принимает задаваемый question
и произвольное количество функций- handler
для вызова.
Как только пользователь предоставляет ответ, функция вызывает обработчики. Мы можем передать два типа обработчиков:
Функция без аргументов, которая вызывается только тогда, когда пользователь дает положительный ответ.
Функция с аргументами, которая вызывается в любом случае и возвращает ответ.
Чтобы правильно вызвать handler
, мы проверяем свойство handler.length
.
Идея состоит в том, что у нас есть простой синтаксис обработчика без аргументов для положительных случаев (наиболее распространенный вариант), но мы также можем поддерживать универсальные обработчики:
функция Ask(вопрос, ...обработчики) { пусть isДа = подтвердить (вопрос); for(пусть обработчик обработчиков) { если (handler.length == 0) { если (isДа) обработчик(); } еще { обработчик (естьДа); } } } // при положительном ответе вызываются оба обработчика // для отрицательного ответа только второй Ask("Вопрос?", () => alert('Вы сказали да'), result => alert(result));
Это частный случай так называемого полиморфизма — обращение с аргументами по-разному в зависимости от их типа или, в нашем случае, в зависимости от length
. Эта идея действительно находит применение в библиотеках JavaScript.
Мы также можем добавлять собственные свойства.
Здесь мы добавляем свойство counter
для отслеживания общего количества вызовов:
функция SayHi() { Оповещение("Привет"); // посчитаем, сколько раз мы запустим сказатьПривет.счетчик++; } SayHi.counter = 0; // начальное значение сказатьПривет(); // Привет сказатьПривет(); // Привет alert(`Вызов ${sayHi.counter} раз`); // Вызывается 2 раза
Свойство не является переменной
Свойство, назначенное функции, например sayHi.counter = 0
не определяет counter
локальной переменной внутри нее. Другими словами, counter
свойств и let counter
переменных — две несвязанные вещи.
Мы можем относиться к функции как к объекту, хранить в ней свойства, но это не влияет на ее выполнение. Переменные не являются свойствами функции и наоборот. Это просто параллельные миры.
Иногда свойства функции могут заменять замыкания. Например, мы можем переписать пример функции счетчика из главы «Область переменных, замыкание», чтобы использовать свойство функции:
функция makeCounter() { // вместо: // пусть счетчик = 0 функция счетчик() { вернуть счетчик.count++; }; счетчик.счет = 0; возвратный счетчик; } пусть счетчик = makeCounter(); предупреждение(счетчик()); // 0 предупреждение(счетчик()); // 1
Теперь count
хранится непосредственно в функции, а не во внешней лексической среде.
Это лучше или хуже, чем использование замыкания?
Основное отличие состоит в том, что если значение count
находится во внешней переменной, внешний код не сможет получить к нему доступ. Его могут изменить только вложенные функции. А если он привязан к функции, то возможно такое:
функция makeCounter() { функция счетчик() { вернуть счетчик.count++; }; счетчик.счет = 0; возвратный счетчик; } пусть счетчик = makeCounter(); счетчик.счет = 10; предупреждение(счетчик()); // 10
Так что выбор реализации зависит от наших целей.
Выражение именованной функции, или NFE, — это термин для выражений функций, имеющих имя.
Например, возьмем обычное функциональное выражение:
пусть говорятПривет = функция (кто) { alert(`Привет, ${who}`); };
И добавим к нему имя:
letsayHi = функция func(кто) { alert(`Привет, ${who}`); };
Добились ли мы здесь чего-нибудь? Какова цель этого дополнительного имени "func"
?
Прежде всего отметим, что у нас все еще есть функциональное выражение. Добавление имени "func"
после function
не сделало ее объявлением функции, поскольку она по-прежнему создается как часть выражения присваивания.
Добавление такого имени тоже ничего не сломало.
Функция по-прежнему доступна sayHi()
:
letsayHi = функция func(кто) { alert(`Привет, ${who}`); }; SayHi("Джон"); // Привет, Джон
В имени func
есть две особенности, которые и являются причиной его появления:
Это позволяет функции ссылаться на себя внутри.
Он не виден вне функции.
Например, функция sayHi
ниже снова вызывает себя с помощью "Guest"
если who
не указано:
letsayHi = функция func(кто) { если (кто) { alert(`Привет, ${who}`); } еще { функция("Гость"); // используем func для повторного вызова самого себя } }; сказатьПривет(); // Привет, Гость // Но это не сработает: функция(); // Ошибка, функция не определена (не видна вне функции)
Почему мы используем func
? Может быть, просто использовать sayHi
для вложенного вызова?
На самом деле, в большинстве случаев мы можем:
пусть говорятПривет = функция (кто) { если (кто) { alert(`Привет, ${who}`); } еще { SayHi("Гость"); } };
Проблема с этим кодом заключается в том, что sayHi
может измениться во внешнем коде. Если вместо этого функции будет присвоена другая переменная, код начнет выдавать ошибки:
пусть говорятПривет = функция (кто) { если (кто) { alert(`Привет, ${who}`); } еще { SayHi("Гость"); // Ошибка: SayHi не является функцией } }; пусть добро пожаловать = SayHi; SayHi = ноль; добро пожаловать(); // Ошибка, вложенный вызов SayHi больше не работает!
Это происходит потому, что функция принимает sayHi
из своего внешнего лексического окружения. Локального sayHi
нет, поэтому используется внешняя переменная. И в момент вызова внешний sayHi
равен null
.
Необязательное имя, которое мы можем ввести в выражение функции, предназначено для решения именно таких проблем.
Давайте воспользуемся этим, чтобы исправить наш код:
letsayHi = функция func(кто) { если (кто) { alert(`Привет, ${who}`); } еще { функция("Гость"); // Теперь все в порядке } }; пусть добро пожаловать = SayHi; SayHi = ноль; добро пожаловать(); // Привет, Гость (вложенный вызов работает)
Теперь это работает, потому что имя "func"
является локальным для функции. Оно не снято снаружи (да и не видно там). Спецификация гарантирует, что она всегда будет ссылаться на текущую функцию.
Внешний код по-прежнему имеет переменную sayHi
или welcome
. А func
— это «внутреннее имя функции», способ надежного вызова функции.
Для объявления функции такого понятия не существует.
Описанная здесь функция «внутреннего имени» доступна только для выражений функций, но не для объявлений функций. Для объявлений функций не существует синтаксиса для добавления «внутреннего» имени.
Иногда, когда нам нужно надежное внутреннее имя, это повод переписать объявление функции в форму выражения именованной функции.
Функции — это объекты.
Здесь мы рассмотрели их свойства:
name
– имя функции. Обычно берется из определения функции, но если его нет, JavaScript пытается угадать его из контекста (например, присваивания).
length
– количество аргументов в определении функции. Остальные параметры не учитываются.
Если функция объявлена как выражение функции (не в основном потоке кода) и содержит имя, то она называется выражением именованной функции. Имя может использоваться внутри для ссылки на себя, для рекурсивных вызовов и т.п.
Кроме того, функции могут иметь дополнительные свойства. Многие известные библиотеки JavaScript широко используют эту функцию.
Они создают «основную» функцию и присоединяют к ней множество других «вспомогательных» функций. Например, библиотека jQuery создает функцию с именем $
. Библиотека lodash создает функцию _
, а затем добавляет к ней _.clone
, _.keyBy
и другие свойства (см. документацию, если вы хотите узнать о них больше). На самом деле они делают это, чтобы уменьшить загрязнение глобального пространства, чтобы одна библиотека давала только одну глобальную переменную. Это уменьшает вероятность конфликтов имен.
Таким образом, функция может выполнять полезную работу сама по себе, а также нести в свойствах множество других функций.
важность: 5
Измените код makeCounter()
, чтобы счетчик также мог уменьшаться и устанавливать число:
counter()
должен вернуть следующее число (как и раньше).
counter.set(value)
должен установить счетчик в value
.
counter.decrease()
должен уменьшить счетчик на 1.
Полный пример использования см. в коде песочницы.
PS Вы можете использовать либо замыкание, либо свойство функции, чтобы сохранить текущий счетчик. Или напишите оба варианта.
Откройте песочницу с тестами.
В решении используется count
в локальной переменной, но методы сложения прописываются прямо в counter
. Они используют одно и то же внешнее лексическое окружение, а также могут получить доступ к текущему count
.
функция makeCounter() { пусть счет = 0; функция счетчик() { количество возврата++; } counter.set = значение => count = значение; counter.decrease = () => count--; возвратный счетчик; }
Откройте решение с тестами в песочнице.
важность: 2
Напишите функцию sum
, которая будет работать следующим образом:
сумма(1)(2) == 3; // 1 + 2 сумма(1)(2)(3) == 6; // 1 + 2 + 3 сумма(5)(-1)(2) == 6 сумма(6)(-1)(-2)(-3) == 0 сумма(0)(1)(2)(3)(4)(5) == 15
PS Подсказка: вам может потребоваться настроить преобразование пользовательского объекта в примитив для вашей функции.
Откройте песочницу с тестами.
Чтобы все это работало, результат sum
должен быть функцией.
Эта функция должна сохранять в памяти текущее значение между вызовами.
Согласно задаче, функция должна стать числом при использовании в ==
. Функции являются объектами, поэтому преобразование происходит, как описано в главе «Преобразование объектов в примитивы», и мы можем предоставить собственный метод, возвращающий число.
Теперь код:
функция сумма(а) { пусть currentSum = а; функция f(b) { текущаяСумма += б; вернуть f; } f.toString = функция() { вернуть текущую сумму; }; вернуть f; } предупреждение(сумма(1)(2)); // 3 Предупреждение(сумма(5)(-1)(2) ); // 6 предупреждение(сумма(6)(-1)(-2)(-3) ); // 0 предупреждение(сумма(0)(1)(2)(3)(4)(5) ); // 15
Обратите внимание, что функция sum
на самом деле работает только один раз. Он возвращает функцию f
.
Затем при каждом последующем вызове f
добавляет свой параметр к сумме currentSum
и возвращает себя.
В последней строке f
нет рекурсии.
Вот как выглядит рекурсия:
функция f(b) { текущаяСумма += б; вернуть Ф(); // <-- рекурсивный вызов }
А в нашем случае мы просто возвращаем функцию, не вызывая ее:
функция f(b) { текущаяСумма += б; вернуть f; // <-- не вызывает себя, возвращает себя }
Этот f
будет использоваться в следующем вызове и снова вернется столько раз, сколько необходимо. Затем, при использовании в качестве числа или строки, toString
возвращает currentSum
. Мы также могли бы использовать здесь для преобразования Symbol.toPrimitive
или valueOf
.
Откройте решение с тестами в песочнице.