JavaScript es un lenguaje basado en objetos que en lugar de estar basado en clases, se basa en prototipos. Debido a esta diferencia, puede resultar menos evidente que JavaScript te permite crear jerarquías de objetos y herencia de propiedades y de sus valores. Este capítulo intenta clarificar estos puntos.
Este capítulo asume que tienes alguna familiaridad con JavaScript y que has usado funciones de JavaScript para crear objetos sencillos.
Lenguajes basados en clases vs. basados en prototipos
Los lenguajes orientados a objetos basados en clases, como Java y C++, se basan en la existencia de dos entidades distintas: clases e instancias.
- Una clase 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, y no un ejemplar del conjunto de objetos que describe. Por ejemplo, la clase
Employee
puede representar el conjunto de todos los empleados. - Una instancia, sin embargo, es la instanciación de una clase; es decir, uno de sus miembros. Por ejemplo,
Victoria
podría ser una instancia de la claseEmployee
,
representando a un individuo particular como un empleado. Una instancia tiene exactamente las propiedades de su clase padre (ni más, ni menos).
Un lenguaje basado en prototipos, como JavaScript, no hace esta distincion: simplemente tiene objetos. Un lenguaje basado en prototipos tiene la nocion del objeto prototipico, un objeto que se utiliza como una plantilla a partir de la cual se obtiene el conjunto inicial de propiedades de un objeto. Cualquier objeto puede especificar sus propias propiedades, ya sea cuando es creado inicialmente o en tiempo de ejecucion. Ademas, cualquier objeto puede ser utilizado como el prototipo de otro objeto, permitiendo al segundo objeto compartir las propiedades del primero.
Definicion de una clase
En los lenguajes basados en clases defines una clase en una definicion de clase separada. En esa definicion puedes especificar metodos especiales, llamados constructores, para crear instancias de la clase. Un metodo constructor puede especificar valores iniciales para las propiedades de la instancia y realizar otro procesamiento de inicializacion apropiado en el momento de la creacion. Puedes utilizar el operador new
junto al constructor para crear instancias de clase.
JavaScript sigue un modelo similar, pero sin tener la definicion de clase separada del constructor. En su lugar, se define una funcion constructor para crear objetos con un conjunto inicial de propiedades y valores. Cualquier funcion JavaScript puede utilizarse como constructor. Se utiliza el operador new
con una funcion constructor para crear un nuevo objeto.
Subclases y herencia
En un lenguaje basado en clases se crea una jerarquía de clases a través de la jerarquía de clases. En una definición de clase se puede especificar que la nueva clase es una subclase de alguna clase ya existente. Esta subclase hereda todas las propiedades de la superclase y adicionalmente puede añadir nuevas propiedades o modificar las que hereda. Por ejemplo, supongamos que la clase Employee
tiene sólo las propiedades name
y dept
, y que Manager
es una subclase de Employee
que añade la propiedad reports
. En este caso, una instancia de la clase Manager
tendría las tres propiedades: name
, dept
, y reports
.
JavaScript implementa la herencia permitiendo al programador asociar un objeto prototípico con cualquier función constructor. De esta forma puedes crear una relación entre Employee
y Manager
, pero usando una terminología diferente, sin clases. En primer lugar, se define la función constructor Employee,
especificando las propiedades name
y dept
. Después hay que definir la función constructor Manager
, especificando la propiedad reports
. Por último, hay que asignar un nuevo objeto Employee
como el prototype
de la función constructor Manager
. De esta forma, cuando se crea un nuevo Manager
, hereda las propiedades name
y dept
del objeto Employee
.
Añadir y quitar propiedades
En lenguajes basados en clases se crea típicamente una clase en tiempo de compilación y entonces se instancian instancias de la clase, ya sea en tiempo de compilación o en tiempo de ejecución. No se puede cambiar el número o el tipo de propiedades de una clase una vez que ha sido definida. En JavaScript sin embargo, en tiempo de ejecución se pueden añadir y quitar propiedades a un objeto. Si se añade una propiedad a un objeto que está siendo utilizado como el prototipo de otros objetos, los objetos para los que es un prototipo también tienen la nueva propiedad añadida.
Resumen de las diferencias
La siguiente tabla muestra un pequeño resumen de algunas de estas diferencias. El resto de este capítulo describe los detalles del uso de los constructores JavaScript y los prototipos para crear una jerarquía de objetos, y compara esta forma de herencia no basada en clases con la basada en clases que utiliza Java.
Basado en clases (Java) | Basado en prototipos (JavaScript) |
---|---|
La clase y la instancia son entidades distintas | Todos los objetos son instancias |
Define una clase en la definición de clase; se instancia una clase con los métodos constructores. | Define y crea un conjunto de objetos con funciones constructoras. |
Se crea un objeto con el operador new . |
Igual. |
Se construye una jerarquía de objetos utilizando la definición de las clases para definir subclases de clases existentes.. |
Se construye una jerarquía de objetos mediante la asignación de un objeto como el prototipo asociado a una función constructor. |
Se heredan propiedades siguiendo la cadena de clases. | Se heredan propiedades siguiendo la cadena de prototipos. |
La definición de una clase especifica todas las propiedades de todas las instnacias de esa clase. No se pueden añadir propiedades dinámicamente en tiempo de ejecución. |
El conjunto inicial de propiedades lo determina la función constructor y el prototipo. Se pueden añadir y quitar propiedades dinámicamente a objetos específicos o a un conjunto de objetos. |
Ejemplo: employee
El resto de este capitulo utiliza la jerarquia employee que se muestra en la siguiente figura.
Figura 8.1: Una jerarquía de objetos sencilla
Este ejemplo utiliza los siguientes objetos:
Employee
tiene las propiedadesname
(cuyo valor por defecto es un string vacío) ydept
(cuyo valor por defecto es "general").Manager
está basado enEmployee
. Añade la propiedadreports
(cuyo valor por defecto es un array vacío, en la que se pretende almacenar un array de objetosEmployee
como su valor).WorkerBee
también está basado enEmployee
. Añade la propiedadprojects
(cuyo valor por defecto es un array vacío en el que se pretende almacenar un array de strings como su valor).SalesPerson
está basado enWorkerBee
. Añade la propiedadquota
(cuyo valor por defecto es 100). También reemplaza la propiedaddept
con el valor "sales", indicando que todas las salespersons están en el mismo departamento.Engineer
se basa enWorkerBee
. Añade la propiedadmachine
(cuyo valor por defecto es un string vacío) y también reemplaza la propiedaddept
con el valor "engineering".
Creacion de la jerarquia
Hay varias formas de definir funciones constructor para implementar la jerarquia Employee. Elegir una u otra forma depende sobre todo de lo que quieras poder hacer con tu aplicacion.
Esta seccion muestra como utilizar definiciones muy sencillas (y comparativamente inflexibles) para mostrar como hacer funcionar la herencia. En estas definiciones no se pueden especificar valores de propiedades cuando se crea un objeto. El nuevo objeto que se crea simplemente obtiene valores por defecto, que pueden cambiarse posteriormente. La figura 8.2 muestra la jerarquia con estas definiciones sencillas.
En una aplicacion real probablemente definiriamos constructores que permitirian proporcionar valores a las propiedades en el momento de la creacion del objeto (para mas informacion ver More Flexible Constructors ). Por ahora, estas definiciones sencillas nos sirven para mostrar como funciona la herencia.
Figura 8.2: Definiciones de los objetos de la jerarquia Employee
Las definiciones Java and JavaScript de Employee
que se muestran a continuacion son similares. Las unicas diferencias son que tienes que especificar el tipo de cada propiedad en Java, mientras que en JavaScript no, y que en Java tienes que crear un metodo constructor esplicitamente en la clase.
JavaScript | Java |
---|---|
function Employee () { this.name = ""; this.dept = "general"; } |
public class Employee { public String name; public String dept; public Employee () { this.name = ""; this.dept = "general"; } } |
Las definiciones de Manager
y WorkerBee
ilustran la diferencia a la hora de especificar el siguiente objeto en la jerarquía de herencia. En JavaScript se añade una instancia prototípica como el valor de la propiedad prototype
en la función constructor. Puede hacerse en cualquier momento una vez definido el constructor. En Java se especifica la superclase en la definición de la clase. No se puede cambiar la superclase fuera de la definición de la clase.
JavaScript | Java |
---|---|
function Manager () { this.reports = []; } Manager.prototype = new Employee; function WorkerBee () { this.projects = []; } WorkerBee.prototype = new Employee; |
public class Manager extends Employee { public Employee[] reports; public Manager () { this.reports = new Employee[0]; } } public class WorkerBee extends Employee { public String[] projects; public WorkerBee () { this.projects = new String[0]; } } |
Las definiciones de Engineer
y SalesPerson
crean objetos que descienden de WorkerBee
y por tanto de Employee
. Un objeto de estos tipos tiene propiedades de todos los objetos por encima de él en la caena. Además, estas definiciones reemplazan los valores heredados de la propiedad dept
con nuevos valores específicos de estos objetos.
JavaScript | Java |
---|---|
function SalesPerson () { this.dept = "sales"; this.quota = 100; } SalesPerson.prototype = new WorkerBee; function Engineer () { this.dept = "engineering"; this.machine = ""; } Engineer.prototype = new WorkerBee; |
public class SalesPerson extends WorkerBee { public double quota; public SalesPerson () { this.dept = "sales"; this.quota = 100.0; } } public class Engineer extends WorkerBee { public String machine; public Engineer () { this.dept = "engineering"; this.machine = ""; } } |
Mediante estas definiciones se pueden crear instancias de estos objetos, que adquieren los valores por defecto de sus propiedades. La figura 8.3 muestra el uso de estas definiciones JavaScript para crear nuevos objetos y muestra los valores de las propiedades de estos nuevos objetos.
jane
es una instancia de Engineer
. Del mismo modo, aunque los términos padre, hijo, ancestro y descendiente no tienen significados formales en JavaScript, se usan informalmente para referirse a objetos por encima o por debajo en la cadena de prototipos.
Figura 8.3: Creación de objetos mediante definiciones simples
Propiedades de objetos
Esta sección describe cómo heredan los objetos sus propiedades de otros objetos en la cadena de prototipos y qué ocurre cuando se añade una propiedad en tiempo de ejecución.
Herencia de propiedades
Supongamos que hemos creado el objeto mark
como un WorkerBee
(como se muestra en la Figura 8.3) con la siguiente sentencia:
var mark = new WorkerBee;
Cuando el intérprete de JavaScript encuentra el operador new
crea un nuevo objeto genérico accesible a través del palabra this
en la función constructor de WorkerBee
. La función constructor asigna explícitamente el valor de la propiedad projects
property, e implícitamente le asigna al valor de la propiedad interna __proto__
el valor de WorkerBee.prototype
. (Este nombre de propiedad tiene dos guiones bajos al principio y al final). La propiedad __proto__
determina la cadena de prototipos que se usará para devolver valores de las propiedades cuando se acceda a ellas. Una vez que estas propiedades tienen sus valores, JavaScript devuelve el nuevo objeto y la sentencia de asignación asigna el nuevo objeto ya inicializado a la variable mark
.
Este proceso no asigna explicitamente valores al objeto mark
(valores locales) para las propiedades que mark
hereda de la cadena de prototipos. Cuando se pregunta por el valor de una propiedad, JavaScript primero comprueba si existe un valor para esa propiedad en objeto. Si existe, se devuelve ese valor. Si el valor no esta ahi localmente, JavaScript comprueba la cadena de prototipos (usando la propiedad __proto__
). Si un objeto en la cadena de prototipos tiene un valor para esa propiedad, se devuelve ese valor. Si no existe en ningun objeto de la cadena de prototipos un valor para esa propiedad, JavaScript dice que el objeto no tiene esa propiedad. De esta forma, el objeto mark
tiene las siguientes propiedades y valores:
mark.name = ""; mark.dept = "general"; mark.projects = [];
El objeto mark
hereda valores para las propiedades name
y dept
su objeto prototipico que enlaza en mark.__proto__
. Se le asigna un valor local la propiedad projects
a traves del constructor WorkerBee
. De esta forma se heredan propiedades y sus valores en JavaScript. En la seccion Property Inheritance Revisited se discuten algunos detalles de este proceso.
Debido a que estos constructores no permiten especificar valores especificos de instancia, esta informacion es generica. Los valores de las propiedades son los valores por omision, compartidos por todos los objetos nuevos creados a partir de WorkerBee
. Por supuesto se pueden cambiar despues los valores de estas propiedades. Por ejemplo podriamos dar valores con informacion especifica a mark
de la siguiente forma:
mark.name = "Doe, Mark"; mark.dept = "admin"; mark.projects = ["navigator"];
Añadir propiedades
En JavaScript se pueden añadir propiedades a los objetos en tiempo de ejecucion. No se esta limitado a utilizar solo las propiedades que proporciona la funcion constructor. Para añadir una propiedad que es especifica para un objeto determinado, se le asigna un valor a la propiedad del objeto de la siguiente forma:
mark.bonus = 3000;
Ahora el objeto mark
tiene una propiedad bonus
, pero ningun otro objeto creado con la funcion constructor WorkerBee
tiene esta propiedad.
Si se añade una nueva propiedad a un objeto que se esta utilizando como el prototipo de una funcion constructor, se provoca que dicha propiedad se añada a todos los objetos que hereden propiedades de dicho prototipo. Por ejemplo, puede añadirse una propiedad specialty
a todos los empleados con la siguientes sentencia:
Employee.prototype.specialty = "none";
Cuando JavaScript ejecuta esta sentencia, los objetos preexistentes como mark
tambien pasan a tener la propiedad specialty
con el valor "none"
. La siguiente figura muestra el efecto de añadir esta propiedad al prototipo Employee
y despues reemplazarlo por el prototipo Engineer
.
Figura 8.4: Añadir propiedades
Constructores mas flexibles
Las funciones constructor que se han mostrado hasta ahora no permiten especificar valores a las propiedades cuando se crea una instancia. Al igual que en Java, se pueden proporcionar argumentos a los constructores para inicializar los valores de las propiedades de las instancias. La siguiente figura muestra una forma de hacerlo.
Figura 8.5: Especificacion de propiedades en un construccion, toma 1
La siguiente tabla muestra las definiciones Java y JavaScript para estos objetos.
JavaScript | Java |
---|---|
function Employee (name, dept) { this.name = name || ""; this.dept = dept || "general"; } |
public class Employee { public String name; public String dept; public Employee () { this("", "general"); } public Employee (String name) { this(name, "general"); } public Employee (String name, String dept) { this.name = name; this.dept = dept; } } |
function WorkerBee (projs) { this.projects = projs || []; } WorkerBee.prototype = new Employee; |
public class WorkerBee extends Employee { public String[] projects; public WorkerBee () { this(new String[0]); } public WorkerBee (String[] projs) { projects = projs; } } |
function Engineer (mach) { this.dept = "engineering"; this.machine = mach || ""; } Engineer.prototype = new WorkerBee; |
public class Engineer extends WorkerBee { public String machine; public Engineer () { dept = "engineering"; machine = ""; } public Engineer (String mach) { dept = "engineering"; machine = mach; } } |
Estas definiciones JavaScript realizan un uso idiomatico especial para asignar valores por defecto:
this.name = name || "";
El operador logico OR de JavaScript (||
) evalua su primer argumento. Si dicho argumento se convierte a true, el operador lo devuelve. Si no, el operador devuelve el valor del segundo argumento. Por tanto, esta linea de codigo compureba si name
tiene un valor util para la propiedad name
, en cuyo caso asigna a this.name
este valor. En caso contrario asigna a this.name
el string vacio. Este capitulo emplea este uso idiomatico por concision. Sin embargo puede resultar chocante las primeras veces que se lee un codigo asi.
false
(como 0
(cero) y el string vacio (""
). En este caso el valor proporcionado en la llamada al constructor no resulta el elegidoCon estas definiciones, cuando se crea una instancia de un objeto, se pueden especificar valores para las propiedades definidas localmente. Tal como se muestra en Figura 8.5, se puede utilizar la siguiente sentencia para crear un nuevo Engineer
:
var jane = new Engineer("belau");
Ahora las propiedades de jane
son:
jane.name == ""; jane.dept == "engineering"; jane.projects == []; jane.machine == "belau"
Nótese que con estas definiciones no se puede dar un valor inicial a las propiedades heredadas como name
. Si se quiere especificar un valor inicial para las propiedades heredadas en JavaScript hay que añadir más código a la función constructora.
Hasta ahora, la función constructora ha creado un objeto genérico y ha especificado propiedades y valores locales para el nuevo objeto. Se puede hacer que el constructor añada más propiedades llamando directamente a la función constructor de un objeto que esté más arriba en la cadena de prototipos. La siguiente figura muestra estas definiciones.
Figura 8.6 Especificación de propiedades en un constructor, toma 2
Veamos los detalles de una de estas definiciones. A continuación se muestra la definición del constructor Engineer
:
function Engineer (name, projs, mach) { this.base = WorkerBee; this.base(name, "engineering", projs); this.machine = mach || ""; }
Supongamos que se crea un nuevo Engineer
de esta forma:
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
JavaScript sigue los siguientes pasos:
- El operador
new
crea un nuevo objeto genérico y le asigna su propiedad__proto__
aEngineer.prototype
. - El operador
new
pasa el nuevo objeto al constructorEngineer
como el valor de la palabra reservadathis
. - El constructor crea una nueva propiedad llamada
base
para ese objeto y le asigna el valor del constructorWorkerBee
. Esto hace que el constructorWorkerBee
pase a ser un método del objetoEngineer
. El nombre de esta propiedad (base
) no es especial. Puede usarse cualquier nombre de propiedad, si bienbase
evoca el uso que se le va a dar. -
El constructor llama al método
base
, pasándole como argumentos dos de los argumentos que se le han pasado al constructor ("Doe, Jane"
y["navigator", "javascript"]
) y también el string"engineering"
. Usar explícitamente"engineering"
en el constructor indica que todos los objetosEngineer
tienen el mismo valor para la propiedad heredadadept
, y este valor reemplaza el valor heredado deEmployee
. - Como
base
es un método deEngineer
, en la llamada abase
, JavaScript liga la palabrathis
al objeto creado en el paso 1. De esta forma, la funciónWorkerBee
a su vez pasa los argumentos"Doe, Jane"
y"engineering"
a la función constructorEmployee
. Cuando retorna la llamada de la función constructorEmployee
, la funciónWorkerBee
utiliza el resto de argumentos para asignarle un valor a la propiedadprojects
. - Cuando retorna de la llamada al método
base
, el constructorEngineer
inicializa la propiedadmachine
del objeto con el valor"belau"
. - Cuando retorna del constructor, JavaScript asigna el nuevo objeto a la variable
jane
.
Podria pensarse que por haber llamado al constructor WorkerBee
desde el constructor Engineer
ya se ha establecido la herencia para los objetos Engineer
. Pero no es asi. Llamar al constructor WorkerBee
hace que un objeto Engineer
comience con las propiedades especificadas en los constructores. Pero si luego se añaden propiedades a los prototipos de Employee
o de WorkerBee
, estas propiedades no se heredan por los objetos Engineer
. Por ejemplo, veamos las siguientes sentencias:
function Engineer (name, projs, mach) { this.base = WorkerBee; this.base(name, "engineering", projs); this.machine = mach || ""; } var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau"); Employee.prototype.specialty = "none";
El objeto jane
no hereda la propiedad specialty
añadida al prototipo de Employee
. Sigue siendo necesario dar valor al prototipo de Employee
para que la herencia buscada se establezca. Veamos las siguientes sentencias:
function Engineer (name, projs, mach) { this.base = WorkerBee; this.base(name, "engineering", projs); this.machine = mach || ""; } Engineer.prototype = new WorkerBee; var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau"); Employee.prototype.specialty = "none";
Ahora el valor de la propiedad specialty
del objeto j
ane
si es "none".
Otra forma de llamar al constructor es mediante el uso de los metodos call()
/ apply()
:
function Engineer (name, projs, mach) { this.base = WorkerBee; this.base(name, "engineering", projs); this.machine = mach || ""; } |
function Engineer (name, projs, mach) { WorkerBee.call(this, name, "engineering", projs); this.machine = mach || ""; } |
Usando el metodo JavaScript call()
se llama a la funcion constructor WorkBee
como un metodo, pasandole explicitamente this
. El efecto es el mismo que el producido al llamar al constructor a traves de la propiedad base
: en la llamada a WorkBee,
this
está ligado al objeto que se está creando en Engineer.
Mas sobre herencia de propiedades
En las anteriores secciones se ha descrito como los constructores de JavaScript y los prototipos proporcionan jerarquias de herencia. Esta seccion muestra algunos detalles que no aparecieron antes.
Valores locales frente a valores heredados
Cuando se accede a una propiedad de un objeto, JavaScript realiza estos pasos, tal como se describio mas arriba en este capitulo:
When you access an object property, JavaScript performs these steps, as described earlier in this chapter:
- Se comprueba si el valor existe localmente. Si existe, se devuelve ese valor.
- Si no hay un valor local, se comprueba la cadena de prototipos (usando la propiedad
__proto__
). - Si algun objeto en la cadena de prototipos tiene un valor para la propiedad especificada, se devuelve ese valor.
- Si no se encuentra la propiedad en la cadena de prototipos, el objeto no tiene la propiedad.
El resultado de estos pasos depende de las definiciones realizadas.:
function Employee () { this.name = ""; this.dept = "general"; } function WorkerBee () { this.projects = []; } WorkerBee.prototype = new Employee;
Con estas definiciones, supongamos que se crea amy
como una instancia de WorkerBee
con la siguiente sentencia:
var amy = new WorkerBee;
El objeto amy
tiene una propiedad local, projects
. Los valores de las propiedades name
y dept
no son locales a amy
y por eso se obtienen de la propiedad __proto__
del objeto. Por ello, amy
tiene estos valores en sus propiedades:
amy.name == ""; amy.dept == "general"; amy.projects == [];
Supongamos que ahora cambiamos el valor de la propiedad name
en el prototipo de Employee
:
Employee.prototype.name = "Unknown"
A primera vista cabria esperar que el nuevo valor se propagase hacia abajo a todas las instancias de Employee
. Pero no es eso lo que ocurre.
Cuando se crea cualquier instancia del objeto Employee
object, esa instancia obtiene un valor local para la propiedad name
(la cadena vacia). Esto significa que cuando se da valor al prototipo de WorkerBee
mediante la creacion de un nuevo objeto Employee
, WorkerBee.prototype
tiene un valor local para la propiedad name
. Por tanto, cuando JavaScript busca la propiedad name
del objeto amy
(una instancia de WorkerBee
), JavaScript encuentra el valor local de esa propiedad en WorkerBee.prototype
. Por tanto no busca mas arriba en la cadena hasta Employee.prototype
.
Si quieres cambiar el valor de una propiedad de un objeto en tiempo de ejecucion y conseguir que el nuevo valor sea heredado por todos los descendientes del objeto, no puedes definir la propiedad en la funcion constructor del objeto. En su lugar, la tienes que añadir al prototipo asociado al constructor. Por ejemplo, supongamos que cambiamos el codigo anterior por este otro:
function Employee () { this.dept = "general"; } Employee.prototype.name = ""; function WorkerBee () { this.projects = []; } WorkerBee.prototype = new Employee; var amy = new WorkerBee; Employee.prototype.name = "Unknown";
En este caso, la propiedad name
de amy
si pasa a ser "Unknown" tras la ultima sentencia.
Tal como muestran estos ejemplos, si lo que se quiere es tener valores por defecto para propiedades de objetos, y se quiere poder cambiar los valores por defecto en tiempo de ejecucion, hay que asignar las propiedades al prototipo del constructor, y no asignarlas dentro de la funcion constructor.
Como determinar las relaciones entre instancias
La busqueda de propiedades en la cadena de prototipos comienza en las propiedades locales del objeto y si no se encuentran localmente, se busca a traves de la propiedad __proto__
del objeto. La busqueda continua recursivamente, conociendose como "busqueda en la cadena de prototipos".
Esta propiedad especial __proto__
que tienen los objetos recibe su valor en el momento en el que el obejto es construido; se le asigna el valor de la propiedad prototype
de la funcion constructor usada para crear el objeto. Por ello, la expresion new Foo()
crea un objeto con __proto__ ==
. Por tanto, los cambios que se realicen en las propiedades de Foo.prototype
Foo.prototype
alteraran la busqueda de propiedades de todos los objetos que se crearon mediante new Foo()
.
Todo objeto tiene una propiedad __proto__
(salvo Object
); toda funcion tiene una propiedad prototype
. Es asi como los objetos pueden relacionarse mediante 'herencia de prototipos' con otros objetos. Se puede comprobar la herencia comparando el valor de la propiedad __proto__
con el valor de prototype
de una funcion constructor. JavaScript proporciona un atajo: el operador instanceof
compara un objeto con una funcion constructor y devuelve true si el objeto hereda del prototipo de la funcion. Por ejemplo,
var f = new Foo(); var isTrue = (f instanceof Foo);
Para ver un ejemplo mas detallado, supongamos que tenemos el conjunto de definiciones mostrado en Inheriting Properties. Creamos un objeto Engineer
somo sigue:
var chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");
En este objeto, las siguientes sentencias son todas true:
chris.__proto__ == Engineer.prototype; chris.__proto__.__proto__ == WorkerBee.prototype; chris.__proto__.__proto__.__proto__ == Employee.prototype; chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype; chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null;
Por tanto podria escribirse una funcion instanceOf
asi:
function instanceOf(object, constructor) { while (object != null) { if (object == constructor.prototype) return true; if (typeof object == 'xml') { return constructor.prototype == XML.prototype; } object = object.__proto__; } return false; }
Usando esta funcion instanceOf
estas expresiones son todas true:
instanceOf (chris, Engineer) instanceOf (chris, WorkerBee) instanceOf (chris, Employee) instanceOf (chris, Object)
Pero la siguiente expresion es false:
instanceOf (chris, SalesPerson)
Informacion global en los constructores
Cuando se crean constructores hay que tener especial cuidado si se asigna informacion global en el constructor. Por ejemplo, supongamos que se quiere tener un ID unico que se asigne automaticamente a cada nuevo empleado. Se podria utilizar la siguiente definicion de Employee
:
var idCounter = 1; function Employee (name, dept) { this.name = name || ""; this.dept = dept || "general"; this.id = idCounter++; }
Con esta definicion, cuando se crea un nuevo Employee
, el constructor le asigna el siguiente ID y luego incrementa el contador global ID. Por tanto, tras ejecutar el siguiente codigo, victoria.id
sera 1 y harry.id
sera 2:
var victoria = new Employee("Pigbert, Victoria", "pubs") var harry = new Employee("Tschopik, Harry", "sales")
A primera vista puede parecer razonable. Sin embargo, idCounter
se incrementa siempre que se crea un nuevo objeto Employee
, cualquiera que sea su proposito. Si se crea toda la jerarquia Employee
mostrada en este capitulo, el constructor Employee
es llamado cada vez que se asigna valor a un prototipo. Supongamos que tenemos el siguiente codigo:
var idCounter = 1; function Employee (name, dept) { this.name = name || ""; this.dept = dept || "general"; this.id = idCounter++; } function Manager (name, dept, reports) {...} Manager.prototype = new Employee; function WorkerBee (name, dept, projs) {...} WorkerBee.prototype = new Employee; function Engineer (name, projs, mach) {...} Engineer.prototype = new WorkerBee; function SalesPerson (name, projs, quota) {...} SalesPerson.prototype = new WorkerBee; var mac = new Engineer("Wood, Mac");
Supongamos ademas que las definiciones que se omiten tienen la propiedad base
y se llama al constructor que tienen encima en la cadena de prototipos. En este caso, cuando se llega a crear el objeto mac
, mac.id
es 5.
Dependiendo de la aplicacion, puede o no importar que el contador se haya incrementado esas veces extra. En caso de que importe, una solucion es utilizar este constructor:
function Employee (name, dept) { this.name = name || ""; this.dept = dept || "general"; if (name) this.id = idCounter++; }
Cuando se crea una instancia de Employee
para usarla como prototipo no se especifican argumentos en el constructor. Mediante esta definicion del constructor, cuando no se proporcionan argumentos el constructor no asigna un valor al id y no actualiza el contador. Por tanto, para que se asigne a un Employee
un id, hay que especificar un name al employee. En este caso mac.id
seria 1.
No existe herencia multiple
Algunos lenguajes orientados a objetos tienen herencia multiple. Es decir, un objeto puede heredar las propiedades y valores de varios objetos padre distintos. JavaScript no proporciona herencia multiple.
La herencia de valores de propiedades se produce en tiempo de ejecucion por JavaScript buscando en la cadena de prototipos de un objeto para encontrar un valor. Debido a que un objeto tiene un solo prototipo asociado, JavaScript no puede heredar dinamicamente de mas de una cadena de prototipos.
En JavaScript se puede hacer que desde una funcion constructor llame a mas de una funcion constructor. Esto ofrece algo parecido a la herencia multiple. Veamos un ejemplo:
function Hobbyist (hobby) { this.hobby = hobby || "scuba"; } function Engineer (name, projs, mach, hobby) { this.base1 = WorkerBee; this.base1(name, "engineering", projs); this.base2 = Hobbyist; this.base2(hobby); this.machine = mach || ""; } Engineer.prototype = new WorkerBee; var dennis = new Engineer("Doe, Dennis", ["collabra"], "hugo")
Ademas, supongamos que la definicion de WorkerBee
que se uso antes en este capitulo. En este caso, el objeto dennis
tiene estas propiedades:
dennis.name == "Doe, Dennis" dennis.dept == "engineering" dennis.projects == ["collabra"] dennis.machine == "hugo" dennis.hobby == "scuba"
Por tanto dennis
obtiene la propiedad hobby
del constructor Hobbyist
constructor. Sin embargo, supongamos que se añade despues una propiedad al prototipo del constructor de Hobbyist
:
Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]
El objeto dennis
no hereda esta nueva propiedad porque no esta en su cadena de prototipos.