forked from hectorip/Eloquent-JavaScript-es
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path06_object.txt
More file actions
1229 lines (1020 loc) · 45.5 KB
/
06_object.txt
File metadata and controls
1229 lines (1020 loc) · 45.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
:chap_num: 6
:prev_link: 05_higher_order
:next_link: 07_elife
:load_files: ["code/mountains.js", "code/chapter/06_object.js"]
:zip: node/html
= La Vida Secreta De Los Objetos =
[chapterquote="true"]
[quote, Joe Armstrong, entrevistado en Coders at Work]
____
El problema con los lenguajes orientados a objetos es que tienen todos
un medio implícito que llevan consigo. Tu quieres un plátano pero
eres un gorila con un plátano y la jungla entera.
____
(((Armstrong+++,+++ Joe)))(((objeto)))(((guerra sagrada)))Cuando un
programador dice ˝objeto˝, este es un término amplio. En mi profesión,
los objetos son una forma de vida, tema de guerras sagradas y una
amada palabra llamativa que todavía no ha perdido su gran poder.
Para alguien ajeno, esto es probablemente un poco confuso. Empecemos
con una introducción a la ((historia)) de los objetos como una forma
de programación.
== Historia ==
(((aislamiento)))(((historia)))(((programación orientada a objetos)))
(((Objeto)))Esta historia, como la mayor parte de las historias de
programación, comienza con el problema de la ((complejidad)). Una
filosofía es que la complejidad puede ser manejable separándola en
pequeñas partes, que son aisladas unas de otras. Estas partes han
terminado con el nombre de _objetos_.
[[interface]]
(((complejidad)))(((encapsulación)))(((método)))(((interfaz)))Un
objeto es una cápsula opaca que oculta una sofisticada complejidad
en su interior y en su lugar nos ofrece unos pocos reguladores y
conectores (como por ejemplo ((método))s) que presentan una _interfaz_
a través de la cual el objeto es usado.
La idea es que la la interfaz es relativamente simple y todas las
cosas complejas que van _dentro_ del objeto pueden ser ignoradas
cuando trabajamos con el.
image::img/object.jpg[alt="Una interfaz simple puede ocultar gran complejidad.",width="6cm"]
Como ejemplo, puedes imaginarte un objeto que te dé una interfaz para
un área de la pantalla. Te da una forma de dibujar formas o texto
en este área pero oculta los detalles de como esas formas son
convertidas a los pixeles que decoran la pantalla actualmete. Puedes
tener un conjunto de métodos-por ejemplo ++dibujarCirculo++-y estos
son lo único que necesitas para usar un objeto.
(((programación orientada a objetos)))Estas ideas fueron puestas en
marcha en los 70, los 80 y los 90, fueron acompañadas por un gran
((bombo))-la revolución de la programación orientada a objetos.
Inmediatamente había un grupo de gente declarando que los objetos
eran el camino _correcto_ a la programación-y que no incluir objetos
era un sin sentido, estaba obsoleto.
Este tipo de fanatismo siempre produce mucha estupidez no práctica
y ha habido una pequeña contra revolución después de esto. Actualmente
en algunos círculos, los objetos tienen una reputación bastante mala.
Yo prefiero abordar el tema desde la práctica, en lugar de desde la
ideología. Hay varios conceptos útiles, más importantes que la
_((encapsulación))_ (distinguir entre la complejidad interna y externa
de la interfaz), que la cultura de la programación orientada a objetos
a popularizado. Estos son dignos de estudio.
En este capítulo se describen de forma excéntrica los objetos y las
técnicas clásicas sobre como se relacionan entre sí los objetos en
JavaScript.
[[obj_methods]]
== Métodos ==
(((ejemplo conejo)))(((método)))(((propiedad)))Los métodos son propiedades
simples que contienen funciones como valores. Este es un método simple:
[source,javascript]
----
var conejo = {};
conejo.hablar = function(linea) {
console.log("El conejo dice '" + linea + "'");
};
conejo.hablar("Estoy vivo.");
// → El conejo dice 'Estoy vivo.'
----
(((this)))(((llamada método)))Normalmente el método necesita hacer algo
con el objeto desde el que se le ha llamado. Cuando una función es llamada
como método-se busca como propiedad y es inmediatamente llamada, como en
++objeto.metodo()++—la variable especial `this` esta en su cuerpo y
apuntará al objeto que la ha llamado.
// test: join
// include_code top_lines:6
[source,javascript]
----
function hablar(linea) {
console.log("El conejo " + this.tipo + " dice '" +
line + "'");
}
var conejoBlanco = {tipo: "blanco", hablar: hablar};
var conejoGordo = {tipo: "gordo", hablar: hablar};
conejoBlanco.hablar("¡Por mis orejas y los pelos de mi " +
"bigote, que tarde se está haciendo!");
// → El conejo blanco dice '¡Por mis orejas y los pelos'
// de mi bigote, que tarde se está haciendo!'
conejoGordo.hablar("Puedes estar seguro de que me comería +"
"una zanahoria.");
// → El conejo gordo dice 'Puedes estar seguro de que
// me comería una zanahoria.'
----
(((método apply)))(((método bind)))(((this)))(((ejemplo conejo)))El
código usa la palabra clave `this` para la salida del tipo de conejo
que está hablando. Se puede rellamar con los métodos `apply` y `bind`,
ambos toman un primer argumento que puede ser utilizado para simular
llamadas al método. El primer argumento es de echo utilizado para dar valor a
`this`.
[[call_method]]
(((método call)))Hay un método similar a `apply`, llamado `call`. Este
llama a la función que es un método, pero toma sus argumentos normalmente,
en lugar de con un array. Como `apply` y `bind`, `call` puede pasar un valor
específico de `this`.
[source,javascript]
----
hablar.apply(conejoGordo, ["¡Burp!"]);
// → El conejo gordo dice '¡Burp!'
hablar.call({tipo: "viejo"}, "¡Oh!, ¡Ah!");
// → El conejo viejo dice '¡Oh!, ¡Ah!'
----
[[prototypes]]
== Prototipos ==
(((método toString)))Fíjate detenidamente.
[source,javascript]
----
var vacio = {};
console.log(vacio.toString);
// → function toString(){…}
console.log(vacio.toString());
// → [object Object]
----
(((magia)))Acabo de extraer una propiedad de un objeto vacío. ¡Magia!
(((propiedad)))(((objeto)))Bien, no realmente. Simplemente he omitido
información acerca de como los Objetos funcionan en JavaScript. Además
de sus propiedades, casi todos los objetos además tienen un _prototipo_.
Un ((prototipo)) es otro objeto que es usado como alternativa fuente de
propiedades. Cuando un objeto tiene una llamada a una propiedad que no
posee, se buscará en su prototipo, después en el prototipo de su prototipo
y así sucesivamente.
(((Object prototype)))Entonces, ¿Cual es el ((prototipo)) de este objeto
vacío? Es el genial prototipo ancestral, la entidad detrás de casi todos
los objetos, `Object.prototype`.
[source,javascript]
----
console.log(Object.getPrototypeOf({}) ==
Object.prototype);
// → true
console.log(Object.getPrototypeOf(Object.prototype));
// → null
----
(((función getPrototypeOf)))Como imaginarás la función
`Object.getPrototypeOf` devuelve el prototipo de un objeto.
(((método toString)))Las relaciones de prototipo en JavaScript tienen
forma de ((árbol)), y la raíz de esta estructura es `Object.prototype`.
Este provee unos pocos ((métodos)) que se mostrarán en casi todos los objetos,
como `toString`, que convierte un objeto en una representación en una cadena
de texto.
(((herencia)))(((Function prototype)))(((Array
prototype)))(((Object prototype)))Muchos objetos no tienen directamente
`Object.prototype` como su ((prototipo)), pero en su lugar tienen otro
objeto, que les provee sus propiedades por defecto. Las funciones derivan
de `Function.prototype`, y los arrays derivan de `Array.prototype`.
[source,javascript]
----
console.log(Object.getPrototypeOf(isNaN) ==
Function.prototype);
// → true
console.log(Object.getPrototypeOf([]) ==
Array.prototype);
// → true
----
(((Object prototype)))Como un objeto prototipo tiene su propio prototipo
normalmente `Object.prototype`, entonces este indirectamente provee de
métodos como `toString`.
(((función getPrototypeOf)))(((ejemplo conejo)))(((función Object.create)))
La función `Object.getPrototypeOf` obviamente devuelve el prototipo de un
objeto. Puedes usar `Object.create` para crear un objeto con un ((prototipo))
específico.
[source,javascript]
----
var protoConejo = {
hablar: function(linea) {
console.log("El conejo " + this.type + " dice '" +
linea + "'");
}
};
var conejoAsesino = Object.create(protoConejo);
conejoAsesino.type = "asesino";
conejoAsesino.hablar("SKREEEE!");
// → El conejo asesino dice 'SKREEEE!'
----
(((propiedad compartida)))El “proto” conejo actúa como container para las
propiedades que son compartidas por todos los conejos. Un objeto conejo
individual, como el conejo asesino, contiene propiedades que se aplican
únicamente a sí mismo, en este caso su tipo y propiedades derivadas de su
prototipo.
[[constructors]]
== Constructores ==
(((operador new)))(((this)))(((palabra clave return)))(((objeto,creación)))Una
forma más conveniente de crear objetos que deriven su forma de prototipos
compartidos es usar un _((constructor))_. En JavaScript, llamar a una función
con la palabra clave `new` delante de ella, hace que sea tratada como un
constructor. El constructor tendrá su variable `this` en los límites del
objeto creado, y si no se específica otro valor de objeto este será el nuevo
objeto que retorne la llamada.
Un objeto creado con `new` se dice que es una _((instancia))_ de su constructor.
(((ejemplo conejo)))(((capitalización)))Tenemos un constructor simple para los
conejos. Es una convención capitalizar (poner la primera letra en mayúscula)
los nombres de los constructores así son fácilmente distinguidos de otras
funciones.
// include_code top_lines:6
[source,javascript]
----
function Conejo(tipo) {
this.tipo = tipo;
}
var conejoAsesino = new Conejo("asesino");
var conejoNegro = new Conejo("negro");
console.log(conejoNegro.tipo);
// → negro
----
(((propiedad prototype)))(((constructor)))Los constructores (de hecho,
todas las funciones) automáticamente tienen una propiedad llamada
`prototype`, que por defecto contiene un objeto plano, vacío que deriva
de `Object.prototype`. Todas las instancias creadas con este constructor
tendrán este objeto como su ((prototipo)). Así que para añadir un método
`hablar` a los conejos creados con el constructor `Conejo`, simplemente
hacemos lo siguiente:
// include_code top_lines:4
[source,javascript]
----
Conejo.prototype.hablar = function(linea) {
console.log("El conejo " + this.tipo + " dice '" +
linea + "'");
};
conejoNegro.hablar("Maldición...");
// → El conejo negro dice 'Maldición...'
----
(((propiedad prototype)))(((función getPrototypeOf)))Es importante
notar la diferencia entre la forma en que un prototipo es asociado con
un constructor (a través de su propiedad `prototype`) y la forma en
la que los objetos _tienen_ un prototipo (que podemos consultar con
`Object.getPrototypeOf`). El prototipo actual de un constructor es
`Function.prototype` desde que los constructores son funciones. Esta
propiedad `prototype` será el prototipo de las instancias creadas a
través de el pero no su _propio_ prototipo.
== Sobre Escribiendo Las Propiedades Derivadas ==
(((propiedad compartida)))(((sobre escribir)))Cuando añades una ((propiedad))
a un objeto, esté presente en el prototipo o no, la propiedad es
añadida a _ese_ objeto, que de ahora en adelante tendrá como su
propiedad. Si _existe_ una propiedad con el mismo nombre en el prototipo,
esta propiedad no afectará más al objeto. El prototipo por si mismo no
cambia.
[source,javascript]
----
Conejo.prototype.dentadura = "pequeña";
console.log(conejoNegro.dentadura);
// → pequeña
conejoAsensino.dentadura = "larga, afilada y sangrienta";
console.log(conejoAsesino.dentadura);
// → larga, afilada y sangrienta
console.log(conejoNegro.dentadura);
// → pequeña
console.log(Conejo.prototype.dentadura);
// → pequeña
----
(((prototipo,diagrama)))El siguiente diagrama representa la situación
después de ejecutar este código. El `Conejo` y `Objeto` ((prototipo))s
están detrás de `conejoAsesino` como una especie telón de fondo,
donde sus propiedades que no son encontradas en el objeto por si mismo
pueden ser buscadas.
image::img/rabbits.svg[alt="Esquema de objeto prototipo conejo",width="8cm"]
(((propiedad compartida)))Sobre escribir propiedades que existen en un prototipo,
es a menudo algo útil que hacer. Como muestra el ejemplo de la dentadura
del conejo, esto puede ser usado para expresar propiedades excepcionales
en instancias de una clase más genérica de objetos, mientras dejamos los
objetos no excepcionales simplemente tomar un valor estándar de su prototipo.
(((método toString)))(((Array prototype)))(((Function prototype)))Esto
es además usado para dar a los prototipos de función y array un método
`toString` diferente del básico prototipo de los objetos.
[source,javascript]
----
console.log(Array.prototype.toString ==
Object.prototype.toString);
// → false
console.log([1, 2].toString());
// → 1,2
----
(((método toString)))(((método join)))(((método call)))Llamar a
`toString` en un array da un resultado similar a `.join(",")`-esto
pone comas entre los valores del array. Una llamada directa a
`Object.prototype.toString` con un array produce una cadena
de texto diferente. Esta función no sabe acerca de arrays, así que
simplemente pone la palabra "object" y el nombre del tipo entre corchetes.
[source,javascript]
----
console.log(Object.prototype.toString.call([1, 2]));
// → [object Array]
----
== Interferencia de prototipos ==
(((prototipo,interferencia)))(((ejemplo conejo)))(((mutabilidad)))Un
((prototipo)) puede ser usado en cualquier momento para añadir nuevas
propiedades y métodos a todos los objetos basados en él. Por ejemplo,
puede ser necesario para poner a nuestros conejos a bailar.
[source,javascript]
----
Conejo.prototype.bailar = function() {
console.log("El conejo " + this.type + " baila un paso.");
};
conejoAsesino.bailar();
// → El conejo asesino baila un paso.
----
(((map)))(((objeto,como mapa)))Esto es conveniente. Pero hay situaciones
donde esto causa problemas. En capítulos anteriores, hemos usado un
objeto como forma de asociar valores con nombres creando propiedades para
los nombres y dándoles los correspondientes valores como su valor.
Aquí hay un ejemplo link:04_data.html#object_map[Chapter 4]:
// include_code
[source,javascript]
----
var mapa = {};
function guardarPhi(evento, phi) {
mapa[evento] = phi;
}
guardarPhi("pizza", 0.069);
guardarPhi("árbol tocado", -0.081);
----
(((bucle for/in)))(((operador in)))Podemos iterar sobre todos los valores
de phi en el objeto usando un bucle `for`/`in` y comprobar cuando un nombre
esta usando el operador regular `in`. Pero desafortunadamente, el objeto
del prototipo continua con su camino.
[source,javascript]
----
Object.prototype.sinSentido = "hola";
for (var nombre in mapa)
console.log(nombre);
// → pizza
// → árbol tocado
// → sinSentido
console.log("sinSentido" in mapa);
// → true
console.log("toString" in mapa);
// → true
// Borrar la propiedad problemática
delete Object.prototype.sinSentido;
----
(((prototype,contaminación)))(((método toString)))Esta todo mal. No hay
evento llamado "sinSentido" en nuestro set de datos. Y _definitivamente_
no hay evento llamado "toString".
(((enumerabilidad)))(((bucle for/in)))(((propiedad)))Extrañamente,
`toString` no se muestra en el bucle `for`/`in`, pero el operador `in`
ha retornado true para el. Esto es por que JavaScript distingue entre
propiedades _enumerable_ (enumerables) y _nonenumerable_ (no enumerables).
(((Objeto prototipo)))Todas las propiedades que creamos simplemente
asignándolas son enumerables. Las propiedades estándar en `Object.prototype`
son todas nonenumerable, que es por lo que no se muestran en un bucle como
un `for`/`in`.
(((función defineProperty)))Es posible definir nuestras propias propiedades
nonenumberable usando la función `Object.defineProperty`, esta nos permite
controlar el tipo de propiedad que estamos creando.
[source,javascript]
----
Object.defineProperty(Object.prototype, "ocultarSinSentido",
{enumerable: false, value: "hola"});
for (var nombre in mapa)
console.log(nombre);
// → pizza
// → árbol tocado
console.log(mapa.ocultarSinSentido);
// → hola
----
(((operador in)))(((mapa)))(((objeto,como mapa)))(((método hasOwnProperty
)))Entonces ahora la propiedad esta, pero no se muestra en un bucle.
Esto es bueno. Pero seguimos teniendo el problema con el operador regular
`in` demandando que las propiedades del `Object.prototype` existen en
nuestro objeto. Para esto, podemos usar el método de objeto `hasOwnProperty`.
[source,javascript]
----
console.log(mapa.hasOwnProperty("toString"));
// → false
----
(((propiedad,propia)))Este método nos dice cuando el objeto _por si mismo_
tiene la propiedad, sin mirar en sus prototipos. Esto es a menudo una
información más útil que la que nos da el operador `in`.
(((prototipo,contaminación)))(((bucle for/in)))Cuando tu estás preocupado
de que algo (algún otro código que has incluido en tu programa) puede
tener problemas con el objeto base prototipo, te recomiendo escribir
bucles `for`/`in` como este:
[source,javascript]
----
for (var nombre in mapa) {
if (mapa.hasOwnProperty(nombre)) {
// ... esta es una propiedad propia
}
}
----
== Objetos sin prototipo ==
(((mapa)))(((objeto,como mapa)))(((método hasOwnProperty)))Pero el
agujero del conejo no acaba aquí. ¿Qué pasa si alguien registra el nombre
`hasOwnProperty` en nuestro objeto `mapa` y le asigna el valor 42? Ahora
la llamada a `mapa.hasOwnProperty` intentará llamar a la propiedad local,
que contiene un número, no una función.
(((función Object.create)))(((prototype,evitar)))En este caso, los
prototipos solo continúan su camino y nosotros podemos preferir tener
objetos sin prototipos por ahora. Vemos la función `Object.create`,
que nos permite crear un objeto con un prototipo específico. Le puedes
pasar `null` como prototipo para crear un objeto vacío sin prototipo.
Para objetos como `map`, donde las propiedades pueden ser cualquiera,
esto es exactamente lo que queremos.
[source,javascript]
----
var mapa = Object.create(null);
mapa["pizza"] = 0.069;
console.log("toString" in mapa);
// → false
console.log("pizza" in mapa);
// → true
----
(((operador in)))(((bucle for/in)))(((Object prototype)))¡Mucho
mejor! Ya no necesitaremos la chapuza de `hasOwnProperty` por que
todas las propiedades que el objeto tiene son sus propias propiedades.
Ahora podemos usar de forma segura bucles `for`/`in`, no hay problema
con lo que la gente le haya estado haciendo a `Object.prototype`.
== Polimorfismo ==
(((método toString)))(((función
String)))(((polimorfismo)))(((sobre escribiendo)))Cuando llamas a la función
`String`, que convierte un valor en una cadena, en un objeto, esta
llamará al método `toString` cuando el objeto trate de crear una cadena
con sentido para retornarla. He mencionado que alguno de los prototipos
estándar definen su propia versión de `toString` así que ellos pueden
crear cadenas que contengan información más útil que `"[object Object]"`.
(((programación orientada a objetos)))Esta es una simple instancia de
una poderosa idea. Cuando un trozo de código es escrito para trabajar
con objetos que tienen una ((interfaz)) concreta -en este caso, un método
`toString`- entonces cualquier tipo de objeto que soporte esta interfaz y
pueda ser introducido en el código, simplemente funcionará.
Esta técnica es llamada __polimorfismo__-aunque no hay cambio de forma
real actualmente involucrado. El código polimórfico puede trabajar con
valores de diferentes formas, tantas como sean soportadas por la interfaz.
[[tables]]
== Dando estilo a una tabla ==
(((MOUNTAINS data set)))(((ejemplo tabla)))Voy a trabajar a través de un
ejemplo un poco más complicado en un intento de darte una idea mejor de
como se utiliza el ((polimorfismo)) y la ((programación orientada a objetos))
en general. El proyecto es este: escribiremos un programa que, dado un array
de arrays, de ((tabla)) celdas, construya una cadena de texto que contenga
un genial diseño de tabla-significa que las columnas y las filas están
correctamente alineadas. Algo como esto:
[source,text/plain]
----
nombre altura país
------------ ------ -------------
Kilimanjaro 5895 Tanzania
Everest 8848 Nepal
Mount Fuji 3776 Japan
Mont Blanc 4808 Italy/France
Vaalserberg 323 Netherlands
Denali 6168 United States
Popocatepetl 5465 Mexico
----
La forma en que nuestro sistema de generación de tablas funcionará es que
la función generadora preguntará a cada celda cual va a ser su ancho y alto
y después usar esa información para determinar la anchura de las columnas
y la altura de las filas. La función generadora después pedirá a las celdas
que se dibujen a sí mismas con el tamaño correcto y ensamblando los
resultados en una sola cadena.
[[table_interface]]
(((ejemplo tabla)))El programa de estilo se comunicará con los objetos
celda a través de una interfaz bien definida. De esta forma, los tipos
de celda que el programa soporta no estarán fijados. Podremos añadir nuevos
tipos de celda más adelante-por ejemplo, celdas subrayadas para la cabecera
de la tabla-y si lo soporta nuestra interfaz, simplemente funcionará, sin
requerir cambios al programa de diseño.
Esta es la interfaz:
* `minAltura()` devuelve un número indicando la altura mínima que la celda
requiere (en lineas).
* `minAnchura()` devuelve un número indicando la anchura mínima de esta celda
en caracteres).
* `dibujar(anchura, altura)` devuelve un array de tamaño
`altura`, que contiene una serie de cadenas que son cada `anchura` en caracteres.
Esto representa el contenido de la celda.
(((función,orden superior)))Voy a hacer uso intensivo de métodos de orden
superior en arrays en este ejemplo, ya que se presta bien a este enfoque.
(((función alturasFila)))(((función anchurasColumna)))(((máximo)))(((método map)))
(((método reduce)))La primera parte del programa calcula arrays de los mínimos
anchos de columna y altos de fila para una grilla de celdas.
La variable `filas` contendrá un array de arrays, con cada array interno
representado una fila de celdas.
// include_code
[source,javascript]
----
function alturasFila(filas) {
return filas.map(function(fila) {
return fila.reduce(function(max, celda) {
return Math.max(max, celda.minAltura());
}, 0);
});
}
function anchurasColumna(filas) {
return filas[0].map(function(_, i) {
return filas.reduce(function(max, fila) {
return Math.max(max, fila[i].minAnchura());
}, 0);
});
}
----
(((carácter guión bajo)))(((estilo de programación)))Usar un nombre
de variable que comience con un guión bajo (_) o que consista en un
simple guión bajo es una forma de indicar (a los lectores humanos) que
este argumento no se utilizará.
La función `alturasFila` no debería ser demasiado difícil de seguir.
Esta usa `reduce` para calcular la altura máxima de un array de
celdas y está dentro de un `map` para conseguir que se haga para
todas las filas en el array `filas`.
(((método map)))(((método filter)))(((método forEach)))
(((array,indexación)))(((método reduce)))Las cosas son un poco
mas complicadas para la función `anchurasColumna` por que el array
exterior es un array de filas, no de columnas. Se me ha olvidado
mencionar que a `map` (como a `forEach`, `filter`, y métodos similares de
array) se les puede pasar un segundo argumento, este es en la función
el ((índice)) del elemento actual. Mapeando los elementos de la primera
fila y usando solo el segundo argumento de la función mapping, `colWidths`
genera un array con un elemento para cada índice de columna. La llamada
a `reduce` se ejecuta sobre el array externo `filas` para cada índice y
se extrae la anchura de la celda más ancha para ese índice.
(((ejemplo tabla)))(((función dibujarTabla)))Aquí esta el código
para dibujar una tabla:
// include_code
[source,javascript]
----
function dibujarTabla(filas) {
var alturas = alturasFilas(filas);
var anchuras = anchurasColumnas(filas);
function dibujarLinea(bloques, numLinea) {
return bloques.map(function(bloque) {
return bloque[numLinea];
}).join(" ");
}
function dibujarFila(fila, numFila) {
var bloques = fila.map(function(celda, numColumna) {
return celda.dibujar(anchuras[numColumna], alturas[numFila]);
});
return bloques[0].map(function(_, numLinea) {
return dibujarLinea(bloques, numLinea);
}).join("\n");
}
return filas.map(dibujarFila).join("\n");
}
----
(((función interna)))(((anidar, funciones)))La función `dibujarTabla`
usa la función auxiliar interna `dibujarFila` para dibujar todas las
filas y después unirlas todas con el caracteres de nueva línea.
(((ejemplo tabla)))La función `dibujarFila` por si misma convierte los
objetos celda en la fila a _bloques_, que son los arrays de cadenas
representando el contenido de las celdas, separados por línea. Una
celda simple contiene simplemente el número 3776 puede ser representado
como un elemento simple de array como `["3776"]`, como una celda
subrayada nos va a ocupar dos lineas será representada por el array
`["nombre", "------"]`.
(((método map)))(((método join)))Los bloques para una fila, que tienen
la misma altura, deben aparecer uno junto a otro en la salida final.
La segunda llamada a `map` en `dibujarFila` genera esta salida línea a
línea mapeando a través de las líneas desde el bloque más a la izquierda y,
para cada uno de estos, coleccionando una línea que ocupa la anchura
total de la tabla. Estas líneas están unidas con el carácter nueva línea
para proveer la fila entera como valor de retorno de `dibujarFila`.
La función `dibujarLinea` extrae líneas que deben aparecer unas junto a
otras de un array de bloques y las une con un carácter espacio para crear
un hueco de un carácter entre las columnas de la tabla.
[[split]]
(((método split)))(((string,métodos)))(((ejemplo tabla)))Ahora
vamos a escribir un constructor, para las celdas que contienen texto,
que implementa la ((interfaz)) para las celdas de la tabla. El constructor
separa una cadena en un array de líneas usando el método de string `split`,
que separa una cadena en cada ocurrencia de su argumento y retorna un
array de piezas. El método `minAnchura` encuentra la máxima anchura de línea
en este array.
// include_code
[source,javascript]
----
function repetir(cadena, veces) {
var resultado = "";
for (var i = 0; i < veces; i++)
resultado += cadena;
return resultado;
}
function CeldaTexto(texto) {
this.texto = texto.split("\n");
}
CeldaTexto.prototype.minAnchura = function() {
return this.texto.reduce(function(anchura, linea) {
return Math.max(anchura, linea.length);
}, 0);
};
CeldaTexto.prototype.minAltura = function() {
return this.texto.length;
};
CeldaTexto.prototype.dibujar = function(anchura, altura) {
var resultado = [];
for (var i = 0; i < altura; i++) {
var linea = this.texto[i] || "";
resultado.push(linea + repetir(" ", anchura - linea.length));
}
return resultado;
};
----
(((tipo CeldaTexto)))El código usa una función auxiliar llamada `repetir`
que genera una cadena cuyo valor es el argumento `cadena` repetido las
`veces` que se indica. El método `dibujar` se usa para añadir “espacio“
a las líneas ya que todas ellas tiene la longitud requerida.
Vamos a probar lo que hemos escrito hasta ahora generando un damero
de 5 x 5.
[source,javascript]
----
var filas = [];
for (var i = 0; i < 5; i++) {
var fila = [];
for (var j = 0; j < 5; j++) {
if ((j + i) % 2 == 0)
fila.push(new CeldaTexto("##"));
else
fila.push(new CeldaTexto(" "));
}
filas.push(fila);
}
console.log(dibujarTabla(filas));
// → ## ## ##
// ## ##
// ## ## ##
// ## ##
// ## ## ##
----
¡Esto funciona! Pero como todas las celda tienen la misma anchura,
el código de diseñar tabla no hace algo realmente interesante.
[[mountains]]
(((data set)))(((MOUNTAINS data set)))La fuente de datos de la tabla de
las montañas que estamos tratando de generar esta disponible en la
variable `MOUNTAINS` en el ((sandbox)) y además es
http://eloquentjavascript.net/code/mountains.js[descargable] y desde la
web (!book (http://eloquentjavascript.net/code#6[_eloquentjavascript.net/code#6_])!).
(((ejemplo tabla)))Queremos destacar la fila de arriba, que contiene los
nombres de las columnas, subrayando las celdas con una serie de caracteres
guión. No hay problema-simplemente escribiremos un tipo de celda que
soporte subrayado.
// include_code
[source,javascript]
----
<<<<<<< HEAD
function CeldaSubrayada(contenido) {
this.contenido = contenido;
};
CeldaSubrayada.prototype.minAnchura = function() {
return this.contenido.minAnchura();
=======
function UnderlinedCell(inner) {
this.inner = inner;
}
UnderlinedCell.prototype.minWidth = function() {
return this.inner.minWidth();
>>>>>>> marijnh/master
};
CeldaSubrayada.prototype.minAltura = function() {
return this.contenido.minAltura() + 1;
};
CeldaSubrayada.prototype.dibujar = function(anchura, altura) {
return this.contenido.dibujar(anchura, altura - 1)
.concat([repetir("-", anchura)]);
};
----
(((tipo CeldaSubrayada)))Una celda subrayada contiene otra celda.
Esto significa que su tamaño mínimo será el mismo que el de la
celda interna (llamando a través de los métodos de estas celdas
`minAnchura` y `minAltura`) pero añade uno a la altura para contar
el espacio tomado por el subrayado.
(((método concat)))(((concatenación)))Dibujar una celda es muy simple
-nosotros tomamos el contenido de la celda interior y le concatenamos
a una línea simple de guiones.
(((función datosTabla)))Teniendo un mecanismo de subrayado, ahora podemos
escribir una función que genere una grilla de celdas para nuestro set de
datos.
// test: wrap, trailing
[source,javascript]
----
function datosTabla(datos) {
var keys = Object.keys(datos[0]);
var encabezados = keys.map(function(nombre) {
return new CeldaSubrayada(new TextCell(nombre));
});
var cuerpo = datos.map(function(row) {
return keys.map(function(nombre) {
return new CeldaTexto(String(row[nombre]));
});
});
return [encabezados].concat(cuerpo);
}
console.log(dibujarTabla(datosTabla(MOUNTAINS)));
// → nombre altura país
// ------------ ------ -------------
// Kilimanjaro 5895 Tanzania
// … etcétera
----
[[keys]]
(((función Object.keys)))(((propiedad)))(((bucle for/in)))La función
estándar `Object.keys` retorna un array de nombres de propiedades en
un objeto. La fila de arriba de la tabla debe contener celdas subrayadas
que den los nombres a las columnas. Debajo, los valores de todos los
objetos en el set de datos parecen celdas normales-los extraeremos
mapeando sobre el array `keys` así que estamos seguros de que el orden
de las celdas es el mismo en cada fila.
(((alinear a la derecha)))La tabla resultante parece la del ejemplo
mostrado antes, excepto por que on tiene el alineamiento a la derecha
de los número en la columna `altura`. Vamos a conseguirlo en un momento.
== Getters y setters ==
(((getter)))(((setter)))(((propiedad)))Cuando especificamos una interfaz,
es posible incluir propiedades que no son métodos. Podemos tener definida
`minAltura` y `minAnchura` para simplemente almacenar números. Pero esto
podría requerir que lo calculáramos en él ((constructor)), esto añade
código en el que no es estrictamente relevante para _construir_ el objeto.
Esto podría causar problemas si, por ejemplo, el interior de una celda
subrayada cambia, en este punto el tamaño del subrayado de la celda debería
cambiar también.
(((estilo de programación)))Esto ha servido como excusa para adoptar el
principio de no incluir nunca propiedades que no sean métodos en las
interfaces. Más que un acceso directo a un propiedad de valor simple,
se pueden usar los métodos `getAlgo` y `setAlgo` para leer y escribir
la propiedad. Esta aproximación tiene el inconveniente de que tu tienes
que escribir -y leer- un montón de métodos adicionales.
Afortunadamente, JavaScript provee de una técnica que nos da lo mejor
de ambos mundos. Podemos especificar propiedades que, dese fuera, parezcan
propiedades normales pero secretamente tienen ((método))s asociados con ellas.
[source,javascript]
----
var pila = {
elementos: ["cascara de huevo", "peladura de naranja", "gusano"],
get altura() {
return this.elementos.length;
},
set altura(valor) {
console.log("Ignorando el intento de guardar la altura: ", valor);
}
};
console.log(pila.altura);
// → 3
pila.altura = 100;
// → Ignorando el intento de guardar la altura: 100
----
(((función defineProperty)))((({}
(objeto))))(((getter)))(((setter)))En un objeto literal, la notación
`get` o `set` para propiedades te permite especificar una función para
ser ejecutada cuando la propiedad es leída o escrita. Podemos incluso
añadir una propiedad a un objeto existente, por ejemplo un prototipo,
usando la función `Object.defineProperty` (que hemos usado previamente
para crear propiedades nonenumerable).
[source,javascript]
----
Object.defineProperty(CeldaTexto.prototype, "alturaProp", {
get: function() { return this.texto.length; }
});
var celda = new CeldaTexto("sin\nsalida");
console.log(celda.alturaProp);
// → 2
celda.alturaProp = 100;
console.log(celda.alturaProp);
// → 2
----
Puedes usar la propiedad similar `set`, en el objeto pasándola a
`defineProperty`, para especificar un método setter. Cuando se define
un getter pero no un setter, escribir la propiedad es simplemente ignorado.
== Herencia ==
(((herencia)))(((ejemplo tabla)))(((alineamiento)))
(((tipo CeldaTexto)))Todavía no hemos acabado el ejercicio de diseño
de tabla. Ayuda a la legibilidad alinear a la derecha las columnas con
números. Debemos crear otro tipo de celda que es como `CeldaTexto`, pero sin
espacio en la parte derecha, estas tienen el espacio en la parte izquierda
así que aliniémoslas a la derecha.
(((tipo DCeldaTexto)))Podemos simplemente escribir un nuevo ((constructor))
entero con los tres métodos en su prototipo. Pero los prototipos pueden
tener sus prototipos, y esto nos permite hacer algo inteligente.
// include_code
[source,javascript]
----
function DCeldaTexto(texto) {
CeldaTexto.call(this, texto);
}
DCeldaTexto.prototype = Object.create(CeldaTexto.prototype);
DCeldaTexto.prototype.dibujar = function(anchura, altura) {
var resultado = [];
for (var i = 0; i < altura; i++) {
var linea = this.text[i] || "";
resultado.push(repetir(" ", anchura - linea.length) + linea);
}
return resultado;
};
----
(((propiedad compartida)))(((sobre escribiendo)))(((interfaz)))Reutilizamos el
constructor y los métodos `minAltura` y `minAnchura` de `CeldaTexto`.
Una `DCeldaTexto` es ahora básicamente equivalente a `CeldaTexto`,
excepto por que su método `dibujar` contiene una función diferente.
(((método call)))Este patrón es llamado _((herencia))_. Este nos permite
generar tipos de datos muy similares desde tipos de datos existente con
poco trabajo relativamente. Típicamente, el nuevo constructor llamará al
viejo ((constructor)) (usando el método `call` para permitir darle al
nuevo objeto su valor `this`). Una vez este constructor se ha llamado,
podemos asumir que todos los campos que el tipo de objeto viejo tenía
han sido añadidos. Arreglamos el constructor del ((prototipo)) para derivarlo
al del viejo prototipo así que las instancias de este prototipo tendrán también
acceso a las propiedades del viejo prototipo. Finalmente podemos sobre escribir
alguna de esas propiedades añadiéndolas a nuestro nuevo prototipo.
(((función datosTabla)))Ahora, si ajustamos un poco la función `datosTabla`
para usar ++DCeldaTexto++s para celdas cuyo valor sea un número, tendremos la
tabla que estábamos buscando.
// start_code bottom_lines: 1
// include_code strip_log
[source,javascript]
----
function datosTabla(datos) {
var keys = Object.keys(datos[0]);
var encabezados = keys.map(function(nombre) {
return new CeldaSubrayada(new CeldaTexto(nombre));
});
var cuerpo = datos.map(function(row) {
return keys.map(function(nombre) {
var valor = row[nombre];
// Esto ha cambiado:
if (typeof valor == "number")
return new DCeldaTexto(String(valor));
else
return new CeldaTexto(String(valor));
});
});
return [encabezados].concat(cuerpo);
}
console.log(dibujarTabla(datosTabla(MOUNTAINS)));
// → … preciosa tabla alineada
----
(((programación orientada a objetos)))La herencia es una parte
fundamental de la tradición de la orientación a objetos, junto
con la encapsulación y el polimorfismo. Pero mientras las dos
últimas son generalmente consideradas como ideas geniales, la
herencia es algo controvertido.
(((complejidad)))La principal razón para esto es que a menudo es
confundida con el ((polimorfismo)), vendido como una herramienta más
poderosa de lo que en realidad es, y posteriormente sobre utilizado de
todas las malas formas posibles. Mientras que la ((encapsulación)) y el
polimorfismo pueden ser usados para _separar_ trozos de código de otros,