Propiedades de las funciones

Figura
Figura. Una función en JavaScript

En el tema anterior acerca de una introducción a las funciones comentamos que toda función es también un objeto en JavaScript. En la Figura puede ver una captura de una declaración de función que muestra sus propiedades. Especialmente hemos de entender la propiedad arguments pues tiene algunos usos interesantes. Dedicaremos una especial atención a los parámetros distinguiéndolos de los argumentos, aclarando el paso por valor o referencia de los argumentos en una llamada a una función.

ECMAScript 2015 (ES6) incorpora los párametros por defecto y la sintaxis para indicar resto de parámetros, similar al nuevo operador de propagación, novedades que veremos en este tema. También tocaremos las propiedades caller y callee explicando que el modo estricto las prohíbe. Dentro de __proto__, el prototipo a partir del cual se construyó la función, encontramos también los métodos call(), apply(), y bind() que son de mucha utilidad. Por ahora dedicaremos el resto de este apartado para hablar sobre las propiedades de una función.

Una de las propiedades es name que nos da el nombre de la función. Es algo que se introduce en ES6 aunque algunos navegadores ya la venían soportando parcialmente. Incluso a fecha de este tema el soporte de la especificación no es completo. Por ejemplo, Chrome 49 no devuelve el nombre en el caso de una expresión de función anónima asignada a una variable. En caso de usar esta propiedad debe consultar el soporte en Kangak ES6. En el siguiente código encontrará algunos ejemplos básicos de nombres de funciones.

//Una declaración de función
function sumar(a,b){
    return a+b;
}
console.log(sumar.name); // "sumar"
//Una expresión de función anónima
console.log((function (){}).name === ""); // true
//Una expresión de función anónima asignada a 
//una variable nos da el nombre de la variable
let sumar2 = function(a,b){
    return a+b;
};
console.log(sumar2.name); // "sumar2" (Nota: "" en Chrome 49)
//Una expresión de función nominada
let sumar3 = function sumarB(a,b){
    return sumarB.name;
};
console.log(sumar3.name); // "sumarB"
console.log(sumar3()); // "sumarB"
try {
    console.log(sumarB.name); 
} catch(e) {
    //sumarB sólo está definido dentro del cuerpo de su funcion
    console.log(e.message); //ERROR: sumarB is not defined
}
//Una función creada con el constructor Function()
let sumar4 = new Function("a", "b", "return a+b");
console.log(sumar4.name); // "anonymous"
    
Con estos temas sobre JavaScript acompaño trozos de código mostrando resultados en la consola del navegador, tras probarlos en el actual Chrome 49 al redactar este tema.

Dentro del cuerpo de una función accedemos a sus argumentos directamente con la referencia con la que se declararon. En los ejemplos anteriores hacemos return a+b porque esas dos variables "a" y "b" fueron declaradas como argumentos. Para ser más precisos deberíamos decir que fueron declaradas como parámetros. Miéntras que cuando hacemos sumar(1 ,2) lo que estamos pasando ya si son argumentos de la llamada a una función. Podemos resumir que son parámetros en la declaración y son argumentos en la ejecución. Esta diferenciación es importante como veremos a continuación.

Dentro del cuerpo tenemos acceso a los argumentos por medio del objeto arguments. En el siguiente ejemplo accedemos con sumar.arguments o directamente con arguments. Pero en modo estricto sólo podemos hacerlo de esta última forma, pues nos saltará un error diciendo que no se puede acceder a los argumentos con sumar.arguments.

function sumar(a, b){ // a y b son parámetros
    //"use strict";
    console.log(sumar.arguments); // [1, 2]
    console.log(arguments); // [1, 2]
    return a+b;
}
// 1 y 2 son argumentos
console.log(sumar(1, 2)); // 3
    

Con length obtenemos el número de parámetros de la función, mientras que en arguments tenemos todos los argumentos disponibles, por lo que arguments.length nos da el número de argumentos recibidos. Y pueden ser diferentes. En JavaScript sucede que podemos llamar a una función con menos argumentos de los necesarios. En el siguiente ejemplo la función declara dos parámetros y recibe un sólo argumento. En la ejecución la función primero asigna el valor undefined a los argumentos y luego va asignando valores recibidos en orden desde la izquierda. Los que no se reciban se quedan con undefined.

function ver(a, b){
    console.log(ver.length); // 2 parámetros
    console.log(arguments.length); // 1 argumento
    return 'a:' + a + ' b:' + b;
}
console.log(ver(1)); // a:1 b:undefined
    

También podemos ejecutarla con más argumentos que parámetros:

function ver(a, b){
    console.log(ver.length); // 2 parámetros
    console.log(arguments.length); // 4 argumentos
    return 'a:' + a + ' b:' + b;
}
console.log(ver(1, 2, 3, 4)); // a:1 b:2
    

A veces veremos el término aridad de una función significando el número de parámetros que se declararon y, por tanto, el número de argumentos que espera recibir una función.

Paso por valor de argumentos a funciones

Los argumentos con tipo String, Number o Boolean cuando fueron creados de forma literal se pasan por valor, es decir, se copia su valor. Por otro lado para los objetos cuando se pasa una referencia al objeto podría entenderse como un paso por referencia. Así cualquier cambio en la copia es como si la hubiésemos hecho en el original y viceversa. A veces vemos que se dice que en JavaScript todos los argumentos se pasan por valor, entendiéndose que se está pasando un valor de referencia que apunta a un objeto.

