Ранее, мы
неоднократно говорили, что функция в JavaScript – это объект –
особый объект, но, тем не менее, объект. А раз – это объект, то есть ли у него
встроенные свойства и можно ли создавать свои? Разберемся с этим.
Первое: у
объекта-функции есть, по крайней мере, два встроенных свойства (которые нельзя
изменять) – это name и length. Например, у нас есть какая-то
функция:
function showMessage(msg) {
console.log(msg);
}
Мы можем
обратиться к свойству name и вывести его в консоль:
console.log(showMessage.name);
Обратите
внимание, мы здесь не выполняем функцию, а лишь обращаемся к объекту-функции –
к его свойству name. Это свойство
корректно работает почти во всех случаях. Если, к примеру, взять такое объявление:
let func = function (msg) {
console.log(msg);
}
То мы увидим имя
«func» - той
переменной, что на нее ссылается:
Или, при
объявлении методов внутри объекта:
let car = {
go() {},
stop: function() {}
};
console.log(car.go.name);
console.log(car.stop.name);
Все работает как
и ожидается. И только в редких случаях, как например, здесь:
let ar = [function() {}];
console.log(ar[0].name);
это свойство
будет содержать пустую строку.
Следующее встроенное
свойство «length» содержит количество параметров функции в её объявлении.
Например:
function func1(a) {}
function func2(a, b) {}
function other(a, b, ...more) {}
console.log(func1.length); // 1
console.log(func2.length); // 2
console.log(other.length); // 2
Обратите
внимание, что в length содержатся явно заданные аргументы,
остаточные аргументы, записанные в массив more – игнорируются.
Второй вопрос:
можно ли у функций создавать свои свойства? Имеет положительный ответ. Например,
объявим функцию:
function funcCount() {
console.log("вызов функции: " + ++funcCount.counter);
}
И добавим ей
свойство:
Каждый раз при
вызове этой функции мы будем увеличивать этот счетчик на единицу:
funcCount();
funcCount();
В результате, мы
получили счетчик числа вызовов функции. Другой пример. Возьмем функцию из темы
«замыкания»:
function createCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = createCounter();
console.log( counter() );
И перепишем ее
вот так:
function createCounter() {
function counter() {
return counter.count++;
}
counter.count = 0;
return counter;
}
Здесь мы убрали
замыкание и используем добавленное свойство count вложенной функции
counter. При создании
счетчика это свойство устанавливается в 0 и, далее, при каждом вызове counter() увеличивается
на 1.
В чем может быть
преимущество такого подхода? Например, теперь мы можем сбрасывать значение
счетчика прямо в глобальном окружении:
counter.count = 0;
console.log( counter() );
Иногда это может
быть полезным, а иногда – источником дополнительных ошибок. Так что как именно
реализовывать функции со свойствами или переменными – это уже решает сам
программист, исходя из конкретной задачи.
Методы call и apply
Наконец, у
объекта-функции есть несколько полезных методов. Первые два – это call и apply. Они позволяют
вызывать функции с указанием их контекста выполнения в соответствии с
синтаксисом:
func.call(context,
arg1, arg2, ...)
func.apply(context, args)
здесь args – это массив
или массивоподобный объект. Рассмотрим пример, когда целесообразно применение
этих методов. Предположим, имеется объект:
let car = {
model: "mercedes",
getModel(model) {
if(model) console.log( model );
else console.log( this.model );
}
};
И мы определяем
глобальную ссылку на его метод:
Тогда, при его
вызове получим ошибку:
связанную с
потерей контекста выполнения этого метода: он начинает выполняться в глобальном
контексте, в котором ссылка this = undefined. Но, с помощью
встроенного метода call это можно поправить:
Здесь мы первым
аргументом указываем верное значение this=car и функция
выполняется как задумано.
Соответственно,
можно вызвать метод и с набором аргументов, например:
В этом случае
будет использован передаваемый аргумент со значением «opel». Число этих
аргументов может быть любым.
Аналогично
работает и метод apply, только вместо набора аргументов он
принимает массив значений, либо массивоподобный объект:
let myMath = {
nameObj: "myMath",
sum(...args) {
return this.nameObj+": "+args.reduce((val, prevVal) => prevVal += val, 0);
}
};
let sum = myMath.sum;
console.log( sum.apply(myMath, [1, 2, 3, 4]) );
Метод bind
Вообще, на
практике чаще используется метод apply, так как он немного более
универсальный, чем call.
Еще один
применяемый метод на практике – это bind:
let bound = func.bind(context, [arg1], [arg2], ...);
он неявно
связывает контекст (this=context) до вызова самой функции.
Например, для нашего случая с myMath его можно использовать так:
let sum = myMath.sum.bind(myMath);
console.log( sum(1, 2, 3, 4) );
Такой вызов
выглядит естественнее и, кроме того, не привязан к переменной myMath, которая может
быть изменена.
Интересно, что
метод bind также позволяет
связывать аргументы с функцией, вот так:
let sum = myMath.sum.bind(myMath, 1, 2);
console.log( sum(3, 4) );
Результат будет
тот же – 10, так как первые два аргумента 1 и 2 уже привязаны к функции и их
указывать не нужно. Однако, на практике этой возможностью почти не пользуются.
Следующий
момент, который мы рассмотрим на этом занятии – это имя для функций, заданных
синтаксисом Function Expression. Они еще называются
Named Function Expression (NFE):
let getName = function Name(name) {
if(name) return name;
else return Name("none");
}
Несмотря на то,
что у функции как бы два имени, из вне она доступна только по ссылке getName:
console.log( getName("Иван") );
console.log( getName() );
Если мы
попытаемся ее вызвать так:
то получим
ошибку, указывающую, что такой переменной не существует. И действительно, Name существует
только в теле функции, то есть, внутри лексического окружения этой функции. Но
зачем это все нужно? Почему бы нам не делать так (вместо Name прописать getName):
let getName = function Name(name) {
if(name) return name;
else return getName("none");
}
И все тоже будет
работать. Дело в том, что такой код потенциально критичен: если переменная getName будет изменена,
например, так:
let getName2 = getName;
getName = null;
то, вызывая эту
функцию через ссылку getName2, получим ошибку:
console.log( getName2() );
А вот первый
вариант записи функции через ее внутреннее имя будет работать в обоих случаях.
Обратите
внимание, что такой трюк с «внутренним» именем работает только для Function
Expression и не работает для Function Declaration. Так как для Function
Declaration синтаксис не предусматривает возможность объявить дополнительное
«внутреннее» имя. Если в программе нам нужно надёжное «внутреннее» имя, стоит
переписать Function Declaration в стиле, о котором мы только что говорили –
Named Function Expression.