javascript - patterns - Patrón OLOO de Kyle Simpson vs Patrón de diseño de prototipo
pattern javascript example (8)
¿El "Patrón OLOO (objetos vinculados a otros objetos)" de Kyle Simpson difiere de alguna manera del patrón de diseño del prototipo? Aparte de acuñarlo por algo que indica específicamente "vinculación" (el comportamiento de los prototipos) y aclarar que no hay "copia" que ocurra aquí (un comportamiento de clases), ¿qué introduce exactamente su patrón?
Aquí hay un ejemplo del patrón de Kyle de su libro, "You Don''t Know JS: this & Object Prototypes":
var Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
var Bar = Object.create(Foo);
Bar.speak = function() {
alert("Hello, " + this.identify() + ".");
};
var b1 = Object.create(Bar);
b1.init("b1");
var b2 = Object.create(Bar);
b2.init("b2");
b1.speak(); // alerts: "Hello, I am b1."
b2.speak(); // alerts: "Hello, I am b2."
"Pensé que hacerlo hacía que cada obj dependiera del otro"
Como explica Kyle cuando dos objetos están
[[Prototype]]
vinculados, no dependen realmente el uno del otro;
en cambio son objeto individual.
Estás vinculando un objeto al otro con un enlace
[[Prototype]]
que puedes cambiar en cualquier momento que desees.
Si toma dos objetos vinculados
[[Prototype]]
creados a través del estilo OLOO como dependientes entre sí, también debe pensar lo mismo acerca de los creados a través de llamadas de
constructor
.
var foo= {},
bar= Object.create(foo),
baz= Object.create(bar);
console.log(Object.getPrototypeOf(foo)) //Object.prototype
console.log(Object.getPrototypeOf(bar)) //foo
console.log(Object.getPrototypeOf(baz)) //bar
Ahora piensa por un segundo, ¿crees que
foo
bar
y
baz
son dependientes el uno del otro?
Ahora hagamos lo mismo este código de estilo de
constructor
function Foo() {}
function Bar() {}
function Baz() {}
Bar.prototype= Object.create(Foo);
Baz.prototype= Object.create(Bar);
var foo= new Foo(),
bar= new Bar().
baz= new Baz();
console.log(Object.getPrototypeOf(foo)) //Foo.prototype
console.log(Object.getPrototypeOf(Foo.prototype)) //Object.prototype
console.log(Object.getPrototypeOf(bar)) //Bar.prototype
console.log(Object.getPrototypeOf(Bar.prototype)) //Foo.prototype
console.log(Object.getPrototypeOf(baz)) //Baz.prototype
console.log(Object.getPrototypeOf(Baz.prototype)) //Bar.prototype
La única diferencia entre el último y el primer código es que en el último, los objetos
foo
,
bar
,
baz
están vinculados entre sí a través de objetos arbitrarios de su función de
constructor
(
Foo.prototype
,
Bar.prototype
,
Baz.prototype
) pero en el primero (estilo
OLOO
) están vinculados directamente.
En ambas formas, solo estás vinculando
foo
,
bar
,
baz
entre sí, directamente en el primero e indirectamente en el último.
Pero, en ambos casos, los objetos son independientes entre sí porque no es realmente como una instancia de una clase que una vez instanciada, no se puede hacer que herede de otra clase.
Siempre puede cambiar qué objeto debe delegar un objeto también.
var anotherObj= {};
Object.setPrototypeOf(foo, anotherObj);
Entonces, todos son independientes entre sí.
"Esperaba que
OLOO
resolviera el problema en el que cada objeto no sabe nada del otro".
Sí, eso es realmente posible
Usemos
Tech
como un objeto de utilidad.
var Tech= {
tag: "technology",
setName= function(name) {
this.name= name;
}
}
cree tantos objetos como desee vinculados a
Tech
:
var html= Object.create(Tech),
css= Object.create(Tech),
js= Object.create(Tech);
Some checking (avoiding console.log)-
html.isPrototypeOf(css); //false
html.isPrototypeOf(js); //false
css.isPrototypeOf(html); //false
css.isPrototypeOf(js); //false
js.isPrototypeOf(html); //false
js.isPrototypwOf(css); //false
Tech.isPrototypeOf(html); //true
Tech.isPrototypeOf(css); //true
Tech.isPrototypeOf(js); //true
¿Crees que los objetos
html
,
css
,
js
están conectados entre sí?
No, no lo son.
Ahora veamos cómo podríamos haber hecho eso con la función de
constructor
.
function Tech() { }
Tech.prototype.tag= "technology";
Tech.prototype.setName= function(name) {
this.name= name;
}
cree tantos objetos como desee vinculados al
Tech.proptotype
-
var html= new Tech(),
css= new Tech(),
js= new Tech();
Algunas comprobaciones (evitando console.log) -
html.isPrototypeOf(css); //false
html.isPrototypeOf(js); //false
css.isPrototypeOf(html); //false
css.isPrototypeOf(js); //false
js.isPrototypeOf(html); //false
js.isPrototypeOf(css); //false
Tech.prototype.isPrototypeOf(html); //true
Tech.prototype.isPrototypeOf(css); //true
Tech.prototype.isPrototypeOf(js); //true
¿Cómo crees que estos objetos de estilo
constructor
(
html
,
css
,
js
) difieren del código de estilo
OLOO
?
De hecho, tienen el mismo propósito.
En el estilo
OLOO
uno delega objetos a
Tech
(la delegación se configuró explícitamente) mientras que en el estilo
constructor
uno delega a
Tech.prototype
(la delegación se configuró implícitamente).
Finalmente, terminas vinculando los tres objetos, sin vinculación entre sí, a un objeto, directamente usando el estilo
OLOO
, indirectamente usando el estilo
constructor
.
"Como está, ObjB tiene que ser creado a partir de ObjA .. Object.create (ObjB), etc."
No,
ObjB
aquí no es como una instancia (en lenguajes clásicos) de ninguna clase
ObjA
.
objB
objeto
objB
se delega en el objeto
ObjA
en el momento de su creación "
. Si utilizara el constructor, habría hecho el mismo ''acoplamiento'', aunque indirectamente haciendo uso de
.prototype
s.
¿Qué introduce exactamente su patrón?
OLOO adopta el prototipo de la cadena tal como está, sin necesidad de superponer otras semánticas (confusas de la OMI) para obtener el enlace.
Entonces, estos dos fragmentos tienen el mismo resultado EXACTO, pero llegan de manera diferente.
Forma de constructor:
function Foo() {}
Foo.prototype.y = 11;
function Bar() {}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.z = 31;
var x = new Bar();
x.y + x.z; // 42
Formulario OLOO:
var FooObj = { y: 11 };
var BarObj = Object.create(FooObj);
BarObj.z = 31;
var x = Object.create(BarObj);
x.y + x.z; // 42
En ambos fragmentos, un objeto
x
está
[[Prototype]]
vinculado a un objeto (
Bar.prototype
o
BarObj
), que a su vez está vinculado a un tercer objeto (
Foo.prototype
o
FooObj
).
Las relaciones y la delegación son idénticas entre los fragmentos.
El uso de la memoria es idéntico entre los fragmentos.
La capacidad de crear muchos "hijos" (es decir, muchos objetos como
x1
a
x1000
, etc.) es idéntica entre los fragmentos.
El rendimiento de la delegación (
xy
y
xz
) es idéntico entre los fragmentos.
El rendimiento de creación de objetos
es
más lento con OLOO, pero la
comprobación de la cordura que
revela que el rendimiento más lento realmente no es un problema.
Lo que sostengo que OLOO ofrece es que es mucho más simple expresar los objetos y vincularlos directamente, que vincularlos indirectamente a través del constructor /
new
mecanismos.
Este último pretende ser sobre clases, pero realmente es una sintaxis terrible para expresar delegación (
nota al margen: ¡también
lo es
class
sintaxis de
class
ES6!).
OLOO solo está eliminando al intermediario.
Aquí hay
otra comparación
de
class
vs OLOO.
¿Hay alguna manera de OLOO más que "dos" objetos ... todos los ejemplos consisten en el ejemplo basado (ver el ejemplo de OP). Digamos que teníamos los siguientes objetos, ¿cómo podemos crear un "cuarto" objeto que tenga los atributos de los "otros" tres? ala ...
var Button = {
init: function(name, cost) {
this.buttonName = name;
this.buttonCost = cost;
}
}
var Shoe = {
speed: 100
}
var Bike = {
range: ''4 miles''
}
Estos objetos son arbitrarios y pueden abarcar todo tipo de comportamientos. Pero lo esencial es que tenemos ''n'' número de objetos, y nuestro nuevo objeto necesita algo de los tres.
en lugar de los ejemplos dados ala:
var newObj = Object.create(oneSingularObject);
newObj.whatever..
PERO, nuestro nuevo Objeto = (Botón, Bicicleta, Zapato) ......
¿Cuál es el patrón para que esto funcione en OLOO?
@Marcus @bholben
Quizás podamos hacer algo como esto.
const Point = {
statics(m) { if (this !== Point) { throw Error(m); }},
create (x, y) {
this.statics();
var P = Object.create(Point);
P.init(x, y);
return P;
},
init(x=0, y=0) {
this.x = x;
this.y = y;
}
};
const Point3D = {
__proto__: Point,
statics(m) { if (this !== Point3D) { throw Error(m); }},
create (x, y, z) {
this.statics();
var P = Object.create(Point3D);
P.init(x, y, z);
return P;
},
init (x=0, y=0, z=0) {
super.init(x, y);
this.z = z;
}
};
Por supuesto, crear un objeto Point3D que se vincule al prototipo de un objeto Point2D es un poco tonto, pero eso no viene al caso (quería ser coherente con su ejemplo). De todos modos, en lo que respecta a las quejas:
-
La asimetría se puede arreglar con Object.setPrototypeOf de ES6 o con el más mal visto
__proto__ = ...
que uso. Ahora también podemos usar super en objetos normales, como se ve enPoint3D.init()
. Otra forma sería hacer algo comoconst Point3D = Object.assign(Object.create(Point), { ... }
aunque no me gusta particularmente la sintaxis.
-
Siempre podemos envolver
p = Object.create(Point)
y luegop.init()
en un constructor. por ejemplo,Point.create(x,y)
. Usando el código anterior, podemos crear una "instancia" dePoint3D
de la siguiente manera.var b = Point3D.create(1,2,3); console.log(b); // { x:1, y:2, z:3 } console.log(Point.isPrototypeOf(b)); // true console.log(Point3D.isPrototypeOf(b)) // true
-
Se me ocurrió este truco para emular métodos estáticos en OLOO. No estoy seguro de si me gusta o no. Requiere llamar a una propiedad especial en la parte superior de cualquier método "estático". Por ejemplo, hice que el método
Point.create()
estático.var p = Point.create(1,2); var q = p.create(4,1); // Error!
Alternativamente, con los Symbols ES6 puede ampliar de forma segura las clases base de Javascript. Por lo tanto, podría ahorrarse algo de código y definir la propiedad especial en Object.prototype. Por ejemplo,
const extendedJS = {};
( function(extension) {
const statics = Symbol(''static'');
Object.defineProperty(Object.prototype, statics, {
writable: true,
enumerable: false,
configurable: true,
value(obj, message) {
if (this !== obj)
throw Error(message);
}
});
Object.assign(extension, {statics});
})(extendedJS);
const Point = {
create (x, y) {
this[extendedJS.statics](Point);
...
@Marcus, al igual que tú, he estado interesado en OLOO y también me disgusta la asimetría como se describe en tu primer punto.
He estado jugando con una abstracción para recuperar la simetría.
Puede crear una función
link()
que se use en lugar de
Object.create()
.
Cuando se usa, su código podría verse así ...
var Point = {
init : function(x,y) {
this.x = x;
this.y = y;
}
};
var Point3D = link(Point, {
init: function(x,y,z) {
Point.init.call(this, x, y);
this.z = z;
}
});
Recuerde que
Object.create()
tiene un segundo parámetro que se puede pasar. Aquí está la función de enlace que aprovecha el segundo parámetro.
También permite un poco de configuración personalizada ...
function link(delegate, props, propsConfig) {
props = props || {};
propsConfig = propsConfig || {};
var obj = {};
Object.keys(props).forEach(function (key) {
obj[key] = {
value: props[key],
enumerable: propsConfig.isEnumerable || true,
writable: propsConfig.isWritable || true,
configurable: propsConfig.isConfigurable || true
};
});
return Object.create(delegate, obj);
}
Por supuesto, creo que @Kyle no respaldaría sombrear la función
init()
en el objeto Point3D.
;-)
@james emanon - Entonces, te estás refiriendo a la herencia múltiple (discutido en la página 75 del libro "No sabes JS: esto y prototipos de objetos"). Y ese mecanismo que podemos encontrar en la función "extender" de subrayado, por ejemplo. Los nombres de objetos que indicó en su ejemplo son un poco mezclando manzanas, naranjas y dulces, pero entiendo el punto detrás. Desde mi experiencia, esta sería la versión OOLO:
var ObjA = {
setA: function(a) {
this.a = a;
},
outputA: function() {
console.log("Invoking outputA - A: ", this.a);
}
};
// ''ObjB'' links/delegates to ''ObjA''
var ObjB = Object.create( ObjA );
ObjB.setB = function(b) {
this.b = b;
}
ObjB.setA_B = function(a, b) {
this.setA( a ); // This is obvious. ''setA'' is not found in ''ObjB'' so by prototype chain it''s found in ''ObjA''
this.setB( b );
console.log("Invoking setA_B - A: ", this.a, " B: ", this.b);
};
// ''ObjC'' links/delegates to ''ObjB''
var ObjC = Object.create( ObjB );
ObjC.setC = function(c) {
this.c = c;
};
ObjC.setA_C = function(a, c) {
this.setA( a ); // Invoking ''setA'' that is clearly not in ObjC shows that prototype chaining goes through ObjB all the way to the ObjA
this.setC( c );
console.log("Invoking setA_C - A: ", this.a, " C: ", this.c);
};
ObjC.setA_B_C = function(a, b, c){
this.setA( a ); // Invoking ''setA'' that is clearly not in ObjC nor ObjB shows that prototype chaining got all the way to the ObjA
this.setB( b );
this.setC( c );
console.log("Invoking setA_B_C - A: ", this.a, " B: ", this.b, " C: ", this.c);
};
ObjA.setA("A1");
ObjA.outputA(); // Invoking outputA - A: A1
ObjB.setA_B("A2", "B1"); // Invoking setA_B - A: A2 B: B1
ObjC.setA_C("A3", "C1"); // Invoking setA_C - A: A3 C: C1
ObjC.setA_B_C("A4", "B2", "C1"); // Invoking setA_B_C - A: A4 B: B2 C: C1
Es un ejemplo simple, pero el punto que se muestra es que solo estamos encadenando objetos en una estructura / formación bastante plana y aún tenemos la posibilidad de usar métodos y propiedades de múltiples objetos. Logramos lo mismo que con el enfoque de clase / "copiar las propiedades". Resumido por Kyle (página 114, "this & Object Prototypes"):
En otras palabras, el mecanismo real, la esencia de lo que es importante para la funcionalidad que podemos aprovechar en JavaScript, se trata de que los objetos estén vinculados a otros objetos .
Entiendo que una forma más natural para usted sería establecer todos los objetos "padres" (cuidadosos :)) en un lugar / llamada de función en lugar de modelar toda la cadena.
Lo que requiere es un cambio en los problemas de pensamiento y modelado en nuestras aplicaciones de acuerdo con eso. También me estoy acostumbrando. Espero que ayude y el veredicto final del propio Kyle sería genial. :)
La discusión en "You Don''t Know JS: this & Object Prototypes" y la presentación del OLOO son estimulantes y he aprendido muchísimo a través del libro. Los méritos del patrón OLOO están bien descritos en las otras respuestas; sin embargo, tengo las siguientes quejas de mascotas (o me falta algo que me impide aplicarlo de manera efectiva):
1
Cuando una "clase" "hereda" otra "clase" en el patrón clásico, las dos funciones pueden declararse sintaxis similar ( "declaración de función" o "declaración de función" ):
function Point(x,y) {
this.x = x;
this.y = y;
};
function Point3D(x,y,z) {
Point.call(this, x,y);
this.z = z;
};
Point3D.prototype = Object.create(Point.prototype);
Por el contrario, en el patrón OLOO, se utilizan diferentes formas sintácticas para definir la base y los objetos derivados:
var Point = {
init : function(x,y) {
this.x = x;
this.y = y;
}
};
var Point3D = Object.create(Point);
Point3D.init = function(x,y,z) {
Point.init.call(this, x, y);
this.z = z;
};
Como puede ver en el ejemplo anterior, el objeto base se puede definir usando la notación literal del objeto, mientras que la misma notación no se puede usar para el objeto derivado. Esta asimetría me molesta.
2
En el patrón OLOO, crear un objeto es dos pasos:
-
llamar a
Object.create
-
llame a un método personalizado no estándar para inicializar el objeto (que debe recordar ya que puede variar de un objeto a otro):
var p2a = Object.create(Point); p2a.init(1,1);
Por el contrario, en el patrón Prototipo, utiliza el operador estándar
new
:
var p2a = new Point(1,1);
3
En el patrón clásico, puedo crear funciones de utilidad "estáticas" que no se aplican directamente a un "instante" asignándolas directamente a la función de "clase" (a diferencia de su
.prototype
).
Por ejemplo, como función
square
en el siguiente código:
Point.square = function(x) {return x*x;};
Point.prototype.length = function() {
return Math.sqrt(Point.square(this.x)+Point.square(this.y));
};
Por el contrario, en el patrón OLOO también hay disponibles funciones "estáticas" (a través de la cadena [[prototype]]) en las instancias de objetos:
var Point = {
init : function(x,y) {
this.x = x;
this.y = y;
},
square: function(x) {return x*x;},
length: function() {return Math.sqrt(Point.square(this.x)+Point.square(this.y));}
};
Leí el libro de Kyle, y lo encontré realmente informativo, particularmente los detalles acerca de cómo está vinculado.
Pros:
Para mí, hay un par de grandes profesionales de OLOO:
1. simplicidad
OLOO se basa en
Object.create()
para crear un nuevo objeto que está
[[prototype]]
vinculado a otro objeto.
No tiene que comprender que las funciones tienen una propiedad
prototype
ni preocuparse por ninguno de los posibles problemas relacionados que surgen de su modificación.
2. Sintaxis más limpia
Esto es discutible, pero creo que la sintaxis de OLOO es (en muchos casos) más ordenada y más concisa que el enfoque de JavaScript ''estándar'', particularmente cuando se trata de polimorfismo (llamadas de
super
estilo).
Contras:
Creo que hay un diseño cuestionable (uno que realmente contribuye al punto 2 anterior), y que tiene que ver con el sombreado:
En la delegación de comportamiento, evitamos si es posible nombrar cosas iguales en diferentes niveles de la cadena
[[Prototype]]
.
La idea detrás de esto es que los objetos tienen sus propias funciones más específicas que luego delegan internamente en funciones más abajo en la cadena.
Por ejemplo, puede tener un objeto de
resource
con una función
save()
que envía una versión JSON del objeto al servidor, pero también puede tener un objeto
clientResource
que tiene una función
stripAndSave()
, que primero elimina las propiedades que No debe enviarse al servidor.
El problema potencial es: si alguien más aparece y decide hacer un objeto especial de
specialResource
, que no es completamente consciente de toda la cadena de prototipos, podría razonablemente * decidir guardar una marca de tiempo para el último guardado bajo una propiedad llamada
save
, que sombrea la base
save()
en el objeto de
resource
dos enlaces en la cadena del prototipo:
var resource = {
save: function () {
console.log(''Saving'');
}
};
var clientResource = Object.create(resource);
clientResource.stripAndSave = function () {
// Do something else, then delegate
console.log(''Stripping unwanted properties'');
this.save();
};
var specialResource = Object.create( clientResource );
specialResource.timeStampedSave = function () {
// Set the timestamp of the last save
this.save = Date.now();
this.stripAndSave();
};
a = Object.create(clientResource);
b = Object.create(specialResource);
a.stripAndSave(); // "Stripping unwanted properties" & "Saving".
b.timeStampedSave(); // Error!
Este es un ejemplo particularmente artificial, pero el punto es que específicamente no ocultar otras propiedades puede conducir a situaciones incómodas y al uso intensivo de un tesauro.
Quizás una mejor ilustración de esto sería un método
init
, particularmente conmovedor, ya que OOLO evita las funciones de tipo constructor.
Dado que cada objeto relacionado probablemente necesitará dicha función, puede ser un ejercicio tedioso nombrarlos apropiadamente, y la singularidad puede hacer que sea difícil recordar cuál usar.
* En realidad no es particularmente razonable (
lastSaved
sería mucho mejor, pero es solo un ejemplo).