Ya hablé sobre esto en el tema de paso de argumentos a funciones, pero en el fondo aparte de los términos y definiciones que se usen, lo importante es saber lo que sucede. Por eso nos extendemos un poco más para ver con detalle que sucede con cada tipo de datos pasado en un argumento con el siguiente ejemplo:

//Tomamos los argumentos y los modificamos
function test(str, num, bol, arr, obj, obx, fun){
    str = str + "b";    // "ab"
    num = num + 1;    // 2
    bol = bol && false; // false
    arr.push(2);        // [1,2]
    obj.b = 2;          // {a:1,b:2}
    obx = {y:2};       // Reasignamos este objeto
    fun = function(){return 1;} //Modificamos la función
    return strVars(str, num, bol, arr, obj, obx, fun);
}
//Extraer una cadena con los valores
function strVars(str, num, bol, arr, obj, obx, fun){
    function itera(ob){
        let cad = "";
        for (let i in ob){
            if (cad!="") cad += ',';
            cad += `${i}:${ob[i]}`;
        }
        return cad;
    }
    return `str:"${str}", num:${num}, bol:${bol}, ` +
           `arr:[${arr}], obj:{${itera(obj)}}, ` +
           `obx:{${itera(obx)}}, fun:${fun}`;
}
    

Tenemos la función test() que recibe los tipos String, Number, Boolean, Array, Object y Function. Los modificamos y luego devolvemos una representación de texto usando la función auxiliar strVars(). Para probar el test adjudicamos una función en el evento click del botón:

//Manejador del botón
document.getElementById("test-paso-args").addEventListener("click",
function(){
    //Declaramos variables locales
    let str="a", num=1, bol=true, arr=[1], obj={a:1}, 
        obx={x:1}, fun = function(){return 0;};
    //Extraemos sus valores definidos aquí
    document.getElementById("pre-paso-args").innerHTML = 
        strVars(str, num, bol, arr, obj, obx, fun);
    //Envíamos a función test() que los modifica allí
    document.getElementById("paso-args").innerHTML = 
        test(str, num, bol, arr, obj, obx, fun);
    //Y volvemos a comprobar aquí cuáles resultaron modificados
    document.getElementById("pos-paso-args").innerHTML = 
        strVars(str, num, bol, arr, obj, obx, fun);
}, false)    
    

Ahora puede probar ese ejemplo:

Ejemplo: Paso de argumentos

En una función declaramos variables locales de los tipos String, Number, Boolean, Array, Object y Function. Estos son sus valores al momento de crearlas:

Luego envíamos esas variables para probar el paso de argumentos a otra función que modifica sus valores. El objeto obx fue reasignado. La función fun también fue modificada para que nos devolviese 1 en lugar de 0. La función test() nos devuelve esto:

Volvemos a comprobar en la función de partida que String, Number, Boolean y Function permanecen igual y por tanto fueron pasados por valor. Mientras que Array y Object fueron modificados y por tanto fueron pasados por referencia. La excepción es el Object obx cuya copia fue reasignada y por tanto perdió su referencia con el original así que éste no sufrió cambio alguno:

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

Se observa que String, Number, Boolean y Function fueron pasados por valor. Las modificaciones en las copias de los valores no afectaron a los originales. Sin embargo para Array y Object las modificaciones en las copias también se reflejan en los originales. Lo que se pasó fueron referencias a esas variables. Sin embargo el paso por referencia no es total, puesto que si reasignamos las copias de los objetos esto no afectará a los originales. En un verdadero paso por referencia no sucedería esto, por lo que suele decirse que se pasa una referencia como valor.

Podemos simular que pasamos variables literales Number, String o Boolean por referencia si las "envolvemos" en un objeto, como por ejemplo un array:

function fun(numRef, strRef, bolRef){
    numRef[0] += 1;
    strRef[0] += "b";
    bolRef[0] = bolRef[0] && false;
}
//Envolvemos las variables en un array
let x = [1], y = ["a"], z = [true];
fun(x, y, z);
console.log(x); // [2]
console.log(y); // ["ab"]
console.log(z); // [false]
    

Si lo que estamos buscando es lo contrario, es decir, pasar por valor los objetos, no nos queda más remedio que clonar el objeto en el destino. Esto es como reasignarlo en el destino pues se trata de iterar por todas sus propiedades copiándolas una a una en un nuevo objeto. En ES6 hay un método Object.assign(objDestino, objOrigen). Para variables literales y objeto built-in podemos simular algo simple usando un recursivo puesto que pueden haber objetos dentro de objetos:

//Para clonar un objeto
function clonar(obj){
    let newObj = (obj.constructor.name == "Array") ? [] : {};
    for (let i in obj){
        if (obj.hasOwnProperty(i)){
            if (typeof obj[i] == "object"){
                newObj[i] = clonar(obj[i]);
            } else {
                newObj[i] = obj[i];
            }
        }
    }
    return newObj;
}
    

Del ejemplo anterior vemos que sólo tenemos que diferenciar cuando el tipo sea object. Recuerde que para los constructores Array y Object el resultado de typeof siempre es "object". No hay un typeof que nos devuelva "array". Por eso determinamos al inicio si el nombre del constructor es "Array" para usar el literal []. El resto del código necesario para el ejemplo es el siguiente:

//Para representar un objeto como texto de código
function iterar(obj, nivel=-1){
    //Obviamos este código, puede verlo en el 
    //enlace del JS original más abajo
}
//manejador del botón del ejemplo
document.getElementById("clonar").addEventListener("click", 
function(){
    let objOrig = {
        str: "abc", 
        num: 123, 
        bol: true,
        fun: function (){return 0;},
        arr: [1, 2, 3],
        obj: {
            a: 567,
            b: ["x", "y"],
            c: {a:1, b:2}
        }
    };
    document.getElementById("obj-orig").innerHTML = iterar(objOrig);
    let conAssign = document.getElementById("usar-assign").checked;
    let objClon = (conAssign) ? 
        Object.assign({}, objOrig) : 
        clonar(objOrig);
    document.getElementById("obj-clon").innerHTML = iterar(objClon);
}, false);
    

La función iterar() simplemente nos servirá para verter en formato de texto un objeto y poder presentarlo en el siguiente ejemplo:

Ejemplo: Clonando objetos

Este es el objeto original:

Clonamos ese objeto que debe ser exactamente igual al anterior pero independientes:

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

Por lo tanto si no queremos tener efectos colaterales cuando manejemos tipos Object o Array provenientes de los argumentos, podemos clonar el objeto en la función de destino.

Analizando el objeto arguments de una función

Figura
Figura. El objeto arguments es como un Array pero no es un Array.

Nos detendremos ahora para observar que es exactamente arguments. Si tengo la función function ver(a, b) y ubico en su interior la sentencia console.log(arguments), tras la llamada ver(1, 2, 3, 4) podríamos haber visualizado en la consola de nuestro navegador algo como la Figura, donde se muestran las propiedades del objeto arguments. En el tema anterior hicimos un ejemplo de propiedades de las variables donde también puede observarlo con la última opción Argumentos de una función.

La propiedad Symbol es algo nuevo de ES6 y por ahora no la vamos a mencionar. Lo que vemos es que arguments se parece a un array. Pero no lo es. Si en la consola desplegamos el prototipo del objeto veremos que su constructor no es Array() sino Object(). No es un array pero podemos iterar por él. En esos casos JavaScript también incluirá una propiedad length.

function iterar(){
    console.log(typeof arguments); // "object"
    let str = "";
    for (let i=0, maxi=arguments.length; i<maxi; i++){
        str += arguments[i] + '-';
    }
    return str;
}
console.log(iterar(1,2,3,4)); // "1-2-3-4-"
    

En el siguiente ejemplo observamos que para arguments el nombre de su constructor es "Object" y no "Array". Lo pasamos a un array usando el método slice(). Con unArray.slice(inicio[, fin]) recortamos un trozo de unArray empezando en inicio y hasta el fin opcionalmente indicado. Si no se indica se tomará hasta el final del array. Para aplicarlo a arguments usaremos la función call() que veremos en uno de los apartados más abajo. Esta función nos permite aplicar métodos de un objeto en otro objeto, pasando los argumentos a continuación.

function iterar(){
    //arguments es como un array, pero su constructor es Object
    console.log(arguments);  // [1, 2]
    console.log(typeof arguments); // "object"
    console.log(arguments.constructor.name); // "Object"
    //Podemos pasarlo a un array cuyo constructor es Array
    let arr = Array.prototype.slice.call(arguments, 0);
    console.log(arr); // [1, 2]
    console.log(typeof arr); // "object"
    console.log(arr.constructor.name); // "Array"
    //Ahora arr es un array y podemos aplicar métodos de array
    return arr.concat([3, 4]); 
}
console.log(iterar(1, 2)); // [1, 2, 3, 4]
    

Como con todas las funciones y slice() lo es, hay que tener en cuenta si pasamos objetos o arrays en los argumentos, puesto que se pasarán por referencia como comentamos en un apartado anterior.

El modo estricto restringue el uso de arguments

El modo estricto de JavaScript restringue algunas cosas para mejorar el lenguaje. Fue incorporado en ES5 y, entre otras particularidades, asegura arguments como una identificador reservado.

Cuando extraemos las propiedades de una función vemos que arguments es una de ellas. En modo normal (no estricto) podemos acceder a los argumentos dentro de una función denominada fun con fun.arguments, o bien directamente con el identificador arguments. En modo estricto sólo es posible con el identificador.

function sumar(a, b){
    "use strict";
    //Podemos usar el identificador arguments en una función
    console.log(arguments); // [1, 2]
    //Pero en modo estricto no podemos usar la propiedad arguments
    console.log(sumar.arguments); 
    // Normal: [1, 2];
    // Estricto: TypeError: 'caller' and 'arguments' are restricted 
    //           function properties and cannot be accessed in this
    //           context.
    return a+b;
}
sumar(1, 2); 
    

En modo estricto el identificador arguments no puede usarse para indentificar cualquier objeto o variable o incluso como nombre de un parámetro, o en la parte izquierda de una asignación. En resumen, viene a ser casi como una palabra reservada que sólo debe usarse en el contexto de una función, no pudiendo reasignarse como objeto, aunque si individualmente.

//No usar arguments como nombre de variable
(function(){
    "use strict";
    let arguments = 10;
    console.log(arguments); 
    // Normal: 10
    // Estricto: SyntaxError: Unexpected arguments in strict mode
})();

//No usar arguments como nombre de parámetro
(function(arguments){
    "use strict";
    console.log(arguments); 
    // Normal: 123
    // Estricto: SyntaxError: Unexpected arguments in strict mode
})(123);

//No reasignar arguments
(function(){
    "use strict";
    arguments = [1, 2];
    console.log(arguments); 
    // Normal: [1, 2]
    // Estricto: SyntaxError: Unexpected arguments in strict mode
})();
    
