Seleccionar página

Todos sabemos que los objetos pueden contener otros objetos. De lo que quizás no somos conscientes es que con mucha facilidad podemos crear una referencia circular, es decir, que si recorremos las propiedades del objeto y vamos profundizando, llegamos de nuevo al objeto partida. Esta referencia circular no es un error o un problema, es una característica del lenguaje que podemos aprovechar, aunque debemos tenerla en cuenta a la hora de realizar algunas operaciones, ya que si no la tenemos en cuenta podemos provocar un problema.

Creando referencias circulares

Vamos a intentar explicar un poco más que es una referencia circular con un ejemplo muy sencillo:

var obj1 = {
    a: 1,
    b: 2
};
obj1.self = obj1;

En este código se puede ver una referencia circular simple, ya que el objeto hace referencia a sí mismo. El objeto obj1 tiene una propiedad self que hace referencia a obj1, es decir, a sí mismo. Seguramente no tiene mucha utilidad, pero el lenguaje acepta este tipo de referencias sin problemas.

Un caso más complejo, y en gran medida más habitual, es cuando la referencia circular se produce a través de otros objetos, es decir, tenemos un objeto que apunta en una de sus propiedades a otro objeto y este segundo hace referencia al objeto inicial.

Vamos a verlo con un ejemplo:

var parent = {
    sons: []
};
var child1 = {
    a: 1,
    b: true,
    c: 'hello',
    parent: parent
};
parent.sons.push(child1);
var child2 = {
    a: 2,
    b: false,
    c: 'bye',
    parent: parent
};
parent.sons.push(child2);
 

