Свойства name, length и методы call, apply, bind

Ранее, мы неоднократно говорили, что функция в JavaScript – это объект – особый объект, но, тем не менее, объект. А раз – это объект, то есть ли у него встроенные свойства и можно ли создавать свои? Разберемся с этим.

Первое: у объекта-функции есть, по крайней мере, два встроенных свойства (которые нельзя изменять) – это name и length. Например, у нас есть  какая-то функция:

function showMessage(msg) {
    console.log(msg);
}

Мы можем обратиться к свойству name и вывести его в консоль:

console.log(showMessage.name);

Обратите внимание, мы здесь не выполняем функцию, а лишь обращаемся к объекту-функции – к его свойству name. Это свойство корректно работает почти во всех случаях. Если, к примеру, взять такое объявление:

let func = function (msg) {
    console.log(msg);
}

То мы увидим имя «func» - той переменной, что на нее ссылается:

console.log(func.name);

Или, при объявлении методов внутри объекта:

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.counter = 0;

Каждый раз при вызове этой функции мы будем увеличивать этот счетчик на единицу:

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 );
    }
};

И мы определяем глобальную ссылку на его метод:

let func = car.getModel;

Тогда, при его вызове получим ошибку:

func();

связанную с потерей контекста выполнения этого метода: он начинает выполняться в глобальном контексте, в котором ссылка this = undefined. Но, с помощью встроенного метода call это можно поправить:

func.call(car);

Здесь мы первым аргументом указываем верное значение this=car и функция выполняется как задумано.

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

func.call(car, "opel");

В этом случае будет использован передаваемый аргумент со значением «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 существует только в теле функции, то есть, внутри лексического окружения этой функции. Но зачем это все нужно? Почему бы нам не делать так (вместо 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.

Видео по теме