[Перевод] Зачем в JavaScript нужен строгий режим?
Строгий режим (strict mode) — это важная часть современного JavaScript. Именно этот режим позволяет разработчикам пользоваться более ограниченным, чем стандартный, синтаксисом.
Семантика строгого режима отличается от традиционного нестрогого режима, который иногда называют «грязным» (sloppy mode). В таком режиме синтаксические правила языка не так строги, а когда происходят некоторые ошибки, система никак не оповещает о них пользователя. То есть — ошибки могут быть проигнорированы, а код, в котором они допущены, сможет выполняться дальше. Это способно привести к неожиданным результатам выполнения кода.
Строгий режим вносит в семантику JavaScript некоторые изменения. Он не даёт системе закрывать глаза на ошибки, выдавая соответствующие исключения. Это приводит к остановке выполнения программ.
Строгий режим, кроме того, помогает в написании программ, в которых нет недочётов, мешающих JS-движкам оптимизировать код. Далее, в этом режиме запрещено использование элементов синтаксиса, которые могут получить особый смысл в будущих версиях языка.
Особенности применения строгого режима
Строгий режим можно применять к отдельным функциям или к целому скрипту. Его нельзя применить только к отдельным инструкциям или к блокам кода, заключённым в фигурные скобки. Для того чтобы использовать строгий режим на уровне целого скрипта, в самое начало файла, до любых других команд, нужно поместить конструкцию "use strict"
или 'use strict'
.
Если в проекте имеются некоторые скрипты, в которых не используется строгий режим, и другие, в которых этот режим используется, тогда может случиться так, что эти скрипты окажутся объединены.
Это приведёт к тому, что код, который не предназначен для выполнения в строгом режиме, окажется в таком состоянии, когда система попытается выполнить его в строгом режиме. Возможно и обратное — код, написанный для строгого режима, попадёт в нестрогий режим. Поэтому лучше всего не смешивать «строгие» и «нестрогие» скрипты.
Как уже было сказано, строгий режим можно применять к отдельным функциям. Для того чтобы это сделать — конструкцию "use strict"
или 'use strict'
надо поместить в верхнюю часть тела функции, до любых других команд. Строгий режим при таком подходе применяется ко всему, что размещено в теле функции, включая вложенные функции.
Например:
const strictFunction = ()=>{
'use strict';
const nestedFunction = ()=>{
// эта функция тоже использует строгий режим
}
}
В 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, приведёт к синтаксической ошибке.
Строгий режим, кроме того, запрещает использование конструкций, затрудняющих оптимизацию. Интерпретатору, перед выполнением оптимизации кода, нужно знать о том, что переменная хранится именно там, где, как считает интерпретатор, она хранится. В строгом режиме запрещается то, что мешает оптимизациям.
Один из примеров подобного запрета касается инструкции with
. Если пользоваться данной инструкцией, то это мешает JS-интерпретатору узнать о том, к какой именно переменной или к какому именно свойству мы обращаемся, так как возможно такое, что сущность с одним и тем же именем имеется и снаружи, и внутри блока инструкции with
.
Предположим, есть такой код:
let x = 1;
with (obj) {
x;
}
Интерпретатор не сможет узнать о том, ссылается ли переменная x
, находящаяся внутри блока with
, на внешнюю переменную x
, или на свойство obj.x
объекта obj
.
В результате неясно — где именно в памяти будет расположено значение x
. Для того чтобы избавиться от подобных неоднозначностей, в строгом режиме использование инструкции with
запрещено. Посмотрим, что случится, если попытаться выполнить в строгом режиме следующий код:
'use strict';
let x = 1;
with (obj) {
x;
}
Результатом этой попытки будет синтаксическая ошибка.
Ещё в строгом режиме запрещено объявлять переменные в коде, переданном методу eval
.
Например, в обычном режиме команда вида eval('let x')
приведёт к объявлению переменной x
. Это позволяет программистам скрывать объявления переменных в строках, что может привести к перезаписи определений тех же переменных, находящихся за пределами eval
.
Для того чтобы это предотвратить, в строгом режиме запрещено объявлять переменные в коде, передаваемом в виде строки методу 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
через эти псевдонимы.
В обычном режиме, если первым параметром функции является a
, то установка в коде функции значения a
приводит и к изменению значения в arguments[0]
. В строгом же режиме в arguments
всегда будет содержаться тот список аргументов, с которыми была вызвана функция.
Предположим, имеется следующий код:
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
требует доступности ссылки на невстроенную функцию при доступе к этому свойству. В строгом режиме использование arguments.callee
приводит к появлению исключения TypeError
.
В строгом режиме ключевое слово this
не обязано всегда быть объектом. В обычных условиях, если this
функции привязывается, с помощью call
, apply
или bind
, к чему-то, что не является объектом, к значению примитивного типа вроде undefined
, null
, number
или boolean
, подобное значение должно быть объектом.
Если контекст this
меняется на что-то, не являющееся объектом, его место занимает глобальный объект. Например — window
. Это означает, что если вызвать функцию, установив её 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
которой мы обращаемся.
В объекте arguments
хранятся аргументы, переданные функции при её вызове. Например, если у нас имеется функция fn
, это значит, что через fn.caller
можно обратиться к функции, вызвавшей данную функцию, а с помощью fn.arguments
можно увидеть аргументы, переданные fn
при вызове.
Эти возможности представляют собой потенциальную угрозу безопасности. В результате в строгом режиме доступ к этим свойствам запрещён.
function secretFunction() {
'use strict';
secretFunction.caller;
secretFunction.arguments;
}
function restrictedRunner() {
return secretFunction();
}
restrictedRunner();
В предыдущем примере мы не можем, в строгом режиме, обратиться к secretFunction.caller
и secretFunction.arguments
. Дело в том, что эти свойства можно использовать для получения стека вызовов функции. Если попытаться запустить этот код — будет выдано исключение TypeError
.
В строгом режиме для именования переменных или свойств объектов нельзя использовать идентификаторы, которые могут найти применение в будущих версиях JavaScript. Речь идёт, например, о следующих идентификаторах: implements
, interface
, let
, package
, private
, protected
, public
, static
и yield
.
В ES2015 и в более поздних версиях стандарта эти идентификаторы стали зарезервированными словами. И их нельзя использовать для именования переменных или свойств в строгом режиме.
Итоги
Строгий режим — это стандарт, который существует уже многие годы. Он пользуется чрезвычайно широкой поддержкой браузеров. Проблемы с кодом, выполняемом в строгом режиме, могут возникать лишь у старых браузеров, таких, как Internet Explorer.
У современных браузеров не должно возникать сложностей со строгим режимом JavaScript. В результате можно сказать, что этот режим стоит использовать ради предотвращения «тихих» ошибок и ради повышения безопасности приложений. «Тихие» ошибки преобразуются в исключения, препятствующие выполнению программ, а в плане повышения безопасности можно, например, отметить механизмы строгого режима, ограничивающие eval
и предотвращающие доступ к стеку вызовов функций. Кроме того, использование строгого режима облегчает оптимизацию кода JS-движками и заставляет программиста осторожно обращаться с зарезервированными словами, которые могут найти применение в будущих версиях JavaScript.
Уважаемые читатели! Пользуетесь ли вы строгим режимом при написании JS-кода своих проектов?