Суворий режим
[!NOTE] Іноді можна зустріти, що усталений, несуворий режим звуть недбалим режимом. Це не офіційний термін, але про нього слід знати, просто про всяк випадок.
Суворий режим JavaScript – це спосіб обрати обмежений варіант JavaScript, таким чином неявно відмовляючись від "недбалого режиму". Суворий режим – це не просто підмножина: він навмисно має інакшу семантику, ніж звичайний код. Браузери, що не підтримують суворий режим, запускають код суворого режиму з іншою поведінкою, ніж браузери, що підтримують його, тому не слід покладатися на суворий режим без перевірки підтримки відповідних аспектів суворого режиму. Код суворого режиму та код недбалого режиму можуть співіснувати, тому сценарії можуть переходити на суворий режим поступово.
Суворий режим вносить до звичайної семантики JavaScript кілька змін:
- Усуває кілька тихих помилок JavaScript, замінюючи їх на викидання помилок.
- Виправляє помилки, через які рушіям JavaScript складно виконувати оптимізацію: код суворого режиму може іноді виконуватися швидше, ніж ідентичний код, що не є в суворому режимі.
- Забороняє певні записи синтаксису, що, ймовірно, будуть визначені в майбутніх версіях ECMAScript.
Заклик суворого режиму
Суворий режим застосовується до цілих сценаріїв або до окремих функцій. Він не застосовується до блокових операторів, що оточені фігурними дужками {}
; спроба застосувати його до таких контекстів нічого не робить. Код eval
, код Function
, атрибути обробників подій, рядки, що передаються до setTimeout()
,, та споріднені з ними функції є або тілами функцій, або цілими сценаріями, і закликання суворого режиму в них працює, як і очікується.
Суворий режим для сценаріїв
Щоб закликати суворий режим для цілого сценарію, необхідно поставити точну інструкцію "use strict";
(або 'use strict';
) перед будь-якими іншими інструкціями.
// Запис суворого режиму для всього сценарію
"use strict";
const v = "Привіт! Я сценарій суворого режиму!";
Суворий режим для функцій
Аналогічно, для заклику суворого режиму для функції, необхідно поставити точну інструкцію "use strict";
(або 'use strict';
) у тілі функції перед будь-якими іншими інструкціями.
function myStrictFunction() {
// Запис суворого режиму на рівні функції
"use strict";
function nested() {
return "Як і я!";
}
return `Привіт! Я функція суворого режиму! ${nested()}`;
}
function myNotStrictFunction() {
return "Я не сувора.";
}
Директива "use strict"
може бути застосована лише до тіл функцій з простими параметрами. Використання "use strict"
у функціях з рештою параметрів, усталеними параметрами або деструктурованими параметрами є синтаксичною помилкою.
function sum(a = 1, b = 2) {
// SyntaxError: "use strict" not allowed in function with default parameter
"use strict";
return a + b;
}
Суворий режим для модулів
Увесь вміст модулів JavaScript автоматично виконується в суворому режимі, і для ініціювання цього не потрібна жодна інструкція.
function myStrictFunction() {
// оскільки це модуль, я усталено сувора
}
export default myStrictFunction;
Суворий режим для класів
Усі частини тіл класів – код суворого режиму, включно як з оголошеннями класів, так і з виразами класів.
class C1 {
// Увесь код тут – виконується в суворому режимі
test() {
delete Object.prototype;
}
}
new C1().test(); // TypeError, адже test() – у суворому режимі
const C2 = class {
// Увесь код тут виконується в суворому режимі
};
// Код тут може не бути в суворому режимі
delete Object.prototype; // Не викине помилки
Зміни в суворому режимі
Суворий режим змінює і синтаксис, і поведінку під час виконання. Зміни, як правило, належать до таких категорій:
- зміни в обробці помилок (як синтаксичних або помилок часу виконання)
- зміни, що спрощують те, як розв'язуються посилання змінних
- зміни, що спрощують
eval
таarguments
- зміни, що полегшують написання "безпечного" JavaScript
- зміни, що спрямовані на очікування майбутнього розвитку ECMAScript.
Викидання помилок
Суворий режим змушує викидати частину раніше прийнятних помилок. JavaScript розроблявся так, аби бути легким для розробників-новачків, і іноді він надає операціям, що повинні бути помилками, непомилкову семантику. Іноді це виправляє актуальну проблему, але іноді – створює гірші проблеми у майбутньому. Суворий режим викидає ці помилки, щоб вони були виявлені та відразу виправлені.
Присвоєння неоголошеним змінним
Суворий режим робить неможливим випадкове створення глобальних змінних. У недбалому режимі хибодрук у змінній при її присвоєнні утворює нову властивість на глобальному об'єкті, і все "працює" далі. Присвоєння, що призвели б до випадкового створення глобальних змінних, викидають помилку у суворому режимі:
"use strict";
let mistypeVariable;
// Коли виходити з припущення, що глобальна змінна mistypeVarible не існує
// то цей рядок викидає ReferenceError, у зв'язку з
// хибодруком у назві "mistypeVariable" (відсутністю "a")
mistypeVarible = 17;
Відмова при присвоєнні властивостей об'єктів
Суворий режим змушує присвоєння, що без нього мовчки не вдаються, викидати виняток. Є три варіанти відмов при присвоєнні властивості:
- присвоєння незаписній властивості даних
- присвоєння властивості з контролем доступу, що має лише гетер
- присвоєння новій властивості на нерозширюваному об'єкті
Наприклад, NaN
– це незаписна глобальна змінна. У недбалому режимі присвоєння змінній NaN
нічого не робить, і розробник не отримує ніякого повідомлення про помилку. У суворому режимі присвоєння змінній NaN
викидає виняток.
"use strict";
// Присвоєння незаписному глобальному значенню
undefined = 5; // TypeError
Infinity = 5; // TypeError
// Присвоєння незаписній властивості
const obj1 = {};
Object.defineProperty(obj1, "x", { value: 42, writable: false });
obj1.x = 9; // TypeError
// Присвоєння властивості, що має лише гетер
const obj2 = {
get x() {
return 17;
},
};
obj2.x = 5; // TypeError
// Присвоєння новій властивості на нерозширюваному об'єкті
const fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = "ohai"; // TypeError
Відмова при видаленні властивості об'єкта
Спроби видалити неналаштовну або ще через щось невидальну (наприклад, у зв'язку з перехопленням обробником deleteProperty
заступника, що повертає false
) властивість викидають помилку у суворому режимі (а раніше такі спроби не мали жодних наслідків):
"use strict";
delete Object.prototype; // TypeError
delete [].length; // TypeError
Також суворий режим забороняє видалення простих імен. Запис delete name
у суворому режимі – це синтаксична помилка:
"use strict";
var x;
delete x; // синтаксична помилка
Якщо ім'я відповідає налаштовній глобальній властивості, то його можна видалити, якщо перед ним поставити globalThis
.
"use strict";
delete globalThis.x;
Повторення імен параметрів
Суворий режим вимагає, щоб імена параметрів функції були неповторними. У недбалому режимі останній повтор аргументу затуляє попередні аргументи з такими ж іменами. Такі попередні аргументи все одно доступні завдяки arguments
, тож вони не є геть недоступними. І все ж, таке приховування має мало змісту і, мабуть, є небажаним (наприклад, воно може приховати хибодрук), тож у суворому режимі повторення імен аргументів – це синтаксична помилка:
function sum(a, a, c) {
// синтаксична помилка
"use strict";
return a + a + c; // некоректний результат, якби цей код запустився
}
Історичні вісімкові літерали
Суворий режим забороняє вісімкові літерали з 0
на початку та вісімкові екранувальні послідовності. У недбалому режимі, якщо число починається з 0
, наприклад, 0644
, то це тлумачиться як вісімкове число (0644 === 420
), якщо всі цифри – менші за 8. Розробники-новачки іноді вважають, що нуль на початку числа не має семантичного значення, тож можуть використати це як засіб вирівнювання – але це змінює значення числа! Запис із нулем на початку для вісімкових чисел рідко є корисним і може бути вжитий помилково, тож суворий режим робить його синтаксичною помилкою:
"use strict";
const sum =
015 + // синтаксична помилка
197 +
142;
Стандартизований спосіб позначати вісімкові літерали – за допомогою префіксу 0o
. Наприклад:
const sumWithOctal = 0o10 + 8;
console.log(sumWithOctal); // 16
Вісімкові екранувальні послідовності, як то "\45"
, що рівносильно "%"
, можуть вживатися для представлення символів за їх кодовими номерами у розширеному ASCII у вісімковому вигляді. У суворому режимі це стає синтаксичною помилкою. Формально, заборонено мати \
, за яким слідує будь-яка десяткова цифра, крім 0
, або \0
, за яким слідує десяткова цифра; наприклад, \9
та \07
.
Задання властивостей на примітивних значеннях
Суворий режим забороняє задавати властивості на примітивних значеннях. Звертання до властивості на примітиві неявно створює об'єкт-обгортку, за котрою неможливо слідкувати, тож у недбалому режимі такі спроби присвоєння ігноруються (не виконуються). У суворому режимі – викидається TypeError
.
"use strict";
false.true = ""; // TypeError
(14).sailing = "home"; // TypeError
"with".you = "far away"; // TypeError
Повторення імен властивостей
Повторення імен властивостей у суворому режимі вважається SyntaxError
. Завдяки запровадженню обчислюваних імен властивостей, у зв'язку з чим повторення можливо під час виконання, це обмеження було знято в ES2015.
"use strict";
const o = { p: 1, p: 2 }; // до ECMAScript 2015 – синтаксична помилка
[!NOTE] Те, що код, який раніше викидав помилку, більше її не викидає, – завжди вважається зворотно сумісним. Це добра частина того, як мова сувора щодо викидання помилок: це залишає простір для майбутніх семантичних змін.
Спрощення керування областю видимості
Суворий режим спрощує те, як імена змінних відображаються на конкретні оголошення змінних у коді. Чимало оптимізацій компілятора залежить від можливості сказати, що змінна X зберігається у такому собі місці, – це критично для повної оптимізації коду JavaScript. JavaScript іноді робить таке базове відображення імені на оголошення змінної у коді неможливим для виконання до часу виконання. Суворий режим усуває більшість випадків, коли це відбувається, тож компілятор може краще оптимізувати код суворого режиму.
Усунення інструкції with
Суворий режим забороняє with
. Проблема з with
у тому, що будь-яке ім'я всередині блоку може відображатись або на властивість об'єкта, переданого with
, або на змінну в навколишній (або навіть глобальній) області видимості, під час виконання; неможливо дізнатися заздалегідь. Суворий режим робить with
синтаксичною помилкою, тож з ним немає шансу на те, що ім'я всередині with
посилається на невідоме місце під час виконання:
"use strict";
const x = 17;
with (obj) {
// Синтаксична помилка
// Якби це не був суворий режим, то чи була б це const x, чи
// натомість obj.x? Загалом, неможливо сказати,
// не запустивши код, тож це ім'я не може бути
// оптимізовано.
x;
}
Інструкція with
легко замінюється альтернативою з присвоєння об'єкта змінній з коротким іменем, а потім звертання до відповідної властивості через цю змінну.
Недірявий eval
У суворому режимі eval
не додає до навколишньої області видимості нові змінні. У недбалому режимі eval("var x;")
додає до навколишньої функції або глобальної області видимості змінну x
. Це означає, що, загалом, у функції, що містить виклик eval
, кожне ім'я, що не посилається на аргумент або локальну змінну, повинно відображатись на певне визначення часу виконання (тому що такий eval
може додати нову змінну, що затулить зовнішню змінну). У суворому режимі eval
створює змінні лише для того коду, що виконується, тож не може вплинути на те, чи посилається ім'я на зовнішню змінну, чи на якусь локальну змінну:
var x = 17;
var evalX = eval("'use strict'; var x = 42; x;");
console.assert(x === 17);
console.assert(evalX === 42);
Те, чи виконується рядок, що передається eval()
, у суворому режимі, залежить від того, як eval()
закликається (безпосередній виклик eval
чи опосередкований виклик eval
).
Оголошення функцій з блоковою областю видимості
Специфікація мови JavaScript від свого початку не дозволяла оголошень функцій, вкладених у блокові інструкції. Проте це було так інтуїтивно, що більшість браузерів реалізували це як граматику-розширення. На жаль, семантика таких реалізацій – різна, і для специфікації мови стало неможливим примирити їх усі. Таким чином, оголошення функцій з блоковою областю видимості явно задані лише в суворому режимі (хоча колись вони були в суворому режимі заборонені), а поведінка недбалого режиму серед браузерів залишається різною.
Спрощення eval і arguments
Суворий режим робить arguments
і eval
менш химерно чудодійними. І перше, і друге у недбалому режимі спричиняє суттєву кількість магічної поведінки: eval
– до додавання або видалення зв'язувань і до змін значень зв'язувань, а arguments
– до синхронізації іменованих аргументів з властивостями за індексами. Суворий режим робить великі кроки у напрямку того, щоб розглядати eval
і arguments
як ключові слова.
Запобігання зв'язуванню та присвоєнню eval і arguments
Імена eval
і arguments
не можуть бути зв'язані та присвоєні в межах синтаксису мови. Усі такі спроби – синтаксичні помилки:
"use strict";
eval = 17;
arguments++;
++eval;
const obj = { set p(arguments) {} };
let eval;
try {
} catch (arguments) {}
function x(eval) {}
function arguments() {}
const y = function eval() {};
const f = new Function("arguments", "'use strict'; return 17;");
Відсутність синхронізації між параметрами та індексами arguments
Код суворого режиму не синхронізує індекси об'єкта arguments
зі зв'язуванням кожного параметра. В недбалому режимі у функціях, чий перший аргумент – arg
, присвоєння arg
також змінює значення arguments[0]
, і навпаки (якщо були передані які-небудь аргументи і якщо arguments[0]
не видалено). Об'єкти arguments
у функціях суворого режиму зберігають вихідні аргументи, станом на заклик функції. Значення arguments[i]
не відстежують значення відповідних іменованих аргументів, як і іменовані аргументи не відстежують значень відповідних arguments[i]
.
function f(a) {
"use strict";
a = 42;
return [a, arguments[0]];
}
const pair = f(17);
console.assert(pair[0] === 42);
console.assert(pair[1] === 17);
"Безпека" JavaScript
Суворий режим полегшує написання "безпечного" JavaScript. Частина вебсайтів нині надає користувачам змогу писати JavaScript, котрий вебсайт запускає від імені інших користувачів. JavaScript у браузерах може звертатися до приватної інформації користувачів, тож такий JavaScript повинен бути частково перетворений перед своїм запуском, аби цензурувати звертання до заборонених можливостей. Завдяки гнучкості JavaScript це фактично можна зробити без безлічі перевірок під час виконання. Частина функцій мови використовується настільки повсюдно, що виконання перевірок під час виконання має значну вартість відносно продуктивності. Деякі зміни суворого режиму, плюс вимога, щоб JavaScript, що надається користувачами, був кодом суворого режиму і запускався певним чином, суттєво зменшують потребу у таких перевірках під час виконання.
Відсутність заміни this
Значення, що передається до функції як this
, у суворому режимі не примушується бути об'єктом (іще кажуть – не "загортається"). У функціях недбалого режиму this
завжди є об'єктом – або переданим, якщо this
від початку є об'єктом, або загорнутим значенням this
, якщо при виклику this
є примітивом; або глобальним об'єктом, якщо this
має значення undefined
або null
. (Для задання конкретного значення this
слід використовувати call
, apply
або bind
.) Річ не лише в тім, що автоматичне загортання має ціну щодо продуктивності, а й у тому, що видача глобального об'єкта в браузерах є загрозою безпеці, адже він надає доступ до можливостей, котрі "безпечні" середовища JavaScript повинні обмежувати. Таким чином, у функціях суворого режиму this
не загортається в об'єкт, і якщо значення взагалі не задано, то this
– undefined
, а не globalThis
:
"use strict";
function fun() {
return this;
}
console.assert(fun() === undefined);
console.assert(fun.call(2) === 2);
console.assert(fun.apply(null) === null);
console.assert(fun.call(undefined) === undefined);
console.assert(fun.bind(true)() === true);
Усунення властивостей, що гуляють стеком
У суворому режимі вже не можна "гуляти" стеком JavaScript. Чимало реалізацій раніше містили можливості-розширення, завдяки яким можна було дізнатися вихідного викликача функції. Коли функція fun
перебуває посередині свого виклику, то fun.caller
– це функція, що останньою викликала fun
, а fun.arguments
– це arguments
цього заклику fun
. Обидва ці розширення є проблематичними для "безпечного" JavaScript, адже дозволяють "убезпеченому" кодові звертатися до "привілейованих" функцій та їхніх (можливо, незахищених) аргументів. Якщо fun
– у суворому режимі, то і fun.caller
, і fun.arguments
– це невидальні властивості, що викидають помилку при присвоєнні та отриманні:
function restricted() {
"use strict";
restricted.caller; // викидає TypeError
restricted.arguments; // викидає TypeError
}
function privilegedInvoker() {
return restricted();
}
privilegedInvoker();
Подібно до цього, arguments.callee
більше не підтримується. У недбалому режимі arguments.callee
посилається на навколишню функцію. Цей випадок використання є непевним: назвіть навколишню функцію! Крім того, arguments.callee
суттєво ускладнює оптимізації, як от підставлення функцій, адже, якщо arguments.callee
використовується, то потрібно зробити можливим посилання на непідставлену функцію. arguments.callee
для функцій суворого режиму – це невидальна властивість, що викидає помилку при присвоєнні та отриманні:
"use strict";
const f = function () {
return arguments.callee;
};
f(); // викидає TypeError
Готовий до майбутнього JavaScript
Додаткові зарезервовані слова
Зарезервовані слова – це ідентифікатори, що не можуть вживатися як імена змінних. Суворий режим резервує трохи більше імен, ніж недбалий, частина з яких – уже вживається в мові, а частина – зарезервована для майбутнього, щоб полегшити реалізацію майбутніх розширень синтаксису.
Перехід на суворий режим
Суворий режим розроблений так, щоб перехід на нього міг бути поступовим. Можна змінювати кожний файл окремо, і навіть переводити код на суворий режим аж на рівні функцій.
Кодову базу можна перевести на суворий режим, спершу додавши "use strict"
до шматка вихідного коду, а потім виправивши всі помилки виконання, слідкуючи за семантичними відмінностями.
Синтаксичні помилки
При доданні 'use strict';
наступні ситуації викинуть SyntaxError
до запуску сценарію:
- Вісімковий запис
const n = 023;
- Інструкція
with
- Застосування
delete
до імені змінноїdelete myVariable
; - Використання
eval
абоarguments
як змінної або імені аргументу функції - Вживання одного з нових зарезервованих слів (передбачення майбутніх можливостей мови):
implements
,interface
,let
,package
,private
,protected
,public
,static
іyield
- Оголошення двох параметрів функції з однаковими іменами
function f(a, b, b) {}
- Оголошення одного імені властивості в одному літералі об'єкта двічі
{a: 1, b: 3, a: 7}
. Це обмеження пізніше було знято (вада 1041128).
Ці помилки – добрі, адже виявляють прості помилки або недобрі практики. Вони виникають до запуску коду, тож їх легко виявити, коли код розбирається середовищем виконання.
Нові помилки виконання
Колись JavaScript мовчки відмовляв у ситуаціях, де відбувалося щось, що повинно було б бути помилкою. Суворий режим у таких випадках викидає помилки. Якщо кодова база містить такі випадки, то щоб пересвідчитися, що нічого не зламано, знадобиться тестування. Перевіряти на предмет присутності таких помилок можна на рівні функцій.
- Присвоєння неоголошеній змінній викидає
ReferenceError
. Колись це присвоювало властивість на глобальному об'єкті, що рідко є бажаним результатом. Якщо справді необхідно присвоїти значення в глобальний об'єкт, це слід зробити явно – як властивість наglobalThis
. - Відмова присвоєння властивості об'єкта (наприклад, коли вона доступна лише для зчитування) викидає
TypeError
. У недбалому режимі це просто не виконується. - Видалення невидальної властивості викидає
TypeError
. У недбалому режимі це просто не виконується. - Звертання до
arguments.callee
,strictFunction.caller
абоstrictFunction.arguments
викидаєTypeError
, якщо функція перебуває у суворому режимі. Якщоarguments.callee
використовується для рекурсивного виклику функції, то замість цього можна використати іменований вираз функції.
Семантичні відмінності
Ці відмінності – дуже тонкі. Можливо, тестовий рушій не вловить таких витончених змін. Можливо, знадобиться уважна перевірка кодової бази, аби пересвідчитися, що ці відмінності не впливають на семантику коду. На щастя, така уважна перевірка може виконуватися поступово, на рівні окремих функцій.
this
У недбалому режимі виклики функцій штибу
f()
передавали глобальний об'єкт за значенняthis
. У суворому режимі тепер цеundefined
. Коли функція викликалася за допомогоюcall
абоapply
, і якщо значення було примітивом, то воно загорталося в об'єкт (або глобальний об'єкт у випадкуundefined
іnull
). У суворому режимі значення передається без перетворень і заміни.arguments
У недбалому режимі внесення змін до значення в об'єкті
arguments
призводить до змін відповідного іменованого аргументу. Це робить оптимізацію складною для рушіїв JavaScript та ускладнює читання та розуміння коду. У суворому режимі об'єктarguments
створюється та ініціалізується такими ж значеннями, що й іменовані аргументи, але зміни в об'єктіarguments
або в іменованих аргументах не відображаються одне на одному.eval
У коді суворого режиму
eval
не створює нових змінних в області видимості, в якій викликаний. Крім цього, звісно, в суворому режимі рядок обчислюється за правилами суворого режиму. Знадобиться ретельне тестування, аби пересвідчитися, що нічого не зламано. Не використовуватиeval
, якщо це не вкрай необхідно, – іще один варіант прагматичного рішення.- Оголошення функцій блокової області видимості
У недбалому режимі оголошення функції всередині блоку може бути видимим за межами блоку, і навіть викликаним. У суворому режимі оголошення функції всередині блоку видиме лише всередині цього блоку.
Специфікації
Специфікація |
---|
ECMAScript Language Specification (ECMAScript) |
Дивіться також
- Посібник Модулі JavaScript
- Лексична граматика