Хабрахабр

[Перевод] Зачем в JavaScript нужен строгий режим?

Строгий режим (strict mode) — это важная часть современного JavaScript. Именно этот режим позволяет разработчикам пользоваться более ограниченным, чем стандартный, синтаксисом.

В таком режиме синтаксические правила языка не так строги, а когда происходят некоторые ошибки, система никак не оповещает о них пользователя. Семантика строгого режима отличается от традиционного нестрогого режима, который иногда называют «грязным» (sloppy mode). Это способно привести к неожиданным результатам выполнения кода. То есть — ошибки могут быть проигнорированы, а код, в котором они допущены, сможет выполняться дальше.

Он не даёт системе закрывать глаза на ошибки, выдавая соответствующие исключения. Строгий режим вносит в семантику JavaScript некоторые изменения. Это приводит к остановке выполнения программ.

Далее, в этом режиме запрещено использование элементов синтаксиса, которые могут получить особый смысл в будущих версиях языка.
Строгий режим, кроме того, помогает в написании программ, в которых нет недочётов, мешающих JS-движкам оптимизировать код.

Особенности применения строгого режима

Строгий режим можно применять к отдельным функциям или к целому скрипту. Его нельзя применить только к отдельным инструкциям или к блокам кода, заключённым в фигурные скобки. Для того чтобы использовать строгий режим на уровне целого скрипта, в самое начало файла, до любых других команд, нужно поместить конструкцию "use strict" или 'use strict'.

Если в проекте имеются некоторые скрипты, в которых не используется строгий режим, и другие, в которых этот режим используется, тогда может случиться так, что эти скрипты окажутся объединены.

Возможно и обратное — код, написанный для строгого режима, попадёт в нестрогий режим. Это приведёт к тому, что код, который не предназначен для выполнения в строгом режиме, окажется в таком состоянии, когда система попытается выполнить его в строгом режиме. Поэтому лучше всего не смешивать «строгие» и «нестрогие» скрипты.

Для того чтобы это сделать — конструкцию "use strict" или 'use strict' надо поместить в верхнюю часть тела функции, до любых других команд. Как уже было сказано, строгий режим можно применять к отдельным функциям. Строгий режим при таком подходе применяется ко всему, что размещено в теле функции, включая вложенные функции.

Например:

const strictFunction = ()=>
}

В JavaScript-модулях, которые появились в стандарте ES2015, строгий режим включён по умолчанию. Поэтому при работе с ними включать его явным образом не нужно.

Изменения, вводимые в работу JS-кода строгим режимом

Строгий режим влияет и на синтаксис кода, и на то, как код ведёт себя во время выполнения программы. Ошибки в коде преобразуются в исключения. То, что в нестрогом режиме тихо даёт сбой, в строгом вызывает сообщение об ошибке. Это похоже на то, как в нестрогом режиме система реагирует на синтаксические ошибки. В строгом режиме упрощается работа с переменными, жёстко регулируется использование функции eval и объекта arguments, упорядочивается работа с конструкциями, которые могут быть реализованы в будущих версиях языка.

▍Преобразование «тихих» ошибок в исключения

«Тихие» ошибки преобразуются в строгом режиме в исключения. В нестрогом режиме на такие ошибки система явным образом не реагирует. В строгом же режиме наличие таких ошибок приводит к неработоспособности кода.

В результате создание переменных без этих директив приведёт к неработоспособности программы. Так, благодаря этому сложно совершить ошибку случайного объявления глобальной переменной, так как переменные и константы в строгом режиме нельзя объявлять без использования директив var, let или const. Например, попытка выполнения следующего кода приведёт к выдаче исключения ReferenceError:

'use strict';
badVariable = 1;

Такой код нельзя запустить в строгом режиме, так как, если бы строгий режим был бы выключен, он создавал бы глобальную переменную badVariable. Строгий режим защищает программиста от непреднамеренного создания глобальных переменных.

В виде ошибок рассматриваются любые неправильные синтаксические конструкции, которые в нестрогом режиме просто игнорировались. Попытка выполнения любого кода, который, в обычном режиме, просто не работает, теперь приводит к выдаче исключения.

Так, например, в строгом режиме нельзя выполнять операции присваивания значений таким сущностям, предназначенным только для чтения, как arguments, NaN или eval.

В строгом режиме исключение, например, будет выдано в следующих случаях: 

  • попытка присваивания значения свойству, предназначенному только для чтения, вроде некоего неперезаписываемого глобального свойства;
  • попытка записи значения в свойство, у которого есть лишь геттер;
  • попытка записи чего-либо в свойство нерасширяемого объекта.

