Validação de caminho de objeto – uma solução para validar objetos profundos

[Leggi la versione italiana]

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

  1. A Solução # 1 – Declaração IF
  2. A Solução # 2 – Declaração Funcional
  3. Variação # 1 – A ligação
  4. 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 objectargumento 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>