Con la nueva versión de Chrome 50 disponible a mediados de abril 2016 he comprobado que en modo normal no podemos usar let para redeclarar arguments dentro del cuerpo de una función, lanzando el error Identifier 'arguments' has already been declared. De hecho una de las características de let es impedir redeclaraciones de variables y parámetros (con var si sería posible). Y es que arguments no deja de ser otra cosa que una variable y es correcto que se impida la redeclaración en modo normal y estricto.
(function(){
    'use strict';
    let arguments = 10;
    //Normal: Identifier 'arguments' has 
    //        already been declared
    //Strict: Unexpected arguments in
    //        strict mode
})();
    
Firefox 45 sigue comportándose como la versión anterior Chrome 49 no acusando error alguno en modo normal.

Ya vimos que los parámetros se corresponden con los argumentos recibidos en el mismo orden de izquierda a derecha. En modo normal podemos cambiar el valor a un parámetro y se actualiza el correspondiente argumento (y viceversa). En modo estricto también lo podemos hacer pero no se actualizan entre ellos.

 //Reasignando el parámetro
(function(x){
    "use strict";
    x = 55;
    console.log(x + ', ' + arguments[0]); // Normal "55, 55"
                                          // Estricto "55, 99"
})(99);
//Reasignando el argumento
(function(x){
    "use strict";
    arguments[0] = 55;
    console.log(x + ', ' + arguments[0]); // Normal "55, 55"
                                          // Estricto "99, 55"
})(99);
    

Parámetros por defecto en ES6

JavaScript no tenía hasta ES6 la facilidad de los parámetros por defecto. Como vimos en un apartado anterior podemos enviar más o menos argumentos que parámetros. Los que no se reciban se inicializan con el valor undefined. En la función del siguiente código disponemos de parámetros de los tipos String, Number, Array y Object. Comprobamos que contienen al inicio y tras evaluarlos los volvemos a mirar. Se trata de realizar una disyuntiva entre el valor recibido y el valor por defecto que queramos asignar. Por ejemplo, para String si no recibimos ese argumento le ponemos el valor "a".

function fun(str, num, arr, obj) {
    console.log([str, num, arr, obj]);
    str = str || "a";
    num = num || 1;
    arr = arr || [];
    obj = obj || {};
    console.log([str, num, bol, arr, obj]);
}
//Una llamada sin argumentos
fun(); 
// [undefined, undefined, undefined, undefined]
// ["a",       1,         [],        {}]
//Una llamada con el primero y tercer argumentos, 
//es necesario pasar el segundo como undefined
fun("abc", undefined, [1,2]);
// ["abc", undefined, [1,2], undefined]
// ["abc", 1,         [1,2],        {}]
//En todo caso los argumentos que se pasen equivaliendo
//a falso (null, undefined, "", 0) se omiten
fun(null, "", undefined, 0);
// [null, "", undefined, 0]
// ["a",  1,  [],        {}]
    

Pero con esa forma de hacer las cosas hay detalles que tener en cuenta. La operación booleana es tal que undefined, "", null y 0 se evalúan a falso. Por lo que lo anterior fallaría si quisiéramos pasar valores vacíos o nulos. Esto son "" para String, el 0 para Number y el valor null para Array y Object:

//Lo anterior no funciona si quiero expresamente 
//pasar valores vacíos o nulos
fun("", 0, null, null);
// ["a", 1, [], {}]
    

Para evitarlo podemos comprobar si el tipo pasado es undefined para asignarle el valor por defecto, o si no usar el argumento:

function fun(str, num, arr, obj) {
    str = (typeof str !== "undefined") ? str : "a";
    num = (typeof num !== "undefined") ? num : 1;
    arr = (typeof arr !== "undefined") ? arr : [];
    obj = (typeof obj !== "undefined") ? obj : {};
    console.log([str, num, arr, obj]);
}
//Con esta forma aún va bien cuando no hay argumentos
fun();
// ["a", 1, [], {}]
//y también podemos pasar valores vacíos/nulos
fun("", 0, null, null);
// ["", 0, null, null]
    

Con ES6 todo esto es ahora más fácil.

function fun(str="a", num=1, arr=[], obj={}) {
    console.log([str, num, arr, obj]);
}
fun();
// ["a", 1, [], {}]
fun("", 0, null, null);
// ["", 0, null, null]
    

Los valores por defecto aceptan también expresiones, como en este tercer párametro z=x+y:

function fun(x=1, y=2, z=x+y) {
    console.log([x, y, z]);
}
fun(); // [1, 2, 3]
    

Las expresiones a la derecha se resuelven con los valores recibidos en los argumentos de la izquierda. En el siguiente ejemplo no definimos valor por defecto para el segundo parámetro, por lo que tomará el valor undefined y, por tanto, la expresión no podrá ser calculada. El valor NaN (Not a Number) es una propiedad global que se devuelve cuando JavaScript intenta realizar un cálculo matemático con valores que no son números.

function fun(x=1, y, z=x+y) {
    console.log([x, y, z]);
}
fun(); // [1, undefined, NaN]
    

Un valor por defecto también puede ser una expresión de función. En el siguiente ejemplo definimos el valor por defecto del tercer parámetro una función que realiza la suma. Luego en una de las llamada le pasamos otra función que realiza la multiplicación.

function fun(x=0, y=0, operar=function(a,b){return a+b;}){
    console.log(operar(x,y));
}
fun(); // 0
fun(3, 7); // 10
fun(3, 7, function(a,b){return a*b}); // 21
    

Por último hay un par de cosas que tener en cuenta. Chrome 49 no permite usar parámetros por defecto en modo estricto, lanzando el error Illegal 'use strict' directive in function with non-simple parameter list. Pero Firefox 45 si lo permite. He buscado información sobre esto pero no la he encontrado. Es de suponer que Chrome debería soportarlo en el modo estricto.

