Definición de clases o objetos ECMAScript
- Página anterior Alcance del objeto
- Página siguiente Modificar objeto
El uso de objetos predefinidos es solo una parte de la capacidad de un lenguaje orientado a objetos, su verdadero poder radica en la capacidad de crear clases y objetos personalizados.
ECMAScript tiene muchos métodos para crear objetos o clases.
Método de fábrica
Método original
Debido a que las propiedades del objeto se pueden definir dinámicamente después de la creación del objeto, muchos desarrolladores escribieron código similar al siguiente cuando se introdujo JavaScript por primera vez:
var oCar = new Object; oCar.color = "blue"; oCar.doors = 4; oCar.mpg = 25; oCar.showColor = function() { alert(this.color); };
En el código anterior, se crea el objeto car. Luego se le asignan algunas propiedades: su color es azul, tiene cuatro puertas y puede recorrer 25 millas por galón. La última propiedad es un puntero a una función, lo que significa que es un método. Después de ejecutar este código, se puede usar el objeto car.
Sin embargo, aquí hay un problema, es que es posible que necesitemos crear múltiples instancias de car.
Solución: método fábrica
Para resolver este problema, los desarrolladores crearon funciones fábrica que pueden crear y devolver objetos de tipos específicos.
Por ejemplo, la función createCar() se puede usar para encapsular las operaciones de creación del objeto car que se enumeraron anteriormente:
function createCar() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar(); var oCar2 = createCar();
Aquí, todo el código del primer ejemplo se incluye dentro de la función createCar(). Además, hay una línea de código adicional que devuelve el objeto car (oTempCar) como valor de la función. Al llamar a esta función, se crea un nuevo objeto y se le asignan todas las propiedades necesarias, copiando un objeto car que se explicó anteriormente. Por lo tanto, mediante este método, podemos crear fácilmente dos versiones del objeto car (oCar1 y oCar2) con propiedades completamente iguales.
Transmitir parámetros a la función
También podemos modificar la función createCar() para que le transmita valores por defecto para las propiedades, en lugar de asignar valores por defecto a las propiedades de manera simple:
function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); // Sale "red" oCar2.showColor(); // Sale "blue"
Añadiendo parámetros a la función createCar(), se pueden asignar valores a las propiedades color, doors y mpg del objeto car a crear. Esto permite que dos objetos tengan propiedades iguales, pero valores diferentes.
Definir métodos de objetos fuera de la función fábrica
A pesar de que ECMAScript se formaliza cada vez más, el método de creación de objetos se ha dejado de lado y su normalización sigue siendo objeto de oposición. Parte de la razón es semántica (no parece tan formal como usar el operador new con un constructor), y parte es funcional. La razón funcional radica en que con este método es necesario crear un método para crear objetos. En el ejemplo anterior, cada vez que se llama a la función createCar(), se crea una nueva función showColor(), lo que significa que cada objeto tiene su propia versión de showColor(). De hecho, todos los objetos comparten la misma función.
Algunos desarrolladores definen métodos de objetos fuera de la función de fábrica y luego los apuntan a través de propiedades para evitar este problema:}}
function showColor() { alert(this.color); } function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg; oTempCar.showColor = showColor; return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); // Sale "red" oCar2.showColor(); // Sale "blue"
En este código rewritten, se definió la función showColor() antes de la función createCar(). Dentro de createCar(), se asigna un puntero a una función showColor() ya existente. En términos funcionales, esto resuelve el problema de la creación repetida de objetos de función; pero en términos semánticos, la función no parece ser un método del objeto.
Todas estas preguntas han provocadodefinido por el desarrolladoraparece el constructor.
Método de constructor
Crear una función constructora es tan fácil como crear una función de fábrica. El primer paso es elegir el nombre de la clase, es decir, el nombre del constructor. Según la convención, la primera letra de este nombre está en mayúscula para separarla de los nombres de variables que generalmente están en minúsculas. Además de esto, la función constructora se parece mucho a una función de fábrica. Considera el siguiente ejemplo:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.showColor = function() { alert(this.color); }; } var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25);
A continuación, le explicamos la diferencia entre el código y el método de fábrica. Primero, dentro del constructor no se crea un objeto, sino que se utiliza la palabra clave this. Al utilizar el operador new en el constructor, se crea un objeto antes de ejecutar la primera línea de código, y solo con this se puede acceder a ese objeto. Luego se puede asignar directamente a las propiedades de this, por defecto es el valor de retorno del constructor (no es necesario usar explícitamente el operador return).
Ahora, crear objetos usando el operador new y el nombre de la clase Car se parece más a la creación de objetos de objetos generales en ECMAScript.
Quizás te preguntes si este método tiene el mismo problema de gestión de funciones que el método anterior. Sí.
Al igual que las funciones de fábrica, las funciones constructoras generan funciones repetidamente, creando una versión de función independiente para cada objeto. Sin embargo, al igual que las funciones de fábrica, también se puede reescribir la función constructora con una función externa, lo que no tiene ningún significado semántico. Esto es exactamente la ventaja del método de prototipo que se mencionará a continuación.
Método de prototipo
Este método utiliza la propiedad prototype del objeto, que se puede considerar como el prototipo dependiente para la creación de nuevos objetos.
Aquí, primero se usa una función constructora vacía para establecer el nombre de la clase. Luego, todas las propiedades y métodos se asignan directamente al atributo prototype. Reescribimos el ejemplo anterior, el código es el siguiente:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25; Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car();
En este código, primero se define la función constructora (Car), que no tiene código. Los siguientes renglones de código agregan propiedades al atributo prototype de Car para definir las propiedades del objeto Car. Al llamar new Car(), todas las propiedades del prototipo se asignan inmediatamente al objeto que se crea, lo que significa que todas las instancias de Car almacenan punteros a la función showColor(). En términos de semántica, todas las propiedades parecen pertenecer a un objeto, por lo que se resuelve el problema de las dos primeras formas.
Además, con este método, se puede usar el operador instanceof para verificar el tipo de objeto al que apunta la variable dada. Por lo tanto, el siguiente código saldrá TRUE:
alert(oCar1 instanceof Car); // Sale "true"
Problemas del método de prototipo
El método de prototipo parece ser una buena solución. Lamentablemente, no cumple con las expectativas.
Primero, esta función constructora no tiene parámetros. Al usar el método de prototipo, no se puede inicializar el valor de las propiedades pasando parámetros a la función constructora, porque las propiedades color de Car1 y Car2 son "blue", las propiedades doors son 4, y las propiedades mpg son 25. Esto significa que los valores predeterminados de las propiedades deben cambiarse después de la creación del objeto, lo que es muy molesto, pero no es todo. El verdadero problema surge cuando las propiedades apuntan a un objeto en lugar de a una función. El compartimiento de funciones no causa problemas, pero los objetos rara vez se comparten entre múltiples instancias. Piensa en el siguiente ejemplo:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25; Car.prototype.drivers = new Array("Mike","John"); Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car(); oCar1.drivers.push("Bill"); alert(oCar1.drivers); // Salida: "Mike,John,Bill" alert(oCar2.drivers); // Salida: "Mike,John,Bill"
En el código anterior, la propiedad drivers es un puntero a un objeto Array, que contiene dos nombres "Mike" y "John". Debido a que drivers es un valor de referencia, los dos实例es de Car apuntan al mismo array. Esto significa que agregar el valor "Bill" a oCar1.drivers también se puede ver en oCar2.drivers. Al mostrar cualquiera de estos punteros, el resultado es una cadena "Mike,John,Bill".
Dado que hay tantos problemas al crear objetos, seguro que te preguntras si hay algún método razonable para crear objetos. La respuesta es que sí, se necesita usar tanto el constructor como el método de prototipo.
Método constructor/prototipo combinado
El uso combinado de constructor y prototipo permite crear objetos de manera similar a otros lenguajes de programación. Este concepto es muy simple, es decir, definir todas las propiedades no funcionales del objeto mediante el constructor, y definir las propiedades funcionales (métodos) del objeto mediante el método de prototipo. Como resultado, todas las funciones se crean una vez, y cada objeto tiene su propia instancia de propiedades de objeto.
Reescribimos el ejemplo anterior, el código es el siguiente:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); } Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25); oCar1.drivers.push("Bill"); alert(oCar1.drivers); // Salida: "Mike,John,Bill" alert(oCar2.drivers); // Salida: "Mike,John"
Ahora es más como crear un objeto común. Todas las propiedades no funcionales se crean en el constructor, lo que significa que también se pueden asignar valores predeterminados a las propiedades mediante los parámetros del constructor. Debido a que solo se crea una instancia de la función showColor(), no hay desperdicio de memoria. Además, agregar el valor "Bill" al array de conductores de oCar1 no afecta al array de oCar2, por lo que cuando se muestran los valores de estos arrays, oCar1.drivers muestra "Mike,John,Bill", mientras que oCar2.drivers muestra "Mike,John". Porque se utiliza el método de prototipo, aún se puede utilizar el operador instanceof para determinar el tipo de objeto.
Esta es la forma principal adoptada por ECMAScript, que tiene las características de otros métodos, pero sin sus efectos secundarios. Sin embargo, algunos desarrolladores aún creen que este método no es perfecto.
Método de prototipo dinámico
Para los desarrolladores acostumbrados a usar otros lenguajes, usar el método de constructor/método de prototipo mixto no parece tan armonioso. Después de todo, al definir una clase, la mayoría de los lenguajes orientados a objetos encapsulan visualmente propiedades y métodos. Considera el siguiente ejemplo de clase Java:
class Car { public String color = "blue"; public int doors = 4; public int mpg = 25; public Car(String color, int doors, int mpg) { this.color = color; this.doors = doors; this.mpg = mpg; } public void showColor() { System.out.println(color); } }
Java encapsula bien todas las propiedades y métodos de la clase Car, por lo que al ver este código se sabe qué funcionalidad va a implementar, definiendo la información de un objeto. Los críticos del método de constructor/método de prototipo mixto consideran que la práctica de buscar propiedades dentro del constructor y métodos en su exterior no es lógica. Por lo tanto, diseñaron el método de prototipo dinámico para proporcionar un estilo de codificación más amigable.
La idea básica del método de prototipo dinámico es similar a la del constructor/método de prototipo mixto, es decir, definir propiedades no funcionales dentro del constructor y las propiedades funcionales utilizando propiedades del prototipo. La única diferencia es la ubicación del método del objeto. A continuación se muestra la clase Car redactada con el método de prototipo dinámico:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); if (typeof Car._initialized == "undefined") { Car.prototype.showColor = function() { alert(this.color); }; Car._initialized = true; } }
Este constructor no cambia hasta que se verifica que typeof Car._initialized sea igual a "undefined". Esta línea de código es la parte más importante de los métodos primitivos dinámicos. Si este valor no está definido, el constructor continuará definiendo métodos del objeto de manera prototípica y luego establecerá Car._initialized en true. Si este valor está definido (su valor es true, typeof tiene un valor Boolean), ya no se creará ese método. En resumen, este método utiliza un indicador (_initialized) para determinar si se han asignado métodos al prototipo. Este método se crea y se asigna solo una vez, y los desarrolladores de OOP tradicionales se alegrarán de ver que este código se parece más a las definiciones de clases en otros lenguajes.
Método de fábrica mixto
Este método generalmente se utiliza como una solución alternativa cuando no se puede aplicar el método anterior. Su objetivo es crear un constructor falso, que solo devuelva una nueva instancia de otro objeto.
Este código parece muy similar a una función fábrica:
function Car() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; }
Diferente del método clásico, este método utiliza el operador new, lo que lo hace parecer como un verdadero constructor:
var car = new Car();
Dado que se llama al operador new en el constructor Car(), se ignorará el segundo operador new (ubicado fuera del constructor), y el objeto creado dentro del constructor se pasará a la variable car.
Este método tiene problemas similares en la gestión interna de métodos del objeto al método clásico. Se recomienda fuertemente: evita usar este método a menos que sea absolutamente necesario.
¿Qué método utilizar
Como se mencionó anteriormente, el más utilizado en la actualidad es la combinación de constructor/fuerza prototípica. Además, los métodos primitivos dinámicos también son muy populares, equivalentes en funcionalidad a la combinación de constructor/fuerza prototípica. Puedes usar cualquiera de estos dos métodos. Sin embargo, no utilices únicamente el constructor o la fuerza prototípica clásica, ya que esto puede introducir problemas en el código.
Ejemplo
Un punto interesante de los objetos es la manera en que resuelven problemas. Uno de los problemas más comunes en ECMAScript es el rendimiento de la concatenación de cadenas. Al igual que en otros lenguajes, las cadenas en ECMAScript son inmutables, es decir, sus valores no pueden cambiar. Considera el siguiente código:
var str = "hello "; str += "world";
En realidad, los pasos que realiza este código en el fondo son los siguientes:
- Crear una cadena para almacenar "hello ";
- Crear una cadena para almacenar "world".
- Crear una cadena para almacenar el resultado de la conexión.
- Copiar el contenido actual de str al resultado.
- Copiar "world" al resultado.
- Actualizar str para que apunte al resultado.
Cada vez que se completa la conexión de cadenas, se ejecutan los pasos 2 a 6, lo que hace que esta operación sea muy consumidora de recursos. Si se repite este proceso cientos o incluso miles de veces, se causará un problema de rendimiento. La solución es almacenar las cadenas en un objeto Array y luego crear la cadena final con el método join() (el parámetro es una cadena vacía). Imagina reemplazar el código anterior con el siguiente:
var arr = new Array(); arr[0] = "hello "; arr[1] = "world"; var str = arr.join("");
De esta manera, no hay problema con la introducción de cuántas cadenas en el array, porque la operación de conexión solo ocurre cuando se llama al método join(). En este momento, los pasos a seguir son los siguientes:
- Crear una cadena para almacenar el resultado
- Copiar cada cadena al lugar adecuado en el resultado
Aunque esta solución es buena, hay un método mejor. El problema es que este código no refleja exactamente su intención. Para que sea más fácil de entender, se puede empacar esta función con la clase StringBuffer:
function StringBuffer () { this._strings_ = new Array(); } StringBuffer.prototype.append = function(str) { this._strings_.push(str); }; StringBuffer.prototype.toString = function() { return this._strings_.join(""); };
Este código debe prestar atención primero a la propiedad strings, que es una propiedad privada. Tiene solo dos métodos, es decir, los métodos append() y toString(). El método append() tiene un parámetro, que adjunta ese parámetro al array de cadenas, y el método toString() llama al método join del array, devolviendo la cadena realmente conectada. Para conectar un grupo de cadenas con un objeto StringBuffer, se puede usar el siguiente código:
var buffer = new StringBuffer(); buffer.append("hello "); buffer.append("world"); var result = buffer.toString();
Puede probar el rendimiento del objeto StringBuffer y el método de conexión de cadenas tradicional con el siguiente código:
var d1 = new Date(); var str = ""; for (var i=0; i < 10000; i++) { str += "text"; } var d2 = new Date(); document.write("Concatenación con más: ") + (d2.getTime() - d1.getTime()) + " milliseconds"); var buffer = new StringBuffer(); d1 = new Date(); for (var i=0; i < 10000; i++) { buffer.append("text"); } var result = buffer.toString(); d2 = new Date(); document.write("<br />Concatenación con StringBuffer: ") + (d2.getTime() - d1.getTime()) + " milliseconds");
Este código realiza dos pruebas de conexión de cadenas, la primera usando el símbolo más (+), la segunda usando la clase StringBuffer. Cada operación conecta 10000 cadenas. Las fechas d1 y d2 se utilizan para determinar el tiempo necesario para completar la operación. Tenga en cuenta que si no se proporciona ningún parámetro al crear un objeto Date, se le asigna la fecha y hora actuales al objeto. Para calcular cuánto tiempo duró la operación de conexión, simplemente restar los valores en milisegundos (devueltos por el método getTime()) de las fechas. Este es un método común para medir el rendimiento de JavaScript. Los resultados de esta prueba pueden ayudarlo a comparar la eficiencia de usar la clase StringBuffer en lugar del símbolo más (+).
- Página anterior Alcance del objeto
- Página siguiente Modificar objeto