Métodos básicos de Object en JavaScript ES6

Métodos básicos de Object

Figura
Figura. Método toLocaleString() para dar formato local a números.

En la siguiente lista de enlaces agrupamos los métodos genéricos (o estáticos) conjuntamente con métodos del prototipo de Object según su finalidad. En este tema exponemos los métodos básicos que encontramos en el prototipo de Object y que todos los objetos heredan, como {}.toString(), {}.toLocaleString() y {}.valueOf().

Los métodos genéricos se referencian con Object.metodo() mientras que los del prototipo con Object.prototype.metodo(). Sin embargo éstos últimos los veremos escritos como {}.metodo() para ahorrar algo de espacio, no sólo en este texto sino también en el código. Ambas expresiones ejecutarán el mismo método, pues el objeto vacío {} es una instancia de Object y, por tanto, tiene acceso a todos los métodos del prototipo de Object.

De hecho Object tiene pocos métodos del prototipo, pues sólo puede tener aquellos que sean básicos para ser heredados por el resto de objetos built-in y de usuario existentes en JavaScript.También hemos incluido en esta página el nuevo método genérico Object.is(). Viene a complementar los operadores de comparación == y ===.

{}.toString()

ES1

El método del prototipo de Object {}.toString() devuelve una representación en String del objeto. Con objetos de Object siempre devuelve [object Object]. Con otros objetos built-in devolverá otra cadena, pues sus constructores pueden sobrescribir el método. Por ejemplo, con [1, 2] obtenemos una lista de los valores del Array separado por comas: 1, 2. JavaScript usará este método cuando necesite convertir en String un objeto. Vimos un ejemplo en el apartado sobre nombres de propiedades computados del tema anterior, donde usábamos objetos como nombres de propiedades.

Aquí puede ver como actúa toString() sobre los tipos primitivos. Es posible porque JavaScript envuelve el valor con el objeto built-in correspondiente. Observe que no podemos aplicarlo sobre null o undefined pues no son propiamente objetos que dispongan de métodos (aunque más abajo veremos algo más sobre estos valores):

try {
    console.log(null.toString());
} catch(e){
    console.log(e.message); // Cannot read property 
                            // 'toString' of null
}
try {
    console.log(undefined.toString());
} catch(e){
    console.log(e.message); // Cannot read property 
                            // 'toString' of undefined
}
//Para otros tipos primitivos se usa el objeto envoltura
console.log(123 .toString()); // 123
console.log("abc".toString()); // abc
console.log(true.toString()); // true
console.log(Symbol.for("x").toString()); // Symbol(x)
    

Como muestra vemos que el valor 123 es un tipo primitivo number, pero se envuelve en un objeto Number, aplicándose el toString() del prototipo de Number que sobrescribe el toString() del prototipo de Object. Vea como actúa para otros objetos:

console.log([1,2].toString()); // 1, 2
console.log((new Set([1,2])).toString()); // [object Set]
console.log((new Map([["a",1],["b",2]])).toString()); // [object Map]
console.log((new Date()).toString()); // Sun Oct 02 2016 19:21:08 
                                      // GMT+0100 (Hora de verano GMT)
function fun(a){return a+1}
console.log(fun.toString()); // function fun(a){return a+1}
    

La cadena devuelta por toString() tiene la estructura [object CONSTRUCTOR]. Esto ha venido sirviendo para saber a que built-in pertenece un objeto. Lo hacemos aplicando el método {}.toString() directamente sobre el objeto usando call() para impedir que llame a su propio toString(). A partir de ES5.1 al aplicarlo sobre null o undefined nos devolverá [object Null] y [object Undefined], aunque no sean precisamente objetos.

console.log({}.toString.call(null)); // [object Null]
console.log({}.toString.call(undefined)); // [object Undefined]
console.log({}.toString.call("abc")); // [object String]
console.log({}.toString.call(123)); // [object Number]
console.log({}.toString.call(true)); // [object Boolean]
console.log({}.toString.call(Symbol.for("x"))); // [object Symbol]
console.log({}.toString.call([1,2])); // [object Array]
console.log({}.toString.call(()=>{})); // [object Function]
console.log({}.toString.call(window)); // [object Window]
function F(){};
console.log({}.toString.call(new F)); // [object Object]
    
