Локальные переменные для объекта

Замыкания можно использовать сотнями способов. Иногда люди сами не замечают, что использовали замыкания – настолько это просто и естественно.

В этой главе мы рассмотрим дополнительные примеры использования замыканий и задачи на эту тему.

Счётчик-объект

Ранее мы сделали счётчик.

Напомню, как он выглядел:

function makeCounter() {
  var currentCount = 1;

  return function() {
    return currentCount++;
  };
}

var counter = makeCounter();

// каждый вызов возвращает результат, увеличивая счётчик
alert( counter() ); // 1
alert( counter() ); // 2
alert( counter() ); // 3

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

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

function makeCounter() {
  var currentCount = 1;

  return { // возвратим объект вместо функции
    getNext: function() {
      return currentCount++;
    },

    set: function(value) {
      currentCount = value;
    },

    reset: function() {
      currentCount = 1;
    }
  };
}

var counter = makeCounter();

alert( counter.getNext() ); // 1
alert( counter.getNext() ); // 2

counter.set(5);
alert( counter.getNext() ); // 5

Теперь функция makeCounter возвращает не одну функцию, а объект с несколькими методами:

  • getNext() – получить следующее значение, то, что раньше делал вызов counter().
  • set(value) – поставить значение.
  • reset() – обнулить счётчик.

Все они получают ссылку [[Scope]] на текущий (внешний) объект переменных. Поэтому вызов любого из этих методов будет получать или модифицировать одно и то же внешнее значение currentCount.

Объект счётчика + функция

Изначально, счётчик делался функцией во многом ради красивого вызова: counter(), который увеличивал значение и возвращал результат.

К сожалению, при переходе на объект короткий вызов пропал, вместо него теперь counter.getNext(). Но он ведь был таким простым и удобным…

Поэтому давайте вернём его!

function makeCounter() {
  var currentCount = 1;

  // возвращаемся к функции
  function counter() {
      return currentCount++;
    }

  // ...и добавляем ей методы!
  counter.set = function(value) {
    currentCount = value;
  };

  counter.reset = function() {
    currentCount = 1;
  };

  return counter;
}

var counter = makeCounter();

alert( counter() ); // 1
alert( counter() ); // 2

counter.set(5);
alert( counter() ); // 5

Красиво, не правда ли? Получился полноценный объект, который можно вдобавок ещё и вызывать.

Этот трюк часто используется при разработке JavaScript-библиотек. Например, популярная библиотека jQuery предоставляет глобальную переменную с именем jQuery (доступна также под коротким именем $), которая с одной стороны является функцией и может вызываться как jQuery(...), а с другой – у неё есть различные методы, например jQuery.type(123) возвращает тип аргумента.

Далее вы найдёте различные задачи на понимание замыканий. Рекомендуется их сделать самостоятельно.

Задачи

важность: 4

Чтобы вторые скобки в вызове работали – первые должны возвращать функцию.

Эта функция должна знать про a и уметь прибавлять a к b. Вот так:

function sum(a) {

  return function(b) {
    return a + b; // возьмет a из внешнего LexicalEnvironment
  };

}

alert( sum(1)(2) );
alert( sum(5)(-1) );

Напишите функцию sum, которая работает так: sum(a)(b) = a+b.

Да, именно так, через двойные скобки (это не опечатка). Например:

sum(1)(2) = 3
sum(5)(-1) = 4
важность: 5

Текущее значение текста удобно хранить в замыкании, в локальной переменной makeBuffer:

function makeBuffer() {
  var text = '';

  return function(piece) {
    if (arguments.length == 0) { // вызов без аргументов
      return text;
    }
    text += piece;
  };
};

var buffer = makeBuffer();

// добавить значения к буферу
buffer('Замыкания');
buffer(' Использовать');
buffer(' Нужно!');
alert( buffer() ); // 'Замыкания Использовать Нужно!'

var buffer2 = makeBuffer();
buffer2(0);
buffer2(1);
buffer2(0);

