Definir propiedades de un objeto en JavaScript

ECMAScript 5 dispone de un par de APIs que nos permite afectar la forma como interactuamos con los objetos que creamos, permitiéndonos especificar getters y setters, evitar la enumeración de propiedades, eliminar propiedades, e incluso prevenir la extensión de objetos (adición/modificación de propiedades), es decir que podemos ejercer un mayor control sobre ellos.

Definiendo propiedades

Antes de comenzar, sugiero repasar las formas básicas para crear objetos en JavaScript, ya que es un artículo corto y sirve de base para el tema que trataremos a continuación.

Como objetivo de este artículo, quiero ilustrar un caso en donde podamos apreciar el resultado de aplicar las diferentes técnicas para definir propiedades en un objeto, ya sea por asignación, o utilizando los métodos Object.defineProperty, Object.defineProperties y Object.create.

Se requiere agregar un mecanismo que permita el ordenamiento de una lista de elementos de acuerdo al criterio específico que sea suministrado.

Nota: leave the underlying JavaScript prototypes untouched, esto significa que deberíamos evitar modificar los objetos nativos de JavaScript, ya que si lo hacemos podríamos generar una colisión de nombres con otras librerías o con futuras versiones del lenguaje (caso que ocurrió con la librería Prototype.js), también puede ocurrir que se sobrescriban métodos con implementaciones incompatibles, además de hacer el código inconsistente para otros desarrolladores y de ahí que su mantenimiento sea una pesadilla.

Como sabemos que el prototipo del objeto Array cuenta con el método .sort() podemos apoyarnos en ese objeto para implementar el método sortBy, claro, teniendo en cuenta el párrafo anterior, por lo que usaremos la herencia y así evitamos modificar el objeto Array:

    //definimos el constructor
    function Collection() {}

    //heredamos del prototipo de Array
    Collection.prototype = new Array;
    //comprobamos que el objeto herede de Array
    var lista = new Collection();

    lista instanceof Collection; //-> true
    //Collection.prototype está en Object.getPrototypeOf(lista)

    lista instanceof Array; //-> true
    //Array.prototype está en Object.getPrototypeOf(lista)
    //realizamos operaciones de un Array
    lista.push({nombre: "julian", edad: 24});
    lista.unshift(
        {nombre: "Luis", edad: 25},
        {nombre: "Alex", edad: 36});
    lista.reverse();

    //comprobamos si existe el método sortBy
    console.log("sortBy" in lista);

Ahora vamos a crear el método sortBy en el prototipo de Collection.

Por asignación

Ésta es la forma más común de definir una propiedad en un objeto. Podemos hacerlo utilizando la notación punto o mediante el indexador de propiedades (vea la sección: Añadir propiedades en un objeto).

    //imprimimos la instancia
    console.log(lista);

    //extendemos el prototipo
    Collection.prototype.sortBy = function (args) {
    	//implementación
    };

    //imprimimos la instancia
    console.log(lista);

Hemos definido la propiedad sortBy mediante la notación punto, sin embargo en la última línea vemos algo extraño, ¿lo notaron?😮 Se supone que al imprimir el objeto lista solo se deberían mostrar los elementos de la colección, ya que hereda de Array, no obstante vemos que se está listando la propiedad sortBy; raro no❓

    //ahora iteramos la colección
    var item;
    for (item in lista)
    	console.log(item, lista[item]);

WTF! ¿Porque se listaron otras propiedades en la iteración? Si iteramos un Array, no ocurre esto. Recordemos que unas líneas arriba comprobamos que Collection hereda de Array, por lo que pudimos operar con los métodos push, unshift, reverse que se encuentran dentro del prototipo de Array, sin embargo no esperábamos lo anterior.

Comparemos con un array nativo:

    //creamos el array nativo
    var array = [
    	{nombre: "julian", edad: 24},
    	{nombre: "Alex", edad: 36},
    	{nombre: "Luis", edad: 25}
    ];

    //iteramos el array nativo
    var item;
    for (item in array)
    	console.log(item, array[item]);

    console.log("-------------");

    //iteramos la colección
    for (item in lista)
    	console.log(item, lista[item]);

❗ Ahora si es más evidente lo que ocurre. En el array nativo, sólo se enumeran los elementos del array, sin embargo en nuestra colección se enumeran los elementos, pero también la propiedad length, y el método sortBy que se agregó en el prototipo.

