Pull to refresh

typeof Everything и утиные недоразумения

Reading time6 min
Views8.9K

image


Каждый, использующий в каких бы то ни было целях замечательный JavaScript, задавался вопросом: мол а почему typeof null — это "object"? typeof от функции возвращает "function", но от Array"object"? а где же getClass у ваших хваленых классов? И хотя на большую часть легко и непринужденно отвечает спецификация или исторические факты, я бы хотел немного подвести черту… в большей степени для самого себя.


Если, читатель, тебе в твоих задачах тоже недостаточно typeof да instanceof и хочется какой-то конкретики, а не "object"ы, то дальше может быть полезно. Ах да, про утки: они будут тоже, только немного неправильные.


Краткая история вопроса


Достоверно получить тип какой-то переменной в JavaScript всегда было задачей нетривиальной, точно не для новичка. В большинстве случаев оно конечно и не требуется, просто:


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

и вот вы уже не ловите Cannot read property of null — местный аналог NPE. Знакомо?


А потом мы начали все чаще использовать функции как конструкторы, а проинспектировать тип созданного таким образом объекта иногда полезно. Но просто использовать typeof от экземпляра не получится, так как мы верно получим "object".


Тогда еще было нормальным использовать прототипную модель ООП в JavaScript, помните? У нас есть некоторый объект, и через ссылку на его прототип мы можем найти свойство constructor, указывающее на функцию, с помощью которой объект был создан. А дальше немного магии с toString от функции и регулярными выражениями и вот он результат:


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

Иногда такое на собеседованиях спрашивали, но зачем?


Да мы просто могли в прототип сохранять специальным свойством строковое представление типа и от объекта получать его по цепочке прототипов:


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

Только два раза приходится писать "Type": в объявлении функции и в свойство.


Для встроенных же объектов (как Array или Date) у нас было секретное свойство [[Class]], которое можно было выцепить через toString от стандартного объекта Object:


Object.prototype.toString.call(new Array);
< "[object Array]"

Сейчас у нас появились классы, и пользовательские типы окончательно закрепились в языке: это вам уже не какой-нибудь LiveScript; мы пишем поддерживаемый код в больших количествах!


Примерно в это же время появились Symbol.toStringTag и Function.name, с помощью которых мы можем по-новому взять наш typeof.


Вообще, прежде чем мы перейдем далее, хочу отметить, что рассматриваемый вопрос эволюционирует на StackOverflow вместе с языком и поднимается от редакции к редакции: 9 лет назад, 7 лет назад, не так давно или это и это еще.


Текущее положение дел


Ранее мы уже рассматривали достаточно подробно Symbol.toStringTag и Function.name. Если коротко, то внутренний символ toStringTag — это современный [[Class]], только мы можем его переопределять для своих объектов. А свойство Function.name — это узаконенный почти во всех браузерах тот самый typeName из примера: возвращает название функции.


Не долго думая, можно определить такую функцию:


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];
}

  1. ЕСЛИ переменная является объектом, то:
    1.1. ЕСЛИ у объекта переопределен toStringTag, то вернуть его;
    1.2. ЕСЛИ у объекта известна функция constructor и у функции определено свойство name, то вернуть его;
  2. НАКОНЕЦ ИНАЧЕ использовать метод toString объекта Object, который сделает за нас всю полиморфную работу для абсолютно любой другой переменной.

Объект с toStringTag:


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

Класс с toStringTag:


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

Использование constructor.name:


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

→ Больше примеров можно посмотреть в этом репозитории


Таким образом, сейчас довольно просто можно определить тип любой переменной в JavaScript. Эта функция позволяет единообразно проверять переменные на тип и использовать простой switch expression вместо утиных проверок в полиморфных функциях. Мне вообще никогда не нравился подход, основанный на утиной типизации мол если у чего-то есть свойство splice, то это массив что ли?


Какие-то неправильные утки


