herencia - ¿Qué puede hacer el prototipo de sistema JavaScript más allá de imitar un sistema de clase clásico?
javascript prototype constructor (4)
El sistema prototipo parece mucho más flexible que el sistema de clases tradicional, pero las personas parecen sentirse satisfechas con las llamadas "mejores prácticas", que imitan el sistema de clases tradicional:
function foo() {
// define instance properties here
}
foo.prototype.method = //define instance method here
new foo()
Debe haber otras cosas que un sistema prototípico puede hacer con toda la flexibilidad.
¿Hay usos para un sistema prototípico fuera de imitar clases? ¿Qué tipo de cosas pueden hacer los prototipos que las clases no pueden o no hay?
Creo que el sistema de herencia de prototipos permite una adición mucho más dinámica de métodos / propiedades.
Puede ampliar fácilmente las clases escritas por otras personas, por ejemplo, todos los complementos de jQuery, y también puede agregar fácilmente a las clases nativas, agregar funciones de utilidad a cadenas, matrices y, bueno, cualquier cosa.
Ejemplo:
// I can just add whatever I want to anything I want, whenever I want
String.prototype.first = function(){ return this[0]; };
''Hello''.first() // == ''H''
También puede copiar métodos de otras clases,
function myString(){
this[0] = ''42'';
}
myString.prototype = String.prototype;
foo = new myString();
foo.first() // == ''42''
También significa que puede extender un prototipo después de que un objeto lo haya heredado, pero esos cambios se aplicarán.
Y, personalmente, creo que los prototipos son realmente convenientes y simples, los métodos de diseño dentro de un objeto son realmente atractivos para mí;)
El sistema prototipo ofrece un modelo cautivador de metaprogramming , mediante la implementación de la herencia a través de objetos estándar. Por supuesto, esto se usa principalmente para expresar el concepto establecido y simple de clases de instancias, pero sin clases como estructuras inmutables a nivel de lenguaje que necesitan una sintaxis específica para crearlas. Al usar objetos simples, todo lo que puede hacer a los objetos (y puede hacer todo) ahora puede hacerlo a las "clases": esta es la flexibilidad de la que habla.
Esta flexibilidad se usa mucho para extender y alterar las clases mediante programación, utilizando solo las capacidades de mutación de objetos de JavaScript:
- mixins y rasgos para herencia múltiple
- los prototipos se pueden modificar después de que se hayan instanciado los objetos que heredan de ellos
- Las funciones de orden superior y los decoradores de métodos se pueden usar fácilmente en la creación de prototipos
Por supuesto, el modelo prototipo en sí es más poderoso que simplemente implementar clases. Estas características se usan con poca frecuencia, ya que el concepto de clase es muy útil y generalizado, por lo que los poderes reales de la herencia de prototipos no son bien conocidos (y no están bien optimizados en los motores JS: - /)
-
El cambio de prototipos de objetos existentes puede usarse para alterar su comportamiento dramáticamente.
(compatibilidad total con
ES6
Reflect.setPrototypeOf
) -
Algunos patrones de ingeniería de software pueden implementarse directamente con objetos. Ejemplos son el patrón de peso mosca con propiedades, una cadena de responsabilidades que incluye cadenas dinámicas, oh, y por supuesto el patrón prototipo .
Un buen ejemplo para el último sería la opción de objetos con valores predeterminados. Todos los crean usando
var myOptions = extend({}, defaultOptions, optionArgument);
pero un enfoque más dinámico sería usar
var myOptions = extend(Object.create(defaultOptions), optionArgument);
En JavaScript, no existe tal concepto de clase. Aquí todo es objeto. Y todos los objetos en JavaScript son descendientes de Object . La propiedad prototipo ayuda en la herencia, cuando estamos desarrollando aplicaciones en forma orientada a objetos. Hay más características en el prototipo que Class en la estructura tradicional orientada a objetos.
En prototype, puede agregar propiedades a la función que ha sido escrito por otra persona.
Por ej.
Array.prototype.print=function(){
console.log(this);
}
Uso en herencia:
Puede usar la herencia mediante el uso de la propiedad prototipo. Así es como puede usar la herencia con JavaScript.
En el sistema de clases tradicional, no puede modificar una vez que se define la clase. Pero en puedes hacerlo en JavaScript con prototipo de sistema.
En junio de 2013, respondí una pregunta sobre los beneficios de la herencia prototípica sobre la clásica . Desde entonces, pasé mucho tiempo reflexionando sobre la herencia, tanto prototípica como clásica, y escribí extensamente sobre el prototype isomorphism -class .
Sí, el uso principal de la herencia prototípica es simular clases. Sin embargo, se puede usar para mucho más que simplemente simular clases. Por ejemplo, las cadenas prototipo son muy similares a las cadenas de alcance.
Isomorfismo de alcance prototipo también
Los prototipos y ámbitos en JavaScript tienen mucho en común. Hay tres tipos comunes de cadenas en JavaScript:
-
Cadenas prototipo.
var foo = {}; var bar = Object.create(foo); var baz = Object.create(bar); // chain: baz -> bar -> foo -> Object.prototype -> null
-
Cadenas de alcance.
function foo() { function bar() { function baz() { // chain: baz -> bar -> foo -> global } } }
-
Método de cadenas.
var chain = { foo: function () { return this; }, bar: function () { return this; }, baz: function () { return this; } }; chain.foo().bar().baz();
De las tres, las cadenas prototipo y las cadenas de alcance son las más similares.
De hecho, puede adjuntar una cadena de prototipo a una cadena de alcance utilizando la declaración
notorious
with
.
function foo() {
var bar = {};
var baz = Object.create(bar);
with (baz) {
// chain: baz -> bar -> Object.prototype -> foo -> global
}
}
Entonces, ¿de qué sirve el isomorfismo de alcance prototipo? Un uso directo es modelar cadenas de ámbitos utilizando cadenas de prototipos. Esto es exactamente lo que hice para mi propio lenguaje de programación Bianca , que implementé en JavaScript.
Primero definí el alcance global de Bianca, global.js con un montón de funciones matemáticas útiles en un archivo llamado global.js siguiente manera:
var global = module.exports = Object.create(null);
global.abs = new Native(Math.abs);
global.acos = new Native(Math.acos);
global.asin = new Native(Math.asin);
global.atan = new Native(Math.atan);
global.ceil = new Native(Math.ceil);
global.cos = new Native(Math.cos);
global.exp = new Native(Math.exp);
global.floor = new Native(Math.floor);
global.log = new Native(Math.log);
global.max = new Native(Math.max);
global.min = new Native(Math.min);
global.pow = new Native(Math.pow);
global.round = new Native(Math.round);
global.sin = new Native(Math.sin);
global.sqrt = new Native(Math.sqrt);
global.tan = new Native(Math.tan);
global.max.rest = { type: "number" };
global.min.rest = { type: "number" };
global.sizeof = {
result: { type: "number" },
type: "function",
funct: sizeof,
params: [{
type: "array",
dimensions: []
}]
};
function Native(funct) {
this.funct = funct;
this.type = "function";
var length = funct.length;
var params = this.params = [];
this.result = { type: "number" };
while (length--) params.push({ type: "number" });
}
function sizeof(array) {
return array.length;
}
Tenga en cuenta que creé el alcance global usando
Object.create(null)
.
Hice esto porque el alcance global no tiene ningún alcance principal.
Después de eso, para cada programa creé un ámbito de programa separado que contiene las definiciones de nivel superior del programa. El código se almacena en un archivo llamado analyzer.js que es demasiado grande para caber en una respuesta. Aquí están las primeras tres líneas del archivo:
var parse = require("./ast");
var global = require("./global");
var program = Object.create(global);
Como puede ver, el alcance global es el padre del alcance del programa.
Por lo tanto, el
program
hereda de
global
, haciendo que la búsqueda de variables de alcance sea tan simple como una búsqueda de propiedad de objeto.
Esto hace que el tiempo de ejecución del lenguaje sea mucho más simple.
El alcance del programa contiene las definiciones de nivel superior del programa. Por ejemplo, considere el siguiente programa de multiplicación de matrices que se almacena en el archivo matrix.bianca :
col(a[3][3], b[3][3], i, j)
if (j >= 3) a
a[i][j] += b[i][j]
col(a, b, i, j + 1)
row(a[3][3], b[3][3], i)
if (i >= 3) a
a = col(a, b, i, 0)
row(a, b, i + 1)
add(a[3][3], b[3][3])
row(a, b, 0)
Las definiciones de nivel superior son
col
,
row
y
add
.
Cada una de estas funciones tiene su propio alcance de función, que hereda del alcance del programa.
El código para eso se puede encontrar en la
línea 67 de analyzer.js
:
scope = Object.create(program);
Por ejemplo, el alcance de la función de
add
tiene las definiciones para las matrices
b
.
Por lo tanto, además de las clases, los prototipos también son útiles para modelar ámbitos de funciones.
Prototipos para modelar tipos de datos algebraicos
Las clases no son el único tipo de abstracción disponible. En lenguajes de programación funcionales, los datos se modelan utilizando tipos de datos algebraicos .
El mejor ejemplo de un tipo de datos algebraicos es el de una lista:
data List a = Nil | Cons a (List a)
Esta definición de datos simplemente significa que una lista de a puede ser una lista vacía (es decir,
Nil
) o bien un valor de tipo "a" insertado en una lista de a (es decir,
Cons a (List a)
).
Por ejemplo, las siguientes son todas las listas:
Nil :: List a
Cons 1 Nil :: List Number
Cons 1 (Cons 2 Nil) :: List Number
Cons 1 (Cons 2 (Cons 3 Nil)) :: List Number
La variable de tipo
a
en la definición de datos habilita el
polimorfismo paramétrico
(es decir, permite que la lista contenga cualquier tipo de valor).
Por ejemplo,
Nil
podría estar especializado en una lista de números o una lista de booleanos porque tiene el tipo
List a
donde
a
podría ser cualquier cosa.
Esto nos permite crear funciones paramétricas como la
length
:
length :: List a -> Number
length Nil = 0
length (Cons _ l) = 1 + length l
La función de
length
podría usarse para encontrar la longitud de cualquier lista independientemente del tipo de valores que contiene porque la función de
length
simplemente no se preocupa por los valores de la lista.
Además del polimorfismo paramétrico, la mayoría de los lenguajes de programación funcionales también tienen alguna forma de polimorfismo ad-hoc . En el polimorfismo ad-hoc, se elige una implementación específica de una función dependiendo del tipo de variable polimórfica.
Por ejemplo, el operador
+
en JavaScript se usa tanto para la suma como para la concatenación de cadenas dependiendo del tipo de argumento.
Esta es una forma de polimorfismo ad-hoc.
Del mismo modo, en lenguajes de programación funcionales, la función de
map
generalmente está sobrecargada.
Por ejemplo, puede tener una implementación diferente de
map
para listas, una implementación diferente para conjuntos, etc. Las clases de tipos son una forma de implementar polimorfismo ad-hoc.
Por ejemplo, la clase de tipo
Functor
proporciona la función de
map
:
class Functor f where
map :: (a -> b) -> f a -> f b
Luego creamos instancias específicas de
Functor
para diferentes tipos de datos:
instance Functor List where
map :: (a -> b) -> List a -> List b
map _ Nil = Nil
map f (Cons a l) = Cons (f a) (map f l)
Los prototipos en JavaScript nos permiten modelar tanto los tipos de datos algebraicos como el polimorfismo ad-hoc. Por ejemplo, el código anterior se puede traducir de uno a uno a JavaScript de la siguiente manera:
var list = Cons(1, Cons(2, Cons(3, Nil)));
alert("length: " + length(list));
function square(n) {
return n * n;
}
var result = list.map(square);
alert(JSON.stringify(result, null, 4));
<script>
// data List a = Nil | Cons a (List a)
function List(constructor) {
Object.defineProperty(this, "constructor", {
value: constructor || this
});
}
var Nil = new List;
function Cons(head, tail) {
var cons = new List(Cons);
cons.head = head;
cons.tail = tail;
return cons;
}
// parametric polymorphism
function length(a) {
switch (a.constructor) {
case Nil: return 0;
case Cons: return 1 + length(a.tail);
}
}
// ad-hoc polymorphism
List.prototype.map = function (f) {
switch (this.constructor) {
case Nil: return Nil;
case Cons: return Cons(f(this.head), this.tail.map(f));
}
};
</script>
Aunque las clases también se pueden usar para modelar polimorfismos ad-hoc, todas las funciones sobrecargadas deben definirse en un solo lugar. Con los prototipos, puedes definirlos donde quieras.
Conclusión
Como puede ver, los prototipos son muy versátiles. Sí, se utilizan principalmente para modelar clases. Sin embargo, se pueden usar para muchas otras cosas.
Algunas de las otras cosas para las que se pueden usar los prototipos:
-
Creación de estructuras de datos persistentes con intercambio estructural.
- Comprender los vectores persistentes de Clojure, pt. 1
- Comprender los vectores persistentes de Clojure, pt. 2
- Comprender los vectores persistentes de Clojure, pt. 3
La idea básica del intercambio estructural es que, en lugar de modificar un objeto, cree un nuevo objeto que herede del objeto original y realice las modificaciones que desee. La herencia prototípica sobresale en eso.
-
Como otros han mencionado, los prototipos son dinámicos. Por lo tanto, puede agregar retroactivamente nuevos métodos de prototipo y estarán disponibles automáticamente en todas las instancias del prototipo.
Espero que esto ayude.