💡 Crear propiedades en un objeto mediante asignación es una buena opción cuando sólo queremos extender un objeto; pero cuando se debe respetar un comportamiento específico, como en el ejemplo anterior, hay mejores opciones para definir una propiedad, y para ello contamos con los métodos defineProperty, defineProperties.

Object.defineProperty

Éste método define una propiedad directamente en un objeto, o modifica una propiedad (configurable) existente en el objeto, y retorna el objeto. Refiérase a la documentación de MDN para más detalles: Object.defineProperty.

La firma del método es la siguiente:
    Object.defineProperty(objeto, propiedad, descriptor)
  • objeto: Object, objeto al cual le definiremos la propiedad.
  • propiedad: String, nombre de la propiedad a definir.
  • descriptor: Object, es un objeto que configura el comportamiento de la propiedad a definir mediante data descriptors o accessor descriptors exclusivamente, pero no puede mezclar ambos esquemas, por ejemplo al intentar usar value con get/set en la misma propiedad, causará una excepción TypeError.
    Data descriptor:
    • value: contiene el valor de la propiedad.
    • writable: Boolean, si se establece en false, la propiedad será de sólo lectura y no podrá ser modificada mediante el operador de asignación; cualquier intento de escritura arrojará una excepción en strict mode.
    Accessor descriptor:
    • get: Function, es la función que será llamada cuando el valor de la propiedad sea accedido (getter: el valor retornado será usado por la propiedad)
    • set: Function, es la función que será llamada cuando el valor de la propiedad sea cambiado (setter: recibe el valor que será asignado a la propiedad)
    Cualquier propiedad puede ser configurada con:
    • configurable: Boolean, si se establece en false, la propiedad queda sellada y no se podrán modificar sus descriptores o ser eliminada; cualquier intento de modificación arrojará una excepción en strict mode.
    • enumerable: Boolean, si se establece en false, la propiedad no será enumerada en una iteración for…in o mediante el método Object.keys.

Nota: Vea la sección de excepciones arrojadas por el método defineProperty.

Continuando con nuestro ejemplo, ahora vamos a definir el método sortBy en el prototipo de Collection, pero configurándolo de modo que sea de solo lectura y no enumerable.

    //extendemos el prototipo
    Object.defineProperty(Collection.prototype, "sortBy", {
        configurable: false, //non-reconfigurable
        enumerable: false, //non-iterable
        writable: false, //read-only
        value: function (args) { }
    });

💡 Verificamos si ahora funciona como se espera, similar a un array nativo:

    //iteramos el array nativo
    for (item in array)
    	console.log(item, array[item]);

    console.log("-------------");

    //iteramos la colección
    for (item in lista)
    	console.log(item, lista[item]);

❗ Ya casi, por ahora logramos que el método sortBy no fuera enumerado, pero la propiedad length sigue siendo enumerada. Prestemos atención al siguiente detalle:

cadena de prototipos

Podemos ver una propiedad local llamada length la cual está siendo enumerada (la propiedad length dentro de __proto__ pertenece al constructor Array y ésta no es enumerada)

Como la propiedad length que queremos modificar es propia de la instancia, significa que debemos configurarla en el constructor y no en el prototipo. Rescribiendo todo quedaría del siguiente modo:

    //definimos el constructor
    function Collection() {
        //configuramos la propiedad "length"
        Object.defineProperty(this, "length", {
            configurable: true,
            enumerable: false,
            writable: true, 
            value: 0
        });
    }
 
    //heredamos del prototipo de Array
    Collection.prototype = new Array;

    //extendemos el prototipo
    Object.defineProperty(Collection.prototype, "sortBy", {
        configurable: false, //non-reconfigurable
        enumerable: false, //non-iterable
        writable: false, //read-only
        value: function (args) { }
    });
    //creamos una instancia
    var lista = new Collection();

    //agregamos elementos
    lista.push(
        {nombre: "julian", edad: 24},
        {nombre: "Alex", edad: 36},
        {nombre: "Luis", edad: 25});

    //ahora iteramos la colección
    for (var item in lista)
        console.log(item, lista[item]);

😎 Hemos extendido el prototipo de un objeto, agregando un nuevo método, y respetando el comportamiento actual de los objetos (por herencia).

Object.defineProperties

Éste método define o modifica las propiedades directamente en un objeto, y retorna el objeto. Refiérase a la documentación para más detalles: Object.defineProperties.

Éste método hace lo mismo que Object.defineProperty con la diferencia que el segundo argumento contiene un Object con el nombre de la propiedad y el descriptor de esa propiedad.

