Propiedades y métodos de las funciones en ES6
Propiedades de las funciones
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"
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:
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:
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
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 })();
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
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
:
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 navegadorAdemá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 objetothis
al ejecutar el metodo()
:Código que adjudica el manejador del evento:Comentario: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.