Вот примеры синтаксических конструкций, приводящих к исключениям в строгом режиме:

'use strict'; let undefined = 5; let Infinity = 5; let obj = {};
Object.defineProperty(obj, 'foo', { value: 1, writable: false });
obj.foo = 1 let obj2 = { get foo() { return 17; } };
obj2.foo = 2 let fixedObj = {};
Object.preventExtensions(fixedObj);
fixed.bar= 1;

Попытка выполнения подобных фрагментов кода в строгом режиме приведёт к выдаче исключения TypeError. Так, например, undefined и Infinity — это глобальные сущности, значения которых нельзя перезаписывать, а свойство foo объекта obj не поддерживает перезапись. Свойство foo объекта obj2 имеет лишь геттер. Объект fixedObj сделан нерасширяемым с помощью метода Object.preventExtensions.

К выдаче TypeError приведёт и попытка удаления неудаляемого свойства:

'use strict';
delete Array.prototype

Строгий режим запрещает назначать объекту свойства с одинаковыми именами. Как результат — попытка выполнения следующего кода приведёт к возникновению синтаксической ошибки:

'use strict';
let o = { a: 1, a: 2 };

Строгий режим требует, чтобы имена параметров функций были бы уникальными. В нестрогом режиме, если, например, два параметра функции имеют одно и то же имя one, тогда, при передаче функции аргументов, значением параметра станет то, что попало в аргумент, объявленный последним.

В результате попытка выполнения следующего кода приведёт к возникновению синтаксической ошибки: В строгом режиме запрещены параметры функций с одинаковыми именами.

'use strict';
const multiply = (x, x, y) => x*x*y;

В строгом режиме нельзя использовать восьмеричную запись чисел, предваряя число нулём. Этого нет в спецификации, но данная возможность поддерживается браузерами.

В строгом режиме попытка воспользоваться числом, в начале которого стоит 0, приведёт к синтаксической ошибке. Такое положение дел путает разработчиков, заставляя их полагать, что 0, предшествующий числу, просто игнорируется, не имея особого смысла.

Интерпретатору, перед выполнением оптимизации кода, нужно знать о том, что переменная хранится именно там, где, как считает интерпретатор, она хранится. Строгий режим, кроме того, запрещает использование конструкций, затрудняющих оптимизацию. В строгом режиме запрещается то, что мешает оптимизациям.

Если пользоваться данной инструкцией, то это мешает JS-интерпретатору узнать о том, к какой именно переменной или к какому именно свойству мы обращаемся, так как возможно такое, что сущность с одним и тем же именем имеется и снаружи, и внутри блока инструкции with. Один из примеров подобного запрета касается инструкции with.

Предположим, есть такой код:

let x = 1;
with (obj) { x;
}

Интерпретатор не сможет узнать о том, ссылается ли переменная x, находящаяся внутри блока with, на внешнюю переменную x, или на свойство obj.x объекта obj.

Для того чтобы избавиться от подобных неоднозначностей, в строгом режиме использование инструкции with запрещено. В результате неясно — где именно в памяти будет расположено значение x. Посмотрим, что случится, если попытаться выполнить в строгом режиме следующий код:

'use strict';
let x = 1;
with (obj) { x;
}

Результатом этой попытки будет синтаксическая ошибка.

Ещё в строгом режиме запрещено объявлять переменные в коде, переданном методу eval.

Это позволяет программистам скрывать объявления переменных в строках, что может привести к перезаписи определений тех же переменных, находящихся за пределами eval. Например, в обычном режиме команда вида eval('let x') приведёт к объявлению переменной x.

Для того чтобы это предотвратить, в строгом режиме запрещено объявлять переменные в коде, передаваемом в виде строки методу eval.

В результате попытка выполнить следующий код приведёт к синтаксической ошибке: Строгий режим, кроме того, запрещает удаление обычных переменных.

'use strict';
let x;
delete x;

▍Запрет некорректных синтаксических конструкций

В строгом режиме запрещено неправильное использование eval и arguments. Речь идёт о запрете всяческих манипуляций с ними. Например — это нечто вроде присваивания им новых значений, использование их имён в роли имён переменных, функций, параметров функций.

Вот примеры некорректного использования eval и arguments:

'use strict';
eval = 1;
arguments++;
arguments--;
++eval;
eval--;
let obj = { set p(arguments) { } };
let eval;
try { } catch (arguments) { }
try { } catch (eval) { }
function x(eval) { }
function arguments() { }
let y = function eval() { };
let eval = ()=>{ };
let f = new Function('arguments', "'use strict'; return 1;");

