Front End

6 ago, 2012

Corrigindo o operador typeof do JavaScript

Publicidade

Trabalhar com operador typeof do JavaScript é um pouco como dirigir um velho carro ultrapassado (ou um modelo antigo Dell Inspiron). Ele faz o trabalho (principalmente) e você aprende a contornar os caprichos – mas você, provavelmente, deseja algo melhor.

Neste artigo, vou dar uma breve descrição do typeof antes de introduzir uma pequena nova função que é uma alternativa totalmente completa e mais confiável que trabalha diretamente com a parte interna de linguagem.

O operador typeof

Como é usado?

Uma vez que o typeof é um operador unitário, o operando segue o operador. Nenhuma pontuação adicional é necessária.

typeof 2 //"number"
typeof "belladonna" //"string"

Mas ele funciona quando eu o chamo como uma função?

O operador typeof não é uma função. Você pode envolver o operando entre parênteses para que a expressão pareça uma chamada de função, mas os parênteses simplesmente agem como um operador de agrupamento (perdendo apenas para o operador vírgula na ordem pecking obscurity!). Na verdade, você pode decorar o operando com todos os tipos de pontuação sem que haja descarrilamento do operador.

typeof (2) //"number"
typeof(2) //"number"
typeof ("a", 3) //"number"
typeof (1 + 1) //"number"

O que ele retorna?

O valor retornado é uma representação um tanto arbitrária do tipo do operando. A tabela abaixo (com base na especificação ES5) fornece um resumo:

Type of val Result
Undefined “undefined“
Null “object“
Boolean “boolean“
Number “number“
String “string“
Object (native and not callable) “object“
Object (native or host and
callable)
“function“
Object (host and not
callable)
Implementation-defined

 
O que há de errado com typeof?
A questão mais gritante é que typeof null retorna “object”. É simplesmente um erro. Fala-se em corrigir na próxima versão da especificação do ECMAScript, embora isso, sem dúvida, introduzirá questões relativas à compatibilidade com versões anteriores.

1 var a;
2 typeof a; //"undefined"
3 typeof b; //"undefined"
4 alert(a); //undefined
5 alert(b); //ReferenceError

Fora isso, typeof não é apenas muito discriminatório. Quando typeof é aplicado a qualquer tipo de objeto que não seja função, ele retorna “object”. Ele não faz distinção entre objetos genéricos e outro tipos built-in (Array, Arguments, Data, JSON, RegExp, Math, Error e os objetos primitivos Number, Boolean e String).

Ah, e você vai ouvir gente reclamando disso…

typeof NaN //"number"

…Mas isso não é culpa do operador typeof, uma vez que a norma afirma claramente que NaN é de fato um número.

Uma maneira melhor?

[[Class]]

Cada objeto JavaScript possui uma propriedade interna conhecida como [[Class]] (A especificação ES5 usa a notação de colchetes duplos para representar propriedades internas, ou seja, propriedades abstratas usadas para especificar o comportamento dos mecanismos JavaScript). De acordo com ES5, [[Class]] é “um valor String indicando uma especificação definida de classificação de objetos”. Para você e para mim, isso significa que cada tipo de objeto built-in possui um único valor de padrão aplicado não editável para a sua propriedade [[Class]]. Isso poderia ser realmente útil se ao menos pudéssemos chegar à propriedade [[Class]]…

Object.prototype.toString

…e acontece que a gente pode. Dê uma olhada na especificação ES 5 para Object.prototype.toString…

  1. Deixe O ser o resultado da chamada ToObject passando o valor this como o argumento.
  2. Deixe class ser o valor da propriedade interna de O [[Class]].
  3. Retorne o valor String, que é o resultado da concatenação de três Strings “[object “, class, e “]”.

Em suma, a função toString padrão do Object retorna uma string com o seguinte formato …

[object [[Class]]]

Onde … [[Class]] é a propriedade de classe do objeto.

Infelizmente, os objetos especializados built-in em sua maioria sobrescrevem Object.prototype.toString com métodos toString próprios…

1 [1,2,3].toString(); //"1, 2, 3"
2
3 (new Date).toString(); //"Sat Aug 06 2011 16:29:13 GMT-0700 (PDT)"
4
5 /a-z/.toString(); //"/a-z/"

