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…
- Deixe O ser o resultado da chamada ToObject passando o valor this como o argumento.
- Deixe class ser o valor da propriedade interna de O [[Class]].
- 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:
Object Internal Properties and Methods (for more about [[Class]])
?
Texto original disponível em http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/