JavaScript Strict Mode

Материал из support.qbpro.ru
Версия от 13:24, 4 августа 2013; imported>Supportadmin (Новая страница: «Источник http://habrahabr.ru/post/118666/#habracut В пятой редакции ECMAScript был представлен строгий режим (д…»)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)

Источник http://habrahabr.ru/post/118666/#habracut

В пятой редакции ECMAScript был представлен строгий режим (далее в статье Strict Mode). Strict Mode накладывает слой ограничений на JavaScript, он отгораживает вас от опасных частей языка (те части, которые есть исторически, но лучше чтобы их не было) и позволяет снизить вероятность ошибки.

Пока читал эту статью я написал 38 тестов, покрывающих все правила Strict Mode, объявленные в спецификации ES5. Вы можете посмотреть насколько ваш браузер поддерживает эти справила вот тут.


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

Firefox 4 уже полностью поддерживает Strict Mode, а Chrome 11 практически полностью. Strict Mode уже не за горами — давайте изучим его подробнее!


Как включить Strict Mode?

Если добавить "use strict" в начало вашего JavaScript кода, то Strict Mode будет применен для всего кода:

"use strict";
012; //Восьмеричный литерал запрешен. Выбросит исключение SyntaxError в Strict Mode

В качестве альтернативы вы можете включить Strict Mode только в отдельной функции, добавив "use strict" в начало тела вашей функции:

012; //Нет ошибки (Strict Mode не включен глобально)
function foo() {
    "use strict";
    x=3; //Strict Mode выбросит исключение - запрещено неявно создавать глобальные переменные
}
foo(); //ReferenceError (Strict Mode включен для функции)

Наследуют ли внутренние функции Strict Mode от внешних функций?

Внутренняя функция, объявленная внутри внешней, в которой включен Strict Mode тоже будет иметь Strict Mode:

var wrapper = function(fn) {
  'use strict';
  var deleteNonConfigurable = function () {
    var obj = {};
    Object.defineProperty(obj, "name", {
      configurable: false
    });
    delete obj.name; // Выбросит исключение TypeError в Strict Mode
  }
  return deleteNonConfigurable;
}
 
wrapper()(); //TypeError (Strict Mode включен)

Важно запомнить, что Strict Mode не распространяется на «нестрогие» (ориг. non-strict) функции, которые выполняются внутри строгой функции (или они отправлены в функцию в качестве аргументов или выполняются, используя call или apply):

var test = function(fn) {
  'use strict';
  fn();
}
 
var deleteNonConfigurable = function () {
  var obj = {};
  Object.defineProperty(obj, "name", {
    configurable: false
  });
  delete obj.name; // Выбросит исключение TypeError в Strict Mode
}
 
test(deleteNonConfigurable); //нет ошибки (Strict Mode не применялся)

Почему я не могу включить Strict Mode в консоли моего браузера?

Когда выполняешь код в консоли фаербага или в других консолях использование "use strict" вне функции не имеет силы. Это потому, что большинство консолей обрамляют ваш код в eval'ом, поэтому ваш "use strict" не является первым выражением. Это можно обойти, обрамив ваш код в замыкание (IIFE), в начало которого мы положим "use strict" (но, когда я тестировал такой способ включения Strict Mode я понял, что это довольно неудобно, особенно если работать в консоли webkit developer tools — лучше тестировать ваш код на странице):

(function() {
    "use strict";
    var a;
    var b;
    function bar() {
        x = 5; //Strict Mode выбросит исключение за попытку создания глобальной переменной
    }
    bar(); // ReferenceError (Strict Mode включен)
})();

Что произойдет если мой браузер не поддерживает Strict Mode?

Ничего. Директива "use strict" это обычное строковое выражение, которое будет проигнорировано всеми движками JavaScript, которые не поддерживают Strict Mode. Это позволяет безопасно использовать синтаксис Strict Mode во всех браузерах без каких-либо опасений, в то время когда браузеры имеющие поддержку Strict Mode будут использовать его.


Какие правила включены в Strict Mode?

Правила определены в спецификации Strict Mode и включают в себя ограничения во время «компиляции» и интерпретации (выполнения скрипта). Это вводный обзор (каждое правило я описал с примерами в следующем параграфе): ecma262-5.com/ELS5_HTML.htm#Annex_C


Синтаксические ошибки Syntax Errors

В большинстве случаев Strict Mode предотвращает выполнение подозрительного или нелегального кода в процессе загрузки. Восьмеричные числа, дубли имен переменных, некорректное использование delete и попытки сделать что-нибудь этакие с eval и ключевым словом arguments, использование with приведет к исключению SyntaxError.


Слово this

В Strict Mode объект this не будет корректироваться. Это возможно самая интересная часть Strict Mode и самая тяжелая(шокирующая) для разработчиков. Все знают, что если первый аргумент call или apply — null или undefined, то значение this выполняемой функции будет преобразование в глобальный объект (для браузеров это window).


Прямое создание глобальных переменных

Не все согласятся с этим, но непрямое создание глобального объекта почти всегда является ошибкой. В Strict Mode вам выдадут красную карточку — ReferenceError.

arguments.caller и arguments.callee

Эти «полезные свойства» (от пер. никогда не применял их) запрещены в Strict Mode. Если вы используете их в вашем кода, то Strict Mode выбросит исключение.


Объявление существующего имени объекта

Когда вы создаете объект с двумя одинаковыми ключами, то Strict Mode выбросит исключение TypeError.


Тесты

Вот исходник моих Strict Mode тестов. Каждый набор тестов снабжен комментарием, ссылающемся на часть спецификации ECMAScript, которую он тестирует. Эта версия может быть выполнена в «режиме консоли». Т.е. вы можете скопировать тест, вставить в консоль и выполнить без изменений. Этот же код, работающий в режиме «HTML» я использовал для создания тестовой страницы, которую я представил вам в начале статьи. Этот исходник с дополнительными объектами в моем github репозитории. Я уверен, что там есть пара ошибок — не стесняйтесь присылать ошибки!

(function() {

  ////////////////////////////////
  //TEST UTILS...
  ////////////////////////////////

  var HTML_MODE = 0;
  var CONSOLE_MODE = 1;

  var mode = CONSOLE_MODE;

  var banner = document.getElementById('banner');
  var currentTestWidget;

  var testsPassed = 0;
  var totalTests = 0;

  window.console = window.console || {log:alert};

  function testException(testName, code, expectedError) {
      'use strict';
      startTest(testName);
      try {
          expectedError == SyntaxError ? eval(code) : code();
          finishTest(false);
      } catch (e) {
          (e instanceof expectedError) ? finishTest(true) : finishTest(false);
      }
  }

  function testValue(testName, fn, expectedValue, options) {
      'use strict';
      options = options || {};
      startTest(testName);
      var result = (fn.apply(options.ctx, options.args || []) === expectedValue);
      finishTest(result);
  }

  function startTest(testName) {
    if (mode == CONSOLE_MODE) {
      console.log("testing..." + testName);
    } else {
      this.currentWidget = document.createElement('DIV');
      this.currentWidget.innerHTML = testName;
      document.body.appendChild(this.currentWidget);
    }
  }

  function finishTest(passed) {
    totalTests++;
    passed && testsPassed++;
    var result = passed ? "passed" : "failed";
    if (mode == CONSOLE_MODE) {
      console.log(result);
    } else {
      this.currentWidget.className = result;
    }
  }

  function startAll() {
    if (mode == HTML_MODE) {
      banner.innerHTML += [":", browser.browserName, browser.fullVersion].join(' ');
    }
  }

  function finishAll() {
    var result = ["","(", testsPassed, "out of", totalTests, "tests passed", ")"].join(' ');
    if (mode == HTML_MODE) {
      banner.innerHTML += result;
    } else if (mode == CONSOLE_MODE) {
      console.log(result);
    }
  }

  ////////////////////////////////
  //THE TESTS...
  ////////////////////////////////

  startAll();

  // A conforming implementation, when processing strict mode code, may not extend the
  //syntax of NumericLiteral (7.8.3) to include OctalIntegerLiteral as described in B.1.1.
  testException("no octal literals", '012', SyntaxError);

  // A conforming implementation, when processing strict mode code (see 10.1.1), may not
  //extend the syntax of EscapeSequence to include OctalEscapeSequence as described in B.1.2.
  testException("no octal escape sequence", '"\\012"', SyntaxError);

  // Assignment to an undeclared identifier or otherwise unresolvable reference does not
  //create a property in the global object. When a simple assignment occurs within strict
  //mode code, its LeftHandSide must not evaluate to an unresolvable Reference. If it does
  //a ReferenceError exception is thrown (8.7.2).
  testException(
    "no implied globals",
    function () {'use strict'; x = 3;},
    ReferenceError
  );

  //The LeftHandSide also may not be a reference to a data property with the attribute
  //value {[[Writable]]:false}, to an accessor property with the attribute value
  //{[[Set]]:undefined}, nor to a non-existent property of an object whose [[Extensible]]
  //internal property has the value false. In these cases a TypeError exception is thrown
  //(11.13.1).
  var assignToNonWritable = function () {
      'use strict';
      var obj = {};
      Object.defineProperty(obj, "name", {
          writable: false
      });
      obj.name = "octopus";
  }

  testException("can't assign to non-writable properties", assignToNonWritable, TypeError);

  var assignWhenSetterUndefined = function () {
      'use strict';
      var obj = {};
      Object.defineProperty(obj, "name", {
          set: undefined
      });
      obj.name = "cuttlefish";
  }

  testException("can't assign when setter undefined", assignWhenSetterUndefined, TypeError);

  var assignToNonExtensible = function () {
      'use strict';
      var obj = {};
      Object.preventExtensions(obj);
      obj.name = "jellyfish";
  }

  testException("can't assign to non extensible", assignToNonExtensible, TypeError);

  //The identifier eval or arguments may not appear as the LeftHandSideExpression of an
  //Assignment operator (11.13) or of a PostfixExpression (11.13) or as the UnaryExpression
  //operated upon by a Prefix Increment (11.4.4) or a Prefix Decrement (11.4.5) operator.
  testException("can't assign to eval", "eval=3", SyntaxError);
  testException("can't assign to arguments", "arguments=3", SyntaxError);
  testException("can't postfix eval", "eval++", SyntaxError);
  testException("can't postfix arguments", "arguments++", SyntaxError);
  testException("can't prefix eval", "++eval", SyntaxError);
  testException("can't prefix arguments", "++arguments", SyntaxError);

  //Arguments objects for strict mode functions define non-configurable accessor properties
  //named "caller" and "callee" which throw a TypeError exception on access (10.6).
  testException(
    "can't use arguments.caller",
    function () {'use strict'; arguments.caller;},
    TypeError
  );

  testException(
    "can't use arguments.callee",
    function () {'use strict'; arguments.callee},
    TypeError
  );

  //Arguments objects for strict mode functions do not dynamically share their array indexed
  //property values with the corresponding formal parameter bindings of their functions. (10.6).
  var assignToArguments = function (x) {
    'use strict';
    arguments[0] = 3;
    return x;
  }

  testValue(
    "arguments not bound to formal params",
    assignToArguments,
    5,
    {args: [5]}
  );

  //For strict mode functions, if an arguments object is created the binding of the local
  //identifier arguments to the arguments object is immutable and hence may not be the
  //target of an assignment expression. (10.5).
  var assignToFormalParams = function (x) {
      'use strict';
      x = 3;
      return arguments[0];
  }

  testValue(
    "arguments object is immutable",
    assignToFormalParams,
    5,
    {args: [5]}
  );

  //It is a SyntaxError if strict mode code contains an ObjectLiteral with more than one
  //definition of any data property (11.1.5).
  testException("no duplicate properties", "({a:1, a:2})", SyntaxError);

  //It is a SyntaxError if the Identifier "eval" or the Identifier "arguments occurs as the
  //Identifier in a PropertySetParameterList of a PropertyAssignment that is contained in
  //strict code or if its FunctionBody is strict code (11.1.5).
  testException(
    "eval not allowed in propertySetParameterList",
    "({set a(eval){ }})",
    SyntaxError
  );

  testException(
    "arguments not allowed in propertySetParameterList",
    "({set a(arguments){ }})",
    SyntaxError
  );

  //Strict mode eval code cannot instantiate variables or functions in the variable environment
  //of the caller to eval. Instead, a new variable environment is created and that environment
  //is used for declaration binding instantiation for the eval code (10.4.2).
  testException(
    "eval cannot create var in calling context",
    function () {'use strict'; eval('var a = 99'); a},
    ReferenceError
  );

  //If this is evaluated within strict mode code, then the this value is not coerced to an object.
  //A this value of null or undefined is not converted to the global object and primitive values
  //are not converted to wrapper objects. The this value passed via a function call (including
  //calls made using Function.prototype.apply and Function.prototype.call) do not coerce the
  //passed this value to an object (10.4.3, 11.1.1, 15.3.4.3, 15.3.4.4).
  var getThis = function () {
      'use strict';
      return this;
  }

  testValue(
    "this is not coerced",
    getThis,
    4,
    {ctx: 4}
  );

  testValue(
    "no global coercion for null",
    getThis,
    null,
    {ctx: null}
  );

  //When a delete operator occurs within strict mode code, a SyntaxError is thrown if its
  //UnaryExpression is a direct reference to a variable, function argument, or function name
  //(11.4.1).
  testException("can't delete variable directly", "var a = 3; delete a", SyntaxError);
  testException("can't delete argument", "function(a) {delete a}", SyntaxError);
  testException("can't delete function by name", "function fn() {}; delete fn", SyntaxError);

  //When a delete operator occurs within strict mode code, a TypeError is thrown if the
  //property to be deleted has the attribute { [[Configurable]]:false } (11.4.1).
  var deleteNonConfigurable = function () {
      'use strict';
      var obj = {};
      Object.defineProperty(obj, "name", {
          configurable: false
      });
      delete obj.name;
  }

  testException("error when deleting non configurable", deleteNonConfigurable, TypeError);

  //It is a SyntaxError if a VariableDeclaration or VariableDeclarationNoIn occurs within
  //strict code and its Identifier is eval or arguments (12.2.1).
  testException("can't use eval as var name", "var eval;", SyntaxError);
  testException("can't use arguments as var name", "var arguments;", SyntaxError);

  //Strict mode code may not include a WithStatement. The occurrence of a WithStatement
  //in such a context is an SyntaxError (12.10).
  testException("can't use with", "with (Math) {round(sqrt(56.67))}", SyntaxError);

  //It is a SyntaxError if a TryStatement with a Catch occurs within strict code and the
  //Identifier of the Catch production is eval or arguments (12.14.1)
  testException("can't use eval as catch id", "try {'cake'} catch(eval) {}", SyntaxError);
  testException("can't use arguments as catch id", "try {'cake'} catch(arguments) {}", SyntaxError);

  //It is a SyntaxError if the identifier eval or arguments appears within a
  //FormalParameterList of a strict mode FunctionDeclaration or FunctionExpression (13.1)
  testException("can't use eval as formal param", "function(eval) {}", SyntaxError);
  testException("can't use arguments as formal param", "function(arguments) {}", SyntaxError);

  //A strict mode function may not have two or more formal parameters that have the same
  //name. An attempt to create such a function using a FunctionDeclaration, FunctionExpression,
  //or Function constructor is a SyntaxError (13.1, 15.3.2).
  testException("can't duplicate formal params", "function(me, me, me) {}", SyntaxError);

  //An implementation may not associate special meanings within strict mode functions to
  //properties named caller or arguments of function instances. ECMAScript code may not
  //create or modify properties with these names on function objects that correspond to
  //strict mode functions (13.2).
  testException(
    "can't use caller obj of function",
    function () {'use strict'; (function () {}).caller},
    TypeError
  );

  //It is a SyntaxError to use within strict mode code the identifiers eval or arguments as
  //the Identifier of a FunctionDeclaration or FunctionExpression or as a formal parameter
  //name (13.1). Attempting to dynamically define such a strict mode function using the
  //Function constructor (15.3.2) will throw a SyntaxError exception.
  testException("can't use eval as function name", "function eval() {}", SyntaxError);
  testException("can't use arguments as function name", "function arguments() {}", SyntaxError);

  var functionConstructorStr = "new Function('eval', 'use strict')";
  testException("can't use eval as param name via constructor", functionConstructorStr, SyntaxError);

  finishAll();

})();

Заключение

Запрещение обращение к некоторым возможностям языка для улучшения кода — вопрос спорный, давайте отложим эти споры. В защиту Strict Mode я хочу сказать, что это отличный компромисс между тотальными переменами (которые сломают обратную совместимость) и ничего не деланием (которое приведет к захламлению языка и научит разработчиков плохому).

Что ещё почитать

  • ECMA-262 5th Edition: The Strict Mode of ECMAScript
  • Asen Bozhilov: Strict tester
  • Таблица совместимости с ECMAScript 5, часть посвященная Strict mode. Это отличный источник, часть большой таблицы совместимости, разработанной Юрием Зайцевым (Juriy Zaytsev aka «kangax»)

От переводчика. Strict Mode поддерживают практически половина всех браузеров, кроме своих прекрасных ограничений и иммунитету к распространенным ошибкам Strict Mode дает и другие преимущества (статья mraleph). Скоро неиспользование Strict Mode станет плохим тоном (аналогично requestAnimationFrame vs setTimeout). Сейчас самое время начать эксперименты!