alert( buffer2() ); // '010'

Начальное значение text = '' – пустая строка. Поэтому операция text += piece прибавляет piece к строке, автоматически преобразуя его к строковому типу, как и требовалось в условии.

Открыть решение с тестами в песочнице.

В некоторых языках программирования существует объект «строковый буфер», который аккумулирует внутри себя значения. Его функционал состоит из двух возможностей:

  1. Добавить значение в буфер.
  2. Получить текущее содержимое.

Задача – реализовать строковый буфер на функциях в JavaScript, со следующим синтаксисом:

  • Создание объекта: var buffer = makeBuffer();.
  • Вызов makeBuffer должен возвращать такую функцию buffer, которая при вызове buffer(value) добавляет значение в некоторое внутреннее хранилище, а при вызове без аргументов buffer() – возвращает его.

Вот пример работы:

function makeBuffer() { /* ваш код */ }

var buffer = makeBuffer();

// добавить значения к буферу
buffer('Замыкания');
buffer(' Использовать');
buffer(' Нужно!');

// получить текущее значение
alert( buffer() ); // Замыкания Использовать Нужно!

Буфер должен преобразовывать все данные к строковому типу:

var buffer = makeBuffer();
buffer(0);
buffer(1);
buffer(0);

alert( buffer() ); // '010'

Решение не должно использовать глобальные переменные.

Открыть песочницу с тестами для задачи.

важность: 5
function makeBuffer() {
  var text = '';

  function buffer(piece) {
    if (arguments.length == 0) { // вызов без аргументов
      return text;
    }
    text += piece;
  };

  buffer.clear = function() {
    text = "";
  }

  return buffer;
};

var buffer = makeBuffer();

buffer("Тест");
buffer(" тебя не съест ");
alert( buffer() ); // Тест тебя не съест

buffer.clear();

alert( buffer() ); // ""

Открыть решение с тестами в песочнице.

Добавьте буферу из решения задачи Функция - строковый буфер метод buffer.clear(), который будет очищать текущее содержимое буфера:

function makeBuffer() {
  ...ваш код...
}

var buffer = makeBuffer();

buffer("Тест");
buffer(" тебя не съест ");
alert( buffer() ); // Тест тебя не съест

buffer.clear();

alert( buffer() ); // ""

Открыть песочницу с тестами для задачи.

важность: 5
var users = [{
  name: "Вася",
  surname: 'Иванов',
  age: 20
}, {
  name: "Петя",
  surname: 'Чапаев',
  age: 25
}, {
  name: "Маша",
  surname: 'Медведева',
  age: 18
}];

function byField(field) {
    return function(a, b) {
      return a[field] > b[field] ? 1 : -1;
    }
  }

users.sort(byField('name'));
users.forEach(function(user) {
  alert( user.name );
});

users.sort(byField('age'));
users.forEach(function(user) {
  alert( user.name );
});

У нас есть массив объектов:

var users = [{
  name: "Вася",
  surname: 'Иванов',
  age: 20
}, {
  name: "Петя",
  surname: 'Чапаев',
  age: 25
}, {
  name: "Маша",
  surname: 'Медведева',
  age: 18
}];

Обычно сортировка по нужному полю происходит так:

// по полю name (Вася, Маша, Петя)
users.sort(function(a, b) {
  return a.name > b.name ? 1 : -1;
});

// по полю age  (Маша, Вася, Петя)
users.sort(function(a, b) {
  return a.age > b.age ? 1 : -1;
});

Мы хотели бы упростить синтаксис до одной строки, вот так:

users.sort(byField('name'));
users.forEach(function(user) {
  alert( user.name );
}); // Вася, Маша, Петя

users.sort(byField('age'));
users.forEach(function(user) {
  alert( user.name );
}); // Маша, Вася, Петя

То есть, вместо того, чтобы каждый раз писать в sort function... – будем использовать byField(...)