Por otro lado en modo normal los parámetros están vinculados con los argumentos. Cuando modificamos uno se cambia el otro. Esto lo vimos al final del apartado anterior. En modo estricto no están vinculados, con lo que modificar uno no afecta al otro. Tampoco lo están en cualquiera de los dos modos cuando hay parámetros con valores por defecto.

Sintaxis ES6 para el resto de parámetros y el operador de propagación

Ya vimos que arguments es como un array (array-like) y que teníamos que transformarlo en un array si queríamos aplicarle métodos de Array. En la versión ES4, que nunca llegó a ser estándar, se pensó en sustituir arguments por otra cosa que fuera un array. Ahora en ES6 vuelven a recuperar esa idea pero manteniendo también arguments. Es lo que se conoce como resto de parámetros con el mismo funcionamiento que el operador de propagación (spread operator).

Podemos tener la siguiente función para sumar un parámetro más un número indeterminado de argumentos:

function sumar(a){
    let suma = a;
    for (let i=1, maxi=arguments.length; i<maxi; i++){
        suma += arguments[i];
    }
    return suma;
}
console.log(sumar(1, 2, 3, 4, 5, 6)); // 21
    

En ese ejemplo iteramos por arguments desde la posición segunda (i=1) acumulando la suma. Para usar la sintaxis de resto de parámetros pondríamos ... antes del nombre del parámetro resto de argumentos (puede ser cualquier nombre de variable permitido):

function sumar(a, ...args){
    let suma = a;
    for (let i=0, maxi=args.length; i<maxi; i++){
        suma += args[i];
    }
    return suma;
}
console.log(sumar(1, 2, 3, 4, 5, 6)); // 21
    

En ese código ...args traerá en un array con el resto de argumentos, accediendo a él con la referencia args e iterando desde la primera posición (i=0). Esta sintaxis ha de ir al final de la lista de parámetros, antes podemos pasar explicítamente cero o más de ellos, pero no después. Para ver que ...args trae un array podemos aplicar su método reduce() que consiste en usar un callback para ir reduciendo los elementos de un array en parejas hasta obtener un único resultado simple:

function sumar(a, ...args){
    return a + args.reduce(function(x, y){
        return x+y;
    });
}
console.log(sumar(1, 2, 3, 4, 5, 6)); // 21
    

El operador de propagación funciona de forma similar en otros contextos. Por ejemplo, en la llamada a una función que admite un número indeterminado de argumentos como Math.max():

let val = [1, 2, 3, 4, 5, 6];
//En lugar de Math.max(1,2,3,4,5,6) hacemos
console.log(Math.max(...val)); // 6
    

Este operador es útil para muchas cosas. A medida que vayamos usándolo iremos aprendiendo sus posibilidades. Por ejemplo, el método push(val1, val2, ..., valN) nos permite agregar un número indeterminado de valores al final de un array. Con el operador de propagación podemos concatenar dos arrays tambien con push():

let arr = [1, 2];
arr.push(3, 4);
console.log(arr); // [1,2,3,4]
let arrB = [5, 6];
arr.push(...arrB);
console.log(arr); // [1,2,3,4,5,6]
    

El operador de propagación no sólo se limita a parámetros y argumentos. También es posible usarlo directamente dentro de un array. El código siguiente haría exactamente lo mismo que el anterior, y de forma más simple:

let arr = [1, 2], arrB = [5, 6]
console.log([...arr, 3, 4, ...arrB]); // [1,2,3,4,5,6]
    

Es una característica con grandes posibilidades y que está ampliamente soportada por los navegadores principales, por lo que tendremos que empezar a acostumbrarnos a usarlo.

caller y callee prohibidos en ES6

Caller y callee
Caller y callee en Chrome 49Imagen no disponible
En Chrome 49 sumar.caller apunta a fun() desde donde se llamó y arguments.callee apunta a sumar() a la que pertenece estos argumentos.

Las propiedades caller y callee merecen un poco de atención. En la primera imagen de la serie adjunta verá un composición que hemos hecho a partir del Developer Tools de varios navegadores. Hemos partido de un código con la función sumar(a, b) ubicada en el alcance global. Luego auto-ejecutamos fun() para llamar a esa función, poniendo un punto de interrupción antes de la devolución para observar el estado de las variables.

Vemos que caller es una propiedad de sumar() y está apuntando a la función desde donde se hizo la llamada. Dígamos que sumar.caller es el objeto llamador, la función fun(). Mientras que callee pertenece a arguments y apunta a la propia función. Podemos decir que arguments.callee es el objeto destinatario de la llamada, devolviéndonos el objeto función sumar() a la que pertenecen esos argumentos.

En Firefox aparecen también callee y caller, éste último con valor nulo, aunque al ejecutar el ejemplo siguiente si lo actualiza. En IE 11 sólo vemos callee pero también aparece caller con la ejecución del ejemplo. Para la ejecución disponemos dos funciones, una sumar(a, b) en modo normal y sumarStrict(a, b) en modo estricto.

Ejemplo: Caller y callee

function sumar(a, b){
    // "use strict";
    return a+b;
}
(function fun(){
    sumar(1, 2);
})();
        
function sumarStrict(a, b){
    "use strict";
    return a+b;
}
(function fun(){
    sumarStrict(1, 2);
})();
        