…felizmente podemos usar a função call para forçar a função genérica toString sobre eles…

1 Object.prototype.toString.call([1,2,3]); //"[object Array]"
2
3 Object.prototype.toString.call(new Date); //"[object Date]"
4
5 Object.prototype.toString.call(/a-z/); //"[object RegExp]"

Introduzindo a função toType

Podemos pegar esta técnica, adicionar uma gota de regEX, e criar uma função minúscula – uma nova e melhorada versão do operador typeof…

1 var toType = function(obj) {
2 return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase()
3 }

(uma vez que um novo objeto genérico utilizará sempre a função toString definida por Object.prototype, podemos usar com segurança ({}).toString como uma abreviação para Object.prototype.toString).

Vamos testá-lo…

01 toType({a: 4}); //"object"
02 toType([1, 2, 3]); //"array"
03 (function() {console.log(toType(arguments))})(); //arguments
04 toType(new ReferenceError); //"error"
05 toType(new Date); //"date"
06 toType(/a-z/); //"regexp"
07 toType(Math); //"math"
08 toType(JSON); //"json"
09 toType(new Number(4)); //"number"
10 toType(new String("abc")); //"string"
11 toType(new Boolean(true)); //"boolean"

…e agora vamos executar os mesmos testes com o operador typeof (e tente não tripudiar)…

01 typeof {a: 4}; //"object"
02 typeof [1, 2, 3]; //"object"
03 (function() {console.log(typeof arguments)})(); //object
04 typeof new ReferenceError; //"object"
05 typeof new Date; //"object"
06 typeof /a-z/; //"object"
07 typeof Math; //"object"
08 typeof JSON; //"object"
09 typeof new Number(4); //"object"
10 typeof new String("abc"); //"object"
11 typeof new Boolean(true); //"object"

Compare com duck-typing

Duck typing verifica as características de um objeto em uma lista de atributos conhecidos para um determinado tipo. Devido à utilidade limitada do operador typeof, duck-typing é popular em JavaScript. É também propenso a erros. Por exemplo, o objeto arguments de uma função tem uma propriedade length e elementos numericamente indexados, mas ainda não é um Array.

Utilizar toType é uma alternativa confiável e fácil ao duck-typing. Confiável, porque ele conversa diretamente com a propriedade do objeto, que é definido pelo mecanismo de navegação e não é editável; fácil, por causa da sua verificação de três palavras.

Aqui está um exemplo ilustrativo – um trecho que define um objeto de não-conformidade JSON. A função jsonParseIt aceita uma função como argumento, que ele pode usar para testar a veracidade do objeto JSON antes de usá-lo para analisar uma string de JSON….

1 window.JSON = {parse: function() {alert("I'm not really JSON - fail!")}};
2
3 function jsonParseIt(jsonTest) {
4 if (jsonTest()) {
5 return JSON.parse('{"a":2}');
6 } else {
7 alert("non-compliant JSON object detected!");
8 }
9 }

Vamos executá-lo, primeiro com duck-typing…

jsonParseIt(function() {return JSON && (typeof JSON.parse == "function")})
//"I'm not really JSON - fail!"

..Ooops! …E agora com o teste toType…

jsonParseIt(function() {return toType(JSON) == "json"});
//"non-compliant JSON object detected!

Poderia o toType confiavelmente proteger contra a péssima troca de objetos JS built-in com objetos falsos? Provavelmente não, já que o autor poderia presumivelmente também trocar a função toType. Um teste mais seguro poderia chamar diretamente ({}).toString…

function() { return ({}).toString.call(JSON).indexOf("json") > -1 }

..embora mesmo assim falharia se Object.prototype.toString fosse maliciosamente reescrito. Ainda assim, cada defesa adicional ajuda.

Compare com instanceof

O operador instanceof testa a corrente do protótipo do primeiro operando para a presença da propriedade do protótipo do segundo operando (o segundo operando deverá ser um construtor, e um TypeError será emitido se não for uma função):

1 new Date instanceof Date; //true
2
3 [1,2,3] instanceof Array; //true
4
5 function CustomType() {};
6 new CustomType instanceof CustomType; //true