La firma del método es la siguiente:
    Object.defineProperties(objeto, propiedades)
    //veamos un ejemplo
    Object.defineProperties(myObj, {
        "property1": {
            value: true,
            writable: true
        },
        "property2": {
            value: "Hello",
            writable: false
        }
    });

💡 Para trabajar con el método defineProperties, seguiremos con el caso anterior, y vamos a corregir un pequeño defecto:

    //creamos una instancia
    var lista = new Collection();
    lista.push("naranjas", "manzanas");

    //imprimimos nuestro objeto
    console.dir(lista);

    //verificamos el constructor de "lista"
    console.log(lista.constructor);
    //-> function Array() { [native code] }

    lista.constructor === Array; //-> true
    lista.constructor === Collection; //-> false

❓ WTF! Pero si lista es una instancia directa de Collection, ¿porqué me dice que su constructor es Array?

Éste es un pequeño issue de la herencia en JavaScript (ES5), pero solucionarlo es muy fácil, basta con restaurar el constructor después de establecer la herencia:

    //heredamos del prototipo de Array
    Collection.prototype = new Array;

    //restauramos el constructor
    Collection.prototype.constructor = Collection;

Sin embargo recordemos que si definimos una propiedad en el prototipo mediante el operador de asignación, esa propiead será navegable por defecto, así que utilizemos Object.defineProperties.

    function Collection() {
        //configuramos la propiedad "length"
        Object.defineProperty(this, "length", {
            configurable: true,
            enumerable: false,
            writable: true, 
            value: 0
        });
    }
 
    //heredamos del prototipo de Array
    Collection.prototype = new Array;

    //restauramos el constructor y extendemos el prototipo
    Object.defineProperties(Collection.prototype, {
        "constructor": {
            configurable: false, //non-reconfigurable
            enumerable: false, //non-iterable
            writable: false, //read-only
            value: Collection
        },
        "sortBy": {
            configurable: false,
            enumerable: false,
            writable: false,
            value: function (args) { }
        }
    });

😎 Deal with it!

    //creamos una instancia
    var lista = new Collection();
    lista.push("naranjas", "manzanas");

    //verificamos el constructor de "lista"
    console.log(lista.constructor);
    //-> function Collection() { ... }

    lista.constructor === Array; //-> false
    lista.constructor === Collection; //-> true

Object.create

El método Object.create crea un nuevo objeto a partir del objeto prototipo especificado y puede definir sus propiedades como en Object.defineProperties. El método posee la siguiente firma:
    Object.create(proto [, propiedades ])
  • proto: Object, objeto que será el prototipo del nuevo objeto a crear.
  • propiedades: Object, este argumento es opcional, y es un objeto que define las propiedades locales del nuevo objeto (es decir que no están en su prototipo) y también especifica los descriptores que configuran las propiedades a definir (ver argumento descriptor de Object.defineProperty)
Si desea crear un objeto vacío, debe proporcionar null como primer argumento:
    //creamos un objeto vacío
    var obj = Object.create(null);

Nota: debemos tener en cuenta que Object.create solo crea un objeto a partir del prototipo especificado, y no a través de un constructor, esto quiere decir que las propiedades definidas en el constructor no serán copiadas, veamos un ejemplo:

    //constructor
    function View (name) {
        this.name = name;
    }

    //prototipo
    View.prototype = {
        setTemplate: function (template) {
            this.template = template;
        },
        trigger: function (name) {
            //trigger the view
        }
    };

    var defaultView = Object.create(View.prototype);

    //miramos el prototipo de "defaultView"
    console.dir( Object.getPrototypeOf(defaultView) );

    //navegamos las propiedades en "defaultView"
    for (var prop in defaultView) console.log(prop);

En este segmento de código podemos ver que al imprimir las propiedades de defaultView no se lista la propiedad name, la cual fue definida en el constructor View.

Ahora si quisiéramos pasar como argumento el constructor View para obtener todas las propiedades, tendríamos lo siguiente:
    var defaultView = Object.create(View);

    //miramos el prototipo de "defaultView"
    console.dir( Object.getPrototypeOf(defaultView) );

    //navegamos las propiedades en "defaultView"
    for (var prop in defaultView) console.log(prop);

En el caso anterior podemos observar que al imprimir el prototipo de defaultView con el método Object.getPrototypeOf, obtenemos la función constructora, lo cual está mal porque debemos obtener el “objeto”, no su constructor; ahora cuando imprimimos las propiedades navegables de defaultView, observamos que no aparece ninguna. (Recomiendo que lean el artículo Object.getPrototypeOf de John Resig)

😡 Y si necesitamos todas las propiedades de un objeto, incluyendo las que están en su constructor, ¿entonces que? — Para ese caso podemos hacer lo siguiente:

    var defaultView = Object.create(new View);

    //miramos el prototipo de "defaultView"
    console.dir( Object.getPrototypeOf(defaultView) );

    //navegamos las propiedades en "defaultView"
    for (var prop in defaultView) console.log(prop);

:/ Pero crear una instancia para crear el mismo objeto es una tontería, jejeje, para esa gracia solo creo la instancia y ya. Object.create es muy útil cuando vamos a extender un objeto con nuevas propiedades, y para la clonación de objetos.

💡 Ahora vamos a seguir con el ejemplo original que veníamos trabajando, pero esta vez utilizando el método Object.create

    var Collection = (function() {
        var CollectionPrototype = {
            "length": {
                configurable: true,
                enumerable: false,
                writable: true, 
                value: 0
            },
            "sortBy": {
                configurable: false,
                enumerable: false,
                writable: false,
                value: function (args) { }
            }
        };
        return function Collection () {
            return Object.create(Array.prototype, CollectionPrototype);
        };
    }());
    //creamos el objeto
    var lista = Collection();
    lista.push("naranjas", "manzanas");

    //iteramos la colección
    for (var item in lista)
        console.log(item, lista[item]);

    console.log(lista.length); //-> 2
    console.log(lista.sortBy); //-> function ...

😎 El resultado es el esperado, el objeto lista tiene las propiedades length y sortBy, las cuales no son enumeradas cuando iteramos la colección. Es de resaltar que en este enfoque no se utilizó una función constructora, ya que el objeto lista nunca se creó mediante el operador new, sino que Collection se definió como un Factory Function, que permite la creación del mismo tipo de objeto mediante la clonación y extensión; en este caso clonamos el prototipo de Array y lo extendemos con el objeto CollectionPrototype.

Ahora validaremos que tipo de objeto es lista:
    lista instanceof Array; //-> true
    lista instanceof Collection; //-> false

❓ ¿Por que al validar si lista es una instancia de Collection se evalúa como falso?🙄 Como se mencionó anteriormente, Collection no es un constructor sino una especie de Factory Function que me permite crear objetos mediante la clonación, por lo tanto el objeto lista nunca se creó mediante instanciación.

Nota: En el siguiente stackoverflow hice una implementación del método sortBy, no lo quise incluir acá para no extender más el artículo: Sort Javascript Object Array

Conclusión

Como pudimos ver, existen varias formas de crear objetos y definir sus propiedades, ECMAScript5 provee varios mecanismos que nos permiten tener un mayor control sobre los objetos que creamos y lo mejor es que podemos aprovechar estas características para construir aplicaciones más robustas.

Si están interesados es aprender sobre Factory Function, y los métodos de Object vistos en este artículo, recomiendo que lean Constructor Functions Vs Factory Functions de Eric Elliott, y ECMAScript 5 Objects and Properties de John Resig.

Y como bonus para subir de level en JavaScript, sugiero que saquen tiempo para digerir este magnífico artículo: The Two Pillars of JavaScript – How to Escape the 7th Circle of Hell.

Happy reading, happy coding!

9 thoughts on “Definir propiedades de un objeto en JavaScript

    • Amigo Rafael, muchas gracias por el comentario, esto me anima a retomar el hábito de escribir, dejé muchos artículos en Draft, pero creo que vale la pena continuar🙂

  1. Saludos desde Venezuela, jherax.

    Apartando el hecho de que este y varios de tus posts (junto con el libro Eloquent Javascript) los usé de base para escribir “Algunas peculiaridades de Javascript” en mi blog (que vale decir está excelente, muchas gracias😀 ), quería hacerte una consulta.

    Noto que tienes el mismo tema que yo, pero en tu caso, parece que el Syntax Highlighter te coloca las letras del código un tanto más legible, ¿tienes alguna opción particular para eso o simplemente es así? En mi caso, las letras son demasiado grandes y no puedo publicar códigos extensos…

    (Otra cosa, ¿como haces para colocar los emojis que no conozco como el de angry y el del “deal with it”? jejeje)

    Gracias de antemano y felicitaciones por tu blog😀

Comentarios

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s