В строгом режиме нельзя создавать псевдонимы для объекта arguments и устанавливать новые значения arguments через эти псевдонимы.

В строгом же режиме в arguments всегда будет содержаться тот список аргументов, с которыми была вызвана функция. В обычном режиме, если первым параметром функции является a, то установка в коде функции значения a приводит и к изменению значения в arguments[0].

Предположим, имеется следующий код:

const fn = function(a) { 'use strict'; a = 2; return [a, arguments[0]];
}
console.log(fn(1))

В консоль попадёт [2,1]. Это так из-за того, что запись значения 2 в a не приводит к записи значения 2 в arguments[0].

▍Оптимизации производительности

В строгом режиме не поддерживается свойство arguments.callee. В обычном режиме оно возвращает имя функции-родителя той функции, свойство callee объекта arguments которой мы исследуем.

В строгом режиме использование arguments.callee приводит к появлению исключения TypeError. Поддержка этого свойства мешает оптимизациям, наподобие встраивания функций, так как использование arguments.callee требует доступности ссылки на невстроенную функцию при доступе к этому свойству.

В обычных условиях, если this функции привязывается, с помощью call, apply или bind, к чему-то, что не является объектом, к значению примитивного типа вроде undefined, null, number или boolean, подобное значение должно быть объектом. В строгом режиме ключевое слово this не обязано всегда быть объектом.

Например — window. Если контекст this меняется на что-то, не являющееся объектом, его место занимает глобальный объект. Это означает, что если вызвать функцию, установив её this в некое значение, не являющееся объектом, вместо этого значения в this попадёт ссылка на глобальный объект.

Рассмотрим пример:

'use strict';
function fn() { return this;
}
console.log(fn() === undefined);
console.log(fn.call(2) === 2);
console.log(fn.apply(null) === null);
console.log(fn.call(undefined) === undefined);
console.log(fn.bind(true)() === true);

Все команды console.log выведут true, так как в строгом режиме значение this в функции не заменяется автоматически ссылкой на глобальный объект в том случае, если this устанавливается в значение, не являющееся объектом.

▍Изменения, имеющие отношение к безопасности

В строгом режиме нельзя делать общедоступными свойства функции caller и arguments. Дело в том, что caller, например, может дать доступ к функции, вызвавшей функцию, к свойству caller которой мы обращаемся.

Например, если у нас имеется функция fn, это значит, что через fn.caller можно обратиться к функции, вызвавшей данную функцию, а с помощью fn.arguments можно увидеть аргументы, переданные fn при вызове. В объекте arguments хранятся аргументы, переданные функции при её вызове.

В результате в строгом режиме доступ к этим свойствам запрещён. Эти возможности представляют собой потенциальную угрозу безопасности.

function secretFunction() { 'use strict'; secretFunction.caller; secretFunction.arguments;
}
function restrictedRunner() { return secretFunction();
}
restrictedRunner();

В предыдущем примере мы не можем, в строгом режиме, обратиться к secretFunction.caller и secretFunction.arguments. Дело в том, что эти свойства можно использовать для получения стека вызовов функции. Если попытаться запустить этот код — будет выдано исключение TypeError.

Речь идёт, например, о следующих идентификаторах: implements, interface, let, package, private, protected, public, static и yield. В строгом режиме для именования переменных или свойств объектов нельзя использовать идентификаторы, которые могут найти применение в будущих версиях JavaScript.

И их нельзя использовать для именования переменных или свойств в строгом режиме. В ES2015 и в более поздних версиях стандарта эти идентификаторы стали зарезервированными словами.

Итоги

Строгий режим — это стандарт, который существует уже многие годы. Он пользуется чрезвычайно широкой поддержкой браузеров. Проблемы с кодом, выполняемом в строгом режиме, могут возникать лишь у старых браузеров, таких, как Internet Explorer.

В результате можно сказать, что этот режим стоит использовать ради предотвращения «тихих» ошибок и ради повышения безопасности приложений. У современных браузеров не должно возникать сложностей со строгим режимом JavaScript. Кроме того, использование строгого режима облегчает оптимизацию кода JS-движками и заставляет программиста осторожно обращаться с зарезервированными словами, которые могут найти применение в будущих версиях JavaScript. «Тихие» ошибки преобразуются в исключения, препятствующие выполнению программ, а в плане повышения безопасности можно, например, отметить механизмы строгого режима, ограничивающие eval и предотвращающие доступ к стеку вызовов функций.

Уважаемые читатели! Пользуетесь ли вы строгим режимом при написании JS-кода своих проектов?

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть