Front End

12 mar, 2012

JavaScript Modo Estrito

Publicidade

A quinta edição da especificação ECMAScript introduziu o Modo Estrito, que cria uma camada de constantes no JavaScript com o propósito de proteger você de aspectos perigosos da própria linguagem.

Enquanto fazia pesquisas para este artigo, eu escrevi 38 testes abrangendo todas as regras do Modo Estrito, como definida na especificação ES5. Você pode ver como o seu navegador favorito se comporta clicando aqui.

O código para cada teste está reproduzido no final deste artigo como um auxílio para o entendimento da especificação. Você também pode rodar os testes manualmente, copiando e colando o código em um console. O código completo está no meu repositório github.

O Firefox 4 e IE10 (visualização 01) já suportam totalmente o Modo Estrito e Chrome 12 está quase lá. O Modo Estrito veio para ficar, então, vamos mergulhar no assunto.

Como invocar o Modo Estrito?

Incluindo o “use strict” como primeira sentença¹ em seu código JavaScript, forçará o Modo Estrito sobre todo o código:

 "use strict";
012; //Octal literal throws a SyntaxError in Strict Mode

Como alternativa, você pode restringir a aplicação do Modo Estrito para uma determinada função, adicionando a sentença “use strict” na primeira linha do corpo da função:

 012; //No Error (Strict Mode not enforced globally)
function foo() {
"use strict";
x=3; //Strict Mode will throw an error for implicit globals
}
foo(); //ReferenceError (Strict Mode is enforced for function foo)

¹@kangax assinalou que o “use strict” pode aparecer em qualquer lugar dentro de um conjunto principal de declarações literais de string, para permitir que futuras diretivas atuem em paralelo.

As funções refletem as diretivas do Modo Estrito de suas funções externas?

As funções internas que estão definidas dentro de uma função externa – que possui o “use strict” em sua declaração -, também estarão sujeitas ao modo estrito:

 var wrapper = function(fn) {
'use strict';
var deleteNonConfigurable = function () {
var obj = {};
Object.defineProperty(obj, "name", {
configurable: false
});
delete obj.name; //Will throw TypeError in Strict Mode
}
return deleteNonConfigurable;
}

wrapper()(); //TypeError (Strict Mode enforced)

No entanto, é importante observar, o Modo Estrito não é aplicado sobre funções não-estritas que são invocadas no interior do corpo de uma função estrita (ou porque eram passadas como argumentos ou invocadas usando call ou apply).

 var test = function(fn) {
'use strict';
fn();
}

var deleteNonConfigurable = function () {
var obj = {};
Object.defineProperty(obj, "name", {
configurable: false
});
delete obj.name; //will throw TypeError in Strict Mode
}

test(deleteNonConfigurable); //no error (Strict Mode not enforced)

Por que não consigo rodar o Modo Estrito no console do meu navegador?

Ao executar o código no firebug e consoles de outros navegadores, o “use strict” no início do código (fora de uma função) não tem efeito. Isso ocorre porque a maioria dos executores de console envolve todo o código em uma chamada eval – então o “use strict” já não será mais a primeira sentença do código. Uma solução parcial é fazer uma função invocando a própria função e começá-la com “use strict” (mas, mesmo assim, eu achei a aplicação do Modo Estrito em um console muito tendencioso – especialmente quando se utiliza ferramentas de webkit para desenvolvedores. É melhor testar o seu código em uma página web):

 (function() {
"use strict";
var a;
var b;
function bar() {
x = 5; //Strict Mode will throw an error for implicit globals
}
bar(); //ReferenceError (Strict Mode is enforced)
})();

O que acontece se o meu navegador não suportar o modo Estrito?

Nada. O “use strict” é simplesmente uma declaração string que será ignorada pelos mecanismos do JavaScript que não suporta o Modo Estrito. Isso permite o uso seguro de cross-browser usando a sintaxe do Modo Estrito, assegurando a compatibilidade em navegadores que poderão suportá-lo no futuro.

Quais são as regras para o Modo Estrito?

As restrições definidas pela especificação do Modo Estrito abrangem tanto o tempo de carga, quanto os comportamentos em tempo de execução. Aqui está um breve resumo (cada regra está detalhada com um exemplo na próxima seção):

  • Erros de Sintaxe:

Em muitos casos, o Modo Estrito vai evitar códigos ambíguos ou supostamente enganosos de serem carregados. Palavras em Octal, duplicar nomes de propriedades, uso incorreto de exclusão e as tentativas de fazer algo desonesto com o eval e palavras-chave de argumentos irão lançar um SyntaxError, assim  como o uso de qualquer declaração with (obrigado a @brendaneich que esclareceu o porquê o with foi omitido).

  • O valor do “this”:

No modo estrito o valor do “this” não será auto-forçado a um objeto. Esta é provavelmente a parte mais interessante do Modo Estrito e terá o impacto mais significativo sobre os desenvolvedores. Ainda mais se o primeiro argumento a ser chamado através do call ou apply for nulo ou indefinido, o valor de “this” da função chamada não será convertido para o objeto global.

  • Globais implícitas:

Muitas pessoas não argumentarão contra esta regra. A criação de globais implícitas é quase sempre um erro. No Modo Estrito, isso resultará em um ReferenceError – o que irá te ensinar a não usar globais implícitas.

  • arguments.caller e arguments.callee:

Estas propriedades são muito úteis, mas são malvistas no Modo Estrito. Os function.arguments menos utilizadas e propriedades function.caller também são proibidos.

Violações na definição de propriedade de objetos

Com a tentativa de executar uma atualização em uma propriedade, quando a sua definição de propriedade define outra forma, irá lançar um TypeError no modo estrito.

  • Os testes:

Aqui está o código fonte completo dos meus testes do Modo Estrito. Cada conjunto de testes é precedido por um comentário tirado a partir da especificação ECMAScript a ser testada. Esta versão do código fonte é definida para ser executada em “modo de console” – o que significa que pode ser copiado e colado em um console do desenvolvedor e ser executado. O mesmo código fonte executado em “modo HTML” é usado para gerar a página visual de teste que mostrei no início desse artigo. Esse código, com objetos de suporte está no meu repositório github. Tenho certeza de que cometi alguns erros nesse artigo – sinta-se livre para me informar!

 (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();

})();
  • Wrap up:

É questionável que restringir o acesso à linguagem é melhor para os desenvolvedores – mas discutiremos sobre isso em um outro momento. Em sua defesa, o Modo Estrito é um bom compromisso entre uma mudança de linguagem completa (que teria quebrado compatibilidade com versões anteriores) e não fazer nada (o que teria inflamado aqueles que insistem que as partes mais chocantes da linguagem devem ser extintas). O Modo Estrito é prato principal de uma refeição para aqueles de nós obcecados com as nuances desta linguagem fascinante. Divirta-se!

Leitura complementar

***

Texto original disponível em: http://javascriptweblog.wordpress.com/2011/05/03/javascript-strict-mode/