arguments:
sumar.arguments:
sumar.caller:
arguments.callee:
Este ejemplo no usa ES6 para ver el funcionamiento en navegadores que no lo soporten. Se llama a la función sumarStrict() en modo estricto, pero el resto del código está en modo normal. Ver el texto para más información. Puedes consultar el código JS original de este ejemplo.

Si activa la opción de modo estricto comprobará que los navegadores no permiten el acceso a caller y callee. Tampoco podrá accederse a sumar.arguments, manteniéndose el acceso sólo con el identificador arguments dentro de la función sumarStrict().

La historia de callee tiene que ver con la necesidad de tener una referencia a la propia función en ejecución. Cuando JavaScript no disponía de las expresiones de función nominadas nos encontrábamos con ese problema. En el tema anterior vimos un apartado sobre la creación de funciones exponiendo un recursivo, siendo uno de los motivos de poner un nombre a las expresiones de funciones anónimas:

let factorial = function fact(n) {
    if (n>1) {
        return n * fact(n-1);
    } else {
        return 1;
    }
};
console.log(factorial(7)); // 5040
    

El nombre fact tiene un alcance limitado al interior de la función, lo que nos permite resolver problemas como esos. Pero antes de esa posibilidad había que usar callee para obtener una referencia a la propia función.

//SIN "use strict";
//Antes de que las expresiones de función pudieran tener nombre...
let factorial = function (n) {
    if (n>1) {
        //...era necesario usar callee para referirse a sí misma
        return n * arguments.callee(n-1);
    } else {
        return 1;
    }
};
console.log(factorial(7)); // 5040
    

Otra cosa a tener en cuenta al usar callee en un recursivo es que this apunta a window (el objeto global) al entrar, pero en las siguientes llamadas this es el objeto arguments. Esto no sucede con el otro ejemplo, apuntando siempre this al objeto global.

Por otro lado caller tampoco funcionará en modo estricto. Esta propiedad nos da una referencia a la función que hizo la llamada, como vimos en el ejemplo que hicimos más arriba. Vea que la función fun() es anónima autoejecutable y su permanencia en memoria queda atada a la referencia que se mantiene a través de sumar.caller:

function sumar(a, b){
    console.log(sumar.caller) // fun(){...}
    return a+b;
}
(function fun(){
    sumar(1, 2);
})();
    

Podríamos conseguir lo mismo sin usar caller pasándole la referencia en un argumento:

function sumar(a, b, miCaller){
    console.log(miCaller); // fun(){...}
    return a+b;
}
(function fun(){
    sumar(1, 2, fun);
})();
    

En resumen, tanto callee como caller no se permiten en modo estricto. Y es posible que debamos empezar a dejar de usarlo. Yo no he podido encontrar casos de uso significativos donde sean imprescindibles y no puedan ser resueltos con otras opciones. Las páginas de Mozilla advierten lo siguiente:

  • Function.caller: Esta característica no es estándar y no está previsto que lo sea. No la use en sitios web en producción: no funcionará para todos los usuarios. Podría haber muchas incompatibilidades entre implementaciones y el comportamiento podría cambiar en el futuro.
  • arguments.callee: La 5ª edición de ECMAScript (ES5) prohíbe el uso de esta característica en modo estricto. Evítela usando expresiones de funciones nominadas o con una declaración de función cuando una función deba llamarse a sí misma.

Los métodos del prototipo de Function call, apply y bind

Los métodos del prototipo de Function son muy pocos pero de gran importancia: call(), apply() y bind(). En el siguiente ejemplo puede consultar dichos métodos disponibles en este navegador.

Ejemplo: Métodos Function en este navegador

en este navegador

Además de los anteriores también verá el método toString(). Está en todos los objetos de JavaScript para representar como una cadena de texto el propio objeto. Es heredado del objeto básico Object y se sobrescribe en las instancias. Por ejemplo, en el caso de Function se presenta el texto del código de la función. Cuando no hay sobrescritura se expone el tipo como [object TIPO]. Los objetos como instancias de Object se representan con [object Object]:

console.log("abc".toString()); // "abc"
console.log((123).toString()); // "123"
console.log(true.toString());  // "true"
console.log([1,2,3].toString()); // "1,2,3"
function fun(a){return a;};
console.log(fun.toString()); // "function fun(a){return a;}"
console.log({a:1, b:2}.toString()); // "[object Object]"
    

Los métodos call() y apply() hacen exactamente lo mismo: aplicar una función con un valor determinado para this y pasándole una lista de argumentos. Ya hice una breve introducción a call y apply hace tiempo que quizás le interese consultar. En el caso de call() la lista se pasa como los argumentos de una llamada ordinaria a una función, mientras que con apply() se pasan en un array. Un ejemplo de uso de call() ya lo vimos en un apartado anterior. Se trata de convertir arguments en un array usando el método slice de Array:

function fun(a, b, c) {
    let args = Array.prototype.slice.call(arguments, 0); 
    console.log(args); // [1, 2, 3]
    //También es posible así:
    let args2 = [].slice.call(arguments, 0);
    console.log(args2); // [1, 2, 3]
}
fun(1, 2, 3);
    

Cuando usamos métodos de un objeto built-in como Array.prototype.slice.call() podemos abreviarlo con [].slice.call(), pues se creará un array vacío e inmediatamente se ejecutará su método del prototipo. De esta forma abreviamos algo de código pues el coste de crear un array vacío no es muy significativo.

Para hacer un ejemplo de aplicación de apply() considere el siguiente código. Se trata de un array con valores númericos y queremos calcular la media de esos valores. Pero además queremos la media truncada, que se obtiene descartando los valores mínimo y máximo de la serie.