Напишите функцию byField(field), которую можно использовать в sort для сравнения объектов по полю field, чтобы пример выше заработал.

важность: 5

Функция фильтрации

function filter(arr, func) {
  var result = [];

  for (var i = 0; i < arr.length; i++) {
    var val = arr[i];
    if (func(val)) {
      result.push(val);
    }
  }

  return result;
}

var arr = [1, 2, 3, 4, 5, 6, 7];

alert(filter(arr, function(a) {
  return a % 2 == 0;
})); // 2, 4, 6

Фильтр inBetween

function filter(arr, func) {
  var result = [];

  for (var i = 0; i < arr.length; i++) {
    var val = arr[i];
    if (func(val)) {
      result.push(val);
    }
  }

  return result;
}

function inBetween(a, b) {
    return function(x) {
      return x >= a && x <= b;
    };
  }

var arr = [1, 2, 3, 4, 5, 6, 7];
alert( filter(arr, inBetween(3, 6)) ); // 3,4,5,6

Фильтр inArray

function filter(arr, func) {
  var result = [];

  for (var i = 0; i < arr.length; i++) {
    var val = arr[i];
    if (func(val)) {
      result.push(val);
    }
  }

  return result;
}

function inArray(arr) {
    return function(x) {
      return arr.indexOf(x) != -1;
    };
  }

var arr = [1, 2, 3, 4, 5, 6, 7];
alert( filter(arr, inArray([1, 2, 10])) ); // 1,2

Открыть решение с тестами в песочнице.

  1. Создайте функцию filter(arr, func), которая получает массив arr и возвращает новый, в который входят только те элементы arr, для которых func возвращает true.
  2. Создайте набор «готовых фильтров»: inBetween(a,b) – «между a,b», inArray([...]) – "в массиве [...]". Использование должно быть таким:
  • filter(arr, inBetween(3,6)) – выберет только числа от 3 до 6,
  • filter(arr, inArray([1,2,3])) – выберет только элементы, совпадающие с одним из значений массива.

Пример, как это должно работать:

/* .. ваш код для filter, inBetween, inArray */
var arr = [1, 2, 3, 4, 5, 6, 7];

alert(filter(arr, function(a) {
  return a % 2 == 0
})); // 2,4,6

alert( filter(arr, inBetween(3, 6)) ); // 3,4,5,6

alert( filter(arr, inArray([1, 2, 10])) ); // 1,2

Открыть песочницу с тестами для задачи.

важность: 5

Что происходит в этом коде

Функция makeArmy делает следующее:

  1. Создаёт пустой массив shooters:

    var shooters = [];
  2. В цикле заполняет массив элементами через shooters.push. При этом каждый элемент массива – это функция, так что в итоге после цикла массив будет таким:

    shooters = [
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); }
    ];

    Этот массив возвращается из функции.

  3. Вызов army[5]() – это получение элемента массива (им будет функция), и тут же – её запуск.

Почему ошибка

Вначале разберемся, почему все стрелки выводят одно и то же значение.

В функциях-стрелках shooter отсутствует переменная i. Когда такая функция вызывается, то i она берет из внешнего LexicalEnvironment.

Чему же будет равно это значение i?

К моменту вызова army[0](), функция makeArmy уже закончила работу. Цикл завершился, последнее значение было i=10.

В результате все функции shooter получают из внешнего лексического окружения это, одно и то же, последнее, значение i=10.

Попробуйте исправить проблему самостоятельно.

Исправление (3 варианта)

