JS: Introducción a la POO

Teoría: Contexto (This)

Para aprender completamente la programación orientada a objetos en JavaScript, es necesario comprender el concepto de contexto this. Casi todo se basa en el contexto, incluyendo cómo funcionan los métodos y las clases.

La situación se complica un poco porque el contexto funciona de manera diferente para las funciones regulares y las funciones de flecha. Y dado que las funciones de flecha se introdujeron en el lenguaje más tarde, es necesario comenzar a aprender sobre el contexto con las funciones regulares. Aquí hay un recordatorio de cómo se ven estas funciones:

// Definición de una función de flecha y asignación a una constante
const f = () => 'soy una función de flecha';

// Definición de una función anónima regular
function() {
  return 'soy una función regular sin nombre';
}

// Definición de una función regular con nombre
function f() {
  return 'soy una función regular con nombre';
}

En esta lección, solo estamos considerando las funciones regulares, las funciones de flecha se abordarán en una de las próximas lecciones. Las funciones regulares se comportan de la misma manera, ya sean con nombre o anónimas.

En JavaScript, las funciones se tratan como datos: se pueden asignar a variables, constantes e incluso a propiedades de objetos. Las funciones que se asignan a las propiedades de los objetos se llaman métodos:

const company = { name: 'Códica' };
// Creación de una función que se asigna inmediatamente a la propiedad getName 
// y se convierte en un método
company.getName = function() {
  return 'Códica';
};

// Llamada al método
company.getName(); // "Códica"

Esto es solo una de las muchas formas posibles de agregar una función a un objeto. Aquí hay algunos ejemplos más:

// Al crear el objeto
const obj = {
  getName: function() {
    return 'Códica';
  },
};

// A través de la asignación de una constante
const company = { name: 'Códica' };

function getCodica() {
  return 'Códica';
};
// El nombre no es importante
company.getName = getCodica;

company.getName(); // "Códica"

Todas las variantes anteriores son equivalentes. Todas conducen al mismo resultado, pero hay un problema. El método devuelve una cadena y no utiliza los datos del objeto. Si se cambia el nombre, el método seguirá devolviendo el valor "codificado" en él, en lugar del nombre actual de la empresa dentro del objeto.

company.getName(); // "Códica"
company.name = 'Códica Plus';
// El nombre ha cambiado, pero obviamente el valor devuelto sigue siendo el mismo
company.getName(); // "Códica"

Para resolver este problema, necesitamos acceder a los datos del objeto dentro del método. Esto se hace a través de la palabra clave especial this, que se llama contexto. Dentro de los métodos, this se refiere al objeto actual al que está vinculado el método.

const company = { name: 'Códica', employees: [] };
company.getName = function getName() {
  return this.name;
};

company.getName(); // "Códica"
company.name = 'Códica Plus';
company.getName(); // "Códica Plus"

this no solo permite leer datos, sino también modificarlos:

company.setName = function setName(name) {
  this.name = name;
};

company.getName(); // "Códica"
company.setName('Códica Plus');
company.getName(); // "Códica Plus"

Otro ejemplo es modificar una matriz interna en un objeto:

// Agregar un nuevo empleado
company.addEmployee = function addEmployee(user) {
  // Es importante tener en cuenta que en el momento de la llamada, employees 
  // ya se ha agregado a company
  this.employees.push(user);
};

const user = { name: 'Miguel' };
company.addEmployee(user);
company.employees; // [{ name: 'Miguel' }]

// O a través de un método

company.getEmployees = function() {
  return this.employees;
};

company.getEmployees(); // [{ name: 'Miguel' }]

Como se puede ver en los ejemplos anteriores, las propiedades se pueden modificar tanto directamente como a través de métodos. Qué método preferir depende de la situación. A medida que avance en los cursos y adquiera experiencia, comenzará a comprender mejor qué método es preferible.

Contexto

Cuando se dio la definición de this anteriormente, se dijo que this se refiere al objeto actual al que está vinculado el método. Y aquí radica la diferencia clave entre this en JavaScript y this en otros lenguajes. En JavaScript, this en un método puede cambiar:

const company1 = { name: 'Códica', getName: function getName() { return this.name } };
const company2 = { name: 'Códica Plus' };

company1.getName(); // "Códica"

company2.getName = company1.getName;

// En ambos casos, es la misma función
company2.getName(); // "Códica Plus"
company1.getName(); // "Códica"

¿Qué sucedió aquí? Llamar a la misma función desde otro objeto cambió el objeto al que se refiere this. Esta característica se llama enlace tardío. El valor de this se refiere al objeto desde el cual se llama el método.

La mejor manera de comprender esta característica es familiarizarse con cómo se llaman las funciones dentro de JavaScript y de dónde proviene this. Dado que en JavaScript las funciones también son objetos, tienen sus propios métodos. Entre ellos se encuentra el método call(), que se utiliza para llamar a una función:

const sayHi = () => '¡Hola!';
sayHi.call(); // "¡Hola!"

¿Por qué se hace esto? El caso es que el primer parámetro de call() toma el contexto, es decir, el objeto al que this se referirá dentro de la función. La función no tiene que ser un método para esto:

const getName = function getName() {
  return this.name;
};

const company1 = { name: 'Códica' };
// La función se llama directamente, no es un método
getName.call(company1); // "Códica"

const company2 = { name: 'Códica Plus' };
getName.call(company2); // "Códica Plus"

En el ejemplo anterior, cambiamos el contexto de la función getName() usando call(), pasando un nuevo contexto.

Ahí radica todo el secreto de this. Es el contexto que JavaScript pasa automáticamente a una función si se llama como un método. En este caso, se puede decir con certeza a qué objeto pertenece.

Ahora que sabes cómo funciona this, intenta responder a la pregunta de qué se imprimirá en la pantalla:

const company = {
  name: 'Códica',
  country: {
    name: 'Finland',
    getName: function getName() {
      return this.name;
    }
  },
};

console.log(company.country.getName()); // => ?

Respuesta correcta: "Finland". ¿Por qué? Porque el contexto para el método getName() es el objeto country, no company. Si modificamos un poco el código, será más fácil entender esta idea:

const { country } = company;
console.log(country.getName()); // "Finland"

Definición abreviada de métodos

Debido a la necesidad de utilizar funciones normales al crear objetos en JavaScript, se introdujo una sintaxis abreviada especial para la creación de métodos al definir objetos:

const company = {
  name: 'Códica',
  getName() {
    return this.name;
  },
  // Es lo mismo que
  // getName: function getName() {
  //   return this.name;
  // },
};

Este método es simplemente 'azúcar sintáctico'. Permite hacer la escritura un poco más corta, pero no cambia el comportamiento. Lo más importante es recordar que es una función normal, no una función de flecha. En el futuro, utilizaremos esta definición exactamente así.