Конспект JS-course

JavaScript Garden. Функции

Источник: http://bonsaiden.github.io/JavaScript-Garden/ru/#function

Выражения и объявление функций

Функции в JavaScript тоже являются объектами (шок, сенсация) — следовательно, их можно передавать и присваивать точно так же, как и любой другой объект. Одним из вариантов использования такой возможности является передача анонимной функции как функции обратного вызова в другую функцию — к примеру, для асинхронных вызовов.

Объявление function:

// всё просто и привычно
function foo() {}

В следующем примере описанная функция резервируется перед запуском всего скрипта; за счёт этого она доступна в любом месте кода, вне зависимости от того, где она определена — даже если функция вызывается до её фактического объявления в коде.

foo(); // сработает, т.к. функция будет создана до выполнения кода
function foo() {}

function как выражение

var foo = function() {};

В этом примере безымянная и анонимная функция присваивается переменной foo.

foo; // 'undefined'
foo(); // вызовет TypeError
var foo = function() {};

Так как в данном примере выражение var — это определение функции, переменная с именем foo будет заранее зарезервирована перед запуском скрипта (таким образом, foo уже будет определена во время его работы).

Но поскольку присвоения исполняются непосредственно во время работы кода, foo по умолчанию будет присвоено значение undefined` (до обработки строки с определением функции):

var foo; // переменная неявно резервируется
foo; // 'undefined'
foo(); // вызовет TypeError
foo = function() {};

Выражения с именованными фунциями

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

var foo = function bar() {
    bar(); // работает
}
bar(); // получим ReferenceError

Здесь объект bar не доступен во внешней области, так как имя bar используется только для присвоения переменной foo; однако bar можно вызвать внутри функции. Такое поведение связано с особенностью работы JavaScript с пространствами имен - имя функции всегда доступно в локальной области видимости самой функции.

Как работает this

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

  1. Глобальная область видимости

     this;
    

    Когда мы используем this в глобальной области, она будет просто ссылаться на глобальный объект.

  2. Вызов функции

     foo();
    

    Тут this также ссылается на глобальный объект.

    ES5 Замечание: В strict-режиме теряется понятие глобальности, поэтому в этом случае this будет иметь значение undefined.

  3. Вызов метода

    test.foo();
    

    В данном примере this ссылается на test.

  4. Вызов конструктора

    new foo();
    

    Если перед вызовом функции присутствует ключевое слово new, то данная функция будет действовать как конструктор. Внутри такой функции this будет указывать на новосозданный Object.

  5. Переопределение this

    function foo(a, b, c) {}
    var bar = {};
    foo.apply(bar, [1, 2, 3]); // массив развернётся в a = 1, b = 2, c = 3
    foo.call(bar, 1, 2, 3); // аналогично
    

    Когда мы используем методы call или apply из Function.prototype, то внутри вызываемой функции this явным образом будет присвоено значение первого передаваемого параметра.

Исходя из этого, в предыдущем примере (строка с apply) правило #3 вызов метода не будет применено, и this внутри foo будет присвоено bar.

Наиболее распространенные ошибки

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

Foo.method = function() {
    function test() {
        // this ссылается на глобальный объект
    }
    test();
}

Распространенным заблуждением будет то, что this внутри test ссылается на Foo, но это не так.

Для того, чтобы получить доступ к Foo внутри функции test, необходимо создать локальную переменную внутри method, которая и будет ссылаться на Foo.

Foo.method = function() {
    var that = this;
    function test() {
        // Здесь используем that вместо this
    }
    test();
}

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

Назначение методов

Еще одной фичей, которая не работает в JavaScript, является создание псевдонимов для методов, т.е. присвоение метода объекта переменной.

var test = someObject.methodTest;
test();

Следуя первому правилу test вызывается как обычная функция; следовательно this внутри него больше не ссылается на someObject.

Хотя позднее связывание this на первый взгляд может показаться плохой идеей, но на самом деле именно благодаря этому работает наследование прототипов.

function Foo() {}
Foo.prototype.method = function() {};

function Bar() {}
Bar.prototype = Foo.prototype;

new Bar().method();

В момент, когда будет вызван method нового экземпляра Bar, this будет ссылаться на этот самый экземпляр.

Замыкания и ссылки

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

Эмуляция приватных свойств

function Counter(start) {
    var count = start;
    return {
        increment: function() {
            count++;
        },

        get: function() {
            return count;
        }
    }
}

var foo = Counter(4);
foo.increment();
foo.get(); // 5

В данном примере Counter возвращает два замыкания: функции increment и get. Обе эти функции сохраняют ссылку на область видимости Counter и, соответственно, имеют доступ к переменной count из этой самой области.

Как это работает

Поскольку в JavaScript нельзя присваивать или ссылаться на области видимости, заполучить count извне не представляется возможным. Единственным способом взаимодействовать с ним остается использование двух замыканий.

var foo = new Counter(4);
foo.hack = function() {
    count = 1337;
};

В приведенном примере мы не изменяем переменную count в области видимости Counter, т.к. foo.hack не объявлен в данной области. Вместо этого будет создана или перезаписана глобальная переменная count;

Замыкания внутри циклов

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

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

Данный код не будет выводить числа с 0 до 9, вместо этого число 10 будет выведено десять раз.

Анонимная функция сохраняет ссылку на i и, когда будет вызвана функция console.log, цикл for уже закончит свою работу, а в i будет содержаться 10.

Для получения желаемого результата необходимо создать копию переменной i.

Во избежание ошибок

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

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);
        }, 1000);
    })(i);
}

Анонимная функция-обертка будет вызвана сразу же, и в качестве первого аргумента получит i, значение которой будет скопировано в параметр e.

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

Еще одним способом реализации является возврат функции из анонимной функции-обертки, поведение этого кода будет таким же, как и в коде из предыдущего примера.

for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

Объект arguments

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

Объект arguments не является наследником Array. Он, конечно же, очень похож на массив и даже содержит свойство length — но он не наследует Array.prototype, а представляет собой Object.

По этой причине, у объекта arguments отсутствуют стандартные методы массивов, такие как push, pop или slice. Хотя итерация с использованием обычного цикла for по аргументам работает вполне корректно, вам придётся конвертировать этот объект в настоящий массив типа Array, чтобы применять к нему стандартные методы массивов.

Конвертация в массив

Указанный код вернёт новый массив типа Array, содержащий все элементы объекта arguments.

Array.prototype.slice.call(arguments);

Эта конвертация занимает много времени и использовать её в критических частях кода не рекомендуется.

Передача аргументов

Ниже представлен рекомендуемый способ передачи аргументов из одной функции в другую.

function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // делаем здесь что-нибудь
}

Другой трюк — использовать и call и apply вместе, чтобы быстро создать несвязанную обёртку:

function Foo() {}

Foo.prototype.method = function(a, b, c) {
    console.log(this, a, b, c);
};

// Создаём несвязанную версию "method"
// Она принимает параметры: this, arg1, arg2...argN
Foo.method = function() {

    // Результат: Foo.prototype.method.call(this, arg1, arg2... argN)
    Function.call.apply(Foo.prototype.method, arguments);

};

Формальные аргументы и индексы аргументов

Объект arguments создаёт по геттеру и сеттеру и для всех своих свойств и для формальных параметров функции.

В результате, изменение формального параметра также изменит значение соответствующего свойства объекта arguments и наоборот.

function foo(a, b, c) {
    arguments[0] = 2;
    a; // 2

    b = 4;
    arguments[1]; // 4

    var d = c;
    d = 9;
    c; // 3
}
foo(1, 2, 3);

Конструктор

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

Внутри конструктора (вызываемой функции) this будет указывать на новосозданный Object. Прототипом этого нового объекта будет prototype функции, которая была вызвана в качестве конструктора.

Если вызываемая функция не имеет явного возврата посредством return, то вернётся this — этот новый объект.

function Foo() {
    this.bla = 1;
}

Foo.prototype.test = function() {
    console.log(this.bla);
};

var test = new Foo();

В этом примере Foo вызывается в виде конструктора, следовательно прототип созданного объекта будет привязан к Foo.prototype.

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

function Bar() {
    return 2;
}
new Bar(); // новый объект

function Test() {
    this.value = 2;

    return {
        foo: 1
    };
}
new Test(); // возвращённый объект

Если же опустить ключевое слово new, то функция не будет возвращать никаких объектов.

function Foo() {
    this.bla = 1; // устанавливается глобальному объекту
}
Foo(); // undefined

Этот пример в некоторых случаях всё-таки может сработать: это связано с поведением this в JavaScript — он будет восприниматься парсером как глобальный объект.

Фабрики

Если хотите избавится от необходимости использования new, напишите конструктор, возвращающий значение посредством return.

function Bar() {
    var value = 1;
    return {
        method: function() {
            return value;
        }
    }
}
Bar.prototype = {
    foo: function() {}
};

new Bar();
Bar();

В обоих случаях при вызове Bar мы получим один и тот же результат — новый объект со свойством method (спасибо замыканию за это).

Также следует заметить, что вызов new Bar() никак не связан с прототипом возвращаемого объекта. Хоть прототип и назначается всем новосозданным объектам, но Bar никогда не возвращает этот новый объект.

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

Создание объектов с использованием фабрик

Часто рекомендуют не использовать new, поскольку если вы его забудете, это может привести к ошибкам.

Чтобы создать новый объект, лучше использовать фабрику и создать новый объект внутри этой фабрики.

function Foo() {
    var obj = {};
    obj.value = 'blub';

    var private = 2;
    obj.someMethod = function(value) {
        this.value = value;
    }

    obj.getPrivate = function() {
        return private;
    }
    return obj;
}

Хотя данный пример и сработает, если вы забыли ключевое слово new и благодаря ему легче работать с приватными переменными, у него есть несколько недостатков

  1. Он использует больше памяти, поскольку созданные объекты не хранят методы в прототипе и соответственно для каждого нового объекта создаётся копия каждого метода.
  2. Чтобы эмулировать наследование, фабрике нужно скопировать все методы из другого объекта или установить прототипом нового объекта старый.
  3. Разрыв цепочки прототипов просто по причине забытого ключевого слова new идёт в разрез с духом языка.

Заключение

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

Области видимости и пространства имён

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

function test() { // область видимости
    for(var i = 0; i < 10; i++) { // не область видимости
        // считаем
    }
    console.log(i); // 10
}

Также JavaScript не знает ничего о различиях в пространствах имён: всё определяется в глобально доступном пространстве имён.

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

Проклятие глобальных переменных

// скрипт A
foo = '42';

// скрипт B
var foo = '42'

Вышеприведённые два скрипта не приводят к одному результату. Скрипт A определяет переменную по имени foo в глобальной области видимости, а скрипт B определяет foo в текущей области видимости.

Повторимся, это вообще не тот же самый эффект. Если вы не используете var — то вы в большой опасности.

// глобальная область видимости
var foo = 42;
function test() {
    // локальная область видимости
    foo = 21;
}
test();
foo; // 21

Из-за того что оператор var опущен внутри функции, функция test перезапишет значение foo. Это поначалу может показаться не такой уж и большой проблемой, но если у вас имеется тысяча строк JavaScript-кода и вы не используете var, то вам на пути встретятся страшные и трудноотлаживаемые ошибки — и это не шутка.

// глобальная область видимости
var items = [/* какой-то список */];
for(var i = 0; i < 10; i++) {
    subLoop();
}

function subLoop() {
    // область видимости subLoop
    for(i = 0; i < 10; i++) { // пропущенный оператор var
        // делаем волшебные вещи!
    }
}

Внешний цикл прекратит работу сразу после первого вызова subLoop, поскольку subLoop перезаписывает глобальное значение переменной i. Использование var во втором цикле for могло бы вас легко избавить от этой ошибки. Никогда не забывайте использовать var, если только влияние на внешнюю область видимости не является тем, что вы намерены получить.

Локальные переменные

Единственный источник локальных переменных в JavaScript - это параметры функций и переменные, объявленные с использованием оператора var.

// глобальная область видимости
var foo = 1;
var bar = 2;
var i = 2;

function test(i) {
    // локальная область видимости для функции test
    i = 5;

    var foo = 3;
    bar = 4;
}
test(10);

Единственный источник локальных переменных в JavaScript - это параметры функций и переменные, объявленные с использованием оператора var.

// глобальная область видимости
var foo = 1;
var bar = 2;
var i = 2;

function test(i) {
    // локальная область видимости для функции test
    i = 5;

    var foo = 3;
    bar = 4;
}
test(10);

В то время как foo и i — локальные переменные в области видимости функции test, присвоение bar переопределит значение одноимённой глобальной переменной.

Высасывание ("поднятие")

JavaScript высасывает определения. Это значит, что оба определения с использованием var и определение function будут перенесены наверх заключающей их области видимости.

bar();
var bar = function() {};
var someValue = 42;

test();
function test(data) {
    if (false) {
        goo = 1;

    } else {
        var goo = 2;
    }
    for(var i = 0; i < 100; i++) {
        var e = data[i];
    }
}

Этот код трансформируется ещё перед исполнением. JavaScript перемещает операторы var и определение function наверх ближайшей оборачивающей области видимости.

// выражения с var переместились сюда
var bar, someValue; // по умолчанию - 'undefined'

// определение функции тоже переместилось
function test(data) {
    var goo, i, e; // потерянная блочная область видимости
                   // переместилась сюда
    if (false) {
        goo = 1;

    } else {
        goo = 2;
    }
    for(i = 0; i < 100; i++) {
        e = data[i];
    }
}

bar(); // вылетает с ошибкой TypeError,
       // поскольку bar всё ещё 'undefined'
someValue = 42; // присвоения не подвержены высасыванию
bar = function() {};

test();

Потерянная область видимости блока не только переместит операторы var вовне циклов и их тел, но и сделает результаты некоторых конструкций с if неинтуитивными.

В исходном коде оператор if изменял глобальную переменную goo, когда, как оказалось, он изменяет локальную переменную — в результате работы высасывания.

Если вы не знакомы с высасываниями, то можете посчитать, что нижеприведённый код должен породить ReferenceError.

// проверить, проинициализована ли SomeImportantThing
if (!SomeImportantThing) {
    var SomeImportantThing = {};
}

Но, конечно же, этот код работает: из-за того, что оператор var был перемещён наверх глобальной области видимости.

var SomeImportantThing;

// другой код может инициализировать здесь переменную SomeImportantThing,
// а может и нет

// убедиться, что она всё ещё здесь
if (!SomeImportantThing) {
    SomeImportantThing = {};
}

Порядок разрешения имён

Все области видимости в JavaScript, включая глобальную области видимости, содержат специальную, определённую внутри них, переменную this, которая ссылается на текущий объект.

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

Например, когда JavaScript пытается получить доступ к переменной foo в области видимости функции, он будет искать её по имени в такой последовательности:

  1. Если в текущей области видимости есть выражение var foo, использовать его.
  2. Если один из параметров функции называется foo, использовать его.
  3. Если функция сама называется foo, использовать её.
  4. Перейти на одну область видимости выше и начать с п. 1

Пространства имён

Нередкое последствие наличия только одного глобального пространства имён — проблемы с перекрытием имён переменных. В JavaScript эту проблему легко избежать, используя анонимные обёртки.

(function() {
    // самостоятельно созданное "пространство имён"

    window.foo = function() {
        // открытое замыкание
    };

})(); // сразу же выполнить функцию

Безымянные функции являются выражениями; поэтому, чтобы вы имели возможность их выполнить, они сперва должны быть разобраны.

( // разобрать функцию внутри скобок
function() {}
) // и вернуть объект функции
() // вызвать результат разбора

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

// Два других способа
+function(){}();
(function(){}());

Заключение

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

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