Есть несколько способов исправить ситуацию.

  1. Первый способ исправить код – это привязать значение непосредственно к функции-стрелку:

    function makeArmy() {
    
      var shooters = [];
    
      for (var i = 0; i < 10; i++) {
    
        var shooter = function me() {
          alert( me.i );
        };
        shooter.i = i;
    
        shooters.push(shooter);
      }
    
      return shooters;
    }
    
    var army = makeArmy();
    
    army[0](); // 0
    army[1](); // 1

    В этом случае каждая функция хранит в себе свой собственный номер.

    Кстати, обратите внимание на использование Named Function Expression, вот в этом участке:

    ...
    var shooter = function me() {
      alert( me.i );
    };
    ...

    Если убрать имя me и оставить обращение через shooter, то работать не будет:

    for (var i = 0; i < 10; i++) {
      var shooter = function() {
        alert( shooter.i ); // вывести свой номер (не работает!)
        // потому что откуда функция возьмёт переменную shooter?
        // ..правильно, из внешнего объекта, а там она одна на всех
      };
      shooter.i = i;
      shooters.push(shooter);
    }

    Вызов alert(shooter.i) при вызове будет искать переменную shooter, а эта переменная меняет значение по ходу цикла, и к моменту вызова она равна последней функции, созданной в цикле.

    Если использовать Named Function Expression, то имя жёстко привязывается к конкретной функции, и поэтому в коде выше me.i возвращает правильный i.

  2. Другое, более продвинутое решение – использовать дополнительную функцию для того, чтобы «поймать» текущее значение i:

    function makeArmy() {
    
      var shooters = [];
    
      for (var i = 0; i < 10; i++) {
    
        var shooter = (function(x) {
    
          return function() {
            alert( x );
          };
    
        })(i);
    
        shooters.push(shooter);
      }
    
      return shooters;
    }
    
    var army = makeArmy();
    
    army[0](); // 0
    army[1](); // 1

    Посмотрим выделенный фрагмент более внимательно, чтобы понять, что происходит:

    var shooter = (function(x) {
      return function() {
        alert( x );
      };
    })(i);

    Функция shooter создана как результат вызова промежуточного функционального выражения function(x), которое объявляется – и тут же выполняется, получая x = i.

    Так как function(x) тут же завершается, то значение x больше не меняется. Оно и будет использовано в возвращаемой функции-стрелке.

    Для красоты можно изменить название переменной x на i, суть происходящего при этом не изменится:

    var shooter = (function(i) {
      return function() {
        alert( i );
      };
    })(i);

    Кстати, обратите внимание – скобки вокруг function(i) не нужны, можно и так:

    var shooter = function(i) { // без скобок вокруг function(i)
      return function() {
        alert( i );
      };
    }(i);

    Скобки добавлены в код для лучшей читаемости, чтобы человек, который просматривает его, не подумал, что var shooter = function, а понял что это вызов «на месте», и присваивается его результат.

  3. Еще один забавный способ – обернуть весь цикл во временную функцию:

    function makeArmy() {
    
      var shooters = [];
    
      for (var i = 0; i < 10; i++)(function(i) {
    
        var shooter = function() {
          alert( i );
        };
    
        shooters.push(shooter);
    
      })(i);
    
      return shooters;
    }
    
    var army = makeArmy();
    
    army[0](); // 0
    army[1](); // 1

    Вызов (function(i) { ... }) обернут в скобки, чтобы интерпретатор понял, что это Function Expression.

    Плюс этого способа – в большей читаемости. Фактически, мы не меняем создание shooter, а просто обертываем итерацию в функцию.

Открыть решение с тестами в песочнице.

Следующий код создает массив функций-стрелков shooters. По замыслу, каждый стрелок должен выводить свой номер:

function makeArmy() {

  var shooters = [];

  for (var i = 0; i < 10; i++) {
    var shooter = function() { // функция-стрелок
      alert( i ); // выводит свой номер
    };
    shooters.push(shooter);
  }

  return shooters;
}

var army = makeArmy();

army[0](); // стрелок выводит 10, а должен 0
army[5](); // стрелок выводит 10...
// .. все стрелки выводят 10 вместо 0,1,2...9

Почему все стрелки́ выводят одно и то же? Поправьте код, чтобы стрелки работали как задумано. Предложите несколько вариантов исправления.

Открыть песочницу с тестами для задачи.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.