Перейти к содержанию

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

Одним из самых мощных инструментов 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
  )
}

Замечание от перев. Переменную e можно тоже назвать i, если вы хотите: это не поменяет поведения кода — внутренняя переменная i всё так же будет копией внешней переменной