Recuerde que {}.toString() es una forma de abreviar Object.prototype.toString(). Como un objeto vacío tiene como prototipo el prototipo de Object, resulta que ambas expresiones ejecutan el mismo método. Pero no debemos olvidar que en el fondo lo que pretendemos es llamar a un método del prototipo directamente.

En la última línea del ejemplo anterior se observa que para constructores propios el método toString() no se sobrescribe, obteniéndose en cualquier caso [object Object]. Podemos sobrescribir el método usando el símbolo bien conocido Symbol.toStringTag. En ese tema pusimos este ejemplo, viendo que de esa forma toString() devuelve el nombre del constructor en cualquier caso.

//Modificando el toString() de un constructor
function MiConstructor(){}
MiConstructor.prototype[Symbol.toStringTag] = MiConstructor.name;
let valor = new MiConstructor();
console.log(valor.toString()); // [object MiConstructor]
console.log({}.toString.call(valor)); // [object MiConstructor]
    

Precisamente el hecho de que Symbol.toStringTag permita cambiar el comportamiento del método toString(), resultará en que ya no podemos confiar en {}.toString.call(valor) para conocer el verdadero constructor del valor. Por ejemplo, en el tema sobre este método para un Array expusimos este ejemplo:

Array.prototype[Symbol.toStringTag] = "MI ARRAY";
let arr = [1, 2, 3];
console.log(arr.toString()); // 1,2,3
console.log({}.toString.call(arr)); // [object MI ARRAY]    
    

Quizás es preferible usar la propiedad name del constructor, disponible desde ES6, aunque no podrá usarse con null o undefined pues no tienen constructores:

console.log({a: 1}.constructor.name); // Object
console.log("abc".constructor.name); // String
console.log(123 .constructor.name); // Number
console.log(true.constructor.name); // Boolean
console.log(Symbol.for("x").constructor.name); // Symbol
console.log([1,2].constructor.name); // Array
function f(){}
console.log(f.constructor.name); // Function
console.log(window.constructor.name); // Window
function F(){};
console.log((new F).constructor.name); // F
    

Todavía es posible cambiarle el constructor a una instancia, de tal forma que el nombre del constructor será erróneo. En el siguiente ejemplo lo cambiamos a String después de instanciar un Array.

Una forma segura de saber si una instancia de un Array sigue siendo un Array es con el operador instanceof. O incluso con el método isArray():

let arr = [1, 2];
console.log(arr);
arr.constructor = String;
console.log(arr.constructor.name); // String
console.log(arr instanceof Array); // true
console.log(Array.isArray(arr));  // true
    

{}.toLocaleString()

ES3

El método toLocaleString() convierte números y fechas a un formato local. En muchos países se usa el punto para separar decimales y la coma para separar grupos de miles. Y las fechas se presentan en formato día/mes/año. El método está disponible en Object sólo para que sea sobrescrito por los built-in Number, Date o Array. Pues si lo ejecutamos directamente sobre Object resulta [object Object] como el método toString(). Para un Array ya expusimos este método toLocaleString(). En el siguiente ejemplo verá todas estas particularidades:

let obj = {
    a: 1234.56,
    //Date tiene argumentos:
    //año, mes 0..11, día 1..31,
    //hora 0..23, minuto 0..59, segundo 0..59
    b: new Date(2016, 9, 2, 20, 49)
};
console.log(obj);
// Object {a: 1234.56, 
// b: Sun Oct 02 2016 20:49:00 GMT+0000 
// (Hora estándar GMT)}
console.log(obj.toLocaleString()); // [object Object]
console.log(obj.a.toLocaleString()); // 1.234,56
console.log(obj.b.toLocaleString()); // 2/10/2016 20:49:00 
    

En el ejemplo anterior el formato es seleccionado automáticamente a partir de la configuración del sistema. Pero podemos cambiar el formato actuando sobre este método en objetos Number y Date, pues tiene dos argumentos opcionales toLocaleString([locales[, options]]). El primero locales define el lenguaje y, en su caso, país o región de uso, por ejemplo: en es lenguaje inglés y US es Estados Unidos. El segundo argumento options es un objeto que admite formatos varios, de los cuales presentamos el formato de moneda en el siguiente ejemplo:

let num = 1234.567;
//Formatos de números locales para lenguajes español (es) e inglés (en)
console.log(num.toLocaleString("es")); // 1.234,567
console.log(num.toLocaleString("en")); // 1,234.567
//Formatos de números locales (es, en) y de moneda (ES, US)
console.log(num.toLocaleString("es-ES",
    {style: "currency", currency: "EUR"})); // 1.234,57 €
