- Объяснять лексическую область видимости, цепочку областей, блочную область и поднятие (hoisting)
- Использовать замыкания, IIFE и паттерн «модуль» для хранения скрытого состояния
- Предсказывать значение
thisпри вызове и исправлять его черезbind,call,applyили стрелочную функцию
На сайте есть кнопка «Нравится», которая должна помнить, сколько раз её нажали. Если держать счётчик в глобальной переменной, любой другой код может случайно его испортить. Как же функции «помнить» свою переменную после того, как она завершилась? Ответ — замыкание (closure), одна из самых мощных идей JavaScript и одна из самых частых тем на собеседованиях.
Лексическая область видимости и цепочка областей
Область видимости — часть кода, где переменная видна. В JavaScript область видимости лексическая: какие переменные видит функция, зависит от того, где она написана, а не от того, где её вызывают. Каждая функция и каждый блок { } с let/const создают новую область. Ища переменную, JavaScript смотрит в текущую область, затем во внешнюю и так до глобальной — это цепочка областей видимости. Поиск идёт только изнутри наружу, никогда наоборот.
const city = 'Baku';
function outer() {
const district = 'Nasimi';
function inner() {
const street = 'Nizami';
console.log(street, district, city);
}
inner();
console.log(typeof street);
}
outer();
if (true) {
let blockVar = 'inside';
var functionVar = 'var ignores blocks';
}
console.log(typeof blockVar, functionVar);▸ Ожидаемый результат
Nizami Nasimi Baku undefined undefined var ignores blocks
inner видит все три области, а outer не видит street. var «сбегает» из блока, а let остаётся внутри него.Перед выполнением кода движок регистрирует все объявления — это называется поднятием (hoisting). Переменная, объявленная через var, «поднимается» в начало своей области со значением undefined. Объявление function поднимается целиком, поэтому функцию можно вызвать раньше строки, где она написана. let и const тоже поднимаются, но до своей строки находятся во временной мёртвой зоне (TDZ): обращение к ним раньше времени выбрасывает ReferenceError.
console.log(early);
var early = 'var is hoisted';
try {
console.log(later);
} catch (error) {
console.log(error.name);
}
let later = 'let waits in the TDZ';
console.log(sayHi());
function sayHi() {
return 'Function declarations are hoisted';
}▸ Ожидаемый результат
undefined ReferenceError Function declarations are hoisted
Замыкания
Функция вместе с переменными той области, где она была создана. Внутренняя функция сохраняет доступ к этим переменным даже после того, как внешняя функция завершилась.
Обычно локальные переменные функции исчезают, когда она заканчивается. Но если внутренняя функция, которая ими пользуется, была возвращена или где-то сохранена, движок сохраняет их жизнь. Каждый вызов внешней функции создаёт новый, отдельный набор переменных — ниже likes и views никак не мешают друг другу.
function createCounter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count,
};
}
const likes = createCounter();
const views = createCounter(100);
likes.increment();
likes.increment();
views.increment();
console.log(likes.value(), views.value());
console.log(likes.count);▸ Ожидаемый результат
2 101 undefined
count — не свойство объекта: оно живёт только в замыкании, поэтому снаружи его не изменить.Ещё одно частое применение замыканий — фабрики функций: ты создаёшь новые функции, которые «помнят» параметр.
function makeMultiplier(factor) {
return (x) => x * factor;
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
console.log(double(5), triple(5));
console.log([1, 2, 3].map(triple));▸ Ожидаемый результат
10 15 [ 3, 6, 9 ]
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log('var', i), 0);
}
for (let j = 1; j <= 3; j++) {
setTimeout(() => console.log('let', j), 0);
}▸ Ожидаемый результат
var 4 var 4 var 4 let 1 let 2 let 3
IIFE и паттерн «модуль»
До появления ES-модулей программисты прятали переменные с помощью IIFE — немедленно вызываемого функционального выражения (Immediately Invoked Function Expression): функции, взятой в скобки и тут же вызванной. Её переменные не «протекают» в глобальную область. Если соединить IIFE с замыканием, получится паттерн «модуль»: скрытые данные и открытый объект (API) только с нужными методами. Сегодня на уровне файлов эту задачу решают ES-модули, но паттерн часто встречается в старом коде и полезен внутри одного файла.
const bank = (function () {
let balance = 0;
const history = [];
return {
deposit(amount) {
balance += amount;
history.push(`+${amount}`);
},
withdraw(amount) {
if (amount > balance) return 'Not enough money';
balance -= amount;
history.push(`-${amount}`);
return 'OK';
},
report: () => ({ balance, history: [...history] }),
};
})();
bank.deposit(100);
console.log(bank.withdraw(30), bank.withdraw(500));
console.log(bank.report());
console.log(typeof balance);▸ Ожидаемый результат
OK Not enough money
{ balance: 70, history: [ '+100', '-30' ] }
undefinedbalance и history меняются только через deposit, withdraw и report. report возвращает копию массива, чтобы историю нельзя было испортить снаружи.this, bind, call и apply
В отличие от обычных переменных, this не лексический: в обычной функции его значение зависит от того, как функцию вызвали, и определяется в момент вызова. У стрелочных функций своего this нет — они берут его из окружающего кода.
| Вызов | Чему равен this |
|---|---|
obj.method() | объект слева от точки — obj |
fn() | undefined в строгом режиме (классы, модули), глобальный объект в старом нестрогом коде |
new Fn() | только что созданный объект |
fn.call(obj, a, b), fn.apply(obj, [a, b]) | obj — функция вызывается сразу |
fn.bind(obj) | новая функция, у которой this навсегда obj |
| стрелочная функция | this окружающего кода |
function introduce(greeting, punctuation) {
return `${greeting}, I am ${this.name}${punctuation}`;
}
const aysel = { name: 'Aysel' };
const murad = { name: 'Murad' };
console.log(introduce.call(aysel, 'Hi', '!'));
console.log(introduce.apply(murad, ['Hello', '.']));
const ayselIntro = introduce.bind(aysel, 'Hey');
console.log(ayselIntro('!!'));
console.log(Math.max.apply(null, [3, 9, 4]));▸ Ожидаемый результат
Hi, I am Aysel! Hello, I am Murad. Hey, I am Aysel!! 9
call принимает аргументы по одному, apply — массивом. bind не вызывает функцию, а возвращает новую, в которой закреплены this (и при желании первые аргументы).Когда метод передают как колбэк, он «отрывается» от своего объекта. Методы классов всегда работают в строгом режиме, поэтому this становится undefined, и чтение свойства выбрасывает TypeError. Решения: bind, обёртка-стрелка или стрелочная функция, записанная как поле класса.
class Player {
score = 0;
addPoint() {
this.score++;
}
addPointArrow = () => {
this.score++;
};
}
const player = new Player();
const detached = player.addPoint;
try {
detached();
} catch (error) {
console.log('Lost this:', error.name);
}
[1, 2].forEach(player.addPoint.bind(player));
[1, 2, 3].forEach(player.addPointArrow);
console.log(player.score);▸ Ожидаемый результат
Lost this: TypeError 5
Напиши createIdGenerator(prefix): она возвращает функцию, которая при каждом вызове выдаёт следующий идентификатор: user-1, user-2… Счётчик храни в замыкании, и у каждого генератора должен быть свой счётчик.
function createIdGenerator(prefix) {
// keep a counter here
return () => {
// increase the counter and return `${prefix}-${count}`
};
}
const userId = createIdGenerator('user');
const orderId = createIdGenerator('order');
console.log(userId());
console.log(userId());
console.log(orderId());
console.log(userId());▸ Ожидаемый результат
user-1 user-2 order-1 user-3
Функция finalPrice берёт скидку из this.discount. 1) С помощью call выведи цену товара за 200 манатов для объекта shop. 2) Через bind создай функцию, привязанную к shop, и передай её в map, чтобы пересчитать цены [50, 80].
const shop = { discount: 10 };
function finalPrice(price) {
return price - (price * this.discount) / 100;
}
// 1) finalPrice for 200 with call
// 2) bind to shop and use it in map for [50, 80]▸ Ожидаемый результат
180 [ 45, 72 ]
Главное
- Лексическая область: функция видит переменные того места, где она написана; поиск идёт наружу по цепочке областей.
let/constимеют блочную область и до своей строки находятся в TDZ;varимеет область функции и поднимается какundefined.- Замыкание — это функция плюс переменные, которые она помнит; каждый вызов внешней функции создаёт новый набор.
- IIFE и паттерн «модуль» прячут данные и возвращают только открытый API.
thisзависит от способа вызова:call/applyзадают его один раз,bindзакрепляет навсегда, а стрелочная функция берёт его снаружи.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.
function outer() { let n = 0; return () => ++n; }. Что вернёт b() после const a = outer(); const b = outer(); a(); a();?