JavaScript — замыкания, прототипы и модули
JavaScript использует лексическую область видимости, функции как значения и прототипную модель наследования. На этих механизмах основаны замыкания, классы, функции высшего порядка и разные способы организации модулей.
Лексическое окружение
Лексическое окружение связывает выполняемый код с доступными ему переменными. Оно содержит локальные привязки и ссылку на внешнее окружение. Поиск имени начинается в текущей области и продолжается наружу.
const globalMessage = "Глобальная переменная";
function outer() {
const outerMessage = "Переменная outer";
function inner() {
const innerMessage = "Переменная inner";
console.log(innerMessage, outerMessage, globalMessage);
}
inner();
}
outer();Область видимости функции определяется местом её объявления, а не вызова:
const value = "глобальное";
function showValue() {
console.log(value);
}
function run() {
const value = "локальное";
showValue();
}
run(); // глобальноеЗамыкания
Замыкание — функция вместе с лексическим окружением, в котором она создана. Функция сохраняет доступ к внешним переменным даже после завершения внешней функции.
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2Каждый вызов createCounter() создаёт независимое окружение:
const first = createCounter();
const second = createCounter();
console.log(first()); // 1
console.log(first()); // 2
console.log(second()); // 1Сокрытие состояния
function createAccount(initialBalance = 0) {
let balance = initialBalance;
return {
deposit(amount) {
if (amount <= 0) throw new Error("Некорректная сумма");
balance += amount;
},
getBalance() {
return balance;
},
};
}
const account = createAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
console.log(account.balance); // undefinedФабрика функций
function createMultiplier(multiplier) {
return (value) => value * multiplier;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(10)); // 20
console.log(triple(10)); // 30Замыкание в цикле
let создаёт отдельную привязку для каждой итерации:
const handlers = [];
for (let i = 0; i < 3; i += 1) {
handlers.push(() => console.log(i));
}
handlers[0](); // 0
handlers[1](); // 1
handlers[2](); // 2С var все функции замкнут одну переменную и после цикла выведут 3.
Замыкания применяют для приватного состояния, фабрик функций, обработчиков, кеширования и частичного применения. Замкнутые данные остаются в памяти, пока существует использующая их функция.
this в разных контекстах
У обычной функции this в основном определяется способом вызова.
Метод объекта
const user = {
name: "Анна",
greet() {
return `Привет, ${this.name}!`;
},
};
console.log(user.greet());Объект перед точкой становится this. Одна функция может использоваться разными объектами:
const anotherUser = {
name: "Максим",
greet: user.greet,
};
console.log(anotherUser.greet()); // Привет, Максим!Потеря контекста
const greet = user.greet;
greet(); // this не равен userВ строгом режиме при обычном вызове this равен undefined. ES-модули работают в строгом режиме автоматически.
Стрелочная функция
Стрелка не создаёт собственный this, а берёт его из внешней области:
const user = {
name: "Анна",
greetLater() {
setTimeout(() => {
console.log(this.name);
}, 1000);
},
};Стрелку обычно не используют как метод, если нужен объект перед точкой:
const user = {
name: "Анна",
greet: () => console.log(this.name), // this не равен user
};Конструктор и обработчик
При вызове с new значение this указывает на создаваемый объект:
function User(name) {
this.name = name;
}
const user = new User("Анна");В обычном DOM-обработчике this обычно указывает на элемент регистрации. Для ясности можно использовать event.currentTarget:
button.addEventListener("click", (event) => {
console.log(event.currentTarget);
});| Вызов | Значение this |
|---|---|
object.method() |
Объект перед точкой |
fn() в строгом режиме |
undefined |
fn.call(value) |
value |
fn.apply(value) |
value |
функция после bind(value) |
Зафиксированное value |
new Constructor() |
Новый объект |
| стрелочная функция | this внешней области |
call, apply, bind
Эти методы явно задают this обычной функции.
call()
Вызывает функцию сразу, аргументы передаются через запятую:
function introduce(city, role) {
return `${this.name}, ${city}, ${role}`;
}
const user = { name: "Анна" };
console.log(introduce.call(user, "Казань", "разработчик"));apply()
Вызывает функцию сразу, аргументы передаются массивом:
introduce.apply(user, ["Казань", "разработчик"]);Для массива аргументов часто удобнее spread:
const numbers = [4, 12, 7];
console.log(Math.max(...numbers)); // 12bind()
Возвращает новую функцию и не вызывает её сразу:
const boundIntroduce = introduce.bind(user, "Казань");
console.log(boundIntroduce("разработчик"));| Метод | Вызывает сразу | Аргументы | Возвращает |
|---|---|---|---|
call |
Да | Через запятую | Результат функции |
apply |
Да | Массивом | Результат функции |
bind |
Нет | Через запятую | Новую функцию |
call, apply и bind не меняют лексический this стрелочной функции.
Прототипное наследование
Объект может иметь ссылку на другой объект — прототип. Если свойства нет в самом объекте, JavaScript ищет его по цепочке прототипов.
const animal = { eats: true };
const dog = Object.create(animal);
dog.name = "Бим";
console.log(dog.name); // собственное свойство
console.log(dog.eats); // унаследованное свойство
console.log(Object.getPrototypeOf(dog) === animal); // trueСобственные свойства
console.log(Object.hasOwn(dog, "name")); // true
console.log(Object.hasOwn(dog, "eats")); // false
console.log("eats" in dog); // trueObject.hasOwn() проверяет только объект, а in — всю цепочку.
Собственное свойство затеняет одноимённое свойство прототипа:
animal.sound = "звук";
dog.sound = "гав";
console.log(dog.sound); // гав
console.log(animal.sound); // звукPrototype chain
const livingBeing = { alive: true };
const animal = Object.create(livingBeing);
const dog = Object.create(animal);dog → animal → livingBeing → Object.prototype → nullПоиск заканчивается при нахождении свойства или при достижении null.
prototype и [[Prototype]]
[[Prototype]] — внутренняя ссылка конкретного объекта. Constructor.prototype — свойство функции-конструктора; этот объект становится прототипом экземпляров, созданных через new.
function User(name) {
this.name = name;
}
User.prototype.greet = function () {
return `Привет, ${this.name}!`;
};
const anna = new User("Анна");
const maxim = new User("Максим");
console.log(anna.greet());
console.log(maxim.greet());
console.log(Object.getPrototypeOf(anna) === User.prototype); // trueМетод хранится один раз в прототипе и доступен всем экземплярам.
Оператор new упрощённо:
- создаёт объект;
- связывает его с
Constructor.prototype; - вызывает конструктор с новым объектом как
this; - возвращает созданный объект.
instanceof проверяет присутствие прототипа конструктора в цепочке:
console.log(anna instanceof User); // true
console.log(anna instanceof Object); // trueНе рекомендуется изменять Array.prototype, Object.prototype и другие встроенные прототипы: это создаёт конфликты и неожиданное поведение.
Классы ES6
class — удобный синтаксис поверх прототипной модели.
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Привет, ${this.name}!`;
}
}
const user = new User("Анна");
console.log(user.greet === User.prototype.greet); // trueКласс вызывают с new. Методы экземпляров размещаются в прототипе класса.
Наследование и super
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} издаёт звук`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
return `${super.speak()}, а точнее — лает`;
}
}
const dog = new Dog("Бим", "корги");
console.log(dog instanceof Animal); // trueВ конструкторе дочернего класса нельзя обращаться к this до super().
Статические методы
Статические элементы принадлежат классу, а не экземплярам:
class User {
static count = 0;
constructor(name) {
this.name = name;
User.count += 1;
}
static createGuest() {
return new User("Гость");
}
}
const guest = User.createGuest();
console.log(User.count); // 1Они подходят для фабрик и вспомогательных операций, относящихся к классу в целом.
Приватные поля
Приватные элементы начинаются с # и доступны только внутри класса:
class BankAccount {
#balance;
constructor(initialBalance = 0) {
this.#balance = initialBalance;
}
deposit(amount) {
if (amount <= 0) throw new Error("Некорректная сумма");
this.#balance += amount;
}
get balance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.balance); // 1500
// account.#balance; // SyntaxErrorИмя _balance является только соглашением и не обеспечивает приватность.
Метод класса может потерять this при передаче отдельно. Используйте bind() или обёртку:
button.addEventListener("click", user.greet.bind(user));
// или
button.addEventListener("click", () => user.greet());Модули
Модуль — файл с собственной областью видимости и явным публичным интерфейсом. Модули разделяют программу, скрывают детали реализации и описывают зависимости.
Основные системы — CommonJS и ES Modules.
ES Modules
ES Modules, или ESM, используют export и import.
Файл math.js:
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export default function multiply(a, b) {
return a * b;
}Файл app.js:
import multiply, { PI, add } from "./math.js";
console.log(add(2, 3));
console.log(multiply(4, 5));Виды импорта и экспорта
// переименование
import { add as sum } from "./math.js";
// пространство имён
import * as math from "./math.js";
// реэкспорт
export { formatDate } from "./date.js";
export { default as User } from "./User.js";Именованных экспортов может быть несколько. default-экспорт в модуле один, а при импорте ему можно дать любое имя.
Импорты являются живыми связями:
// counter.js
export let count = 0;
export const increment = () => { count += 1; };
// app.js
import { count, increment } from "./counter.js";
increment();
console.log(count); // 1Перезаписать импортированную привязку нельзя.
Динамический импорт
import() возвращает Promise и позволяет загружать код по требованию:
button.addEventListener("click", async () => {
const { openDialog } = await import("./dialog.js");
openDialog();
});В браузере и Node.js
<script type="module" src="app.js"></script>ES-модули работают в строгом режиме, имеют файловую область видимости и обычно выполняются один раз. В Node.js ESM можно включить через .mjs или package.json:
{
"type": "module"
}CommonJS
CommonJS традиционно используется в Node.js. Импорт выполняется через require(), экспорт — через module.exports.
// math.cjs
function add(a, b) {
return a + b;
}
module.exports = { add };// app.cjs
const { add } = require("./math.cjs");
console.log(add(2, 3));Отдельные свойства можно экспортировать через exports:
exports.add = (a, b) => a + b;Изначально exports ссылается на module.exports, но присваивание нового значения ломает эту связь:
exports = { add }; // не заменяет экспорт модуля
module.exports = { add }; // заменяет экспорт правильно| Характеристика | CommonJS | ES Modules |
|---|---|---|
| Импорт | require() |
import |
| Экспорт | module.exports |
export |
| Происхождение | Node.js | Стандарт JavaScript |
| Строгий режим | Не автоматически | Автоматически |
| Браузерная поддержка | Нужны инструменты сборки | Встроенная |
| Типичные расширения | .cjs, .js |
.mjs, .js |
В новом браузерном коде обычно используют ESM. В Node.js выбор зависит от проекта. Без необходимости не смешивайте две системы.
IIFE и паттерн модуля
IIFE — функциональное выражение, вызываемое сразу после создания:
(function () {
const message = "Локальная переменная";
console.log(message);
})();
(() => {
console.log("Стрелочная IIFE");
})();До появления let, const и ESM IIFE часто использовали для изоляции переменных.
Module Pattern
IIFE и замыкание позволяют создать приватное состояние и публичный интерфейс:
const counterModule = (() => {
let count = 0;
function increment() {
count += 1;
}
function getValue() {
return count;
}
return { increment, getValue };
})();
counterModule.increment();
console.log(counterModule.getValue()); // 1
console.log(counterModule.count); // undefinedСовременный аналог — ES-модуль:
// counter.js
let count = 0;
export function increment() {
count += 1;
}
export function getValue() {
return count;
}count не экспортируется и остаётся внутренней переменной модуля.
Функции высшего порядка
Функция высшего порядка принимает функции как аргументы, возвращает функцию или делает и то и другое.
function calculate(a, b, operation) {
return operation(a, b);
}
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
console.log(calculate(4, 5, add)); // 9
console.log(calculate(4, 5, multiply)); // 20Методы map, filter, reduce, find, some, every и forEach — функции высшего порядка.
Возврат функции
function createValidator(minLength) {
return (value) => value.length >= minLength;
}
const isValidPassword = createValidator(8);
console.log(isValidPassword("secret123")); // trueКомпозиция
function pipe(...functions) {
return (initialValue) =>
functions.reduce((value, fn) => fn(value), initialValue);
}
const createSlug = pipe(
(value) => value.trim(),
(value) => value.toLowerCase(),
(value) => value.replaceAll(" ", "-")
);
console.log(createSlug(" JavaScript Modules "));
// javascript-modulesДекоратор функции
function withLogging(fn) {
return function (...args) {
console.log("Аргументы:", args);
const result = fn.apply(this, args);
console.log("Результат:", result);
return result;
};
}
const loggedAdd = withLogging((a, b) => a + b);
loggedAdd(2, 3);apply(this, args) сохраняет контекст и передаёт все аргументы исходной функции.
Частые ошибки
Стрелка как метод с динамическим this
const user = {
name: "Анна",
greet: () => console.log(this.name), // неверное ожидание this
};Используйте сокращённый метод greet() { ... }.
Передача метода без объекта
setTimeout(user.greet, 1000); // контекст потерян
setTimeout(user.greet.bind(user), 1000); // исправлениеПутаница прототипов
function User() {}
const user = new User();
console.log(User.prototype === Object.getPrototypeOf(user)); // trueUser.prototype — свойство конструктора; внутренний прототип принадлежит экземпляру.
Циклические зависимости
Если a.js импортирует b.js, а b.js импортирует a.js, могут появиться частично инициализированные значения. Общую логику лучше вынести в третий модуль.
Удержание лишних данных замыканием
Замыкание может сохранять большой объект или удалённый DOM-элемент. Удаляйте ненужные обработчики и ссылки, чтобы сборщик мусора мог освободить память.
Общий пример
Файл Task.js:
export class Task {
static #nextId = 1;
#completed = false;
constructor(title) {
if (!title.trim()) throw new Error("Название обязательно");
this.id = Task.#nextId++;
this.title = title.trim();
}
complete() {
this.#completed = true;
}
get completed() {
return this.#completed;
}
toJSON() {
return {
id: this.id,
title: this.title,
completed: this.#completed,
};
}
}Файл PriorityTask.js:
import { Task } from "./Task.js";
export class PriorityTask extends Task {
constructor(title, priority = "normal") {
super(title);
this.priority = priority;
}
toJSON() {
return { ...super.toJSON(), priority: this.priority };
}
}Файл taskStore.js:
const tasks = [];
export const addTask = (task) => tasks.push(task);
export const getTasks = (predicate = () => true) =>
tasks.filter(predicate).map((task) => task.toJSON());
export function createCompletionCounter() {
let count = 0;
return (task) => {
if (!task.completed) {
task.complete();
count += 1;
}
return count;
};
}Файл app.js:
import { Task } from "./Task.js";
import { PriorityTask } from "./PriorityTask.js";
import { addTask, getTasks, createCompletionCounter } from "./taskStore.js";
const registerCompletion = createCompletionCounter();
const first = new Task("Изучить модули");
const second = new PriorityTask("Повторить прототипы", "high");
addTask(first);
addTask(second);
registerCompletion(first);
console.log(getTasks((task) => task.completed));Здесь используются приватные поля, наследование, super, ES-модули, функция-предикат и замыкание со счётчиком.
Краткие рекомендации
- Помните: область видимости функции определяется местом её объявления.
- Используйте замыкания для фабрик и приватного состояния, не удерживая ненужные данные.
- У обычной функции
thisзависит от вызова; стрелка получаетthisизвне. - Применяйте
bind()при передаче метода отдельно от объекта. - Проверяйте собственные свойства через
Object.hasOwn(). - Не изменяйте встроенные прототипы без крайней необходимости.
- Используйте классы как удобный синтаксис, помня о прототипной основе.
- Для реальной приватности полей класса используйте
#. - В новом браузерном коде предпочитайте ES Modules.
- Экспортируйте публичный интерфейс, оставляя детали внутри модуля.