let valores = [10, 12, 2, 15, 11, 88]; 
let suma = valores.reduce(function(a, b){return a+b;});
/* El metodo reduce() de un array sirve para sumar 
sus elementos. Sería lo mismo que hacer esto:
let suma = 0;
for (let i=0, maxi=valores.length; i<maxi; i++){
    suma += valores[i];
}
*/
let media = suma/valores.length;
console.log(media); // 23
//Para obtener la media truncada hemos de deducir
//los valores máximo y mínimo de la suma
let max = Math.max.apply(null, valores);
let min = Math.min.apply(null, valores); 
let mediaTruncada = (suma-max-min)/(valores.length-2);
console.log(mediaTruncada); // 12
    

Lo primero que hacemos es obtener la suma de los valores. Usamos el método de Array reduce() que ya vimos antes. Para obtener la media truncada tenemos que encontrar los valores máximo y mínimo. Las funciones Math.max(v1, v2, ...) y Math.min(v1, v2, ...) nos permiten encontrar esos extremos entre los valores pasados en sus argumentos. En el código anterior aplicamos con apply() esos métodos a los argumentos que se pasan en el array de valores. Luego deducimos esos extremos para obtener la media truncada.

Muchos de los métodos de Array se pueden aplicar a otros objetos que se parecen a un array, pues podemos iterar por ellos. Es el caso de los String. Con el método que vimos antes reduce() también lo podemos aplicar a un String para invertir el orden de los caracteres. En este caso usamos call para pasar el callback de reduce() como su primer argumento:

let str = 'ABCDEFGHIJK';
let rts = [].reduce.call(str, function(anterior, actual){
    return actual + anterior;
});
console.log(rts); // KJIHGFEDCBA
    

Los métodos anteriores call() y apply() son llamadas a una función. Devolverán lo que esa función devuelva. Por otro lado bind() devuelve siempre una función, tambien fijando el this y los argumentos que se le pasen. En el tema anterior vimos un uso para obtener una aplicación parcial con bind(). En el siguiente código se expone un uso para bind() que nos evitará tener que repetir la llamada a un método de un prototipo como el del ejemplo último Array.prototype.reduce.call() o su forma abreviada [].reduce.call():

let reducir = Function.prototype.call.bind([].reduce);
//A partir de aquí podemos usar reducir(likeArray, callback) 
let str = 'ABCDEFGHIJK';
let rts = reducir(str, function(anterior, actual){
    return actual + anterior;
});
console.log(rts); // KJIHGFEDCBA
    

Se trata de fijar call(), método de Function, al método [].reduce y obtener una nueva función reducir(likeArray, callback), lo que nos permitirá aplicar reduce() de una forma abreviada sobre cualquier objeto que sea como un array (like array).

Otro ejemplo de uso de bind() es para fijar la referencia del this cuando declaramos un evento de un elemento HTML. En este código de ejemplo tenemos un objeto con una propiedad y un método. Luego adjudicamos un evento al elemento que llame al método del objeto. En principio funciona, pero si luego dentro del método hacemos this.prop ese this está apuntando al elemento HTML y no al objeto.

<div id="xxx">XXX</div>
<script>
    let obj = {
        prop: "valor",
        metodo: function(event){
            console.log(event.type);
            console.log("this:", this); 
            console.log("this.prop:", this.prop);
            /* Para el primer evento el this apunta al elemento HTML
            mousedown
            this: <div id="xxx">XXX</div>
            this.prop: undefined

            Para el segundo evento si está apuntando al objeto
            mouseup
            this: Object {prop: "valor"}
            this.prop: valor
            */
        }
    };
    let elemento = document.getElementById("xxx")
    elemento.addEventListener("mousedown", obj.metodo, false);
    elemento.addEventListener("mouseup", obj.metodo.bind(obj), false);
</script>    
    

Esto se soluciona haciendo obj.metodo.bind(obj) que fija el this a ese objeto, como se observa en el segundo evento del código anterior. En el siguiente y último apartado veremos un ejemplo más largo sobre esto mismo de fijar el this en los eventos, pero usando un constructor de objetos.

Otro detalle interesante es que con bind() podemos pasar argumentos al manejador, que de otra forma tendríamos que usar una función intermedia:

<div id="xxx">XXX</div>
<script>
    //Si requerimos el evento, para Firefox hay que 
    //explicitarlo en un argumento.
    function manejador(a, b, event){
        console.log(event);       // mousedown {...}      mouseup {...}
        console.log(this);        // Window {...}         <div id="xxx">
        console.log(event.target);// <div id="xxx">       <div id="xxx">
        console.log(a+b);         // 3                    3
    }
    let elemento = document.getElementById("xxx");
    //Para pasar argumentos al manejador necesitamos usar una función.
    elemento.addEventListener("mousedown",
        function(event){
            //Para Firefox es necesario trasladar el evento
            //en caso de necesitarlo en el manejador.
            manejador(1, 2, event);
        }, false);
    //Pero con bind() podemos acompañar argumentos
    elemento.addEventListener("mouseup", 
        //Podemos pasar en this este elemento.
        //Si pasamos null allí tendremos Window.
        //Con bind no es necesario trasladar event.
        manejador.bind(elemento, 1, 2), 
        false);
</script>
    

Fijando la referencia this en un evento de un elemento HTML

A continuación haremos un ejemplo interactivo para exponer diversas técnicas para fijar el this en la declaración de eventos dentro de un objeto. Tenemos una función constructora que nos devuelve un objeto con sólo una propiedad asignada con this.nombre. Al construir una instancia nueva ubicamos un elemento HTML, un <button>, en la página. Luego le adjudicamos un evento a ese botón que llamará al método() en el prototipo del objeto:

