¿Qué es la encapsulación de JavaScript? Hay varias formas de encapsularla.
Aunque JS es un lenguaje orientado a objetos, no es un lenguaje típico orientado a objetos. La relación orientada a objetos de Java / C es una relación objeto-clase, mientras que JS es una relación objeto-objeto. El prototipo está conectado en el medio y la clase principal y la subclase forman una cadena de prototipo. Este artículo analiza la encapsulación de objetos JS, luego analiza la forma correcta de implementar la herencia, luego analiza varios problemas y finalmente brinda una breve explicación de la palabra clave de clase recién introducida en ES6.
Una clase JS es en realidad una función. Dado que no es una clase típica de programación orientada a objetos, también se la denomina pseudoclase. Para comprender las clases de JS, es necesario comprender mejor las funciones en JS. En primer lugar, la función en sí es un objeto, que puede usarse como parámetro de la función o como valor de retorno, como un objeto normal. Entonces la función se puede usar como una clase, por ejemplo, para implementar una clase String
1 var MyString = function(str){2 this.content = str 3}; ( "hanMeimei"); 6 var addr = new MyString("China"); 7 console.log(name.content " live in " addr.content);
La primera línea declara una función MyString, Obtenga una clase MyString, y esta función también es el constructor de MyString. La línea 5 crea un nuevo objeto, que ejecutará el constructor. Esto apunta al objeto recién generado. La línea 2 agrega un atributo de contenido a este objeto y luego asigna la dirección del nuevo objeto al nombre. La línea 6 crea un nuevo objeto nuevamente. Tenga en cuenta que esto aquí apunta al nuevo objeto, por lo que el contenido recién generado es diferente del anterior.
Hay un problema con el código anterior que se ejecuta en el navegador, porque este código se ejecuta en el ámbito global y la variable de nombre definida también es global, por lo que en realidad ejecuta var nombre = new MyString(" " ) es equivalente a window.name = new MyString(""). Dado que el nombre es una variable que ya existe en la ventana, como segundo parámetro de window.open, se puede utilizar para transferir datos entre dominios. Sin embargo, dado que window.name no admite su configuración como instancia de una función personalizada, la configuración no es válida y se mantiene el valor predeterminado: una cadena con el valor "[objeto Objeto]". La solución es cambiar el entorno de ejecución del código a uno local, es decir, envolverlo con una función:
(function(){ var name = new MyString("hanMeimei");
console.log(name.content); //Correcto, salida hanMeimei})();?
Entonces desde aquí puedes ver que el código está envuelto en una función y No contamina el ámbito global. Sigue siendo bastante necesario. A continuación, volvamos al tema.
Cada función en JS tiene un atributo prototipo, que apunta a un objeto ordinario, que almacena la dirección del objeto. Cada instancia de esta función nueva se traerá con un puntero (generalmente __proto__) que apunta al objeto señalado por el prototipo.
El proceso es similar a:
var name = new MyString(); //Generar un objeto y ejecutar el constructor name.__proto__ = MyString.prototype ? //Agregar un atributo __proto__ para apuntar a la clase; prototipo (esta línea de código es solo para ilustración)
Como se muestra en la figura siguiente, el __proto__ de nombre y dirección apunta al objeto prototipo de MyString:
Se puede ver que en JS, la clase El método se coloca en el prototipo de la función, y cada instancia de la misma obtendrá el método de clase. ?
Ahora agregue un método toString para MyString:
MyString.prototype.toString = function(){ return this.content;
};
p>
El objeto prototipo (objeto) de MyString agregará un nuevo atributo.
En este momento, el nombre de la instancia y la dirección tienen este método, llame a este método:
console.log(name.toString()); //Salida hanMeimeiconsole.log(name); " vive en " addr); // Cuando " " conecta caracteres, se llama automáticamente a toString y genera hanMeimei vive en China
Esto logra una encapsulación básica: los atributos de la clase se definen en el constructor, como por ejemplo El contenido de MyString; y los métodos de clase se agregan en el prototipo de la función, como el método toString de MyString.
En este momento, considere una pregunta básica: ¿por qué los objetos de la clase pueden hacer referencia a los métodos agregados al prototipo? Porque JS primero buscará el método en el objeto y, si no lo encuentra, buscará en su prototipo. Por ejemplo, cuando se ejecuta name.toString(), el primer paso es que el objeto de nombre en sí no tiene toString (solo un atributo de contenido), por lo que busca el objeto prototipo de nombre, es decir, el objeto señalado por __proto__, y encuentra que hay un atributo toString, por lo que lo encuentra.
¿Qué pasa si el método toString no se agrega a MyString? Dado que MyString es en realidad un objeto de función, la sintaxis de MyString definida anteriormente es equivalente a:
//Es solo por ejemplo, esta forma de sintaxis debe evitarse porque provocará dos compilaciones y afectará la eficiencia.
p>var MyString = new Function("str", "this.content = str");
Al comparar MyString y el __proto__ de Function, podemos ver desde el costado que MyString es en realidad Función Un ejemplo de:
console.log(MyString.__proto__); //Salida [Función: Vacío] console.log(Función.__proto__); //Salida [Función: Vacío]
El puntero __proto__ de MyString apunta al prototipo de la función. A través de la función de depuración del navegador, puede ver que este prototipo es el prototipo de Objeto, como se muestra a continuación:
Porque el objeto está en JS La clase raíz, todo otras clases heredan de él, esta clase raíz proporciona 6 métodos como toString y valueOf.
Por lo tanto, se encuentra el método toString del prototipo del Objeto, se completa y ejecuta la búsqueda:
console.log(name.toString()); //Salida { contenido: 'hanMeimei' }
Como puedes ver aquí, la herencia en JS es dejar que el __proto__ del prototipo de una función (como MyString) apunte al prototipo de otra función (como Object). En base a esto, escriba una clase personalizada UnicodeString que herede de MyString
var UString = function(){ };
Implementar herencia:
UString.prototype = MyString.prototype; //Implementación de error
Tenga en cuenta que el método de herencia anterior es incorrecto. Esto simplemente apunta el prototipo de UString al prototipo de MyString, es decir, UString y MyString usan el mismo prototipo y subclase. Cuando UString agrega, elimina o modifica el método prototipo, MyString cambiará en consecuencia y otra clase que hereda MyString, como AsciiString, también cambiará en consecuencia. Según el análisis anterior, el atributo __proto__ en el prototipo de UString debería apuntar al prototipo de MyString, en lugar de permitir que el prototipo de UString apunte a MyString.
En otras palabras, UString debe tener su propio prototipo independiente y agregar un puntero a su prototipo para que apunte al prototipo de la clase principal:
UString.prototype.__proto__ = MyString.prototype;//Not; Implementación correcta
Debido a que __proto__ no es una sintaxis estándar y no es visible en algunos navegadores, si ejecuta el código anterior en Firefox, Firefox le dará una advertencia:
mutando el [[ El prototipo]] de un objeto hará que su código se ejecute muy lentamente; en su lugar, cree el objeto con el valor [[Prototype]] inicial correcto usando Object.create
Un enfoque razonable debería ser dejar que el prototipo sea igual a un objeto. El __proto__ de este objeto apunta al prototipo de la clase principal. Por lo tanto, este objeto debe ser una instancia de una función, y el prototipo de esta función apunta al prototipo de la clase principal, por lo que se obtiene la siguiente implementación:
1 Object.create = function(o){2 var F = function(){}; 3 F.prototype = o; 4 return new F() }; Object.create( MyString.prototype);
La segunda línea del código define una función temporal. La tercera línea hace que el prototipo de esta función apunte al prototipo de la clase principal. instancia. El __proto__ de esta instancia apunta al prototipo de la clase principal y luego asigna esta instancia al prototipo de la subclase en la línea 7. La implementación de la herencia básicamente se completa aquí.
Pero hay un pequeño problema.
En un prototipo normal, habrá un constructor que apunta a la función constructora en sí, como MyString arriba:
El propósito de este constructor es llamar al constructor en el prototipo, por ejemplo, agregando una función de copia. a la clase MyString:
1 MyString.prototype.copy = function(){2 // ?return MyString(this.content); ?//Hay problemas con esta implementación, que serán analizados. a continuación3 return new this.constructor( this.content); ?//Implementación correcta 4}; 5 6 var anotherName = name.copy(); 7 console.log(anotherName.toString()); //Salida hanMeimei8 console.log (otro nombre de instancia de MyString); //Salida verdadera
El problema es: la línea 7 del código Object.create sobrescribe completamente el prototipo UString y lo reemplaza con un nuevo objeto cuyo __proto__ apunta a la clase principal. el prototipo de MyString, por lo que cuando se busca UString.prototype.constructor, UString.prototype no tiene el atributo constructor, por lo que busca el __proto__ al que apunta y encuentra el constructor de MyString, por lo que el constructor de UString es en realidad MyString. contuctor, como se muestra a continuación, ustr2 es en realidad una instancia de MyString, no la UString esperada. En lugar de usar un constructor, llamar directamente usando el nombre (línea 2 del código anterior) también tendrá este problema.
var ustr = new UString(); var ustr2 = ustr.copy();
console.log(ustr ?instanceof UString); UString); //Salida falseconsole.log(ustr2 instanciade Mystring); //Salida true
Entonces, después de implementar la herencia, debe agregar un paso más y señalar el constructor en el prototipo de la subclase UString. a sí mismo:
UString.prototype.constructor = UString;
Al ejecutar this.constructor() en la función de copia, en realidad es UString(). Será normal hacer un juicio instantáneo en este momento:
console.log(ustr2 instanciade Ustring); //Salida verdadera
Las operaciones relevantes se pueden encapsular en una función para facilitar su reutilización. .
El núcleo básico de la herencia termina aquí. Aún quedan varias cuestiones por considerar.
El primero es cómo llamar al constructor de la clase principal en el constructor de la subclase, usando directamente el constructor de la clase principal como una función ordinaria y pasando este puntero de la subclase:
p>
1 var UString = function(str){2 // MyString(str ? //Implementación incorrecta 3 MyString.call(this, str 4 }); = new UString( "hanMeimei"); 7 console.log(ustr ""); ?//Salida hanMeimei
Tenga en cuenta que la línea 3 pasa un puntero this Cuando se llama a MyString, esto apunta al. Objeto UString recién generado, si la línea 2 se usa directamente, el contexto de ejecución es ventana, esto apuntará a ventana, this.content = str es equivalente a window.content = str.
El segundo problema es la implementación de propiedades privadas. Las instancias de variables definidas en el constructor inicial son públicas y se puede acceder a ellas directamente, de la siguiente manera:
var MyString = function(str). ){ this.content = str;
}; var str = new MyString("hola"); p>Pero en la programación típica orientada a objetos, los atributos deben ser privados y se debe acceder a los atributos de operación a través de los métodos/interfaces proporcionados por la clase, para lograr el propósito de la encapsulación. Para lograr privacidad en JS, debe usar el alcance de la función:
var MyString = function(str){ this.sayHi = function(){ return "hi " str;
}
};var str = new MyString("hanMeimei");
console.log(str.sayHi()); //Salida hola, hanMeimei
Pero uno de esos problemas es que la definición de la función debe colocarse en el constructor en lugar del prototipo discutido anteriormente. Como resultado, cada vez que se genera una instancia, se agregará una función idéntica a la instancia, lo que provocará una. Desperdicio de espacio en la memoria. Por tanto, dicha implementación se produce a expensas de la memoria. Si se generan muchas instancias, el espacio de memoria aumentará significativamente. Este problema no se puede ignorar. Por lo tanto, no es práctico implementar propiedades privadas en JS, ni siquiera en la sintaxis de clase ES6.
Sin embargo, puede agregar variables miembro privadas estáticas a la clase. Esta variable privada es compartida por todas las instancias de la clase, como en el siguiente caso:
var Worker
(function). (){ var id = 1000;
Trabajador = función(){
id
}
Trabajador.prototipo. getId = function(){ ID de retorno
};
})(); var trabajador1 = nuevo trabajador(); .getId()); //Salida 1001var trabajador2 = new Worker();
console.log(worker2.getId()); //Salida 1002
El ejemplo utiliza variables estáticas de la clase para generar una identificación única para cada trabajador. Al mismo tiempo, la instancia de trabajo no puede modificar este ID directamente.
El tercer problema son las funciones virtuales. No tiene mucho sentido hablar de funciones virtuales en JS. Una gran función de las funciones virtuales es realizar la dinámica de tiempo de ejecución. Esta dinámica de tiempo de ejecución se determina en función del tipo de subclase. Sin embargo, JS es un lenguaje de tipo débil. Los tipos de subclases son todos var. un método correspondiente, puede pasar parámetros y ejecutarlo "polimórficamente". Está muy simplificado en comparación con lenguajes fuertemente tipados como C/Java.
Finalmente, hablemos brevemente sobre la palabra clave class recientemente introducida en ES6
1 //Necesita ejecutarse en modo estricto 2 'use estricto' 3 class MyString{ 4 constructor(str); ){ 5 this.content = str; 6 } 7 toString(){ 8 return this.content; 9 }10 // Se agregó la palabra clave de función estática 11 static concat(str1, str2){12 return str1 str2; 15 16 //extiende la palabra clave de herencia 17 class UString extends MyString{18 constructor(str){19 //Usa super para llamar al método de la clase principal 20 super(str); 21 }22 }23 24 var str1 = new MyString("hello); "), 25 str2 = new MyString(" mundo"); 26 console.log(str1); ? //Salida MyString {contenido: "hola"} 27 console.log(str1.content); ? //Salida hola28 consola .log(str1.toString()); //Salida hola29 console.log(MyString.concat(str1, str2)); //Salida hola mundo
3031 var ustr = new UString("ustring" ?
32 console.log(ustr); //Salida MyString {content: "ustring"}33 console.log(ustr.toString());
A juzgar por los resultados de salida, la nueva clase aún no implementa la función de privacidad de atributos, consulte la línea 27. Y en la línea 26, podemos ver que la llamada clase es en realidad el compilador que nos ayuda a implementar el complejo proceso anterior. Su esencia es la misma, pero hace que el código sea más simple y claro. Una diferencia es que con la adición de la palabra clave estática, la función de la clase se puede llamar directamente usando el nombre de la clase. El soporte para ES6 aún no es alto. Los últimos Chrome y Safari ya admiten clases, y el soporte de Firefox aún no es muy bueno.
Finalmente, aunque muchos JS en general páginas web son proyectos pequeños, parece que no hay necesidad de encapsular y heredar estas tecnologías, pero si puedes escribir código con pensamiento orientado a objetos, sin importar el tamaño. del proyecto, siempre que se aplique correctamente, o incluso se combine con algunas ideas de patrones de diseño, hará que el código sea más mantenible y escalable. Por lo general, puedes intentar escribir así.