Пошук уроків, статей та іншого контенту
Розглянете відмінності this у скриптах і ES-модулях та навчитеся уникати помилок під час змішування форматів.
ES-модуль відрізняється від звичайного скрипту не лише синтаксисом import та export. Він має власну область видимості й завжди виконується в строгому режимі.
У браузері:
звичайний скрипт <script> виконується в глобальному контексті;
ES-модуль <script type="module"> виконується в модульному контексті;
кожен модуль має власну область видимості;
модулі автоматично використовують strict mode;
модульні змінні та функції не стають властивостями window.
Найважливіша відмінність:
// Звичайний браузерний скрипт
console.log(this === window); // true// ES-модуль
console.log(this); // undefinedУ браузері this на верхньому рівні звичайного скрипту посилається на глобальний об’єкт. У ES-модулі this на верхньому рівні дорівнює undefined.
Для кросплатформного доступу до глобального об’єкта краще використовувати globalThis, а не window:
console.log(globalThis);globalThis працює в браузері, Web Worker та Node.js, хоча набір глобальних властивостей у цих середовищах відрізняється.
Наступний приклад можна зберегти як HTML-файл і відкрити в браузері:
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>this у скрипті та модулі</title>
</head>
<body>
<script>
var classicValue = "значення зі скрипту";
function classicFunction() {
return this;
}
console.log("Скрипт:", this === window);
console.log("Змінна скрипту:", globalThis.classicValue);
console.log(
"Виклик функції скрипту:",
classicFunction() === globalThis
);
</script>
<script type="module">
const moduleValue = "значення з модуля";
function moduleFunction() {
return this;
}
const moduleArrow = () => this;
const object = {
method() {
return this;
},
functionValue: moduleFunction
};
console.log("Модуль:", this);
console.log("Модульна змінна глобальна:", globalThis.moduleValue);
console.log("Звичайний виклик функції:", moduleFunction());
console.log("Стрілкова функція:", moduleArrow());
console.log("Метод об'єкта:", object.method() === object);
console.log(
"Функція як властивість:",
object.functionValue() === object
);
</script>
</body>
</html>Очікувані результати:
у звичайному скрипті верхньорівневий this — це window;
var classicValue стає властивістю глобального об’єкта;
у модулі верхньорівневий this — undefined;
moduleValue не стає властивістю window;
звичайний виклик функції в модулі повертає undefined;
під час виклику як методу object.method() значення this — це object.
undefinedES-модулі автоматично працюють у строгому режимі. Це еквівалентно неявному "use strict":
"use strict";
function getThis() {
return this;
}
console.log(getThis()); // undefinedУ нестрогому режимі звичайного браузерного скрипту функція без явного отримувача могла отримати глобальний об’єкт:
function getThis() {
return this;
}
console.log(getThis() === window); // true у нестрогому скрипті браузераУ модулі така поведінка неможлива:
function getThis() {
return this;
}
console.log(getThis()); // undefinedЦе стосується саме звичайного виклику:
getThis();На значення this впливає спосіб виклику, а не місце, де функцію було оголошено.
function showThis() {
return this;
}
console.log(showThis()); // undefined у ES-модуліconst user = {
name: "Олена",
getName() {
return `${this.name}`;
}
};
console.log(user.getName()); // "Олена"У виразі user.getName() значенням this стає об’єкт перед крапкою, тобто user.
Якщо від’єднати метод від об’єкта, зв’язок із user буде втрачено:
const user = {
name: "Олена",
getName() {
return this.name;
}
};
const getName = user.getName;
console.log(getName()); // TypeError або помилка через звернення до undefinedУ модулі getName() викликається без отримувача, тому this дорівнює undefined.
Виправити це можна за допомогою bind:
const boundGetName = user.getName.bind(user);
console.log(boundGetName()); // "Олена"call і applyМетоди call та apply дозволяють явно встановити значення this:
function formatName(prefix, suffix) {
return `${prefix}${this.name}${suffix}`;
}
const user = {
name: "Олена"
};
console.log(formatName.call(user, "Користувач: ", "."));
// "Користувач: Олена."
console.log(formatName.apply(user, ["Ім'я: ", "!"]));
// "Ім'я: Олена!"Strict mode модуля не забороняє встановлювати this через call, apply або bind.
Стрілкові функції не мають власного this. Вони захоплюють this із зовнішнього контексту.
На верхньому рівні ES-модуля зовнішній this дорівнює undefined:
const arrowThis = () => this;
console.log(arrowThis()); // undefinedМетод call не може змінити this стрілкової функції:
const arrowThis = () => this;
const object = {
value: 42
};
console.log(arrowThis.call(object)); // undefinedЦе важливо під час створення методів об’єкта:
const counter = {
value: 0,
increment() {
this.value += 1;
},
incrementIncorrectly: () => {
this.value += 1;
}
};
counter.increment();
console.log(counter.value); // 1
counter.incrementIncorrectly();
// Помилка: this у стрілковій функції не є counterЯкщо функція має працювати як метод і звертатися до властивостей об’єкта через this, використовуйте звичайний синтаксис методу:
const counter = {
value: 0,
increment() {
this.value += 1;
}
};Стрілкові функції добре підходять для колбеків, коли потрібно зберегти this зовнішнього методу:
const timer = {
seconds: 0,
start() {
setTimeout(() => {
this.seconds += 1;
console.log(this.seconds);
}, 1000);
}
};
timer.start();Стрілка захоплює this методу start, тому через секунду this усе ще посилається на timer.
this у функціях, експортованих із модуляЕкспорт функції не прив’язує її до модуля:
// formatter.js
export function formatUser() {
return this;
}// app.js
import { formatUser } from "./formatter.js";
console.log(formatUser()); // undefinedФункція не отримує як this ні модуль, ні об’єкт імпорту. Імпортована функція викликається як звичайна функція.
За потреби контекст потрібно передати явно:
// formatter.js
export function formatUser() {
return this.name;
}// app.js
import { formatUser } from "./formatter.js";
const user = {
name: "Олена"
};
console.log(formatUser.call(user)); // "Олена"Або можна зберегти функцію як метод об’єкта:
import { formatUser } from "./formatter.js";
const user = {
name: "Олена",
formatUser
};
console.log(user.formatUser()); // "Олена"Тут функція викликається як user.formatUser(), тому this дорівнює user.
У звичайному браузерному скрипті верхньорівнева змінна, оголошена через var, зазвичай стає властивістю глобального об’єкта:
var scriptValue = 10;
console.log(globalThis.scriptValue); // 10У модулі цього не відбувається:
var moduleValue = 20;
console.log(globalThis.moduleValue); // undefinedЦе стосується var, let, const, функцій і класів, оголошених на верхньому рівні модуля.
Якщо модулю потрібно свідомо надати глобальне значення, це потрібно зробити явно:
globalThis.applicationConfig = {
debug: true
};Інший код зможе звернутися до нього так:
console.log(globalThis.applicationConfig.debug); // trueОднак глобальні змінні створюють приховані залежності. У більшості випадків краще використовувати явні export та import.
У HTML можна одночасно підключити звичайний скрипт і модуль:
<script src="legacy.js"></script>
<script type="module" src="application.js"></script>Але вони мають різні правила:
звичайний скрипт може створити глобальні властивості;
модуль не бачить верхньорівневі змінні звичайного скрипту як локальні імена;
модуль може звертатися до явних глобальних властивостей через globalThis;
звичайний скрипт не може використати import та export;
модульні оголошення не стають доступними в window;
модулі виконуються в строгому режимі.
Наприклад, legacy.js:
var legacyApi = {
getMessage() {
return "Дані зі старого скрипту";
}
};
globalThis.legacyApi = legacyApi;application.js:
import "./some-module.js";
console.log(globalThis.legacyApi.getMessage());Якщо в legacy.js оголосити var legacyApi, але не опублікувати його через глобальний об’єкт або інший механізм, модуль не зможе звернутися до нього за іменем legacyApi.
Краще явно визначити межу між форматами:
// legacy.js
globalThis.legacyApi = {
getMessage() {
return "Дані зі старого скрипту";
}
};// application.js
const message = globalThis.legacyApi.getMessage();
console.log(message);Під час міграції старого коду такий підхід є робочим, але для нового коду перевагу слід надавати модульним залежностям.
Синтаксис import призначений для JavaScript-модулів. Звичайний файл, який не є модулем, не можна безпечно підключити як звичайний ES-модуль і очікувати від нього export.
Цей код не перетворює звичайний скрипт на модуль:
import { legacyFunction } from "./legacy.js";Якщо legacy.js не містить коректного експорту в модульному форматі, імпорт завершиться помилкою.
Для старого коду зазвичай використовують один із підходів:
перетворюють файл на справжній ES-модуль;
залишають його глобальним скриптом і звертаються через globalThis;
використовують механізми конкретного середовища, наприклад CommonJS або сумісність, яку надає інструмент збирання.
Не слід змішувати ці підходи без чіткої межі відповідальності.
this у браузерних обробниках подійМодульність скрипту не змінює правил this у браузерних API. Важливо розрізняти звичайний обробник і стрілкову функцію.
const button = document.querySelector("button");
button.addEventListener("click", function () {
console.log(this === button); // true
});
button.addEventListener("click", () => {
console.log(this); // this зовнішнього контексту модуля, тобто undefined
});У звичайній функції, переданій до addEventListener, браузер встановлює this як елемент, що отримав подію.
Стрілкова функція власного this не має. Вона захоплює верхньорівневий this модуля, тобто undefined.
Тому в сучасному коді часто використовують об’єкт події замість this:
const button = document.querySelector("button");
button.addEventListener("click", (event) => {
console.log(event.currentTarget === button); // true
});Це робить залежність явною і не залежить від типу функції.
У Node.js потрібно розрізняти два модульні формати:
ES-модулі;
CommonJS-модулі.
У Node.js ES-модуль також має this === undefined на верхньому рівні:
// application.mjs
console.log(this); // undefinedУ CommonJS верхньорівневий this має іншу поведінку:
// application.cjs
console.log(this === module.exports); // trueCommonJS-файл фактично виконується всередині функціональної обгортки Node.js. Через це його верхньорівневий this пов’язаний із module.exports, а не з глобальним об’єктом.
Не варто переносити припущення між форматами:
// Це не універсально:
console.log(this === globalThis);У браузерному класичному скрипті це може бути true, у браузерному ES-модулі — false, у Node.js CommonJS — також false, а в Node.js ES-модулі верхньорівневий this — undefined.
Для глобального об’єкта використовуйте:
console.log(globalThis);А для експорту та імпорту — синтаксис, характерний для поточного формату.
this у модулі — це window// ES-модуль
console.log(this === window); // falseУ модулі використовуйте globalThis, якщо справді потрібен глобальний об’єкт:
console.log(globalThis === window); // true у браузері// module.js
const config = {
debug: true
};Інший скрипт не отримає її через window.config або globalThis.config.
Якщо значення має бути доступним іншим модулям, експортуйте його:
export const config = {
debug: true
};const user = {
name: "Олена",
getName() {
return this.name;
}
};
const getName = user.getName;
getName(); // ПомилкаВикористовуйте bind:
const getName = user.getName.bind(user);Або викликайте метод безпосередньо через об’єкт:
user.getName();const user = {
name: "Олена",
getName: () => this.name
};Такий this не посилається на user. Використовуйте метод зі звичайною функцією:
const user = {
name: "Олена",
getName() {
return this.name;
}
};import автоматично прив’яже thisimport { method } from "./module.js";
method();Імпортована функція не отримує модуль як this. Якщо їй потрібен контекст, передайте його через call, bind або викликайте як метод іншого об’єкта.
window у середовищі без браузераwindow.applicationState = {};Цей код не працюватиме в Node.js або Web Worker. Якщо потрібен саме глобальний об’єкт, використовуйте:
globalThis.applicationState = {};thisКоли потрібно визначити значення this, перевірте код у такому порядку:
Чи є функція стрілковою?
Так — вона використовує this із зовнішнього контексту.
Чи функція є методом об’єкта?
Перевірте об’єкт перед крапкою під час фактичного виклику.
Чи використано call, apply або bind?
Вони можуть явно встановити контекст.
Чи це звичайний виклик функції?
У ES-модулі this буде undefined.
Чи це верхній рівень файлу?
У ES-модулі this буде undefined.
У браузерному класичному скрипті — глобальний об’єкт.
У CommonJS — зазвичай module.exports.
Чи викликається функція браузерним API?
Перевірте правила конкретного API та відмінності між звичайною і стрілковою функцією.
ES-модулі завжди виконуються у strict mode.
На верхньому рівні ES-модуля this дорівнює undefined.
Верхньорівневі змінні модуля не стають властивостями window або globalThis.
У звичайній функції значення this визначається способом виклику.
Під час виклику object.method() this дорівнює object.
Під час звичайного виклику функції в модулі this дорівнює undefined.
Стрілкові функції не мають власного this.
Експорт функції не прив’язує її до модуля.
Для явного контексту використовуйте call, apply або bind.
Для глобального об’єкта використовуйте globalThis.
Під час змішування скриптів і модулів визначайте глобальні залежності явно або поступово переводьте код на import та export.