typeof Everything and duck misunderstandings

    image


    Everyone using wonderful JavaScript for whatever purpose , wondered: why does typeof null mean “object” ? typeof from function returns "function" , but from Array - "object" ? and where is getClass in your praised classes? And although most of them are easily and naturally answered by a specification or historical facts , I would like to draw a little line ... more for myself.


    If, reader, in your tasks too typeof and instanceof are not enough, and you want some specifics, and not "objects" , then it may be useful. Oh yeah, about the ducks: they will be too, just a little wrong.


    Brief Background


    To reliably get the type of a variable in JavaScript has always been a non-trivial task, certainly not for a beginner. In most cases, it is of course not required, simply:


    if (typeof value === 'object' && value !== null) {
        // awesome code around the object
    }

    and now you do not catch Cannot read property of null- the local analogue of NPE. Familiar?


    And then we began to increasingly use functions as constructors, and sometimes it is useful to inspect the type of object created in this way. But just using typeof from the instance does not work, since we correctly get the "object" .


    Then it was still normal to use the prototype OOP model in JavaScript, remember? We have some object, and through a link to its prototype we can find the constructor property that points to the function with which the object was created. And then a bit of magic with toString from the function and regular expressions and here it is the result:


    f.toString().match(/function\s+(\w+)(?=\s*\()/m)[1]

    Sometimes such interviews were asked, but why?


    Yes, we could just save the prototype with a special property of a string representation of the type and receive it from the object through a chain of prototypes:


    functionType() {};
    Type.prototype.typeName = 'Type';
    var o = new Type;
    o.typeName;
    < "Type"

    Only two times you have to write "Type" : in the function declaration and in the property.


    For the built-in objects (like Array or Date ), we had the secret property [[Class]] , which could be hooked through toString from the standard Object :


    Object.prototype.toString.call(newArray);
    < "[objectArray]"

    Now we have classes, and the custom types have finally become entrenched in the language: this is no longer any LiveScript for you; we write supported code in large quantities!


    Around the same time, Symbol.toStringTag and Function.name appeared , with which we can take our typeof in a new way .


    In general, before we go further, I want to note that the issue under consideration evolves on StackOverflow along with the language and rises from the editorial board to the editorial board: 9 years ago , 7 years ago , not so long ago, or this and that .


    Current state of affairs


    We have previously discussed Symbol.toStringTag and Function.name in sufficient detail . In short, the internal toStringTag symbol is a modern [[Class]] , only we can redefine it for our objects. And the Function.name property is the same typeName from the example institutionalized in almost all browsers : returns the name of the function.


    Without hesitation, you can define this function:


    functiongetTag(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;
            }
        }
        returnObject.prototype.toString.call(any).match(/\[object\s(\w+)]/)[1];
    }

    1. IF a variable is an object, then:
      1.1. If the object is overridden toStringTag , then return it;
      1.2. If the object has a constructor function and the function has a name property , then return it;
    2. FINALLY otherwise use the method toString Object the Object , which will make for us all polymorphic work for absolutely any other variable.

    Object with toStringTag :


    let kitten = {
        [Symbol.toStringTag]: 'Kitten'
    };
    getTag(kitten);
    < "Kitten"

    Class with toStringTag :


    classCat{
        get [Symbol.toStringTag]() {
            return'Kitten';
        }
    }
    getTag(new Cat);
    < "Kitten"

    Using constructor.name :


    classDog{}
    getTag(new Dog);
    < "Dog"

    → More examples can be found in this repository.


    Thus, it is now quite simple to determine the type of any variable in JavaScript. This function allows you to uniformly check variables for type and use a simple switch expression instead of duck checks in polymorphic functions. I never liked the approach based on duck typing that if something has a splice property , then is it an array or something?


    Some wrong ducks


    Understanding the type of a variable according to the presence of certain methods or properties is a matter of course for everyone and depends on the situation. But I will take this getTag and inspect some things of the language.


    Classes?


    My favorite duck in javascript are classes. The guys who started writing in JavaScript with ES-2015 happen and have no idea what these classes are. And the truth is:


    classPerson{
        constructor(name) {
            this.name = name;
        }
        hello() {
            returnthis.name;
        }
    }
    let user = new Person('John');
    user.hello();
    < "John"

    We have the keyword class , constructor, some methods, even extends . We also create an instance of this class through new . It looks like a class in its usual sense - it means a class!


    However, when you start adding new methods in real-time to the "class", and at the same time they become immediately accessible to the already created instances, some are lost:


    Person.prototype.hello = function() { 
        return`Is not ${this.name}`; 
    }
    user.hello();
    < "IsnotJohn"

    Don't do that!


    And not some reliably know that this is just syntactic sugar over the prototype model, because conceptually nothing has changed in the language. If you call getTag from Person , you get "Function" , rather than the fictitious "Class" , and this is worth remembering.


    Other features


    There are several ways to declare a function in JavaScript: FunctionDeclaration , FunctionExpression, and recently ArrowFunction . We all know when and what to use: things are quite different. In this case, if you call getTag from a function declared by any of the proposed options, then we get "Function" .


    In fact, there are many more ways to define a function in a language. Add to the list at least considered ClassDeclaration , then asynchronous functions and generators, etc.: AsyncFunctionDeclaration , AsyncFunctionExpression , AsyncArrowFunction , GeneratorDeclaration , GeneratorExpression , ClassExpression , MethodDefinition (the list is not complete). And it seems they say so and what? All of the above behaves as a function - it means that getTag will also return "Function" . But there is one feature: all options are certainly functions, but not all directly - Function .


    There are built-in subtypes of Function :


    The function constructor is designed to be subclassable.
    Except for the built-in subclasses and subclasses.

    We have a Function and within it are the GeneratorFunction and AsyncFunction inherited from it with its constructors. This emphasizes that asinks and generators have their own unique nature. And as a result:


    asyncfunctionsleep(ms) {
        returnnewPromise(resolve => setTimeout(resolve, ms));
    }
    getTag(sleep);
    < "AsyncFunction"

    At the same time, we cannot instantiate such a function through the new operator , and its call returns us a Promise :


    getTag(sleep(100));
    < "Promise"

    Example with generator function:


    function* incg(i) { while(1) yield i += 1; }
    getTag(incg);
    < "GeneratorFunction"

    Calling such a function returns an instance to us — the Generator object :


    let inc = incg(0);
    getTag(inc);
    < "Generator"

    The toStringTag symbol is rightly redefined for asinks and generators. But typeof for any function will show "function" .


    Embedded objects


    We have things like Set , Map , Date, or Error . Applying getTag to them will return "Function" , because these are the functions — the constructors of iterable collections, dates, and errors. From the instances we get, respectively - "Set" , "Map" , "Date" and " Error ".


    But watch out! there are still objects like JSON or Math . If you hurry, you can assume a similar situation. But no! this is completely different - embedded single objects. They are not instantiable ( is not a constructor). Calling typeof will return "object" (who would doubt). But getTag will refer to toStringTag and get "JSON" and "Math" . This is the last observation that I want to share.


    Let's go without fanaticism


    I was a little deeper than the usual question about the type of a variable in JavaScript quite recently, when I decided to write my own simple object inspector for dynamic code analysis (indulge). The material does not just publish in Abnormal programming , since you do not need it in production: there is typeof , instanceof , Array.isArray , isNaN and everything else that is worth remembering when performing the necessary checks. In most projects, it does TypeScript, Dart or Flow. I just like javascript !


    Also popular now: