
Todos los que usan JavaScript maravilloso para cualquier propósito se preguntaron: ¿por qué typeof es un "objeto" nulo ? typeof de una función devuelve "function" , pero de Array - "object" ? y donde getClass en tus clases alardeadas? Y aunque en su mayor parte las especificaciones y los hechos históricos responden de manera fácil y natural, me gustaría trazar un poco ... más para mí.
Si, lector, su tipo y su instancia no son suficientes para usted en sus tareas, y desea algunos detalles, en lugar de "objetos" , entonces puede ser útil más adelante. Ah, sí, sobre los patos: ellos también lo estarán, solo un poco equivocados.
Breve antecedentes
Obtener el tipo de variable de manera confiable en JavaScript siempre ha sido una tarea no trivial, ciertamente no para un principiante. En la mayoría de los casos, por supuesto, no es obligatorio, solo:
if (typeof value === 'object' && value !== null) {
y ahora no Cannot read property of null
captar Cannot read property of null
, un análogo local de NPE. ¿Eso es familiar?
Y luego comenzamos a usar funciones como constructores cada vez más a menudo, y a veces es útil inspeccionar el tipo de objeto creado de esta manera. Pero simplemente usar typeof de la instancia no funcionará, ya que obtenemos el "objeto" correctamente.
Entonces todavía era normal usar un prototipo de modelo OOP en JavaScript, ¿recuerdas? Tenemos algún objeto, y a través del enlace a su prototipo podemos encontrar la propiedad del constructor que apunta a la función con la que se creó el objeto. Y luego un poco de magia con toString de la función y las expresiones regulares, y aquí está el resultado:
f.toString().match(/function\s+(\w+)(?=\s*\()/m)[1]
A veces hacían esas preguntas en las entrevistas, pero ¿por qué?
Sí, podríamos guardar una representación de cadena del tipo como una propiedad especial y obtenerla del objeto a través de la cadena de prototipo:
function Type() {}; Type.prototype.typeName = 'Type'; var o = new Type; o.typeName; < "Type"
Solo dos veces tiene que escribir "Tipo" : en la declaración de función y en la propiedad.
Para los objetos integrados (como Array o Date ) teníamos una propiedad secreta [[Class]] , que se podía conectar a toString desde el objeto estándar:
Object.prototype.toString.call(new Array); < "[object Array]"
Ahora tenemos clases, y los tipos personalizados finalmente se arreglan en el lenguaje: este no es un LiveScript para usted; ¡Escribimos código compatible en grandes cantidades!
Casi al mismo tiempo, aparecieron Symbol.toStringTag y Function.name , con lo que podemos tomar nuestro typeof de una nueva manera.
En general, antes de continuar, quiero señalar que el problema en consideración evoluciona en StackOverflow junto con el lenguaje y sube de editor a editor: hace 9 años , hace 7 años , no hace tanto tiempo, o es esto y aquello .
Estado actual de las cosas
Anteriormente, ya examinamos con suficiente detalle Symbol.toStringTag y Function.name . En resumen, el carácter interno de toStringTag es moderno [[Clase]] , solo que podemos redefinirlo para nuestros objetos. Y la propiedad Function.name está legalizada en casi todos los navegadores con el mismo typeName del ejemplo: devuelve el nombre de la función.
Sin dudarlo, puede definir dicha función:
function getTag(any) { if (typeof any === 'object' && any !== null) { if (typeof any[Symbol.toStringTag] === 'string') { return any[Symbol.toStringTag]; } if (typeof any.constructor === 'function' && typeof any.constructor.name === 'string') { return any.constructor.name; } } return Object.prototype.toString.call(any).match(/\[object\s(\w+)]/)[1]; }
- SI la variable es un objeto, entonces:
1.1. SI el objeto se reemplaza por StringTag , devuélvalo;
1.2. SI la función constructora es conocida para el objeto y la función tiene una propiedad de nombre , devuélvala; - FINALMENTE DE OTRA MANERA use el método toString del objeto Object , que hará todo el trabajo polimórfico por nosotros para absolutamente cualquier otra variable.
Objeto con toStringTag :
let kitten = { [Symbol.toStringTag]: 'Kitten' }; getTag(kitten); < "Kitten"
Clase con toStringTag :
class Cat { get [Symbol.toStringTag]() { return 'Kitten'; } } getTag(new Cat); < "Kitten"
Usando constructor.name :
class Dog {} getTag(new Dog); < "Dog"
→ Se pueden encontrar más ejemplos en este repositorio.
Por lo tanto, ahora es bastante fácil determinar el tipo de cualquier variable en JavaScript. Esta función le permite verificar uniformemente las variables para el tipo y usar una expresión de cambio simple en lugar de verificaciones de pato en funciones polimórficas. En general, nunca me gustó el enfoque basado en la escritura de patos, dicen que si algo tiene la propiedad de empalme , ¿es una matriz o algo así?
Algunos patos equivocados
Entender el tipo de una variable por la presencia de ciertos métodos o propiedades es, por supuesto, asunto de todos y depende de la situación. Pero tomaré este getTag e inspeccionaré algunas cosas del lenguaje.
Clases?
Mi "pato" favorito en JavaScript son las clases. Los chicos que comenzaron a escribir JavaScript con ES-2015, a veces no sospechan cuáles son estas clases. Y la verdad es:
class Person { constructor(name) { this.name = name; } hello() { return this.name; } } let user = new Person('John'); user.hello(); < "John"
Tenemos una palabra clave de clase , un constructor, algunos métodos, incluso se extiende . También creamos una instancia de esta clase a través de new . Parece una clase en su sentido habitual, ¡significa una clase!
Sin embargo, cuando comienza a agregar nuevos métodos en tiempo real a la "clase", y al mismo tiempo están disponibles de inmediato para las instancias ya creadas, algunas se pierden:
Person.prototype.hello = function() { return `Is not ${this.name}`; } user.hello(); < "Is not John"
¡No hagas esto!
Y no algunos saben de manera confiable que esto es solo azúcar sintáctico sobre el modelo prototipo, porque conceptualmente nada ha cambiado en el lenguaje. Si llamamos a getTag desde Persona , obtenemos "Función" , no la "Clase" inventada, y vale la pena recordarlo.
Otras funciones
Hay varias formas de declarar una función en JavaScript: FunctionDeclaration , FunctionExpression y recientemente ArrowFunction . Todos sabemos cuándo y qué usar: las cosas son bastante diferentes. Además, si llamamos a getTag desde la función declarada por cualquiera de las opciones propuestas, obtenemos "Función" .
De hecho, el lenguaje tiene muchas más formas de definir una función. Agregamos a la lista al menos la ClassDeclaration considerada, luego las funciones y generadores asíncronos, etc.: AsyncFunctionDeclaration , AsyncFunctionExpression , AsyncArrowFunction , GeneratorDeclaration , GeneratorExpression , ClassExpression , MethodDefinition (la lista no está completa). Y parece que dicen ¿y qué? todo lo anterior se comporta como una función, lo que significa que getTag también devolverá "Función" . Pero hay una característica: todas las opciones son ciertamente funciones, pero no todas directamente: Función .
Hay subtipos de función incorporados :
El constructor de funciones está diseñado para ser subclase.
No hay medios sintácticos para crear instancias de subclases de funciones, excepto las subclases GeneratorFunction y AsyncFunction incorporadas.
Tenemos Function y dentro de GeneratorFunction y AsyncFunction con sus constructores "heredados" de él. Esto enfatiza que los enlaces y los generadores tienen su propia naturaleza única. Y como resultado:
async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } getTag(sleep); < "AsyncFunction"
Al mismo tiempo, no podemos crear una instancia de dicha función a través del nuevo operador, y su llamada nos devuelve la promesa :
getTag(sleep(100)); < "Promise"
Un ejemplo con una función generadora:
function* incg(i) { while(1) yield i += 1; } getTag(incg); < "GeneratorFunction"
Llamar a tal función devuelve una instancia: el objeto Generador :
let inc = incg(0); getTag(inc); < "Generator"
El símbolo toStringTag está bastante redefinido para enlaces y generadores. Pero typeof para cualquier función mostrará "function" .
Objetos en línea
Tenemos cosas como Establecer , Mapa , Fecha o Error . La aplicación de getTag a ellos devolverá "Función" , porque estas son las funciones: constructores de colecciones iterables, fechas y errores. De las instancias que obtenemos respectivamente: "Establecer" , "Mapa" , "Fecha" y " Error ".
Pero ten cuidado! Todavía hay objetos como JSON o Math . Si te das prisa, puedes asumir una situación similar. Pero no! Esto es completamente diferente: objetos individuales integrados. No son instanciables ( is not a constructor
). Una llamada typeof devolverá "objeto" (quién lo dudaría). Pero getTag recurrirá a toStringTag y obtendrá "JSON" y "Math" . Esta es la última observación que quiero compartir.
Vamos sin fanatismo
Recientemente pregunté un poco más de lo habitual sobre el tipo de variable en JavaScript cuando decidí escribir mi simple inspector de objetos para el análisis de código dinámico (jugar). El material no solo se publica en la programación Anormal , ya que no lo necesita en producción: hay typeof , instanceof , Array.isArray , isNaN y todo lo demás que vale la pena recordar al realizar la verificación necesaria. La mayoría de los proyectos tienen TypeScript, Dart o Flow. ¡Me encanta JavaScript !