console.log(num.toLocaleString("en-US",
    {style: "currency", currency: "USD"})); // $1,234.57
//Formatos de fecha locales para lenguajes español (es) e inglés (en)
let fecha = new Date(2016, 9, 2, 20, 49);
console.log(fecha.toLocaleString("es")); // 2/10/2016 20:49:00
console.log(fecha.toLocaleString("en")); // 10/2/2016, 8:49:00 PM
    

{}.valueOf()

ES1

JavaScript usa el método valueOf() para convertir a un tipo primitivo el valor del objeto. Cuando usamos un constructor para crear números, cadenas o booleanos observamos la propiedad interna [[PrimitiveValue]]:

console.log(new Number("1")); // Number {[[PrimitiveValue]]: 1}
console.log(new String("a")); // String {0: "a", length: 1, 
                              // [[PrimitiveValue]]: "a"}
console.log(new Boolean("true")); // Boolean {[[PrimitiveValue]]: true}
    

Cuando trabajamos con objetos si JavaScript espera un tipo primitivo usará esa propiedad [[PrimitiveValue]], como en estas operaciones que suma números y concatena una cadena a un número. El método valueOf() extrae esa propiedad interna [[PrimitiveValue]]:

let obj = new Number("5");
console.log(obj.valueOf()); // 5
console.log(obj + 1); // 6
obj = new String("a");
console.log(obj.valueOf()); // "a"
console.log(obj + 1); // "a1"
    

El método valueOf() tiene una utilidad interna y es raro que lleguemos a usarlo. Pero en algún caso podría servirnos externamente. Supongamos que quiero construir un tipo de números Romanos. En este ejemplo tenemos una clase para eso, donde sobrescribimos el método valueOf() para extraer el valor decimal del número romano.

class Romanos {
    constructor(str){
        this.str = str;
    }
    static get letras(){return "IVXLCDM"}
    static valorLetra(letra){
        let index = Romanos.letras.indexOf(letra);
        if (index%2){
            return 5*Math.pow(10, (index-1)/2);
        } else {
            return Math.pow(10, index/2);
        }
    }
    toString() {
        return this.str;
    }
    valueOf(){
        let valor = 0, i = 0;
        let cad = this.str.trim().toUpperCase() + " ";
        while (i < cad.length-1) {
            let k = Romanos.letras.indexOf(cad[i]); 
            let k1 = Romanos.letras.indexOf(cad[i+1]);
            if (k < k1){
                valor += Romanos.valorLetra(cad[i+1]) -
                         Romanos.valorLetra(cad[i]);
                i = i+2;
            } else {
                valor += Romanos.valorLetra(cad[i]);
                i++;
            }
        }
        return valor;
    }
}
    

Cuando JavaScript intenté realizar una suma entre dos instancias de números romanos utilizará ese método valueOf() para saber el valor de cada operando.

//Creamos nuevos números romanos directamente
let num1 = new Romanos("IX");
let num2 = new Romanos("VIII");
//Métodos toString() y valueOf() del prototipo de Romanos
console.log(num1.toString(), num2.toString()); // IX VIII
console.log(num1.valueOf(), num2.valueOf()); // 9 8
//Operaciones sumar y multiplicar ahora aplican a tipo Romanos
console.log(num1 + num2); // 17
console.log(num1 * num2); // 72
//Incluso podemos combinar romanos con otros números
console.log(num1 / 2); // 4.5
    

Observe que también sobrescribimos el método toString(), pues de otra forma si hiciéramos num1.toString() obtendríamos [object Object], que es la forma básica de representar un objeto, tal como vimos más arriba al exponer este método toString().

En el siguiente ejemplo interactivo ampliamos el tipo Romanos para incluirle más cosas. Por un lado vamos a controlar errores, pues en el código anterior no hay ningún control de errores. Los números romanos han de seguir unas reglas de sintaxis, pues si escribimos el número 45 como VL será erróneo, siendo el correcto XLV.

Agregaremos métodos estáticos o genéricos, como fromNumber(decimal) que permite convertir un número decimal en romano. O el método getValue(romano) para lo contrario. En el enlace al pie del ejemplo podrá consultar el código completo.

Ejemplo: Uso de valueOf() para un tipo de Números Romanos

