Javascript 8 Prototipos
Javascript 8 Prototipos
AVANZADO
CLASE 8 : PROTOTIPOS
PROTOTIPOS
JavaScript provoca cierta confusión en desarrolladores con experiencia en lenguajes
basados en clases (como Java o C++), por ser dinámico y no proporcionar una
implementación de clases en sí mismo (la palabra clave class se introdujo en ES2015, pero
sólo para endulzar la sintaxis, ya que JavaScript sigue estando basado en prototipos).
https://hackernoon.com/prototypes-in-javascript-5bba2990e04b
Casi todos los objetos en JavaScript son instancias de O
bject que se sitúa a la cabeza de la
cadena de prototipos.
A pesar de que a menudo esto se considera como una de las principales debilidades de
JavaScript, el modelo de herencia de prototipos es de hecho más potente que el modelo
clásico. Por ejemplo, es bastante simple construir un modelo clásico a partir de un modelo
de prototipos 1 .
Los lenguajes orientados a objetos basados en clases, como Java y C++, se basan en la
existencia de dos entidades distintas: clases y ejemplificaciones de estas -mal llamadas
instancias-.
● Una c lase define todas las propiedades (considerando como propiedades los
métodos y campos de Java, o los miembros de C++) que caracterizan un
determinado conjunto de objetos. Una clase es una entidad abstracta, en lugar de
algún miembro en particular del conjunto de objetos que describe. Por ejemplo, la
clase Employee puede representar el conjunto de todos los empleados.
● Un e jemplo (o "una instancia"), por otro lado, es la ejemplificación de una clase; es
decir, uno de sus miembros. Por ejemplo, Victoria podría ser una ejemplificación de
la clase Employee, representando a un individuo en particular como un empleado.
Una ejemplificación de una clase ("instance") tiene exactamente las propiedades de
su clase padre (ni más, ni menos).
Un lenguaje basado en prototipos, como JavaScript, no hace esta distinción: simplemente
tiene objetos. Un lenguaje basado en prototipos tiene la noción del o bjeto prototípico, un
objeto que se utiliza como una plantilla a partir de la cual se obtiene el conjunto inicial de
propiedades de un nuevo objeto. Cualquier objeto puede especificar sus propias
propiedades, ya sea cuando es creado o en tiempo de ejecución. Adicionalmente, cualquier
objeto puede ser utilizado como el prototipo de otro objeto, permitiendo al segundo objeto
compartir las propiedades del primero 2 .
Cada objeto en JavaScript tiene un segundo objeto JavaScript (o nulo, pero esto es raro)
asociado a él. Este segundo objeto se conoce como prototipo y el primer objeto hereda
propiedades del prototipo. Todos los objetos creados por literales de objeto tienen el mismo
objeto prototipo, y podemos referirnos a este objeto prototipo en código JavaScript como
Object.prototype, pero recordemos que esta no es la única forma que conocemos hasta el
momento para crear objetos, entonces podríamos tener 3 formas distintas en cuenta :
● Objetos literales : { }
● Operador new : new Object( )
● Método create : Object.create( )
MÉTODO CREATE
ECMAScript 5 define un método, Object.create (), que crea un nuevo objeto, utilizando su
primer argumento como el prototipo de ese objeto. Object.create () también toma una
opción
segundo argumento que describe las propiedades del nuevo objeto.
Object.create () es una función estática, no un método invocado en objetos individuales. A
Úselo, simplemente pase el objeto prototipo deseado:
bject.create (Object.prototype);
var o3 = O
// o3 es como {} o new Object ().
La capacidad de crear un nuevo objeto con un prototipo arbitrario (dicho de otra manera: el
la capacidad de crear un "heredero" para cualquier objeto) es poderosa, y podemos
simularla en ECMAScript 3 con una función como la del siguiente ejemplo:
// heredar() devuelve un objeto recién creado que hereda propiedades del
// objeto prototipo p. Utiliza la función Object.create () de ECMAScript
5 si
// está definida, y de lo contrario vuelve a una técnica anterior.
función heredar (p) {
if (p == null) lanza TypeError (); // p debe ser un objeto no nulo
if (Object.create) // Si Object.create () está definida ...
return Object.create (p); // entonces úsala.
var t = typeof p; // De lo contrario, haga un poco más de comprobación
de tipo
if (t! == "object" && t! == "function") throw TypeError ();
función f () {}; // Define una función de constructor ficticio.
f.prototype = p; // Establezca su propiedad de prototipo en p.
devolver nuevo f (); // Usa f () para crear un "heredero" de p.
}
Los objetos de JavaScript tienen un conjunto de "propiedades propias" y también heredan
un conjunto de propiedades de su objeto prototipo. Para entender esto, debemos considerar
el acceso a la propiedad en más detalle.
ACCESO A PROPIEDADES
Supongamos que consulta la propiedad x en el objeto o
. Si o
no tiene una propiedad propia
con ese nombre, el objeto prototipo de o es consultado por la propiedad x. Si el prototipo
objeto no tiene una propiedad propia con ese nombre, pero tiene un prototipo en sí mismo,
la consulta se realiza en el prototipo del prototipo. Esto continúa hasta que la propiedad x es
encontrado o hasta que se busque un objeto con un prototipo nulo. Como puedes ver, el
prototipo atributo de un objeto crea una cadena o lista vinculada de la cual las propiedades
son heredadas.
var o = {} // o hereda métodos de objeto de Object.prototype
o.x = 1; // y tiene una propiedad propia x.
var p = heredar (o); // p hereda propiedades de o y Object.prototype
p.y = 2; // y tiene una propiedad propia y.
var q = heredar (p); // q hereda propiedades de p, o, y Object.prototype
q.z = 3; // y tiene una propiedad propia z.
var s = q.toString (); // toString se hereda de Object.prototype
q.x + q.y // => 3: x y y se heredan de o y p
Ahora suponga que asigna a la propiedad x del objeto o . Si o
ya tiene una propia
(no heredada) propiedad llamada x, entonces la asignación simplemente cambia el valor de
esta propiedad existente. De lo contrario, la asignación crea una nueva propiedad llamada x
en el objeto o. Si o anteriormente heredó la propiedad x, esa propiedad heredada ahora está
oculta por la propiedad propia recién creada con el mismo nombre.
La asignación de propiedades examina la cadena de prototipos para determinar si la
asignación está permitida. Si o hereda una propiedad de solo lectura llamada x , por ejemplo,
entonces la la asignación no está permitida. Si la asignación está permitida, sin embargo,
siempre crea o establece una propiedad en el original objeto y nunca modifica la cadena del
prototipo. El hecho de que la herencia ocurre en el momento de consultar propiedades pero
no al configurarlas es una característica clave de JavaScript porque nos permite anular
selectivamente las propiedades heredadas:
var unitcircle = {r: 1}; // Un objeto para heredar de
var c = inherit (unitcircle); // c hereda la propiedad r
c.x = 1; c.y = 1; // c define dos propiedades propias
c.r = 2; // c anula su propiedad heredada
unidadcircle.r; // => 1: el objeto prototipo no se ve afectado
Hay una excepción a la regla de que una asignación de propiedad falla o crea o establece
una propiedad en el objeto original. Si o hereda la propiedad x, y esa propiedad es
una propiedad de acceso con un método setter, luego se llama ese método setter
en lugar de crear una nueva propiedad x en o . Tenga en cuenta, sin embargo, que el método
setter es invocado en el objeto o, no en el objeto prototipo que define la propiedad, por lo
que si el método setter define cualquier propiedad, lo hará en o , y volverá a dejar la
cadena de prototipos sin modificar.
No es un error consultar una propiedad que no existe. Si la propiedad x no se encuentra
como una propiedad propia o una propiedad heredada de o , la expresión de acceso a la
propiedad o.x e
valúa a u
ndefined.
Sin embargo, es un error intentar consultar una propiedad de un objeto que no existe.
Los valores nulos e indefinidos no tienen propiedades, y es un error consultar propiedades
de estos valores:
// Crea una excepción TypeError. indefinido no tiene una propiedad de
longitud
var libro = { }
var len = libro.subtitulo.length;
A menos que esté seguro de que tanto el libro como libro.subtitulo son (o se comportan
como) objetos, no debe escribir la expresión libros.subtitulo.length, ya que podría generar
una excepción. Aquí hay dos maneras de protegerse contra este tipo de excepción:
var o = {
// Una propiedad ordinaria
data_prop: value,
// una propiedad accessor definida como pares de
funciones
get accessor_prop() { /* function body here */ },
set accessor_prop(value) { /* function body here */ }
};
Las propiedades de acceso se definen como una o dos funciones cuyo nombre es el mismo
que el nombre de propiedad, y con la palabra clave function reemplazada por g
et y / o s
et.
Tenga en cuenta que no se usa dos puntos para separar el nombre de la propiedad de las
funciones que tienen acceso a esa propiedad, pero que aún se necesita una coma después
del cuerpo de la función para separar el método del siguiente método o propiedad de datos.
Como ejemplo, considere el siguiente objeto que representa un punto cartesiano 2D. Tiene
propiedades de datos ordinarios para representar las coordenadas X e Y del punto, y tiene
propiedades de acceso para el equivalente coordenadas polares del punto:
var p = {
// x e y son propiedades de datos de lectura y escritura
regulares.
x: 1.0,
y: 1.0,
// r es una propiedad de acceso de lectura-escritura con getter y
setter.
// No olvides poner una coma después de los métodos de acceso.
get r () {return Math.sqrt (this.x * this.x + this.y * this.y);
},
set r (newvalue) {
var oldvalue = Math.sqrt (this.x * this.x + this.y * this.y);
var ratio = newvalue / oldvalue;
this.x * = ratio;
this.y * = ratio;
},
// theta es una propiedad de acceso de solo lectura con getter
solamente.
get theta () {return Math.atan2 (this.y, this.x); }
};
Tenga en cuenta el uso de la palabra clave t his e
n getters y setter arriba. JavaScript invoca
estas funciones como métodos del objeto sobre el que están definidas, lo que significa que
dentro del cuerpo de la función esto se refiere al objeto puntual. Entonces el método getter
para la propiedad r puede hacer referencia a las propiedades x e y como this.x y this.y.
Las propiedades de acceso se heredan, al igual que las propiedades de datos, por lo que
puede usar el objeto p
definido anteriormente como un prototipo para otros puntos. Puedes
darles a los nuevos objetos sus propias propiedades x e y, y heredarán las propiedades r y
theta:
var q = heredar (p);
// Crea un nuevo objeto que hereda getters y setters
q.x = 0, q.y = 0; // Crea las propias propiedades de datos de q
console.log (q.r); // Y usa las propiedades de acceso heredadas
console.log (q.theta);
ATRIBUTOS DE PROPIEDADES
Además de un nombre y un valor, las propiedades tienen atributos que especifican si
puede escribirse, enumerarse y configurarse. En ECMAScript 3, no hay forma de establecer
estos atributos: todas las propiedades creadas por los programas de ECMAScript 3 son
editables, enumerables, y configurable, y no hay forma de cambiar esto.
Para los fines de esta sección, vamos a considerar los métodos getter y setter de
una propiedad de acceso como atributos de propiedad. Siguiendo esta lógica, incluso
diremos que el valor de una propiedad de datos también es un atributo. Por lo tanto,
podemos decir que una propiedad tiene un nombre y cuatro atributos. Los cuatro atributos
de una propiedad de datos son value, writable, e numerable y c
onfigurable. Las propiedades
de acceso no tienen un atributo de valor o una atributo de escritura: su capacidad de
escritura está determinada por la presencia o ausencia de un setter. Entonces, los cuatro
atributos de una propiedad de acceso son get, set, enumerable y c onfigurable.
Las propiedades w ritable, enumerable y c
onfigurable s
on valores booleanos, y g et y s
et son
valores funciones, por supuesto.
Para obtener el descriptor de propiedad para una propiedad con nombre de un objeto
especificado, llame Object.getOwnPropertyDescriptor ():
// Devuelve {valor: 1, escribible: verdadero, enumerable:
verdadero, configurable: verdadero}
Object.getOwnPropertyDescriptor ({x: 1}, "x");
// Devuelve indefinido para propiedades heredadas y propiedades
que no existen.
Object.getOwnPropertyDescriptor ({}, "x"); // undefined, no tal
prop
Object.getOwnPropertyDescriptor ({}, "toString"); // indefinido,
heredado
Ya hemos visto el método Object.create () y prendimos allí que el el primer argumento para
ese método es el objeto prototipo para el objeto recién creado. Este método también acepta
un segundo argumento opcional, que es el mismo que el segundo argumento a
Object.defineProperties (). Si pasa un conjunto de descripciones de propiedad a
Object.create (), luego se usan para agregar propiedades al objeto recién creado.
Object.defineProperty () y Object.defineProperties () arrojan TypeError si el intento
para crear o modificar una propiedad no está permitido. Esto sucede si intentas agrega una
nueva propiedad a un objeto no extensible. El atributo writable rige los intentos de cambiar
el atributo de valor y el atributo configurable rige los intentos de cambiar los otros atributos
(y también especifica si un propiedad puede ser eliminada). Sin embargo, las reglas no son
completamente sencillas. Es posible cambiar el valor de una propiedad no escribible si esa
propiedad es configurable, por ejemplo. Además, es posible cambiar una propiedad de
escribible a no escribible incluso si esa propiedad no es configurable. Aquí están las reglas
completas. Llamadas a Object.defineProperty () o Object.defineProperties () que intentan
lanzar TypeError:
● Si un objeto no es extensible, puede editar sus propias propiedades existentes, pero
no puede agregar nuevas propiedades
● Si una propiedad no es configurable, no puede cambiar sus atributos configurable o
enumerable.
● Si una propiedad de acceso no es configurable, no puede cambiar su getter o setter y
no puede cambiarlo a una propiedad de datos.
● Si una propiedad de datos no es configurable, no puede cambiarla a una propiedad
de acceso.
● Si una propiedad de datos no es configurable, no puede cambiar su atributo de
escritura de falso a verdadero, pero puede cambiarlo de verdadero a falso.
● Si una propiedad de datos no es configurable y no se puede escribir, no puede
cambiar su valor.
Puede cambiar el valor de una propiedad que es configurable pero no se puede escribir, sin
embargo , porque eso sería lo mismo que hacer que se pueda escribir, luego cambiar el
valor, luego convirtiéndolo de nuevo a no escribible.
Para entender la siguiente forma de creación de nuevos objetos bajo el operador new vamos
a tener que tener además un conocimiento adicional sobre las funciones, ya que estas
representan el núcleo del siguiente tema.
FUNCIONES
Además de muchas de las cosas que podríamos mencionar sobre funciones, nos vamos a
focalizar en los siguientes aspectos para tener un mejor entendimiento de las mismas :
● Son funciones de primer orden
● Son funciones variadicas
● Tienen ámbito
● Tienen contexto
PRIMER ORDEN
Funciones de orden superior son funciones que pueden tomar otras funciones como
argumentos o devolverlos como resultados. En cálculo , un ejemplo de una función de orden
superior es el operador diferencial d / dx , que devuelve la derivada de una función f .
Las funciones de orden superior están estrechamente relacionadas con las funciones de
primera clase en las cuales las funciones de orden superior y las funciones de primera clase
pueden recibir como argumentos y resultados otras funciones. La distinción entre los dos es
sutil: "de orden superior", describe un concepto matemático de funciones que operan sobre
otras funciones, mientras que la "primera clase" es un término informático que describe las
entidades del lenguaje de programación que no tienen ninguna restricción de su utilización
(por lo tanto funciones de primera clase pueden aparecer en cualquier parte del programa
que otras entidades de primer nivel como los números pueden, incluidos como argumentos
a otras funciones y como sus valores de retorno) 3.
FUNCIONES VARIADICAS
En matemáticas y en programación de computadoras, una función variadica es una función
de aridad indefinida, es decir, una que acepta un número variable de argumentos. El soporte
para funciones variadic difiere ampliamente entre los lenguajes de programación 4 5.
AMBITO
El ámbito de una función(scope) podría ser definido como el alcance que tiene la misma
sobre variables dentro del programa en ejecución. El mismo en Javascript tiene un alcance
similar que en otros lenguajes de programación desde el punto de vista en que todas las
funciones pueden tener acceso a variables globales y locales(pre inicializadas o declaradas
dentro del cuerpo de la función) pero a esto le sumamos el concepto de c losure.
CLOSURE
Al igual que la mayoría de los lenguajes de programación modernos, JavaScript usa el
alcance léxico. Esto significa que las funciones se ejecutan usando el alcance variable que
estaba en efecto cuando(ellas mismas, las funciones) eran definidas, no el ámbito de la
variable que está en vigencia cuando se invocan. A fin de que implementar el alcance léxico,
el estado interno de un objeto de función de JavaScript debe incluir no solo el código de la
función sino también una referencia a la cadena de alcance actual. Esta combinación de un
objeto de función y un alcance (un conjunto de enlaces de variables) en el que se resuelven
las variables de la función se llama closure en la literatura de ciencias de la computación.
Técnicamente, todas las funciones de JavaScript son closures: son objetos y tienen un
alcance cadena asociada a ellos. La mayoría de las funciones se invocan utilizando la
misma cadena de alcance que estaba en efecto cuando se definió la función, y realmente no
importa que haya un closure involucrado. Los closures se vuelven interesantes cuando se
invocan bajo una cadena de alcance diferente a la que estaba en efecto cuando se
definieron. Esta ocurre más comúnmente cuando se devuelve un objeto de función anidada
de la función dentro de la cual fue definida. Hay una serie de potentes técnicas de
programación que involucran este tipo de closures de funciones anidadas, y su uso se ha
vuelto relativamente común en la programación de JavaScript. Los closures pueden parecer
confusos cuando te encuentras por primera vez con ellos, pero es importante que los
entiendas lo suficientemente bien como para usarlos cómodamente. Podríamos analizar el
siguiente ejemplo :
var a = 1
function externa(x){
var b = 10
console.log(x,b,a)
}
Como podemos apreciar, la función externa puede hacer uso de la variable global a, la pre
inicializada x y local b. Estamos acostumbrados a este comportamiento por lo cual no
debería ser de sorpresa. Recordemos además que las funciones pueden contener otras
funciones en su interior y hasta incluso retornarlas como su valor :
var a = 1
function externa(x){
var b = 10
console.log(x,b,a)
return function interna(y){
console.log(y)
}
}
var foo = externa(20)
Ahora estamos devolviendo una función como valor de la ejecución de la función externa y
lo estamos guardando en la variable foo. De esta forma , foo, pasa a valer una función,
particularmente la función interna, con lo cual podemos ejecutar la variable foo.
Tengamos en cuenta que luego de que una función se ejecuta, todas las variables locales de
la misma son destruidas por el Garbage Collector, el algoritmo que se encarga de “limpiar”
nuestra memoria para no sobrecargar el programa. Es decir que a partir de la creación de la
variable foo y su posterior asignación, la variable b
y x d
e externa ya dejaron de existir. Si
esto es cierto, entonces cómo puede ser posible que la función interna pueda adoptar el
siguiente comportamiento :
var a = 1
function externa(x){
var b = 10
console.log(x,b,a)
return function interna(y){
console.log(y + x)
}
}
console.log(foo)
//La ejecución de foo nos suma el valor original de x ya
que mantiene su valor gracias al closure generado
foo(15) //35
Es importante entonces entender el funcionamiento de cuando un closure es creado y hasta
incluso cerrado. Consideremos el siguiente ejemplo :
// Esta función retorna una función que siempre retorna v
function constfunc(v) {
return function() {
return v;
};
}
// Creamos un array de funciones constantes :
var funcs = [];
for(var i = 0; i < 10; i++)
funcs[i] = constfunc(i);
// La función guardada en el elemento 5 del array retorna su valor
5.
funcs[5]() // => 5
Si no tuviéramos este conocimiento podríamos caer en el error común de optar por un
código como el siguiente :
// Retorna un array de funciones que retornan un valor del 0 al 9
function constfuncs() {
var funcs = [];
for(var i = 0; i < 10; i++)
funcs[i] = function() {
return i;
};
return funcs;
}
//window.globalCtx = function(){}
function globalCtx(){
console.log(this)
}
//window.globalCtx()
globalCtx() //window{...}
En este primer ejemplo podemos observar cómo el contexto “natural” de una función global
apunta al objeto global window, dado que dicha función “le pertenece”, ergo su contexto es
él mismo.
var persona = {
nombre : "Juan",
saludo : function(){
console.log(this)//{nombre:"Juan"...}
var self = this
setTimeout(function(){
console.log(this)//window
console.log(self)//{nombre:"Juan"...}
},0)
}
}
persona.saludo()
En el ejemplo anterior podemos observar como invocando al método saludo del objeto
persona, su contexto nos muestra al mismo objeto persona, ya que el método le pertenece.
0(cero) segundos más tarde, intentamos realizar la misma operación, con la diferencia que
ahora obtenemos otro resultado. Esto se debe a que el contexto de ejecución de la función
setTimeout dentro de su callback es window, ya que ambas dos pertenecen a window. Para
evitar este comportamiento podemos guardar nuestro contexto original en otra variable la
cual podemos usar posteriormente.
APPLY vs CALL
Las funciones de JavaScript son objetos y, como todos los objetos de JavaScript, tienen
métodos. Dos de estos métodos, call () y apply () invocan indirectamente la función. Ambos
métodos le permite especificar explícitamente este valor para la invocación, lo que significa
que puede invocar cualquier función como método de cualquier objeto, incluso si no es
realmente un método de ese objeto Ambos métodos también le permiten especificar los
argumentos para la invocación. El método call () usa su propia lista de argumentos como
argumentos para la función y el el método apply () espera que una matriz de valores se use
como argumentos.
function foo(a,b){
console.log(this)
console.log(a,b)
}
foo(1,2) //window{...} 1 2
foo.apply({contexto:"apply"},[10,20]) //
{contexto:"apply"} 10 20
foo.call({contexto:"call"},100,200) //{contexto:"call"}
100 200
OPERADOR NEW
El nuevo operador crea e inicializa un nuevo objeto. La nueva palabra clave debe ser seguida
por una invocación de función. Una función utilizada de esta manera se llama constructor y
sirve para inicializar un objeto recién creado. El núcleo de JavaScript incluye constructores
incorporados para tipos nativos. Por ejemplo:
var o = new Object(); // Crea un objeto vacio. Mismo que {}.
var a = new Array(); // Crea un array vacio. Mismo que [].
var d = new Date(); // Crea un objeto con la fecha actual
var r = new RegExp("js"); // Crea un objeto de expresión regular
Usemos como ejemplo el siguiente constructor :
function Foo(){
}
Cuando se ejecuta la función asociada al operador new suceden las siguientes cosas:
1. Se crea un nuevo objeto que hereda de Foo.prototype.
2. La función del constructor Foo se llama con los argumentos especificados, y con
esto enlazado al objeto recién creado. New Foo equivale a newFoo (), es decir, si no
se especifica ninguna lista de argumentos, se llama a Foo sin argumentos.
3. El objeto devuelto por la función constructora se convierte en el resultado de la
nueva expresión completa. Si la función constructora no devuelve explícitamente un
objeto, en su lugar se usa el objeto creado en el paso 1. (Normalmente, los
constructores no devuelven un valor, pero pueden optar por hacerlo si desean anular
el proceso normal de creación de objetos).
De donde:
new Foo()
//Ejecutar new Foo() podría verse también de la siguiente manera :
var buffer = {}
Foo.call(buffer)
return buffer
//Ya que creamos un objeto nuevo, ejecutamos la función
asignándole el contexto con el nuevo objeto creado y terminamos
retornando dicho objeto.
Dado que el contexto de ejecución de la función Foo, o a fines prácticos cualquier función,
va a ser un objeto nuevo y vacío, podemos usar a las funciones como creadoras de “clases”.
Es por eso que les solemos decir a estas funciones : funciones constructoras.
FUNCIONES CONSTRUCTORAS
Un constructor es una función diseñada para la inicialización de objetos creados
recientemente. Se invocan constructores utilizando la palabra clave new como se describe
en anteriormente. Invocaciones de constructor utilizando new automáticamente crea el
nuevo objeto, por lo que el constructor solo necesita inicializar el estado de ese nuevo
objeto. La característica fundamental de las invocaciones de constructores es que la
propiedad prototype del constructor se usa como el prototipo del nuevo objeto. Esto
significa que todos los objetos creados con el mismo constructor heredan del mismo objeto
y por lo tanto son miembros de la misma clase. Consideremos entonces el siguiente
ejemplo :
function Rango(desde, hasta) {
// Guardamos los puntos de comienzo y fin.
// Son propiedades no heredadas, únicas para el objeto.
this.desde = desde;
this.hasta = hasta;
}
Rando.prototype = {
// Retorna true si x se encuentra en el rango, falso de lo
contrario
includes: function(x) {
return this.from <= x && x <= t
his.to;
}
};