Пошук уроків, статей та іншого контенту
Заглибтеся у внутрішню модель зберігання змінних і функцій відповідно до лексичної структури коду. Розберіть роль Environment Record та зовнішнього посилання у формуванні областей видимості.
Lexical Environment — це внутрішня структура специфікації ECMAScript, яка описує, як код знаходить змінні, функції та інші ідентифікатори.
Лексичне середовище складається з двох частин:
Environment Record — запис, що містить прив’язки імен до значень.
Посилання на зовнішнє лексичне середовище — Outer, через яке виконується пошук у батьківській області видимості.
Концептуально структуру можна подати так:
LexicalEnvironment
├── EnvironmentRecord
│ ├── x → 10
│ └── greet → function
└── Outer → ParentLexicalEnvironmentВажливо: Lexical Environment — це модель, яку використовує специфікація JavaScript. Вона не зобов’язує рушій створювати буквально такий об’єкт у пам’яті. JavaScript-рушій може оптимізувати цю структуру, наприклад зберігати локальні змінні в стеку або регістрах.
Environment Record відповідає за зберігання прив’язок імен.
Прив’язка — це зв’язок між ідентифікатором і певним значенням або станом:
let count = 10;У цій ситуації Environment Record містить прив’язку:
count → 10Прив’язка не завжди одразу має доступне значення. Наприклад, let і const до моменту виконання ініціалізації перебувають у стані uninitialized:
{
count: uninitialized
}Спроба прочитати таку прив’язку спричиняє ReferenceError.
У специфікації Environment Record описується через абстрактні операції, серед яких важливі:
HasBinding(name) — перевірити, чи існує прив’язка;
CreateMutableBinding(name, deletable) — створити змінювану прив’язку;
CreateImmutableBinding(name, canDelete) — створити незмінювану прив’язку;
InitializeBinding(name, value) — ініціалізувати прив’язку;
SetMutableBinding(name, value, strict) — змінити значення;
GetBindingValue(name, strict) — отримати значення;
DeleteBinding(name) — видалити прив’язку.
Це не JavaScript-методи, доступні безпосередньо в коді. Це частина формальної моделі виконання мови.
Наприклад, виконання:
let score = 42;
score = 100;концептуально можна описати так:
1. CreateMutableBinding("score")
2. InitializeBinding("score", 42)
3. SetMutableBinding("score", 100, ...)OuterКожне лексичне середовище може мати посилання на зовнішнє середовище.
Коли JavaScript зустрічає ідентифікатор, він шукає його у такому порядку:
У поточному Environment Record.
Якщо прив’язку не знайдено — у середовищі, на яке вказує Outer.
Потім у наступному зовнішньому середовищі.
Пошук триває до глобального середовища.
Якщо прив’язку ніде не знайдено — виникає ReferenceError.
const company = "Acme";
function printCompany() {
const department = "Engineering";
function printDetails() {
const employee = "Olena";
console.log(employee);
console.log(department);
console.log(company);
}
printDetails();
}
printCompany();Під час виконання printDetails пошук має приблизно таку структуру:
Environment printDetails
├── employee
└── Outer → Environment printCompany
├── department
└── Outer → Global Environment
└── companyprintDetails не має власної прив’язки department, тому пошук переходить до зовнішнього середовища.
JavaScript використовує лексичну область видимості. Це означає, що область видимості визначається місцем, де код написано, а не місцем, звідки функцію викликали.
const message = "global";
function showMessage() {
console.log(message);
}
function run() {
const message = "local";
showMessage();
}
run();Результат:
globalshowMessage була оголошена на глобальному рівні, тому її Outer посилається на глобальне лексичне середовище. Вона не отримує доступ до локального message у run, хоча викликається всередині run.
Схематично:
Global Environment
├── message → "global"
├── showMessage → function
└── run → function
Environment run
└── Outer → Global Environment
Environment showMessage
└── Outer → Global EnvironmentЛанцюжок визначається під час створення функції, а не під час її виклику.
ECMAScript використовує кілька типів Environment Record.
Declarative Environment Record зберігає прив’язки, створені деклараціями JavaScript:
let;
const;
class;
функції;
параметри функцій;
блокові декларації.
{
let user = "Ihor";
const age = 30;
class Account {}
}Для цього блоку створюється окреме лексичне середовище з декларативним записом.
{
let secret = "hidden";
console.log(secret);
}
console.log(secret); // ReferenceErrorПісля завершення блоку його прив’язки більше не доступні через звичайний пошук імен.
Object Environment Record використовує об’єкт як джерело прив’язок.
Такий механізм історично пов’язаний із:
глобальним середовищем;
конструкцією with.
Наприклад, у нестрогому коді:
const user = {
name: "Olena"
};
with (user) {
console.log(name);
}Усередині with пошук name може виконуватися як пошук властивості об’єкта user.
with заборонений у strict mode і вважається небезпечною конструкцією, оскільки змінює передбачуваний ланцюжок пошуку ідентифікаторів. У сучасному коді його не використовують.
Для кожного виклику звичайної функції створюється Function Environment Record.
Він містить або обробляє:
параметри;
локальні прив’язки;
значення this;
super;
new.target;
зв’язок із функцією, яка була викликана.
function multiply(a, b) {
const result = a * b;
return result;
}
multiply(3, 4);Під час виклику концептуально створюється середовище:
Function Environment Record
├── a → 3
├── b → 4
└── result → 12Це середовище має Outer, який відповідає лексичному середовищу місця оголошення функції.
Не слід ототожнювати this із лексичною змінною. Для звичайної функції this визначається способом виклику, а не лексичним оточенням.
Для кожного JavaScript-модуля створюється Module Environment Record.
Він обробляє:
локальні змінні модуля;
import;
export;
live bindings між модулями.
Наприклад:
// counter.js
export let count = 0;
export function increment() {
count += 1;
}Імпортована прив’язка не є просто копією значення:
// app.js
import { count, increment } from "./counter.js";
console.log(count); // 0
increment();
console.log(count); // 1Імпорт посилається на прив’язку, яку експортує модуль. Тому після зміни count модулем-джерелом імпортований ідентифікатор бачить актуальне значення.
Імпортовані прив’язки не можна переприсвоїти в модулі-одержувачі:
import { count } from "./counter.js";
count = 10; // TypeErrorГлобальне середовище є спеціальним. Воно може містити:
глобальні декларативні прив’язки;
прив’язки, пов’язані з глобальним об’єктом;
глобальні функції та змінні, залежно від способу їх оголошення.
У браузері глобальним об’єктом зазвичай є window, але глобальне середовище не слід повністю ототожнювати з window.
var і let на глобальному рівніvar legacyValue = 1;
let modernValue = 2;
console.log(globalThis.legacyValue); // 1
console.log(globalThis.modernValue); // undefinedГлобальне var зазвичай створює властивість глобального об’єкта. Глобальне let створює глобальну лексичну прив’язку, але не стає властивістю globalThis.
Точна поведінка може залежати від типу середовища виконання: браузерний скрипт, модуль, Node.js та інші host environment.
Блоки створюють окремі лексичні середовища для let, const і class.
const value = "outside";
{
const value = "inside";
console.log(value); // inside
}
console.log(value); // outsideМожна уявити такі записи:
Зовнішнє середовище
└── value → "outside"
Внутрішнє середовище
├── value → "inside"
└── Outer → Зовнішнє середовищеВнутрішня прив’язка затіняє зовнішню. Зовнішня змінна не видаляється — вона просто не використовується під час пошуку, доки внутрішня прив’язка має те саме ім’я.
Для let, const і class прив’язка створюється до виконання рядка декларації, але залишається неініціалізованою до моменту фактичного виконання декларації.
Проміжок між створенням прив’язки та її ініціалізацією називається Temporal Dead Zone, або TDZ.
{
console.log(value); // ReferenceError
let value = 10;
}Це не те саме, що відсутність прив’язки.
У випадку let:
value → uninitializedУ випадку невідомого ідентифікатора:
прив’язка value не знайденаВ обох випадках результатом читання буде ReferenceError, але причина різна.
let value = "outer";
{
// Внутрішня прив’язка вже існує, але ще не ініціалізована
console.log(value); // ReferenceError
let value = "inner";
}Через лексичне затінення внутрішня value обирається ще до виконання декларації. Тому зовнішня value не використовується.
var і лексичні середовищаvar має функціональну, а не блокову область видимості.
function example() {
if (true) {
var value = 10;
}
console.log(value); // 10
}
example();Блок if не створює окремого середовища для var. Прив’язка належить функціональному середовищу example.
Натомість let створює блокову прив’язку:
function example() {
if (true) {
let value = 10;
}
console.log(value); // ReferenceError
}
example();Під час створення середовища функції декларації var піднімаються та ініціалізуються значенням undefined:
function example() {
console.log(value); // undefined
var value = 10;
}
example();Концептуально це близько до:
function example() {
var value = undefined;
console.log(value);
value = 10;
}Це не означає, що вихідний код буквально переписується. Hoisting — це зручна модель опису етапу створення прив’язок.
Функція зберігає посилання на лексичне середовище, у якому була створена. Саме це посилання дозволяє реалізувати замикання.
function createCounter() {
let count = 0;
return function increment() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3Після завершення createCounter її виконання закінчується, але середовище з count залишається доступним для increment.
Схематично:
Environment createCounter
├── count → 3
└── Outer → зовнішнє середовище
Function increment
└── [[Environment]] → Environment createCounterУ специфікації функція має внутрішній слот [[Environment]]. Він містить посилання на лексичне середовище, у якому функцію створено.
Це не означає, що функція зберігає знімок усіх значень. Вона зберігає доступ до прив’язок. Тому зміни count видно під час наступних викликів.
function createUserService(prefix) {
const serviceName = `${prefix} service`;
return function createUser(name) {
const normalizedName = name.trim();
return {
serviceName,
name: normalizedName
};
};
}
const createUser = createUserService("Admin");
console.log(createUser(" Olena "));Під час доступу до serviceName пошук відбувається так:
Environment createUser
└── normalizedName
└── Outer → Environment createUserService
├── prefix
└── serviceNameОдна з практичних демонстрацій роботи Environment Record — let у циклі.
const callbacks = [];
for (let i = 0; i < 3; i += 1) {
callbacks.push(() => i);
}
console.log(callbacks[0]()); // 0
console.log(callbacks[1]()); // 1
console.log(callbacks[2]()); // 2Для кожної ітерації for із let специфікація створює окрему прив’язку i. Кожне замикання посилається на прив’язку своєї ітерації.
Для порівняння, var використовує одну спільну прив’язку:
const callbacks = [];
for (var i = 0; i < 3; i += 1) {
callbacks.push(() => i);
}
console.log(callbacks[0]()); // 3
console.log(callbacks[1]()); // 3
console.log(callbacks[2]()); // 3Усі функції читають ту саму прив’язку i, яка після завершення циклу має значення 3.
Під час запуску коду JavaScript концептуально проходить дві фази.
Створюються необхідні Environment Records і прив’язки:
параметри функцій;
декларації var;
декларації функцій;
декларації let, const, class.
При цьому різні прив’язки можуть мати різний початковий стан:
var — undefined;
let, const, class — uninitialized;
функціональні декларації — готове значення функції.
Виконуються інструкції:
присвоєння;
ініціалізація let і const;
виклики функцій;
обчислення виразів;
повернення значень.
Приклад:
console.log(typeof declaredFunction); // "function"
console.log(varValue); // undefined
var varValue = 10;
function declaredFunction() {
return "ready";
}
console.log(varValue); // 10До виконання першого console.log:
varValue → undefined
declaredFunction → functionАле let поводиться інакше:
console.log(value); // ReferenceError
let value = 10;Розглянемо приклад:
const globalValue = "global";
function outer() {
const outerValue = "outer";
function inner() {
const innerValue = "inner";
return `${globalValue}-${outerValue}-${innerValue}`;
}
return inner();
}
console.log(outer());Під час виконання виразу в inner:
globalValue не знайдено в середовищі inner.
globalValue не знайдено в середовищі outer.
globalValue знайдено в глобальному середовищі.
Для outerValue:
У inner прив’язки немає.
У outer прив’язку знайдено.
Для innerValue:
Прив’язку знайдено одразу в inner.
Оператор typeof має окрему поведінку для невизначених глобальних ідентифікаторів:
console.log(typeof completelyMissing); // "undefined"Але typeof не обходить TDZ:
console.log(typeof notInitialized); // ReferenceError
let notInitialized = 1;thisЗмінні та this — різні концепції.
const object = {
value: 10,
method() {
return this.value;
}
};
console.log(object.method()); // 10this у звичайному методі визначається викликом object.method().
Стрілкова функція не створює власного this. Вона використовує значення this із зовнішнього лексичного контексту:
const object = {
value: 10,
method() {
const getValue = () => this.value;
return getValue();
}
};
console.log(object.method()); // 10У специфікації this пов’язаний із функціональним середовищем, але це не означає, що this працює як звичайна змінна, яку можна оголосити через let або const.
Властивості об’єкта не є лексичними прив’язками:
const user = {
name: "Ihor"
};
console.log(user.name);name тут не шукається в ланцюжку лексичних середовищ. Спочатку обчислюється значення user, після чого виконується доступ до властивості "name".
Порівняйте:
const name = "Global";
const user = {
name: "Object"
};
console.log(name); // Global
console.log(user.name); // ObjectУ першому випадку name — ідентифікатор, який шукається через Environment Records. У другому випадку name — ключ властивості об’єкта.
eval і зміна середовищаПрямий виклик eval може виконувати код у поточному лексичному контексті.
function run() {
eval("var createdByEval = 10");
console.log(createdByEval);
}
run(); // 10Такий код ускладнює статичний аналіз і оптимізацію, тому eval майже не використовують у прикладному коді.
Непрямий eval виконується в глобальному контексті:
const code = eval;
function run() {
const value = 10;
code("console.log(typeof value)");
}
run(); // "undefined"Різниця між прямим і непрямим eval є однією з причин, чому його застосування потребує особливої обережності.
Наступний приклад демонструє, що функція використовує прив’язку із зовнішнього середовища, а не копію значення:
function createState() {
let state = "idle";
return {
read() {
return state;
},
update(nextState) {
state = nextState;
}
};
}
const state = createState();
console.log(state.read()); // idle
state.update("loading");
console.log(state.read()); // loading
state.update("success");
console.log(state.read()); // successОб’єкт, який повертає createState, не містить властивості state. Значення зберігається в лексичному середовищі виклику createState.
Це дає змогу створювати приватний стан без прямого доступу до нього:
console.log(state.state); // undefinedДоступ до state відбувається тільки через функції, які замкнули це середовище.
Не можна написати:
function getEnvironment() {
// Такого стандартного API не існує
}Lexical Environment — внутрішня абстракція специфікації. Рушій може реалізовувати її не як об’єкт і навіть оптимізувати окремі прив’язки.
const value = "global";
function print() {
console.log(value);
}
function run() {
const value = "local";
print();
}
run(); // globalПошук залежить від місця оголошення print, а не від місця її виклику.
{
console.log(value); // TDZ
let value = 1;
}Прив’язка value існує, але не ініціалізована.
{
console.log(value); // прив’язка не знайдена
}У другому випадку в цьому ланцюжку середовищ немає відповідної декларації.
const робить об’єкт незміннимconst settings = {
theme: "dark"
};
settings.theme = "light"; // дозволеноconst забороняє змінювати прив’язку settings, але не забороняє змінювати об’єкт, на який вона посилається.
settings = {}; // TypeErrorЗамикання зберігає доступ до прив’язки:
function create() {
let value = 1;
return {
get() {
return value;
},
set(nextValue) {
value = nextValue;
}
};
}get не отримує одноразову копію value. Він щоразу звертається до тієї самої прив’язки.
У модулів власне лексичне середовище, а топ-level декларації модуля не стають властивостями глобального об’єкта:
// module.js
const internalValue = 10;
console.log(globalThis.internalValue); // undefinedКрім того, модулі виконуються в strict mode за замовчуванням.
Lexical Environment — внутрішня модель області видимості JavaScript.
Кожне лексичне середовище містить Environment Record і посилання Outer.
Environment Record зберігає прив’язки імен до значень або станів.
Пошук ідентифікатора починається з поточного середовища і рухається через Outer.
Ланцюжок середовищ визначається лексично — місцем оголошення коду.
let, const і class мають блокову область видимості та проходять через TDZ.
var має функціональну область видимості й ініціалізується значенням undefined.
Функції зберігають посилання на середовище створення через [[Environment]].
Замикання працюють завдяки доступу функції до прив’язок зовнішнього середовища.
Глобальне, декларативне, функціональне, об’єктне та модульне середовища мають різні призначення.
Властивості об’єктів і лексичні прив’язки — різні механізми доступу до даних.
this не є звичайною лексичною змінною, хоча його обробка пов’язана з функціональним середовищем.