Em face do que isso parece ser uma promessa de um bom tipo de verificador para built-ins, no entanto, existem pelo menos dois obstáculos com essa abordagem:

1. Vários objetos built-in (Math, JSON e arguments) não têm associado objetos construtores – assim, eles não podem ter seu tipo verificado com o operador instanceof.

Math instanceof Math //TypeError

2. Como @kangax e outros já destacaram, uma janela pode incluir vários frames, o que significa vários contextos globais e portanto construtores múltiplos construtores para cada tipo. Em tal ambiente, um determinado tipo de objeto não é garantido para ser um instanceof de um determinado construtor…

1 var iFrame = document.createElement('IFRAME');
2 document.body.appendChild(iFrame);
3
4 var IFrameArray = window.frames[1].Array;
5 var array = new IFrameArray();
6
7 array instanceof Array; //false
8 array instanceof IFrameArray; //true;

Tipo de verificação de objetos host

Objetos host são objetos criados pelo navegador não especificados pela norma ES5. Todos os elementos DOM e as funções globais são objetos host. ES5 recusa-se a especificar um valor de retorno para typeof quando aplicado a objetos host, nem sugere um valor para a propriedade [[Class]] de objetos host. O resultado é que o tipo de verificação cross-browser de objetos host geralmente não é confiável:

01 toType(window);
02 //"global" (Chrome) "domwindow" (Safari) "window" (FF/IE9) "object" (IE7/IE8)
03
04 toType(document);
05 //"htmldocument" (Chrome/FF/Safari) "document" (IE9) "object" (IE7/IE8)
06
07 toType(document.createElement('a'));
08 //"htmlanchorelement" (Chrome/FF/Safari/IE) "object" (IE7/IE8)
09
10 toType(alert);
11 //"function" (Chrome/FF/Safari/IE9) "object" (IE7/IE8)

O teste mais confiável de cross-browser para um elemento poderia ser verificar a existência de uma propriedade nodeType…

function isElement(obj) {
return obj.nodeType;
}

… mas isso é duck-typing, então não há nenhuma garantia.

Onde uma função toType deve existir?

Para resumir, meus exemplos definem toType como uma função global. Se estender Object.prototype, você será lançado aos dragões – a minha preferência seria estender Object diretamente, o que espelha a convenção estabelecida por ES5 (e prototype.js antes disso).

1 Object.toType = function(obj) {
2 return ({}).toString.call(obj).match(/\s([a-z|A-Z]+)/)[1].toLowerCase();
3 }

Alternativamente, você pode optar por adicionar a função toType a um namespace de sua preferência, como util.

Poderíamos ficar um pouco mais espertos (inspirado pelo uso do Chrome de “global” para a window.[[Class]]). Empacotando a função em um módulo global, podemos identificar o objeto global também:

1 Object.toType = (function toType(global) {
2 return function(obj) {
3 if (obj === global) {
4 return "global";
5 }
6 return ({}).toString.call(obj).match(/\s([a-z|A-Z]+)/)[1].toLowerCase();
7 }
8 })(this)

Vamos experimentar…

1 Object.toType(window); //"global" (all browsers)
2 Object.toType([1,2,3]); //"array" (all browsers)
3 Object.toType(/a-z/); //"regexp" (all browsers)
4 Object.toType(JSON); //"json" (all browsers)
5 //etc..

O que toType não faz

A função toType não pode proteger tipos desconhecidos de gerar ReferenceErrors …

Object.toType(fff); //ReferenceError

Mais precisamente, é a chamada para toType que gera o erro, e não a própria função. A única proteção contra isso (como acontece com as chamadas para qualquer função) é a prática de um código limpo…

window.fff && Object.toType(fff); 

Encerrando

OK! Eu tagarelei muito mais tempo do que eu queria – por isso, parabéns se você chegou até aqui. Espero que tenha achado útil. Abordei muita coisa e, provavelmente, cometi alguns erros.

Leitura complementar

Juriy Zaytsev (“kangax”):
‘instanceof’ considered harmful (or how to write a robust ‘isArray’)

ECMA-262 5 ª Edição:

The typeof operator

Object Internal Properties and Methods (for more about [[Class]])

Object.prototype.toString

The instanceof operator

?

Texto original disponível em http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/