En este caso el objeto parent contiene una matriz con todos sus hijos (sons, y cada uno de los objetos child1 y child2 tienen una referencia a parent. En estas circunstancias, si recorremos en profundidad las propiedades, antes o después volveremos al objeto de partida, ya que existe una referencia circular.

Las referencias circulares son mucho más comunes de lo que podemos pensar a primera vista. En el DOM de los navegadores hay una gran cantidad de referencias circulares, ya que existen referencias entre unos elementos y otros.

Referencias circulares y funciones nativas

Para empezar vamos a ver qué ocurre cuando se hace un sencillo console.log() con un objeto con referencia circular. Realmente el resultado depende del navegador o del entorno en el que estamos trabajando: o bien permite ir navegando interactivamente de forma infinita por los elementos anidados o bien muestra algún mensaje del tipo parent: [Circular] para indicarnos esta característica.

Una situación muy habitual, y que causa muchas molestias, es utilizar JSON.stringify() con objetos con referencias circulares. Obtendremos un error del tipo TypeError: Converting circular structure to JSON. Esto es muy habitual al intentar serializar con JSON.stringify() un objeto del DOM, ya que -como hemos comentado- estos objetos tienen numerosas referencias circulares.

Aunque no es una solución completa al problema de JSON.stringify() con las referencias circulares, podemos resolver parte de este problema por medio de la función que podemos pasar como segundo parámetro a JSON.stringify(). Esa función es llamada por cada uno de los elementos de los objetos y si guardamos una referencia a los objetos ya procesados en una matriz podremos detectar la referencia circular y hacer algo específico en ese caso.

var result = JSON.stringify(parent, (function() {
    var stack = [];
    return function (key, value) {
        if (typeof value === 'object' && value !== null) {
            if (stack.indexOf(value) !== -1) {
                return undefined;
            }
            stack.push(value);
        }
        return value;
    };
})());

En este caso simplemente hemos devuelto undefined ya que JSON.stringify() ignora estos valores. También podríamos haber devuelto un, dar un aviso o lo que se nos ocurra. No es una solución perfecta, pero al menos no da un error y podremos obtener una representación aproximada del objeto.

Implementación en equal()

Desde hace algunas semanas venimos viendo con diferentes artículos cómo desarrollar una función universal que sea capaz de comparar todos los tipos de datos Javascript. Ahora vamos a ver cómo implementar una solución cuando nos encontramos con referencias circulares.

Hasta ahora, la versión que hemos desarrollado, no tiene en cuenta esta circunstancia, y en el caso de encontrar referencias circulares seguirá analizando hasta que se produzca un error del tipo RangeError: Maximum call stack size exceded. Al realizarse una llamada recursiva a la propia función equal() y existir referencias circulares en los objetos, las llamadas se van encadenando hasta que desbordan el máximo número de llamadas recursivas que son soportadas.

Para gestionar adecuadamente los objetos con referencias circulares utilizaremos una estrategia similar a la que hemos mostrado antes en el caso de JSON.stringify(), crear una matriz en la que almacenamos los objetos ya analizados y comprobaremos si el nuevo valor ya existe dentro de esa matriz. Si existe, es que ya lo hemos procesado y no seguiremos profundizando.

Pasaremos de un código de este tipo:

function equal(a, b, options) {
    var aValue, bValue, aKeys, bKeys, i,   // Define variables
        aDescriptor, bDescriptor,
        aType = typeof a,                  // Get value types
        bType = typeof b;
    options = options || {};               // Optional parameter

    // ...
}

A un código estructurado de esta forma:

function equal(a, b, options) {
    var aStack = [],                       // Stack array
        bStack = [];
    options = options || {};               // Optional parameter
    return (function check(a, b) {
        if (aStack.indexOf(a) > -1 &&      // Check if the object has been previously processed
            bStack.indexOf(b) > -1)
        {
            return OBJECT;
        }
        aStack.push(a);                    // Storage objects into stacks for recursive reference
        bStack.push(b);

        //...
    })(a, b);
}

Aunque puede parecer un poco enrevesado, lo que se hace es verdaderamente sencillo: dentro de la función equal() se ha creado una par de matrices donde vamos guardando referencias a los valores pasados como parámetros. También hemos creado una función check() que llamamos directamente y es la que realmente hace la comparación de forma recursiva. Si encontramos los valores en la matriz, es que ya hemos procesado esos valores y no debemos seguir llamando recursivamente a la función check().

Hemos puesto aquí solo un esquema de funcionamiento. Si quieres puedes consultar el código completo en GitHub y el juego de pruebas que incluye este caso.

Conclusiones

Los objetos con referencias recursivas pueden ser muy útiles para gestionar estructuras de datos anidadas, listas enlazadas, con referencias complejas, etc. No son un error y debemos entender que se pueden gestionar con un poco más de esfuerzo por nuestra parte.

En mecanismo planteado para gestionar las referencias circulares es verdaderamente sencillo y podemos implementarlo sin demasiada dificultad en nuestras funciones. Incluso si queremos utilizar JSON.stringify() podemos pasar una función que gestione esta circunstancia de forma adecuada. No tenemos excusa para no utilizar esta característica de Javascript por miedo a encontrarnos con una llamada recursiva que exceda la pila de llamadas. Simplemente tenemos que ser capaces de identificar esta referencia circular y gestionarla.

Novedades

JSDayES en directo

Sigue en directo las charlas y talleres del impresionante JSDayES 2017. Ponentes de primer orden, tanto nacionales como internacionales, nos mostrarán todos los aspectos de Javascript. El viernes 12 de mayo de 16:00 a 21:00 y el sábado 13 de mayo de 9:30 a 21:00 (hora de Madrid).

The CITGM Diaries by Myles Borins [video]

Myles Borins de Google, miembro del CTC de Node.js (Node.js Core Technical Committee), nos cuenta (en inglés, por supuesto) como funciona CITGM (Canary In The Gold Mine), una herramienta que permite obtener un módulo de NPM y probarlo usando una versión específica de NodeJS.

Debate: Tecnologías de Front Web [vídeo]

Desde las principales comunidades de desarrollo de tecnologías de front (Madrid JS, Polymer Madrid, Angular Madrid y VueJS Madrid) se ha organizado este debate que pretende ser un ejercicio de sentido común en relación a las tecnologías de front actuales centradas en componentes.

breves

Descrubir algunas características de console

En el día a día nos encontramos muy a menudo utilizando console. Es una navaja multiusos que nos facilita la vida a la hora de depurar nuestro código. La mayoría de nosotros ha utilizado console.log(), pero tiene otras muchas funcionalidades.

Matrices dispersas o sparse arrays en JS

Una característica que puede producir algunos problemas, si no lo tenemos en cuenta, es la posibilidad de tener matrices con huecos, es decir, con algunos de sus elementos sin definir. Es lo que se suele denominar una matriz dispersa o sparse array. Veamos cómo trabajar con esta características de las matrices.

Algunos operadores de bits usados con asiduidad

Cada día más a menudo podemos encontrar operadores binarios utilizados como formas abreviadas de algunas operaciones que de otra forma sería algo menos compactas y, quizás, más comprensibles. Veamos algunos casos en detalle.

Cómo diferenciar arrow function de function

En un reciente artículo Javier Vélez Reyes hace patente las principales diferencias entre las funciones tradicionales y las funciones flecha, ya que ambos modelos no son equivalentes e intercambiables. Veamos cómo es posible saber si una función ha sido construida por medio de la instrucción function o como una arrow function.