Понимать тип переменной по наличию определенных методов или свойств — дело конечно каждого и зависит от ситуации. Но я возьму этот getTag и поинспектирую некоторые штуки языка.


Классы?


Моя любимая "утка" в JavaScript — это классы. Ребята, которые начинали писать на JavaScript c ES-2015, бывает и не подозревают, что эти классы из себя представляют. И правда:


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

У нас есть ключевое слово class, констуктор, какие-то методы, даже extends. Еще мы создаем экземпляр этого класса через new. Выглядит как класс в привычном его понимании — значит класс!


Однако, когда начинаешь добавлять новые методы в real-time в "класс", и при этом они становятся сразу доступными для уже созданных экземляров, некоторые теряются:


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

Не надо так делать!


А не некоторые достоверно знают, что это всего лишь синтаксический сахар над прототипной моделью, потому что концептуально в языке ничего не поменялось. Если вызвать getTag от Person, то получим "Function", а не выдуманный "Class", и об этом стоит помнить.


Другие функции


В JavaScript есть несколько способов объявления функции: FunctionDeclaration, FunctionExpression и недавно ArrowFunction. Все мы знаем, когда и что следует использовать: вещи то довольно разные. При этом если вызвать getTag от функции, объявленной любым из предложенных вариантов, то получим "Function".


На самом деле, в языке гораздо больше способов задать функцию. Добавим в список как минимум рассмотренный ClassDeclaration, потом асинхронные функции и генераторы, прочее: AsyncFunctionDeclaration, AsyncFunctionExpression, AsyncArrowFunction, GeneratorDeclaration, GeneratorExpression, ClassExpression, MethodDefinition (список не полный). И кажется мол ну и что? все перечисленное ведет себя как функция — значит тоже getTag вернет "Function". Но есть одна особенность: все варианты безусловно являются функциями, но не все напрямую — Function.


Имеют место встроенные подтипы Function:


The Function constructor is designed to be subclassable.
There is no syntactic means to create instances of Function subclasses except for the built-in GeneratorFunction and AsyncFunction subclasses.

У нас есть Function и внутри "унаследованные" от него GeneratorFunction и AsyncFunction со своими конструкторами. Это подчеркивает, что асинки и генераторы имеют свою уникальную природу. И как итог:


async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
getTag(sleep);
< "AsyncFunction"

При этом мы не можем инстанцировать такую функцию через оператор new, а ее вызов возвращает нам Promise:


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

Пример с функцией-генератором:


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

Вызов такой функции возвращает нам экземпляр — объект Generator:


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

Символ toStringTag справедливо переопределен для асинков и генераторов. А вот typeof для любой функции покажет "function".


Встроенные объекты


У нас есть такие вещи, как например Set, Map, Date или Error. Применение getTag к ним вернет "Function", потому что это и есть функции — конструкторы итерируемых коллекций, даты и ошибки. От экземпляров мы получим соответственно — "Set", "Map", "Date" и "Error".


Но осторожно! еще есть такие объекты, как JSON или Math. Если поторопиться, то можно предположить аналогичную ситуацию. Но нет! это совсем другое — встроенные объекты-одиночки. Они не инстанцируемы (is not a constructor). Вызов typeof вернет "object" (кто бы сомневался). А вот getTag обратится к toStringTag и получит "JSON" и "Math". Это последнее наблюдение, которым мне хочется поделиться.


Давайте без фанатизма


Я чуть глубже обычного задался вопросом о типе переменной в JavaScript совсем недавно, когда решил написать свой простенький инспектор объекта для динамического анализа кода (побаловаться). Материал не просто так публикую в Ненормальном программировании, так как оно вам не нужно в продакшне: есть typeof, instanceof, Array.isArray, isNaN и все остальное, о чем стоит помнить, осуществляя необходимую проверку. В большинстве проектов и вовсе TypeScript, Dart или Flow. Мне просто нравится JavaScript!

Tags:
Hubs:
+16
Comments1

Articles

Change theme settings