He de admitir que el desconocimiento de estos métodos, junto con la sintáxis Class fueron los mayores desencadenantes de querer profundizar más en los fundamentos del lenguaje.
Ahora que te los he mencionado, los comenzarás a ver en todas partes. En realidad ya estaban ahí pero, ¿sabías que hacen?. Pues son muy populares, y mucho más en proyectos ES5 y previos.
Estos métodos forman parte de la base de la programación orientada a objetos de JavaScript, y son cruciales para entender el lenguaje y es una pena que las sintáxis como Class y la keyword new los estén dejando en deshuso. Y digo que es una pena porque son fundamentales y muy importantes en el sistema de herencia prototipal, mientras que los otros son sugar syntax que lo único que hacen es oscurecer el lenguaje.
Antes de comenzar, estos dos métodos están disponibles en el prototype del objeto global Function:
Function.prototype.call
Function.prototype.apply
En este post mostraré ejemplos de uso para que lo entiendas y pongas a prueba.
Métodos call() y apply()
Antes de nada y te voy a ahorrar dolores de cabeza, son exactamente lo mismo. La única diferencia reside en el segundo argumento, donde call() será un listado de argumentos infinito y apply() será un array.
fn.call(this, arg1, arg2, arg3...)
fn.apply(this, [arg1, arg2, arg3...])
Un tip que me ayuda a recodar cuál es cuál es la letra C de call, la cual me recuerda a comas; la a de apply, me recuerda a array
Vamos a un ejemplo con call(). Vamos a crear una pseudoherencia clásica. Se trata de una pseudoherencia clásica porque estamos definiendo la estructura del objeto final, "instancia" de Person.
De manera sencilla, llamamos a la función Person a la cual le pasamos primero un objeto vacío como this, y a continuación los argumentos necesarios por la función.
const alberto = Person.call({}, 'male', 18);
¿Sabes que habría pasado si en vez de poner {} hubiésemos puesto this? Habría pasado lo siguiente:
Como puedes ver alberto ahora tiene un montón de propiedades nuevas, y esto se debe a que this en el momento de ser ejecutado con .call hace referencia al objeto window del navegador (o global si estamos en Node como es el caso), y por lo tanto estaríamos pasando como contexto a Person un objeto no deseado.
¿Y si en vez de usar this y call(), llamo a la función directamente?
function Human(gender) {
this.gender = gender;
this.isAlive = true;
}
function person(gender, age) {
// Está recibiendo un this implícito
// this = global: { ... }
Human.call(this, gender);
// this = global: { ..., gender: 'male', isAlive: true }
this.age = age;
// this = global: { ..., gender: 'male', isAlive: true, age: 18 }
return this;
}
const alberto = person('male', 18); // Invocando a la función sin call()
/*
Es lo mismo! La transformación la ejecuta el parser de JS (internals)
person('male', 18) 'implícito' === 'explícito' person.call(this, 'male', 18)
*/
console.log(alberto);
Sucedería exactamente lo mismo. Podrás observar varias cosas:
No he usado el sandbox porque por lo visto tiene algún mecanismo de seguridad para prevenir esta práctica, dado que supone un riesgo de seguridad. Puedes probarlo en tu navegador o en Node.
He renombrado Person a person, y eso se debe a lo mismo. Se creó una regla para que no se pueda llamar a las funciones directamente si comienzan en letra capital, dado que puede darse el caso de que un desarrollador llame a la función directamente y el this quede referenciado al global/window.
Hay que andarse con ojo a la hora de usar this . Y este es uno de los principales motivos por los que se empiezan a crear nuevos mecanismos como new y Class, para evitar equivocaciones y proveer de una opción más sencilla a los desarrolladores que vienen de lenguajes OOP de clases.
Ahora toca el turno a explicar qué sucede dentro de Person. Retomamos el primer ejemplo. Como puedes ver volvemos a hacer uso de call()^2, pero esta vez en vez de {} usamos la palabra this^3. El this siempre depende del contexto desde el cual se ha ejecutado, y en este caso proviene del call(this)^1 de alberto, que es {}.
(el símbolo > lo uso para que podáis encontrar lo que digo)
function Person(gender, age) {
// this = {}
Human.2>call(3>this, gender);
...
const alberto = Person.1>call(1>{}, 'male', 18)
Por lo tanto, continuando la llamada, Human continúa recibiendo el objeto vacío a través del contexto^3 que estamos enviando explícitamente a través de call(this)^2
Aprovecho para mencionar, que es usual decir contexto y referirnos a this, dado que todo this dependerá del contexto desde el que se le llame.
Todo this dependerá del contexto desde el que se le llame. Es por lo tanto una referencia contextual.
function Human(gender) {
3> // this = {} ( proviene de Human.call(this, gender) )
this.gender = gender;
this.isAlive = true;
}
function Person(gender, age) {
// this = {}
Human.2>call(2>this, gender);
...
const alberto = Person.1>call(1>{}, 'male', 18)
Ahora viene lo bonito 💕❤🌹🎉en JavaScript existe una técnica llamada Aumentación. Es más fácil leerla y encontrarla como "Augmentation", su variante inglesa.
Lo que está haciendo Human es aumentar el contexto desde el que se le llama. En otras palabras, aumentar el this, añadirle más propiedades (también podrían ser métodos).
Y ahora lo no tan bonito 👺, si queremos aprovechar el potencial de JavaScript, hay que saber cuándo aumentar el contexto. Lo digo porque al final, acaba convirtiéndose en una composición que no aprovecha los prototypes. Digamos que el call() sería como un super(). Pero esto es para otro tema.
Ahora, el contexto tendrá dos propiedades nuevas, que son gender y isAlive. El this que hay en person ha aumentado. Volvemos a aumentar el contexto añadiendo la propiedad age^1. Y finalmente retornamos^2 el contexto aumentado.
function Person(gender, age) {
...
// this = { gender: 'male', isAlive: true }
this.age = age^1;
// this = { gender: 'male', isAlive: true, age: 18 }
return this^2;
}
¿Has entendido las diferencias entre estos dos métodos? ¿Me animo a crear un post con más ejemplos?
Espero tus comentarios y que te haya sido de utilidad.
Top comments (7)
Buena Sergio, me gusto mucho los ejemplos.
Un placer, saludos Carlos.
Clarísima explicación. Coincido en que las clases en javascript oscurecen a pesar de ser elegantes
Totalmente, gracias por tu comentario Agustín.
Para extender un poco el tema, añadí la diferencia entre apply y call en este post:
ricardogeek.com/bind-call-y-apply-...
Excelente explicación me ha quedado muy claro, lo digo de verdad.
Me alegra!