La programación basada en prototipos es un estilo de programación orientada a objetos en el que la reutilización del comportamiento (conocida como herencia ) se realiza mediante un proceso de reutilización de objetos existentes que sirven como prototipos . Este modelo también puede conocerse como programación prototípica , orientada a prototipos, sin clases o basada en instancias .
La programación basada en prototipos utiliza el proceso de objetos generalizados, que luego pueden clonarse y ampliarse. Usando la fruta como ejemplo, un objeto "fruta" representaría las propiedades y funcionalidad de la fruta en general. Se clonaría un objeto "plátano" a partir del objeto "fruta" y se agregarían propiedades generales específicas de los plátanos. Cada objeto "banana" individual se clonaría a partir del objeto "banana" genérico. Compárese con el paradigma basado en clases , donde una clase "fruta" se ampliaría con una clase "plátano" .
Los primeros lenguajes de programación basados en prototipos fueron Director, también conocido como Ani (además de MacLisp ) (1976-1979), y contemporáneamente y no de forma independiente, ThingLab (además de Smalltalk ) (1977-1981), respectivos proyectos de doctorado de Kenneth Michael Kahn en MIT y Alan Hamilton Borning en Stanford (pero trabajando con Alan Kay en Xerox PARC). Borning introdujo la palabra "prototipo" en su artículo TOPLAS de 1981. El primer lenguaje de programación basado en prototipos con más de un implementador o usuario fue probablemente Yale T Scheme (1981-1984), aunque al igual que Director y ThingLab inicialmente, solo habla de objetos sin clases. El lenguaje que popularizó el nombre y la noción de prototipos fue Self (1985-1995), desarrollado por David Ungar y Randall Smith para investigar temas de diseño de lenguajes orientados a objetos.
Desde finales de los años 1990, el paradigma sin clases se ha vuelto cada vez más popular. Algunos lenguajes actuales orientados a prototipos son JavaScript (y otras implementaciones de ECMAScript como JScript y ActionScript 1.0 de Flash ), Lua , Cecil , NewtonScript , Io , Ioke, MOO , REBOL y AHK .
Desde la década de 2010, ha aparecido una nueva generación de lenguajes con prototipos funcionales puros, que reducen la programación orientada a objetos hasta su núcleo: Jsonnet es un lenguaje funcional puro , dinámico y perezoso con un sistema de objetos prototipo incorporado que utiliza herencia mixin ; Nix es un lenguaje funcional puro , dinámico y perezoso que construye un sistema de objetos equivalente ("extensiones" de Nix) en solo dos definiciones de funciones breves (además de muchas otras funciones de conveniencia). Ambos lenguajes se utilizan para definir grandes configuraciones de software distribuido (Jsonnet está directamente inspirado en GCL, el lenguaje de configuración de Google, con el que Google define todas sus implementaciones, y tiene una semántica similar aunque con enlace dinámico de variables). Desde entonces, otros lenguajes como Gerbil Scheme han implementado sistemas de prototipos perezosos funcionales puros basados en principios similares.
Etimológicamente, un "prototipo" significa "primer elenco" ("fundido" en el sentido de haber sido fabricado). Un prototipo es algo concreto a partir del cual se pueden crear otros objetos copiándolos y modificándolos. Por ejemplo, el prototipo internacional del kilogramo es un objeto real que realmente existe, a partir del cual se pueden crear nuevos objetos-kilogramo mediante copia. En comparación, una "clase" es algo abstracto, al que pueden pertenecer los objetos. Por ejemplo, todos los objetos kilogramos están en la clase KilogramObject, que podría ser una subclase de MetricObject, y así sucesivamente.
Douglas Crockford describe la herencia prototípica en JavaScript como
Creas prototipos de objetos y luego... creas nuevas instancias. Los objetos son mutables en JavaScript, por lo que podemos aumentar las nuevas instancias, dándoles nuevos campos y métodos. Estos pueden actuar como prototipos de objetos aún más nuevos. No necesitamos clases para crear muchos objetos similares... Los objetos heredan de los objetos. ¿Qué podría ser más orientado a objetos que eso? [1]
Los defensores de la programación basada en prototipos argumentan que anima al programador a centrarse en el comportamiento de algún conjunto de ejemplos y sólo más tarde preocuparse por clasificar estos objetos en objetos arquetípicos que luego se utilizan de manera similar a las clases . [2] Muchos sistemas basados en prototipos fomentan la alteración de los prototipos durante el tiempo de ejecución , mientras que sólo muy pocos sistemas orientados a objetos basados en clases (como el sistema dinámico orientado a objetos, Common Lisp , Dylan , Objective-C , Perl , Python , Ruby o Smalltalk ) permiten modificar las clases durante la ejecución de un programa.
Casi todos los sistemas basados en prototipos se basan en lenguajes interpretados y tipificados dinámicamente . Sin embargo , los sistemas basados en lenguajes tipados estáticamente son técnicamente viables. El lenguaje Omega discutido en Programación basada en prototipos [3] es un ejemplo de tal sistema, aunque según el sitio web de Omega incluso Omega no es exclusivamente estático, sino que su "compilador puede optar por utilizar enlace estático cuando esto sea posible y puede mejorar". la eficiencia de un programa."
En los lenguajes basados en prototipos no existen clases explícitas. Los objetos heredan directamente de otros objetos a través de una propiedad prototipo. La propiedad prototipo se llama prototype
en Self y JavaScript , o proto
en Io . Hay dos métodos para construir nuevos objetos: la creación de objetos ex nihilo ("de la nada") o mediante la clonación de un objeto existente. El primero se admite a través de algún tipo de literal de objeto , declaraciones donde los objetos se pueden definir en tiempo de ejecución mediante una sintaxis especial, como {...}
y pasar directamente a una variable. Si bien la mayoría de los sistemas admiten una variedad de clonaciones, la creación de objetos ex nihilo no es tan prominente. [4]
En los lenguajes basados en clases, una nueva instancia se construye a través de la función constructora de una clase , una función especial que reserva un bloque de memoria para los miembros del objeto (propiedades y métodos) y devuelve una referencia a ese bloque. Se puede pasar un conjunto opcional de argumentos del constructor a la función y generalmente se mantienen en propiedades. La instancia resultante heredará todos los métodos y propiedades que se definieron en la clase, que actúa como una especie de plantilla a partir de la cual se pueden construir objetos de tipo similar.
Los sistemas que admiten la creación de objetos ex nihilo permiten crear nuevos objetos desde cero sin necesidad de clonarlos a partir de un prototipo existente. Estos sistemas proporcionan una sintaxis especial para especificar las propiedades y comportamientos de nuevos objetos sin hacer referencia a objetos existentes. En muchos lenguajes de prototipos existe un objeto raíz, a menudo llamado Objeto , que se establece como el prototipo predeterminado para todos los demás objetos creados en tiempo de ejecución y que incluye métodos comúnmente necesarios, como una toString()
función para devolver una descripción del objeto como una cadena. . Un aspecto útil de la creación de objetos ex nihilo es garantizar que los nombres de las ranuras (propiedades y métodos) de un nuevo objeto no tengan conflictos de espacio de nombres con el objeto Objeto de nivel superior . (En el lenguaje JavaScript , se puede hacer esto usando un prototipo nulo, es decir Object.create(null)
).
La clonación se refiere a un proceso mediante el cual se construye un nuevo objeto copiando el comportamiento de un objeto existente (su prototipo). El nuevo objeto porta entonces todas las cualidades del original. A partir de este momento, se puede modificar el nuevo objeto. En algunos sistemas, el objeto hijo resultante mantiene un vínculo explícito (mediante delegación o semejanza ) con su prototipo, y los cambios en el prototipo hacen que los cambios correspondientes sean evidentes en su clon. Otros sistemas, como el lenguaje de programación Kevo, similar a Forth , no propagan los cambios desde el prototipo de esta manera y, en cambio, siguen un modelo más concatenativo en el que los cambios en los objetos clonados no se propagan automáticamente entre los descendientes. [2]
// Ejemplo de estilo de herencia prototípico verdadero en JavaScript.// Creación de objetos usando la notación literal de objetos {}. const foo = { nombre : "foo" , uno : 1 , dos : 2 }; // Otro objeto. barra constante = { dos : "dos" , tres : 3 }; // Object.setPrototypeOf() es un método introducido en ECMAScript 2015. // En aras de la simplicidad, supongamos que la siguiente // línea funciona independientemente del motor utilizado: Object . setPrototypeOf ( barra , foo ); // foo es ahora el prototipo de bar. // Si intentamos acceder a las propiedades de foo desde la barra de ahora en adelante, // lo lograremos. bar . uno ; // Se resuelve en 1. // También se puede acceder a las propiedades del objeto secundario. bar . tres ; // Se resuelve en 3. // Propiedades propias que ocultan las propiedades del prototipo. bar . dos ; // Se resuelve en "dos". bar . nombre ; // No afectado, se resuelve en "foo". foo . nombre ; // Se resuelve en "foo".
Para otro ejemplo:
const foo = { uno : 1 , dos : 2 }; // bar.[[prototipo]] = foo const bar = Objeto . crear ( foo ); bar . tres = 3 ; bar . uno ; // 1 barra . dos ; // 2 barras . tres ; // 3
En los lenguajes basados en prototipos que usan delegación , el tiempo de ejecución del lenguaje es capaz de enviar el método correcto o encontrar el dato correcto simplemente siguiendo una serie de punteros de delegación (desde el objeto hasta su prototipo) hasta que se encuentra una coincidencia. Todo lo que se necesita para establecer este comportamiento compartido entre objetos es el puntero de delegación. A diferencia de la relación entre clase e instancia en los lenguajes orientados a objetos basados en clases, la relación entre el prototipo y sus vástagos no requiere que el objeto hijo tenga una memoria o una similitud estructural con el prototipo más allá de este vínculo. Como tal, el objeto hijo puede seguir modificándose y enmendándose con el tiempo sin reorganizar la estructura de su prototipo asociado como en los sistemas basados en clases. También es importante tener en cuenta que no sólo se pueden agregar o cambiar datos, sino también métodos. Por esta razón, algunos lenguajes basados en prototipos se refieren tanto a los datos como a los métodos como "ranuras" o "miembros". [ cita necesaria ]
En la creación de prototipos concatenativos , el enfoque implementado por el lenguaje de programación Kevo, no hay punteros ni enlaces visibles al prototipo original a partir del cual se clona un objeto. El objeto prototipo (principal) se copia en lugar de vincularse y no hay delegación. Como resultado, los cambios en el prototipo no se reflejarán en los objetos clonados. [5] Por cierto, el lenguaje de programación Cosmos logra lo mismo mediante el uso de estructuras de datos persistentes . [6]
La principal diferencia conceptual bajo este acuerdo es que los cambios realizados en un objeto prototipo no se propagan automáticamente a los clones. Esto puede verse como una ventaja o una desventaja. (Sin embargo, Kevo proporciona primitivas adicionales para publicar cambios entre conjuntos de objetos en función de su similitud (las llamadas semejanzas familiares o mecanismo de familia clonada [5] ) en lugar de a través del origen taxonómico, como es típico en el modelo de delegación). A veces también se afirma que la creación de prototipos basados en delegación tiene una desventaja adicional en el sentido de que los cambios en un objeto hijo pueden afectar el funcionamiento posterior del objeto padre. Sin embargo, este problema no es inherente al modelo basado en delegación y no existe en lenguajes basados en delegación como JavaScript, que garantizan que los cambios en un objeto hijo siempre se registren en el objeto hijo mismo y nunca en los padres (es decir, en el objeto hijo). value oculta el valor del padre en lugar de cambiar el valor del padre).
En implementaciones simplistas, la creación de prototipos concatenativos tendrá una búsqueda de miembros más rápida que la creación de prototipos basada en delegación (porque no hay necesidad de seguir la cadena de objetos principales), pero a la inversa utilizará más memoria (porque se copian todas las ranuras, en lugar de haber una sola). ranura que apunta al objeto principal). Sin embargo, implementaciones más sofisticadas pueden evitar este problema, aunque se requieren compensaciones entre velocidad y memoria. Por ejemplo, los sistemas con creación de prototipos concatenativos pueden utilizar una implementación de copia en escritura para permitir el intercambio de datos detrás de escena, y Kevo sigue ese enfoque. [7] Por el contrario, los sistemas con creación de prototipos basados en delegación pueden utilizar el almacenamiento en caché para acelerar la búsqueda de datos.
Los defensores de los modelos de objetos basados en clases que critican los sistemas basados en prototipos a menudo tienen preocupaciones similares a las que los defensores de los sistemas de tipos estáticos para lenguajes de programación tienen sobre los sistemas de tipos dinámicos (ver tipo de datos ). Por lo general, estas preocupaciones implican corrección , seguridad , previsibilidad , eficiencia y desconocimiento del programador.
En los primeros tres puntos, las clases son a menudo vistas como análogas a los tipos (en la mayoría de los lenguajes orientados a objetos de tipo estático cumplen esa función) y se proponen para proporcionar garantías contractuales a sus instancias, y a los usuarios de sus instancias, de que se comportarán. de alguna manera dada.
En cuanto a la eficiencia, declarar clases simplifica muchas optimizaciones del compilador que permiten desarrollar métodos eficientes y búsquedas de variables de instancia. Para el lenguaje Self , se dedicó mucho tiempo a desarrollar, compilar e interpretar técnicas para mejorar el rendimiento de los sistemas basados en prototipos frente a los sistemas basados en clases.
Una crítica común a los lenguajes basados en prototipos es que la comunidad de desarrolladores de software no está familiarizada con ellos, a pesar de la popularidad y la penetración en el mercado de JavaScript . Sin embargo, el conocimiento sobre los sistemas basados en prototipos está aumentando con la proliferación de marcos de JavaScript y el uso complejo de JavaScript a medida que la World Wide Web (Web) madura. [8] [ cita necesaria ] ECMAScript 6 introdujo clases como azúcar sintáctico sobre la herencia basada en prototipos existente de JavaScript, proporcionando una forma alternativa de crear objetos y gestionar la herencia. [9]
Kevo implementó un modelo de objetos basado puramente en concatenación en el que se creaban nuevos objetos copiando y los espacios de nombres de todos los objetos siempre eran completamente autónomos. … Además, Kevo tenía un mecanismo interno
de familia de clones
que hacía posible rastrear la "genealogía" de los cambios entre grupos de objetos, de modo que los cambios en objetos individuales pudieran propagarse a otros objetos cuando fuera necesario.