9
8
XLIX
let num1 = new Romanos("IX");
let num2 = new Romanos("VIII");
let num3 = new Romanos(49);
        

Ejecución de algunos métodos del prototipo como toString() y valueOf() y los métodos genéricos Romanos.fromNumber() y Romanos.getValue():

Tomamos una tabla de Wikipedia números romanos. Cada fila de la tabla tiene un número romano erróneo, tal como aparece en la primera fila con el valor VL, el correcto XLV, el valor decimal 45 y el tipo de error "Letra de tipo 5 restando". El test será correcto si detecta los errores. Todas las entradas han de resultar ERROR OK con el mensaje de error propio y los valores instanciando el valor correcto de la tabla, haciendo let num = new Romanos("XLV") y obteniendo el valor decimal con num.valueOf(). En caso de errores aparecería ERROR !OK.

Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Object.is(valor1, valor2)

ES6

El método de ES6 Object.is(valor1, valor2) nos dice si los dos valores de los argumentos son iguales. Esto, más que a sustituir, viene a complementar las comparaciones no estricta == y estricta ===. Es fuertemente recomendable usar siempre la comparación estricta === en lugar de ==, pues esta realiza una coerción de los tipos de datos antes de realizar la comparación. El método Object.is() se comporta igual que la comparación estricta excepto para algunos valores numéricos.

Uno de los valores es NaN, donde las comparaciones con coerción y estricta resultan falsas:

console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
    

Un caso donde Object.is() compara adecuadamente es con los valores +0 y -0:

console.log(+0 == -0); // true
console.log(+0 === -0); // true
console.log(Object.is(+0, -0)); // false
    

Otro caso es con 0/0:

console.log(0/0 == NaN); // false
console.log(0/0 === NaN); // false
console.log(Object.is(0/0, NaN)); // true
    

Casos como los anteriores justifican el uso de Object.is(). Para el resto deberíamos usar siempre la comparación estricta. Puede ver más información en el sitio de comparaciones de Dorey en Github y también en Mozilla comparaciones

En el siguiente ejemplo puede ver en ejecución en este navegador los anteriores casos y algunos más. Observe que a excepción de los comentados antes, la estricta y Object.is() se comportan igual, no coercionando los tipos de datos.

Ejemplo: Comparar valores

val1val2val1 == val2val1 === val2Object.is(val1, val2)
NaNNaN
+0-0
0/0NaN
""false
"abc"true
0false
1true
123true
"123"123
nullundefined
[]false
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Puede ver el resultado obtenido en Chrome 53 en el siguiente desplegable:

Esto de las coerciones merecería un tema para explicarlo, pero dejemos algunos apuntes. La comparación con coerción no sigue una lógica esperada. Por eso deberíamos usar la comparación estricta. Por ejemplo, la comparación "" == false resulta cierto. Sin embargo "abc" == true resulta falso. Lo esperable es que si una cadena vacía equivale a falso, una cadena no vacía resultara cierto.

Lo que está sucediendo es que al comparar un String con un Boolean JavaScript convierte ambos a número (usando el método interno toNumber()) y luego se comparan estrictamente. La conversión a número puede simularse usando el operador unario +. Una cadena vacía y el valor falso se convierten ambos a cero y se comparan siendo iguales. Por otro lado un valor falso se convierte al número 1, mientras que una cadena no vacía "abc" no puede convertirse a ningún número obteniéndose un NaN. Y NaN === 1 será falso.

//""==false es cierto
console.log(+"", +false); // 0 0
console.log(+"" === +false); // true
//"abc"==true es falso
console.log(+"abc", +true); // NaN 1
console.log(+"abc" === +true); // false
    

Esto de las coerciones previas a las operaciones puede darnos más de una sorpresa. En el siguiente código la operación + se efectua sobre cadenas, concatenándolas. Así la concatenación de cadenas prevalece sobre la suma de números, por lo que JavaScript coerciona el número 1 a la cadena "1" antes de aplicar la operación. Mientras que con la resta no sucede igual pues no hay una operación equivalente en String. En ese caso lo que se coerciona es la cadena "123" al número 123:

console.log("123" + 1); // 1231
console.log("123" - 1);  // 122
    

Podemos evitar todas estas anomalías de las coerciones y comparaciones manteniendo una coherencia en los tipos de datos a operar y, por supuesto, usando siempre la comparación estricta.