"use strict";
//Constructor de elementos en el DOM que manejamos con objetos JS
function Constructor(nombre){
    //Agregamos una propiedad
    this.nombre = nombre;
    //Configuramos un elemento HTML
    let elemento = document.createElement("button");
    elemento.type = "button";
    elemento.id = this.nombre;
    elemento.textContent = this.nombre;
    //Adjudicamos evento click según instancia
    if (this.nombre=="noFunciona"){
        elemento.addEventListener("click", this.metodo, false);
    } else if (this.nombre=="xJS.nombreInstancia"){
        elemento.setAttribute("onclick", this.nombre + ".metodo()");
    } else if (this.nombre=="thisPorThat"){
        let that = this;
        elemento.addEventListener("click", function(){
            that.metodo();
        }, false);
    } else if (this.nombre=="conClosure"){
        elemento.addEventListener("click", (function(that){
            return function(){
                that.metodo();
            };
        })(this), false);
    } else if (this.nombre=="conCall") {
        let that = this;
        elemento.addEventListener("click", function(){
            Constructor.prototype.metodo.call(that);
        }, false);
    } else if (this.nombre=="conBind"){
        elemento.addEventListener("click", 
            Constructor.prototype.metodo.bind(this), 
            false);
    } else if (this.nombre=="conArrow"){
        elemento.addEventListener("click", 
            () => this.metodo(), 
            false);
    }
    //Agregamos el elemento al DOM
    document.getElementById("conten-bind").appendChild(elemento);
}
//Agregando métodos al prototipo
Constructor.prototype.metodo = function(){
    //Este método nos presenta el objeto que hay aquí en this
    //usando (this.ver) ? this.ver() : this.toString();
    //También se actualiza el texto del código y comentario
    //(ver código en el enlace junto al ejemplo)
};
Constructor.prototype.ver = function(){
    //Presenta el objeto
    //(ver código en el enlace junto al ejemplo)
};
//En este objeto pondremos todas las instancias
var xJS = {};
//Creamos los elementos en el DOM y a la  vez instancias del Constructor
document.getElementById("boton-bind").addEventListener("click", 
function(event){
    xJS.objNoFunciona = new Constructor("noFunciona");
    //En este han de coincidir nombre variable y argumento
    xJS.nombreInstancia = new Constructor("xJS.nombreInstancia");
    xJS.objThisPorThat = new Constructor("thisPorThat");
    xJS.objConClosure = new Constructor("conClosure");
    xJS.objConCall = new Constructor("conCall");
    xJS.objConBind = new Constructor("conBind");
    xJS.objConArrow = new Constructor("conArrow");
    event.target.disabled = true;
}, false);
    

Para abreviar obviamos el código de los métodos que podrá consultar en el enlace del ejemplo. Usamos seis posibles formas de adjudicación del evento. La última con bind() debería ser la óptima.

Ejemplo: Diferentes formas de adjudicar eventos dentro de objetos

El objeto this al ejecutar el metodo():
Código que adjudica el manejador del evento:
Comentario:

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

El método llamará a this.ver() que se encarga de representar el objeto que hay en this. La primera forma con elemento.addEventListener("click", this.metodo, false) no funcionará, porque el this estará apuntando al elemento que causó el evento del botón. Ese elemento no tiene un método this.ver(), disponiendo en el ejemplo que si no lo tuviera aplique this.toString(). Como vimos en un apartado anterior, todos los objetos disponen de este método, presentándose entonces [object HTMLButtonElement].

La segunda forma consiste en pasar el nombre de la instancia donde creamos el objeto como una propiedad del mismo. En el ejemplo vamos a crear los objetos en window.xJS para lo cual hacemos var xJS = {} en el cuerpo del script. En cualquier caso ha de ser en un objeto con alcance global. Con xJS.nombreInstancia = new Constructor ("xJS.nombreInstancia") tenemos que la variable de la instancia coincide con el string del argumento para el nombre. Ahora no usamos addEventListener() para adjudicar el evento, sino directamente con onclick haciendo elemento.setAttribute("onclick", this.nombre + ".metodo()"). Finalmente tendríamos el siguiente elemento HTML construido en la página:

<button type="button" onclick="xJS.nombreInstancia.metodo()">

En lugar de usar document.createElement() y elemento.setAttribute() podíamos haber construido un literal HTML así:

let html = '<button type="button" id="' + this.nombre + '" ' +
           'onclick="' + this.nombre + '.metodo()">' + 
           '···</button>';
document.getElementById("conten-bind").innerHTML = html;
    

Los tres siguientes thisPorThat, conClosure y conCall necesitan fijar el this del objeto que estamos construyendo en el closure de la función anónima que estamos adjudicando en el evento. Referenciando this a una nueva variable con cualquier nombre como that podemos conseguirlo. O fijándolo a través del argumento de una función autoejecutable. El caso de call no aporta nada nuevo, pues también hay que fijar this.

Por último bind() está específicamente creado para resolver problemas de este tipo. Así con Constructor.prototype.metodo.bind(this) fijamos el this a la función método() en el prototipo del constructor, devolviendo ese método con el correcto this apuntando al objeto creado.

El uso del método bind() para arreglar un problema como el anterior es algo que ya podíamos hacer con ES5. Pero ES6 trae algo nuevo que incluso llega a simplificar más este problema con this. Se trata de las funciones flecha (arrow functions) que veremos en el tema siguiente.