Preâmbulo
O que é um caminho de objeto ? É uma interpretação pessoal do identificador que define um caminho claro de um objeto raiz a um subobjeto ou propriedade.
O problema
Quantas vezes tivemos a necessidade de verificar se object.foo.bar.baz
é um caminho de objeto válido ?
Índice
- A Solução # 1 – Declaração IF
- A Solução # 2 – Declaração Funcional
- Variação # 1 – A ligação
- Variação # 2 – Prototipagem
<a name=”solution1″> </a>
A Solução # 1 – Declaração IF
Bem, como primeira solução, podemos olhar para uma declaração muito básica, muitas vezes usada nestes casos:
if ( object && object.foo && object.foo.bar && object.foo.bar.baz ) {
// do something
}
A instrução if verificará a validade (ou seja, se existe) para cada identificador, e apenas se todos os identificadores são válidos executará seu bloco de código.
PRÓ
- Sua intenção é clara para todos que olham para esse código.
- É rápido devido à avaliação de curto-circuito da instrução if (ela para no primeiro identificador inválido).
CONTRAS
- Quanto mais profundo for o caminho, mais identificadores serão necessários para avaliar a instrução. Em casos específicos, isso pode nos levar a uma declaração if muito longa e confusa
- Se usado com objetos DOM, a execução será mais lenta devido ao acesso de baixo desempenho do DOM aos seus objetos.
<a name=”solution2″> </a>
A Solução # 2 – Declaração Funcional
Como vimos acima, a solução da instrução if é boa o suficiente quando o caminho do objeto não é muito profundo ou não é necessário verificar a existência de um objeto DOM.
Mas, como faríamos se tivéssemos um objeto muito profundo para verificar? Pior ainda, como faríamos se tivéssemos que verificar caminhos de objetos com profundidades diferentes?
A solução acima não é tão boa quanto parece. Mas podemos resolver esse problema usando uma abordagem funcional.
Então, a primeira coisa primeiro, temos que definir nossa função:
var safeObjectPath = function safeObjectPath( object, properties ) {
var path = [],
root = object,
prop;
if ( !root ) {
// if the root object is null we immediately returns
return false;
}
if ( typeof properties === 'string' ) {
// if a string such as 'foo.bar.baz' is passed,
// first we convert it into an array of property names
path = properties ? properties.split('.') : [];
} else {
if ( Object.prototype.toString.call( properties ) === '[object Array]' ) {
// if an array is passed, we don't need to do anything but
// to assign it to the internal array
path = properties;
} else {
if ( properties ) {
// if not a string or an array is passed, and the parameter
// is not null or undefined, we return with false
return false;
}
}
}
// if the path is valid or empty we return with true (because the
// root object is itself a valid path); otherwise false is returned.
while ( prop = path.shift() ) {
// UPDATE: before it was used only the if..else statement, but
// could generate an exception if a inexistent member was found.
// Now I fixed with a try..catch statement. Thanks to @tarikozket
// (https://coderwall.com/tarikozket) for the contribution!
try {
if ( prop in root ) {
root = root[prop];
} else {
return false;
}
} catch(e) {
return false;
}
}
return true;
}
Agora, podemos verificar nossos caminhos de objeto usando uma abordagem funcional, da seguinte maneira:
var module = { foo: { bar: { baz: 'hello' } } };
// checks if `module` isn't null or undefined
safeObjectPath( module ) // -> true
// checks if `foo.bar.baz` is a valid path in `module`
safeObjectPath( module, 'foo.bar.baz' ) // -> true
// same as above, but with an array
safeObjectPath( module, ['foo', 'bar', 'baz'] ) // -> true
// checks if `foo.baz` is a valid path in `module`
safeObjectPath( module, ['foo', 'baz'] ) // -> false
Como podemos ver, esta abordagem nos permite verificar diferentes tipos de caminho (string ou array, para que possamos até mesmo gerar dinamicamente esses caminhos), e com uma declaração simples (nossa função) é possível verificar qualquer profundidade de caminho que nós quer. Não importa se 1, 5, 10 ou 100 propriedades em profundidade.
PRÓ
- Declarações mais curtas, mesmo com caminhos profundos
- Suporte para caminhos de string ou array
- Suporte para caminhos de qualquer profundidade
- Caminhos intermediários são armazenados em cache . O processamento de objetos DOM é mais rápido do que a solução # 1
CONTRAS
- As funções são mais lentas do que as instruções if
- Seu propósito pode não ser imediatamente claro para quem lê o código-fonte
<a name=”variation1″> </a>
Variação # 1 – A ligação
Agora que vimos como verificar se há um caminho de objeto válido, podemos tentar ultrapassar a segunda solução e procurar algumas alterações mais avançadas.
Como vimos, a função funciona em todos os dados (já que em javascript quase tudo é um objeto). Portanto, um primeiro passo pode ser remover o object
argumento e aproveitar a ligação. Para fazer isso, temos que fazer algumas pequenas alterações:
var safeObjectPath = function safeObjectPath( properties ) {
var path = [],
root = this, // before, this was `object`
prop;
Removemos object dos argumentos e definimos root como este. Agora a função sempre fará referência ao contexto do objeto vinculado a ela. Portanto, para usar essa versão modificada, teremos maneiras diferentes:
safeObjectPath.call( object, 'foo.bar.baz' )
safeObjectPath.apply( object, [ ['foo', 'bar', 'baz'] ] )
(safeObjectPath.bind( object ))( 'foo.bar.baz' )
No entanto, como podemos ver, as novas alterações que introduzimos realmente não nos proporcionam nenhuma melhoria prática, porque ainda temos que vinculá-las aos métodos apply (), call () ou bind (), tornando-o mais longo que antes, e mais complexo. Então, agora podemos ver qual é a próxima etapa final para melhorar esta solução e torná-la útil para nossos objetivos.
<a name=”variation2″> </a>
Variação # 2 – Prototipagem
Sim, posso ouvir todas as legiões de programadores reclamando porque é uma má prática prototipar diretamente em um objeto nativo. Sim, você está certo. Mas não podemos deixar de levar em conta a grande ajuda que esse tipo de método pode nos dar. (Aliás, Prototype.js fez sua fortuna graças a essa abordagem, então por que pelo menos não dar uma olhada nisso?;).
;(function(root, factory, undefined){
var Object = root.Object;
try {
// We take care to not do a mess with `Object`.
// If the method doesn't exists, an exception is thrown and
// the it will be added.
Object.isSafePath();
} catch (e) {
// This will automatically makes the new method available to all
// the other objects.
Object.prototype.isSafePath = factory();
}
}( window, function(){
return function ( properties ){
var path = [],
root = this,
prop;
if ( typeof properties === 'string' ) {
// if a string such as 'foo.bar.baz' is passed,
// first we convert it into an array of property names
path = properties ? properties.split('.') : [];
} else {
if ( Object.prototype.toString.call( properties ) === '[object Array]' ) {
// if an array is passed, we don't need to do anything but
// to assign it to the internal array
path = properties;
} else {
if ( properties ) {
// if not a string or an array is passed, and the parameter
// is not null or undefined, we return with false
return false;
}
}
}
// if the path is valid or empty we return with true (because the
// root object is itself a valid path); otherwise false is returned.
while ( prop = path.shift() ) {
try {
if ( prop in root ) {
root = root[prop];
} else {
return false;
}
} catch(e) {
return false;
}
}
return true;
};
}));
O que fizemos aqui é muito simples (espero: D). Usamos um IIFE para instalar nosso método no protótipo de objeto usando um padrão de fábrica . Desta forma, podemos ter certeza de não sobrescrever um método eventualmente já presente com o mesmo nome.
Também removemos a verificação interna de uma raiz inicial válida porque, neste caso, se não temos um objeto válido, também não temos uma referência ao método.
Dito isso, tudo o que temos que fazer será chamar o novo método, toda vez que quisermos testar um caminho de objeto válido:
object.isSafePath( 'foo.bar.baz' )
object.isSafePath( ['foo', 'bar', 'baz'] )
Object.prototype.isSafePath.apply( object, ['foo', 'bar', 'baz'] )
Object.prototype.isSafePath.call( object, 'foo.bar.baz' )
Isso é tudo, pessoal! Espero que seja útil para alguém.
O código-fonte está disponível no Gist .
O teste de desempenho está disponível em jsPerf . (Graças a@tarikozket ).
<small> 7E67F7C4750AD1619A0C2D381CD9BD6E </small>