Fundamentos de la encapsulación: Ocultamiento de datos y control de acceso en JavaScript
La encapsulación es uno de los pilares fundamentales de la programación orientada a objetos que permite ocultar los detalles internos de implementación de un objeto y exponer solo aquellas partes que son necesarias para su uso. En JavaScript, este concepto ha evolucionado significativamente con el tiempo, adaptándose a las particularidades del lenguaje.
Principios básicos de la encapsulación
La encapsulación se basa en dos conceptos principales:
- Ocultamiento de datos: Restringir el acceso directo a ciertos componentes del objeto
- Control de acceso: Proporcionar interfaces controladas para interactuar con esos datos
El objetivo principal es proteger la integridad de los datos internos de un objeto, evitando modificaciones accidentales o malintencionadas desde el exterior.
Convenciones de nomenclatura
Históricamente, JavaScript ha utilizado convenciones de nomenclatura para indicar que ciertas propiedades deberían tratarse como privadas:
class BankAccount {
constructor(owner) {
this.owner = owner;
this._balance = 0; // El prefijo _ indica "privado por convención"
}
deposit(amount) {
if (amount > 0) {
this._balance += amount;
return true;
}
return false;
}
getBalance() {
return this._balance;
}
}
En este ejemplo, _balance
utiliza el prefijo _
para indicar que es una propiedad que no debería accederse directamente desde fuera de la clase. Sin embargo, esta es solo una convención y no impone ninguna restricción real:
const account = new BankAccount("Alice");
account.deposit(100);
console.log(account.getBalance()); // 100 (acceso correcto)
account._balance = 1000000; // Funciona, pero rompe la encapsulación
Closures para encapsulación
Antes de la introducción de clases en ES6, los closures eran el mecanismo principal para implementar encapsulación real en JavaScript:
function createCounter() {
let count = 0; // Variable privada
return {
increment() {
count++;
},
decrement() {
count--;
},
getValue() {
return count;
}
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 1
console.log(counter.count); // undefined - no hay acceso directo
En este patrón, la variable count
está encapsulada dentro del closure y solo es accesible a través de los métodos proporcionados. Este enfoque ofrece una verdadera encapsulación, ya que no hay forma de acceder directamente a la variable privada.
Getters y setters
Los getters y setters proporcionan una forma más elegante de controlar el acceso a las propiedades:
class Temperature {
constructor(celsius) {
this._celsius = celsius;
}
get celsius() {
return this._celsius;
}
set celsius(value) {
if (value < -273.15) {
throw new Error("Temperature below absolute zero is not possible");
}
this._celsius = value;
}
get fahrenheit() {
return this._celsius * 9/5 + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) * 5/9;
}
}
Los getters y setters permiten:
- Validar datos antes de asignarlos
- Calcular valores derivados bajo demanda
- Mantener la consistencia entre propiedades relacionadas
- Ocultar la implementación interna
const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
temp.celsius = 30;
console.log(temp.fahrenheit); // 86
// Validación en acción
try {
temp.celsius = -300; // Error: Temperature below absolute zero
} catch (e) {
console.log(e.message);
}
Métodos de objeto para control de acceso
JavaScript ofrece métodos nativos para controlar el comportamiento de las propiedades de un objeto:
class SafeBox {
constructor(content) {
this._content = content;
// Hacer que _content no sea enumerable ni configurable
Object.defineProperty(this, '_content', {
enumerable: false, // No aparece en Object.keys()
configurable: false, // No puede ser eliminado
writable: false // No puede ser modificado directamente
});
}
getContent(password) {
if (password === "secret") {
return this._content;
}
return "Access denied";
}
}
Con Object.defineProperty()
podemos:
- Controlar si una propiedad es enumerable (visible en iteraciones)
- Definir si es configurable (puede ser eliminada)
- Establecer si es writable (puede ser modificada)
- Implementar getters y setters personalizados
Símbolos para propiedades semi-privadas
Los símbolos introducidos en ES6 ofrecen otra forma de implementar propiedades semi-privadas:
const passwordSymbol = Symbol('password');
class User {
constructor(username, password) {
this.username = username;
this[passwordSymbol] = password;
}
validatePassword(input) {
return this[passwordSymbol] === input;
}
}
const user = new User("admin", "1234");
console.log(user.validatePassword("1234")); // true
console.log(user[passwordSymbol]); // Accesible si se conoce el símbolo
console.log(Object.keys(user)); // ["username"] - el símbolo no aparece
Las propiedades basadas en símbolos:
- No aparecen en iteraciones estándar como
Object.keys()
- No son accesibles directamente sin conocer el símbolo
- Proporcionan un nivel de privacidad práctica aunque no absoluta
La encapsulación en JavaScript ha evolucionado desde simples convenciones hasta mecanismos más robustos. Aunque ninguna de estas técnicas tradicionales ofrece una encapsulación perfecta, proporcionan diferentes niveles de protección y control de acceso que son útiles en distintos contextos de desarrollo.
¿Te está gustando esta lección?
Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.
Más de 25.000 desarrolladores ya confían en CertiDevs
Técnicas modernas para la implementación de encapsulación: Campos privados y patrones de módulo
JavaScript ha evolucionado significativamente en los últimos años, introduciendo características nativas que permiten implementar una encapsulación más robusta y elegante. Las técnicas modernas ofrecen soluciones que van más allá de las convenciones y patrones tradicionales, proporcionando mecanismos de encapsulación con mayor integridad y claridad sintáctica.
Campos privados con # (ECMAScript 2022)
La adición más importante para la encapsulación en JavaScript moderno es la implementación de campos privados utilizando el prefijo #
. Esta característica proporciona una verdadera privacidad a nivel de lenguaje:
class CreditCard {
#number;
#cvv;
#expirationDate;
name;
constructor(number, cvv, expirationDate, name) {
this.#number = number;
this.#cvv = cvv;
this.#expirationDate = expirationDate;
this.name = name;
}
getLastFourDigits() {
return this.#number.slice(-4);
}
isValid() {
const now = new Date();
return now < this.#expirationDate;
}
}
A diferencia de las convenciones con guion bajo, los campos privados con #
están estrictamente encapsulados:
const myCard = new CreditCard("1234567890123456", "123", new Date(2025, 0), "John Doe");
console.log(myCard.name); // "John Doe" - campo público
console.log(myCard.getLastFourDigits()); // "3456"
// Intentos de acceso directo generan errores
try {
console.log(myCard.#number); // SyntaxError
} catch (e) {
console.log("No se puede acceder a campos privados desde fuera");
}
Los campos privados con #
ofrecen varias ventajas clave:
- Son verdaderamente privados y no solo por convención
- Generan un error de sintaxis si se intenta acceder desde fuera
- No pueden ser descubiertos mediante técnicas de introspección como
Object.keys()
- Funcionan con métodos, getters y setters privados
Métodos privados y accesorios
La sintaxis de #
también se aplica a métodos y accesorios privados:
class PaymentProcessor {
#apiKey;
#endpoint = "https://payment.example.com/api";
constructor(apiKey) {
this.#apiKey = apiKey;
}
async processPayment(amount, currency) {
const data = await this.#sendRequest({
amount,
currency,
timestamp: this.#generateTimestamp()
});
return this.#parseResponse(data);
}
#generateTimestamp() {
return Date.now();
}
async #sendRequest(payload) {
const response = await fetch(this.#endpoint, {
method: "POST",
headers: {
"Authorization": `Bearer ${this.#apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
return response.json();
}
#parseResponse(data) {
return {
success: data.status === "approved",
transactionId: data.id,
timestamp: new Date(data.processed_at)
};
}
}
Este enfoque permite crear APIs limpias donde solo los métodos públicos son visibles para los consumidores de la clase, mientras que los detalles de implementación permanecen encapsulados.
Getters y setters privados
También podemos definir getters y setters privados para operaciones internas:
class TemperatureSensor {
#celsius;
#lastUpdated;
#maxReadings = 100;
#readings = [];
constructor(initialCelsius) {
this.#celsius = initialCelsius;
this.#lastUpdated = new Date();
}
get temperature() {
return {
celsius: this.#celsius,
fahrenheit: this.#fahrenheit,
lastUpdated: this.#lastUpdated
};
}
// Getter privado
get #fahrenheit() {
return this.#celsius * 9/5 + 32;
}
updateReading(newCelsius) {
this.#celsius = newCelsius;
this.#lastUpdated = new Date();
this.#addReading(newCelsius);
return this.temperature;
}
// Método privado
#addReading(value) {
this.#readings.push({
value,
timestamp: new Date()
});
if (this.#readings.length > this.#maxReadings) {
this.#readings.shift();
}
}
}
Patrón de módulo moderno con ESM
El sistema de módulos nativo de JavaScript (ESM) proporciona otra capa de encapsulación a nivel de archivo:
// userService.js
const API_KEY = "secret_api_key"; // Variable privada al módulo
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
export function createUser(userData) {
if (!validateEmail(userData.email)) {
throw new Error("Invalid email format");
}
// Lógica que usa API_KEY internamente
return fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(userData)
}).then(res => res.json());
}
export function getUser(id) {
// Implementación que usa API_KEY
}
// No exportamos validateEmail ni API_KEY
En otro archivo, solo podemos acceder a lo que se exporta explícitamente:
// app.js
import { createUser, getUser } from './userService.js';
// Podemos usar las funciones exportadas
createUser({ name: "Alice", email: "alice@example.com" });
// Pero no tenemos acceso a las partes privadas
// API_KEY y validateEmail no son accesibles aquí
Este patrón permite:
- Encapsular variables y funciones a nivel de módulo
- Exponer solo una API pública bien definida
- Mantener la seguridad de datos sensibles como claves API
- Organizar el código en unidades cohesivas
Combinando módulos y clases con campos privados
La combinación de módulos ESM y campos privados proporciona múltiples capas de encapsulación:
// database.js
const DB_CONNECTION_STRING = "mongodb://username:password@localhost:27017";
class DatabaseConnection {
#connection = null;
#retryCount = 0;
#maxRetries = 3;
async #connect() {
try {
// Lógica de conexión usando DB_CONNECTION_STRING
this.#retryCount = 0;
return true;
} catch (error) {
if (this.#retryCount < this.#maxRetries) {
this.#retryCount++;
return this.#connect();
}
throw new Error("Failed to connect to database");
}
}
async query(sql, params) {
if (!this.#connection) {
await this.#connect();
}
// Implementación de la consulta
return results;
}
}
// Solo exportamos una instancia, no la clase ni la cadena de conexión
export const db = new DatabaseConnection();
Este enfoque proporciona:
- Encapsulación a nivel de módulo para la cadena de conexión
- Encapsulación a nivel de clase para los detalles de implementación
- Una API pública mínima (solo el método query)
- Patrón singleton para la conexión a la base de datos
WeakMaps para datos privados (enfoque alternativo)
Antes de los campos privados nativos, los WeakMaps ofrecían una solución elegante para almacenar datos privados:
// Enfoque alternativo para entornos que no soportan campos privados
const privateData = new WeakMap();
class User {
constructor(name, role) {
privateData.set(this, {
name,
role,
loginAttempts: 0
});
}
authenticate(password) {
const data = privateData.get(this);
if (password !== "correct-password") {
data.loginAttempts++;
if (data.loginAttempts >= 3) {
return { success: false, locked: true };
}
return { success: false, attempts: data.loginAttempts };
}
data.loginAttempts = 0;
return {
success: true,
userData: { name: data.name, role: data.role }
};
}
}
Esta técnica sigue siendo útil en ciertos contextos, especialmente cuando se necesita compatibilidad con navegadores antiguos o cuando se trabaja con objetos que no son instancias de clases.
Las técnicas modernas de encapsulación en JavaScript proporcionan mecanismos robustos para proteger la integridad de los datos y crear APIs limpias y bien definidas. Los campos privados con #
y el sistema de módulos ESM representan un avance significativo en la madurez del lenguaje para la programación orientada a objetos.
Encapsulación en arquitecturas complejas: Interfaces públicas, inmutabilidad y contratos de diseño
Cuando desarrollamos sistemas de software complejos, la encapsulación va más allá de simplemente ocultar datos. Se convierte en una herramienta arquitectónica fundamental que define cómo interactúan los diferentes componentes de un sistema. En arquitecturas modernas de JavaScript, la encapsulación se manifiesta a través de interfaces bien definidas, inmutabilidad y contratos de diseño explícitos.
Interfaces públicas estables
Una interfaz pública bien diseñada actúa como un contrato entre el componente y sus consumidores, permitiendo que la implementación interna evolucione sin afectar al código cliente:
// Módulo que expone una interfaz pública estable
export class UserRepository {
// La interfaz pública se mantiene estable
async findById(id) { /* implementación */ }
async findByEmail(email) { /* implementación */ }
async save(user) { /* implementación */ }
async delete(id) { /* implementación */ }
// Los métodos privados pueden cambiar libremente
#validateUser(user) { /* implementación */ }
#normalizeEmail(email) { /* implementación */ }
}
Las interfaces públicas efectivas siguen estos principios clave:
- Minimalismo: Exponer solo lo estrictamente necesario
- Consistencia: Mantener patrones coherentes en nombres y comportamientos
- Completitud: Proporcionar todas las operaciones necesarias para usar el componente
- Estabilidad: Cambiar con poca frecuencia para no romper el código cliente
Inmutabilidad como forma de encapsulación
La inmutabilidad es una técnica poderosa de encapsulación que garantiza que los objetos no cambien después de su creación, eliminando efectos secundarios inesperados:
class ImmutablePoint {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
// Congelamos el objeto para prevenir la adición de propiedades
Object.freeze(this);
}
get x() { return this.#x; }
get y() { return this.#y; }
// En lugar de modificar, creamos nuevas instancias
translate(dx, dy) {
return new ImmutablePoint(this.#x + dx, this.#y + dy);
}
scale(factor) {
return new ImmutablePoint(this.#x * factor, this.#y * factor);
}
}
Los objetos inmutables ofrecen varias ventajas arquitectónicas:
- Seguridad en concurrencia: No hay riesgo de modificaciones simultáneas
- Razonamiento simplificado: El estado no cambia inesperadamente
- Facilidad para testing: Comportamiento predecible y reproducible
- Compatibilidad con patrones funcionales: Facilita composición y transformaciones
Contratos de diseño explícitos
Los contratos de diseño formalizan las expectativas sobre cómo debe usarse un componente, reforzando la encapsulación mediante validaciones explícitas:
class BankAccount {
#balance = 0;
#minimumBalance;
#owner;
constructor(owner, initialDeposit = 0, minimumBalance = 0) {
// Precondiciones - validamos antes de crear el objeto
if (typeof owner !== 'object' || !owner.id || !owner.name) {
throw new TypeError('Owner must be an object with id and name');
}
if (initialDeposit < minimumBalance) {
throw new RangeError(`Initial deposit must be at least ${minimumBalance}`);
}
this.#owner = { ...owner }; // Copia defensiva
this.#balance = initialDeposit;
this.#minimumBalance = minimumBalance;
}
withdraw(amount) {
// Precondiciones
if (amount <= 0) {
throw new RangeError('Withdrawal amount must be positive');
}
// Invariantes
if (this.#balance - amount < this.#minimumBalance) {
throw new Error('Insufficient funds');
}
this.#balance -= amount;
// Postcondiciones
return {
success: true,
newBalance: this.#balance,
timestamp: new Date()
};
}
}
Los contratos de diseño incluyen:
- Precondiciones: Requisitos que deben cumplirse antes de ejecutar una operación
- Postcondiciones: Garantías sobre el estado después de la operación
- Invariantes: Condiciones que siempre deben ser verdaderas durante la vida del objeto
Patrones de fachada para sistemas complejos
El patrón fachada proporciona una interfaz simplificada a un subsistema complejo, encapsulando la complejidad interna:
// Subsistemas complejos encapsulados
class PaymentProcessor { /* implementación compleja */ }
class InventoryManager { /* implementación compleja */ }
class ShippingService { /* implementación compleja */ }
class NotificationSystem { /* implementación compleja */ }
// Fachada que simplifica el uso del sistema
export class OrderService {
#paymentProcessor = new PaymentProcessor();
#inventory = new InventoryManager();
#shipping = new ShippingService();
#notifications = new NotificationSystem();
async placeOrder(orderData) {
try {
// Validación de la orden
this.#validateOrder(orderData);
// Reserva de inventario
await this.#inventory.reserveItems(orderData.items);
// Procesamiento del pago
const paymentResult = await this.#paymentProcessor.processPayment(
orderData.payment
);
if (!paymentResult.success) {
await this.#inventory.releaseItems(orderData.items);
return { success: false, reason: 'payment_failed' };
}
// Creación del envío
const shipment = await this.#shipping.createShipment(
orderData.items,
orderData.shippingAddress
);
// Notificación al cliente
await this.#notifications.sendOrderConfirmation(
orderData.customer,
{ orderId: shipment.trackingId }
);
return {
success: true,
orderId: shipment.trackingId,
estimatedDelivery: shipment.estimatedDelivery
};
} catch (error) {
// Manejo de errores y compensación
return { success: false, reason: error.message };
}
}
#validateOrder(orderData) {
// Implementación de validación
}
}
Este patrón:
- Simplifica la API expuesta a los clientes
- Oculta las dependencias y relaciones entre subsistemas
- Centraliza la lógica de coordinación entre componentes
- Reduce el acoplamiento entre el cliente y los subsistemas
Encapsulación a nivel de dominio con objetos de valor
Los objetos de valor encapsulan conceptos del dominio, garantizando su integridad y validez:
class EmailAddress {
#value;
constructor(email) {
if (!this.#isValid(email)) {
throw new Error('Invalid email format');
}
this.#value = email.toLowerCase();
}
#isValid(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
get value() {
return this.#value;
}
equals(other) {
return other instanceof EmailAddress && other.value === this.#value;
}
toString() {
return this.#value;
}
}
class Money {
#amount;
#currency;
constructor(amount, currency) {
if (typeof amount !== 'number' || isNaN(amount)) {
throw new TypeError('Amount must be a number');
}
if (typeof currency !== 'string' || currency.length !== 3) {
throw new TypeError('Currency must be a 3-letter ISO code');
}
this.#amount = amount;
this.#currency = currency.toUpperCase();
}
get amount() { return this.#amount; }
get currency() { return this.#currency; }
add(other) {
if (!(other instanceof Money) || other.currency !== this.#currency) {
throw new Error('Cannot add different currencies');
}
return new Money(this.#amount + other.amount, this.#currency);
}
multiply(factor) {
return new Money(this.#amount * factor, this.#currency);
}
toString() {
return `${this.#amount.toFixed(2)} ${this.#currency}`;
}
}
Los objetos de valor:
- Encapsulan reglas de validación específicas del dominio
- Son inmutables por diseño
- Tienen igualdad basada en valores (no en identidad)
- Expresan intención más claramente que tipos primitivos
Composición sobre herencia para encapsulación flexible
La composición ofrece una forma más flexible de encapsulación que la herencia, permitiendo combinar comportamientos sin exponer la implementación interna:
// Comportamientos encapsulados como objetos independientes
class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
class EventEmitter {
#listeners = new Map();
on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
this.#listeners.get(event).push(callback);
}
emit(event, data) {
const callbacks = this.#listeners.get(event) || [];
callbacks.forEach(callback => callback(data));
}
}
class HttpClient {
async get(url) { /* implementación */ }
async post(url, data) { /* implementación */ }
}
// Componente que utiliza composición para encapsular comportamientos
class UserService {
#logger = new Logger();
#events = new EventEmitter();
#http = new HttpClient();
async getUser(id) {
this.#logger.log(`Fetching user ${id}`);
try {
const user = await this.#http.get(`/users/${id}`);
this.#events.emit('user:loaded', user);
return user;
} catch (error) {
this.#logger.log(`Error fetching user: ${error.message}`);
this.#events.emit('user:error', { id, error });
throw error;
}
}
onUserLoaded(callback) {
this.#events.on('user:loaded', callback);
}
}
La composición:
- Encapsula comportamientos en unidades independientes
- Permite cambiar implementaciones sin afectar la interfaz
- Facilita la prueba unitaria de cada componente
- Evita los problemas de la herencia como el acoplamiento rígido
Encapsulación en arquitecturas de microservicios frontend
En arquitecturas frontend modernas, la encapsulación se extiende a componentes autónomos que encapsulan su estado, lógica y presentación:
// Componente encapsulado con estado interno y API pública
class ShoppingCart extends HTMLElement {
#items = [];
#root;
constructor() {
super();
this.#root = this.attachShadow({ mode: 'closed' });
this.#render();
}
// API pública
addItem(product, quantity = 1) {
const existingItem = this.#items.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.#items.push({ ...product, quantity });
}
this.#render();
this.dispatchEvent(new CustomEvent('cart:changed', {
detail: { itemCount: this.itemCount }
}));
}
removeItem(productId) {
this.#items = this.#items.filter(item => item.id !== productId);
this.#render();
this.dispatchEvent(new CustomEvent('cart:changed', {
detail: { itemCount: this.itemCount }
}));
}
get itemCount() {
return this.#items.reduce((total, item) => total + item.quantity, 0);
}
get total() {
return this.#items.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
}
// Métodos privados
#render() {
this.#root.innerHTML = `
<style>/* Estilos encapsulados */</style>
<div class="cart">
<h2>Your Cart (${this.itemCount} items)</h2>
<ul>
${this.#renderItems()}
</ul>
<div class="total">Total: $${this.total.toFixed(2)}</div>
</div>
`;
// Agregar event listeners
this.#attachEventListeners();
}
#renderItems() {
return this.#items.map(item => `
<li data-id="${item.id}">
${item.name} - $${item.price} × ${item.quantity}
<button class="remove">×</button>
</li>
`).join('');
}
#attachEventListeners() {
const removeButtons = this.#root.querySelectorAll('.remove');
removeButtons.forEach(button => {
const li = button.closest('li');
button.addEventListener('click', () => {
this.removeItem(li.dataset.id);
});
});
}
}
// Registrar el componente
customElements.define('shopping-cart', ShoppingCart);
Este enfoque de encapsulación:
- Utiliza Shadow DOM para encapsular el DOM interno
- Mantiene el estado privado inaccesible desde el exterior
- Expone una API pública bien definida
- Comunica cambios mediante eventos personalizados
La encapsulación en arquitecturas complejas de JavaScript va más allá del simple ocultamiento de datos, convirtiéndose en un principio arquitectónico que define cómo se estructuran, comunican y evolucionan los componentes de un sistema. Mediante interfaces públicas estables, inmutabilidad, contratos de diseño explícitos y patrones como fachada y composición, podemos construir sistemas robustos, mantenibles y adaptables a los cambios.
Aprendizajes de esta lección
- Comprender qué es la encapsulación en programación orientada a objetos.
- Aprender a implementar ocultamiento de datos en JavaScript.
- Controlar el acceso a los datos mediante getters y setters.
- Usar técnicas modernas de encapsulación en ECMAScript 2022.
- Aplicar closures para lograr encapsulación en JavaScript.
Completa JavaScript y certifícate
Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.
Asistente IA
Resuelve dudas al instante
Ejercicios
Practica con proyectos reales
Certificados
Valida tus conocimientos
Más de 25.000 desarrolladores ya se han certificado con CertiDevs