Особенности использования типа данных Symbol в JavaScript
Символьные примитивы — это одно из новшеств стандарта ES6, которое принесло в JavaScript некоторые ценные возможности. Символы, представленные типом данных Symbol, особенно полезны при использовании их в качестве идентификаторов свойств объектов. В связи с таким сценарием их применения напрашивается вопрос о том, что такого они могут, чего не могут строки.

В материале, перевод которого мы сегодня публикуем, речь пойдёт о типе данных Symbol в JavaScript. Начнём мы с обзора некоторых возможностей JavaScript, в которых нужно ориентироваться для того, чтобы разобраться с символами.
Предварительные сведения
В JavaScript, по сути, существует два вида значений. Первый вид — примитивные значения, второй — объектные (в их число входят и функции). Примитивные значения включают в себя простые типы данных наподобие чисел (сюда входит всё — от целых чисел, до чисел с плавающей точкой, значений Infinity и NaN ), логических значений, строк, значений undefined и null . Обратите внимание на то, что, хотя при проверке вида typeof null === ‘object’ получается true , null — это примитивное значение.
Примитивные значения иммутабельны. Их нельзя изменять. Конечно, в переменную, хранящую примитивное значение, можно записать что-то новое. Например, здесь выполняется запись нового значения в переменную x :
let x = 1; x++;
Но при этом не происходит изменения (мутации) примитивного числового значения 1 .
В некоторых языках, например — в C, есть концепции передачи аргументов функций по ссылке и по значению. В JavaScript тоже есть нечто подобное. То, как именно организуется работа с данными, зависит от их типа. Если в функцию передают примитивное значение, представленное некоей переменной, а потом изменяют его в этой функции, значение, хранящееся в исходной переменной, при этом не меняется. Однако если в функцию передать объектное значение, представленное переменной, и модифицировать его, то изменится и то, что хранится в этой переменной.
Рассмотрим следующий пример:
function primitiveMutator(val) < val = val + 1; >let x = 1; primitiveMutator(x); console.log(x); // 1 function objectMutator(val) < val.prop = val.prop + 1; >let obj = < prop: 1 >; objectMutator(obj); console.log(obj.prop); // 2
Примитивные значения (за исключением таинственного NaN , которое не равно самому себе) всегда оказываются равными другим примитивным значениям, выглядящим так же, как они сами. Например:
const first = "abc" + "def"; const second = "ab" + "cd" + "ef"; console.log(first === second); // true
Однако конструирование объектных значений, внешне выглядящих одинаково, не приведёт к тому, что получатся сущности, при сравнении которых будет выявлено их равенство друг другу. Проверить это можно так:
const obj1 = < name: "Intrinsic" >; const obj2 = < name: "Intrinsic" >; console.log(obj1 === obj2); // false // При этом их свойства .name являются примитивными значениями: console.log(obj1.name === obj2.name); // true
Объекты играют фундаментальную роль в JavaScript. Они применяются буквально повсюду. Например, часто их используют в виде коллекций вида ключ/значение. Но до появления типа данных Symbol в качестве ключей объектов можно было применять лишь строки. В этом крылось серьёзное ограничение использования объектов в виде коллекций. При попытке назначения нестрокового значения в виде ключа объекта это значение приводилось к строке. Убедиться в этом можно так:
const obj = <>; obj.foo = 'foo'; obj['bar'] = 'bar'; obj[2] = 2; obj[<>] = 'someobj'; console.log(obj); //
Кстати, хотя это немного уводит нас от темы символов, хочется отметить, что структура данных Map была создана для того чтобы позволить использовать хранилища данных формата ключ/значение в ситуациях, когда ключ не является строкой.
Что такое символ?
Теперь, когда мы выяснили особенности примитивных значений в JavaScript, мы наконец готовы к тому, чтобы приступить к разговору о символах. Символ — это уникальное примитивное значение. Если подходить к символам с этой позиции, то можно заметить, что символы в этом плане похожи на объекты, так как создание нескольких экземпляров символов приведёт к созданию разных значений. Но символы, кроме того, являются иммутабельными примитивными значениями. Вот пример работы с символами:
const s1 = Symbol(); const s2 = Symbol(); console.log(s1 === s2); // false
При создании экземпляра символа можно воспользоваться необязательным первым строковым аргументом. Этот аргумент представляет собой описание символа, которое предназначено для использования при отладке. На сам символ это значение не влияет.
const s1 = Symbol('debug'); const str = 'debug'; const s2 = Symbol('xxyy'); console.log(s1 === str); // false console.log(s1 === s2); // false console.log(s1); // Symbol(debug)
Символы как ключи свойств объектов
Символы можно использовать в качестве ключей свойств объектов. Это очень важно. Вот пример использования их в таком качестве:
const obj = <>; const sym = Symbol(); obj[sym] = 'foo'; obj.bar = 'bar'; console.log(obj); // < bar: 'bar' >console.log(sym in obj); // true console.log(obj[sym]); // foo console.log(Object.keys(obj)); // ['bar']
Обратите внимание на то, что ключи, заданные символами, не возвращаются при вызове метода Object.keys() . Код, написанный до появления в JS символов, ничего о них не знает, в результате сведения о ключах объектов, представленных символами, не должны возвращаться древним методом Object.keys() .
На первый взгляд может показаться, что вышеописанные особенности символов позволяют использовать их для создания приватных свойств JS-объектов. Во многих других языках программирования можно создавать скрытые свойства объектов с использованием классов. Отсутствие этой возможности уже давно считается одним из недостатков JavaScript.
К сожалению, код, который работает с объектами, может беспрепятственно обращаться к их строковым ключам. Код может обращаться и к ключам, заданным символами, причём, даже в том случае, если у кода, из которого работают с объектом, нет доступа к соответствующему символу. Например, с помощью метода Reflect.ownKeys() можно получить список всех ключей объекта, и тех, что являются строками, и тех, что являются символами:
function tryToAddPrivate(o) < o[Symbol('Pseudo Private')] = 42; >const obj = < prop: 'hello' >; tryToAddPrivate(obj); console.log(Reflect.ownKeys(obj)); // [ 'prop', Symbol(Pseudo Private) ] console.log(obj[Reflect.ownKeys(obj)[1]]); // 42
Обратите внимание на то, что в настоящее время ведётся работа над тем, чтобы оснастить классы возможностью использования приватных свойств. Эта возможность называется Private Fields (приватные поля). Она, правда, не затрагивает абсолютно все объекты, относясь лишь к тем из них, которые созданы на основе предварительно подготовленных классов. Поддержка приватных полей уже имеется в браузере Chrome версии 72 и старше.
Предотвращение коллизий имён свойств объектов
Символы, конечно, не добавляют в JavaScript возможностей по созданию приватных свойств объектов, но они являются ценным новшеством языка по другим причинам. А именно, они полезны в ситуациях, когда неким библиотекам нужно добавлять свойства в объекты, описанные за их пределами, и при этом не опасаться коллизии имён свойств объектов.
Рассмотрим пример, в котором две различные библиотеки хотят добавить к объекту метаданные. Возможно, и той и другой библиотеке нужно оснастить объект некими идентификаторами. Если для имени подобного свойства просто использовать нечто вроде строки id , состоящей из двух букв, можно столкнуться с ситуацией, когда одна библиотека перезапишет свойство, заданное другой.
function lib1tag(obj) < obj.id = 42; >function lib2tag(obj)
Если же воспользоваться в нашем примере символами, то каждая библиотека может сгенерировать, при инициализации, нужные ей символы. Затем эти символы могут быть использованы для назначения свойств объектам и для доступа к этим свойствам.
const library1property = Symbol('lib1'); function lib1tag(obj) < obj[library1property] = 42; >const library2property = Symbol('lib2'); function lib2tag(obj)
Именно глядя на подобный сценарий можно ощутить пользу от появления символов в JavaScript.
Однако тут может возникнуть вопрос, касающийся использования библиотеками, для имён свойств объектов, случайных строк или строк, со сложной структурой, включающих в себя, например, название библиотеки. Подобные строки могут образовывать нечто вроде пространств имён для идентификаторов, используемых библиотеками. Например, это может выглядеть так:
const library1property = uuid(); // вызов функции для получения случайного значения function lib1tag(obj) < obj[library1property] = 42; >const library2property = 'LIB2-NAMESPACE-id'; // использование пространства имён библиотеки function lib2tag(obj)
В общем-то, можно поступить и так. Подобные подходы, на самом деле, очень похожи на то, что происходит при использовании символов. И если, используя случайные идентификаторы или пространства имён, пара библиотек не сгенерирует, по воле случая, одинаковые имена свойств, то проблем с именами не будет.
Проницательный читатель сказал бы сейчас, что два рассматриваемых подхода к именованию свойств объектов не являются полностью эквивалентными. У имён свойств, которые формируются случайным образом или с использованием пространств имён, есть недостаток: соответствующие ключи очень легко обнаружить, особенно если в коде выполняется перебор ключей объектов или их сериализация. Рассмотрим следующий пример:
const library2property = 'LIB2-NAMESPACE-id'; // используется пространство имён function lib2tag(obj) < obj[library2property] = 369; >const user = < name: 'Thomas Hunter II', age: 32 >; lib2tag(user); JSON.stringify(user); // ''
Если бы в этой ситуации для имени ключа использовался бы символ, тогда JSON-представление объекта не содержало бы значения символа. Почему это так? Дело в том, что то, что в JavaScript появился новый тип данных, ещё не означает того, что изменения внесены и в спецификацию JSON. JSON поддерживает, в качестве ключей свойств объектов, только строки. При сериализации объекта не делается попыток представить символы в каком-то особом виде.
Рассматриваемую проблему попадания имён свойств в JSON-представление объектов можно решить благодаря использованию Object.defineProperty() :
const library2property = uuid(); // случайное значение function lib2tag(obj) < Object.defineProperty(obj, library2property, < enumerable: false, value: 369 >); > const user = < name: 'Thomas Hunter II', age: 32 >; lib2tag(user); // '' console.log(JSON.stringify(user)); console.log(user[library2property]); // 369
Строковые ключи, «скрытые» благодаря установке их дескриптора enumerable в значение false , ведут себя практически так же, как и ключи, представленные символами. И те и другие не выводятся при вызове Object.keys() , и те и другие можно обнаружить, воспользовавшись Reflect.ownKeys() . Вот как это выглядит:
const obj = <>; obj[Symbol()] = 1; Object.defineProperty(obj, 'foo', < enumberable: false, value: 2 >); console.log(Object.keys(obj)); // [] console.log(Reflect.ownKeys(obj)); // [ 'foo', Symbol() ] console.log(JSON.stringify(obj)); // <>
Тут, надо сказать, мы почти воссоздали возможности символов, пользуясь другими средствами JS. В частности, и ключи, представленные символами, и скрытые ключи не попадают в JSON-представление объекта. И те и другие можно узнать, обратившись к методу Reflect.ownKeys() . В результате и те и другие нельзя назвать по-настоящему приватными. Если предположить, что для формирования имён ключей используются некие случайные значения или пространства имён библиотек, то это означает, что от риска возникновения коллизии имён мы избавились.
Однако между использованием имён-символов и имён, созданных с использованием других механизмов, есть одно маленькое различие. Так как строки иммутабельны, а символы гарантированно уникальны, всегда есть возможность того, что кто-то, перебрав все возможные сочетания символов в строке, вызовет коллизию имён. С математической точки зрения это означает, что символы действительно дают нам ценную возможность, которая отсутствует у строк.
В Node.js, при исследовании объектов (например, с использованием console.log() ), если обнаруживается метод объекта, имеющий имя inspect , то, для получения строкового представления объекта и последующего его вывода на экран, используется именно этот метод. Несложно понять, что абсолютно все не могут этого учитывать, поэтому подобное поведение системы может привести к вызову метода объекта inspect , который предназначен для решения задач, не относящихся к формированию строкового представления объекта. Эта возможность признана устаревшей в Node.js 10, в 11 версии методы с подобным именем просто игнорируются. Теперь для реализации этой возможности предусмотрен символ require(‘util’).inspect.custom . А это значит, что никто уже никогда не сможет непреднамеренно нарушить работу системы, создав метод объекта с именем inspect .
Имитация приватных свойств
Вот интересный подход, который можно использовать для имитации приватных свойств объектов. Этот подход предусматривает применение ещё одной современной возможности JavaScript — прокси-объектов. Такие объекты служат обёртками для других объектов, которые позволяют программисту вмешиваться в действия, выполняемые с этими объектами.
Прокси-объекты предлагают много способов для перехвата действий, выполняемых над объектами. Нас интересует возможность управления операциями чтения ключей объекта. В подробности о прокси-объектах мы тут углубляться не будем. Если вам они интересны — взгляните на эту публикацию.
Мы можем использовать прокси для того чтобы управлять тем, какие свойства объекта видны извне. В данном случае мы хотим создать прокси, который скрывает два известных нам свойства. Одно имеет строковое имя _favColor , а второе представлено символом, записанным в переменную favBook :
let proxy; < const favBook = Symbol('fav book'); const obj = < name: 'Thomas Hunter II', age: 32, _favColor: 'blue', [favBook]: 'Metro 2033', [Symbol('visible')]: 'foo' >; const handler = < ownKeys: (target) => < const reportedKeys = []; const actualKeys = Reflect.ownKeys(target); for (const key of actualKeys) < if (key === favBook || key === '_favColor') < continue; >reportedKeys.push(key); > return reportedKeys; > >; proxy = new Proxy(obj, handler); > console.log(Object.keys(proxy)); // [ 'name', 'age' ] console.log(Reflect.ownKeys(proxy)); // [ 'name', 'age', Symbol(visible) ] console.log(Object.getOwnPropertyNames(proxy)); // [ 'name', 'age' ] console.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(visible)] console.log(proxy._favColor); // 'blue
Справиться со свойством, имя которого представлено строкой _favColor , несложно: достаточно почитать исходный код. Динамические ключи (наподобие uuid-ключей, которые мы видели выше), можно подобрать брутфорсом. Но без ссылки на символ получить доступ к значению Metro 2033 из объекта proxy нельзя.
Тут надо отметить, что в Node.js есть одна особенность, нарушающая приватность прокси-объектов. Эта особенность не существует в самом языке, поэтому она не актуальна для других сред выполнения JS, таких, как браузер. Дело в том, что эта особенность позволяет получать доступ к объекту, скрытому за прокси-объектом, при наличии доступа к прокси-объекту. Вот пример, демонстрирующий возможность обхода механизмов, показанных в предыдущем фрагменте кода:
const [originalObject] = process .binding('util') .getProxyDetails(proxy); const allKeys = Reflect.ownKeys(originalObject); console.log(allKeys[3]); // Symbol(fav book)
Теперь, чтобы предотвратить использование этой особенности в конкретном экземпляре Node.js, нужно либо модифицировать глобальный объект Reflect , либо привязку процесса util . Однако это — та ещё задача. Если вам это интересно — взгляните на эту публикацию, посвящённую защите API, основанных на JavaScript.
Итоги
В этом материале мы поговорили о типе данных Symbol , о том, какие возможности он даёт JavaScript-разработчикам, и о том, какие существующие механизмы языка можно использовать для имитации этих возможностей.
Уважаемые читатели! Пользуетесь ли вы символами в своих JavaScript-проектах?
Типы данных
Значение в JavaScript всегда относится к данным определённого типа. Например, это может быть строка или число.
Есть восемь основных типов данных в JavaScript. В этой главе мы рассмотрим их в общем, а в следующих главах поговорим подробнее о каждом.
Переменная в JavaScript может содержать любые данные. В один момент там может быть строка, а в другой – число:
// Не будет ошибкой let message = "hello"; message = 123456;
Языки программирования, в которых такое возможно, называются «динамически типизированными». Это значит, что типы данных есть, но переменные не привязаны ни к одному из них.
Число
let n = 123; n = 12.345;
Числовой тип данных ( number ) представляет как целочисленные значения, так и числа с плавающей точкой.
Существует множество операций для чисел, например, умножение * , деление / , сложение + , вычитание — и так далее.
Кроме обычных чисел, существуют так называемые «специальные числовые значения», которые относятся к этому типу данных: Infinity , -Infinity и NaN .
-
Infinity представляет собой математическую бесконечность ∞. Это особое значение, которое больше любого числа. Мы можем получить его в результате деления на ноль:
alert( 1 / 0 ); // Infinity
Или задать его явно:
alert( Infinity ); // Infinity
alert( "не число" / 2 ); // NaN, такое деление является ошибкой
Значение NaN «прилипчиво». Любая математическая операция с NaN возвращает NaN :
alert( NaN + 1 ); // NaN alert( 3 * NaN ); // NaN alert( "не число" / 2 - 1 ); // NaN
Математические операции – безопасны
Математические операции в JavaScript «безопасны». Мы можем делать что угодно: делить на ноль, обращаться с нечисловыми строками как с числами и т.д.
Скрипт никогда не остановится с фатальной ошибкой (не «умрёт»). В худшем случае мы получим NaN как результат выполнения.
Специальные числовые значения относятся к типу «число». Конечно, это не числа в привычном значении этого слова.
Подробнее о работе с числами мы поговорим в главе Числа.
BigInt
В JavaScript тип number не может безопасно работать с числами, большими, чем (2 53 -1) (т. е. 9007199254740991 ) или меньшими, чем -(2 53 -1) для отрицательных чисел.
Если говорить совсем точно, то, технически, тип number может хранить большие целые числа (до 1.7976931348623157 * 10 308 ), но за пределами безопасного диапазона целых чисел ±(2 53 -1) будет ошибка точности, так как не все цифры помещаются в фиксированную 64-битную память. Поэтому можно хранить «приблизительное» значение.
Например, эти два числа (прямо за пределами безопасного диапазона) совпадают:
console.log(9007199254740991 + 1); // 9007199254740992 console.log(9007199254740991 + 2); // 9007199254740992
То есть все нечетные целые числа, большие чем (2 53 -1) , вообще не могут храниться в типе number .
В большинстве случаев безопасного диапазона чисел от -(2 53 -1) до (2 53 -1) вполне достаточно, но иногда нам требуется весь диапазон действительно гигантских целых чисел без каких-либо ограничений или пропущенных значений внутри него. Например, в криптографии или при использовании метки времени («timestamp») с микросекундами.
Тип BigInt был добавлен в JavaScript, чтобы дать возможность работать с целыми числами произвольной длины.
Чтобы создать значение типа BigInt , необходимо добавить n в конец числового литерала:
// символ "n" в конце означает, что это BigInt const bigInt = 1234567890123456789012345678901234567890n;
Так как необходимость в использовании BigInt –чисел появляется достаточно редко, мы рассмотрим их в отдельной главе BigInt. Ознакомьтесь с ней, когда вам понадобятся настолько большие числа.
В данный момент BigInt поддерживается только в браузерах Firefox, Chrome, Edge и Safari, но не поддерживается в IE.
Строка
Строка ( string ) в JavaScript должна быть заключена в кавычки.
let str = "Привет"; let str2 = 'Одинарные кавычки тоже подойдут'; let phrase = `Обратные кавычки позволяют встраивать переменные $`;
В JavaScript существует три типа кавычек.
- Двойные кавычки: «Привет» .
- Одинарные кавычки: ‘Привет’ .
- Обратные кавычки: `Привет` .
Двойные или одинарные кавычки являются «простыми», между ними нет разницы в JavaScript.
Обратные же кавычки имеют расширенную функциональность. Они позволяют нам встраивать выражения в строку, заключая их в $ . Например:
let name = "Иван"; // Вставим переменную alert( `Привет, $!` ); // Привет, Иван! // Вставим выражение alert( `результат: $` ); // результат: 3
Выражение внутри $ вычисляется, и его результат становится частью строки. Мы можем положить туда всё, что угодно: переменную name , или выражение 1 + 2 , или что-то более сложное.
Обратите внимание, что это можно делать только в обратных кавычках. Другие кавычки не имеют такой функциональности встраивания!
alert( "результат: $" ); // результат: $ (двойные кавычки ничего не делают)
Мы рассмотрим строки более подробно в главе Строки.
Нет отдельного типа данных для одного символа.
В некоторых языках, например C и Java, для хранения одного символа, например «a» или «%» , существует отдельный тип. В языках C и Java это char .
В JavaScript подобного типа нет, есть только тип string . Строка может содержать ноль символов (быть пустой), один символ или множество.
Булевый (логический) тип
Булевый тип ( boolean ) может принимать только два значения: true (истина) и false (ложь).
Такой тип, как правило, используется для хранения значений да/нет: true значит «да, правильно», а false значит «нет, не правильно».
let nameFieldChecked = true; // да, поле отмечено let ageFieldChecked = false; // нет, поле не отмечено
Булевые значения также могут быть результатом сравнений:
let isGreater = 4 > 1; alert( isGreater ); // true (результатом сравнения будет "да")
Мы рассмотрим булевые значения более подробно в главе Логические операторы.
Значение «null»
Специальное значение null не относится ни к одному из типов, описанных выше.
Оно формирует отдельный тип, который содержит только значение null :
let age = null;
В JavaScript null не является «ссылкой на несуществующий объект» или «нулевым указателем», как в некоторых других языках.
Это просто специальное значение, которое представляет собой «ничего», «пусто» или «значение неизвестно».
В приведённом выше коде указано, что значение переменной age неизвестно.
Значение «undefined»
Специальное значение undefined также стоит особняком. Оно формирует тип из самого себя так же, как и null .
Оно означает, что «значение не было присвоено».
Если переменная объявлена, но ей не присвоено никакого значения, то её значением будет undefined :
let age; alert(age); // выведет "undefined"
Технически мы можем присвоить значение undefined любой переменной:
let age = 123; // изменяем значение на undefined age = undefined; alert(age); // "undefined"
…Но так делать не рекомендуется. Обычно null используется для присвоения переменной «пустого» или «неизвестного» значения, а undefined – для проверок, была ли переменная назначена.
Объекты и символы
Тип object (объект) – особенный.
Все остальные типы называются «примитивными», потому что их значениями могут быть только простые значения (будь то строка, или число, или что-то ещё). В объектах же хранят коллекции данных или более сложные структуры.
Объекты занимают важное место в языке и требуют особого внимания. Мы разберёмся с ними в главе Объекты после того, как узнаем больше о примитивах.
Тип symbol (символ) используется для создания уникальных идентификаторов в объектах. Мы упоминаем здесь о нём для полноты картины, изучим этот тип после объектов.
Оператор typeof
Оператор typeof возвращает тип аргумента. Это полезно, когда мы хотим обрабатывать значения различных типов по-разному или просто хотим сделать проверку.
У него есть две синтаксические формы:
// Обычный синтаксис typeof 5 // Выведет "number" // Синтаксис, напоминающий вызов функции (встречается реже) typeof(5) // Также выведет "number"
Если передается выражение, то нужно заключать его в скобки, т.к. typeof имеет более высокий приоритет, чем бинарные операторы:
typeof 50 + " Квартир"; // Выведет "number Квартир" typeof (50 + " Квартир"); // Выведет "string"
Другими словами, скобки необходимы для определения типа значения, которое получилось в результате выполнения выражения в них.
Вызов typeof x возвращает строку с именем типа:
typeof undefined // "undefined" typeof 0 // "number" typeof 10n // "bigint" typeof true // "boolean" typeof "foo" // "string" typeof Symbol("id") // "symbol" typeof Math // "object" (1) typeof null // "object" (2) typeof alert // "function" (3)
Последние три строки нуждаются в пояснении:
- Math — это встроенный объект, который предоставляет математические операции и константы. Мы рассмотрим его подробнее в главе Числа. Здесь он служит лишь примером объекта.
- Результатом вызова typeof null является «object» . Это официально признанная ошибка в typeof , ведущая начало с времён создания JavaScript и сохранённая для совместимости. Конечно, null не является объектом. Это специальное значение с отдельным типом.
- Вызов typeof alert возвращает «function» , потому что alert является функцией. Мы изучим функции в следующих главах, где заодно увидим, что в JavaScript нет специального типа «функция». Функции относятся к объектному типу. Но typeof обрабатывает их особым образом, возвращая «function» . Так тоже повелось от создания JavaScript. Формально это неверно, но может быть удобным на практике.
Итого
В JavaScript есть 8 основных типов данных.
- Семь из них называют «примитивными» типами данных:
- number для любых чисел: целочисленных или чисел с плавающей точкой; целочисленные значения ограничены диапазоном ±(2 53 -1) .
- bigint для целых чисел произвольной длины.
- string для строк. Строка может содержать ноль или больше символов, нет отдельного символьного типа.
- boolean для true / false .
- null для неизвестных значений – отдельный тип, имеющий одно значение null .
- undefined для неприсвоенных значений – отдельный тип, имеющий одно значение undefined .
- symbol для уникальных идентификаторов.
- object для более сложных структур данных.
Оператор typeof позволяет нам увидеть, какой тип данных сохранён в переменной.
- Имеет две формы: typeof x или typeof(x) .
- Возвращает строку с именем типа. Например, «string» .
- Для null возвращается «object» – это ошибка в языке, на самом деле это не объект.
В следующих главах мы сконцентрируемся на примитивных значениях, а когда познакомимся с ними, перейдём к объектам.
Тип данных Symbol
По спецификации, в качестве ключей для свойств объекта могут использоваться только строки или символы. Ни числа, ни логические значения не подходят, разрешены только эти два типа данных.
До сих пор мы видели только строки. Теперь давайте разберём символы, увидим, что хорошего они нам дают.
Символы
«Символ» представляет собой уникальный идентификатор.
Создаются новые символы с помощью функции Symbol() :
// Создаём новый символ - id let
При создании, символу можно дать описание (также называемое имя), в основном использующееся для отладки кода:
// Создаём символ id с описанием (именем) "id" let
Символы гарантированно уникальны. Даже если мы создадим множество символов с одинаковым описанием, это всё равно будут разные символы. Описание – это просто метка, которая ни на что не влияет.
Например, вот два символа с одинаковым описанием – но они не равны:
let id1 = Symbol("id"); let id2 = Symbol("id"); alert(id1 == id2); // falseЕсли вы знаете Ruby или какой-то другой язык программирования, в котором есть своего рода «символы» – пожалуйста, будьте внимательны. Символы в JavaScript имеют свои особенности, и не стоит думать о них, как о символах в Ruby или в других языках.
Символы не преобразуются автоматически в строки
Большинство типов данных в JavaScript могут быть неявно преобразованы в строку. Например, функция alert принимает практически любое значение, автоматически преобразовывает его в строку, а затем выводит это значение, не сообщая об ошибке. Символы же особенные и не преобразуются автоматически.
К примеру, alert ниже выдаст ошибку:
let // TypeError: Cannot convert a Symbol value to a stringЭто – языковая «защита» от путаницы, ведь строки и символы – принципиально разные типы данных и не должны неконтролируемо преобразовываться друг в друга.
Если же мы действительно хотим вывести символ с помощью alert , то необходимо явно преобразовать его с помощью метода .toString() , вот так:
let // Symbol(id), теперь работаетИли мы можем обратиться к свойству symbol.description , чтобы вывести только описание:
let // id«Скрытые» свойства
Символы позволяют создавать «скрытые» свойства объектов, к которым нельзя нечаянно обратиться и перезаписать их из других частей программы.
Например, мы работаем с объектами user , которые принадлежат стороннему коду. Мы хотим добавить к ним идентификаторы.
Используем для этого символьный ключ:
let user = < name: "Вася" >; let = 1; alert( user[id] ); // мы можем получить доступ к данным по ключу-символуПочему же лучше использовать Symbol(«id») , а не строку «id» ?
Так как объект user принадлежит стороннему коду, и этот код также работает с ним, то нам не следует добавлять к нему какие-либо поля. Это небезопасно. Но к символу сложно нечаянно обратиться, сторонний код вряд ли его вообще увидит, и, скорее всего, добавление поля к объекту не вызовет никаких проблем.
Кроме того, предположим, что другой скрипт для каких-то своих целей хочет записать собственный идентификатор в объект user . Этот скрипт может быть какой-то JavaScript-библиотекой, абсолютно не связанной с нашим скриптом.
Сторонний код может создать для этого свой символ Symbol(«id») :
// . let = "Их идентификатор";Конфликта между их и нашим идентификатором не будет, так как символы всегда уникальны, даже если их имена совпадают.
А вот если бы мы использовали строку «id» вместо символа, то тогда был бы конфликт:
let user = < name: "Вася" >; // Объявляем в нашем скрипте свойство "id" user.id = "Наш идентификатор"; // . другой скрипт тоже хочет свой идентификатор. user.id = "Их идентификатор" // Ой! Свойство перезаписано сторонней библиотекой!Символы в литеральном объекте
Если мы хотим использовать символ при литеральном объявлении объекта <. >, его необходимо заключить в квадратные скобки.
let user = < name: "Вася", [id]: 123 // просто "id: 123" не сработает >;Это вызвано тем, что нам нужно использовать значение переменной id в качестве ключа, а не строку «id».
Символы игнорируются циклом for…in
Свойства, чьи ключи – символы, не перебираются циклом for..in .
let user = < name: "Вася", age: 30, [id]: 123 >; for (let key in user) alert(key); // name, age (свойства с ключом-символом нет среди перечисленных) // хотя прямой доступ по символу работает alert( "Напрямую: " + user[id] );Это – часть общего принципа «сокрытия символьных свойств». Если другая библиотека или скрипт будут работать с нашим объектом, то при переборе они не получат ненароком наше символьное свойство. Object.keys(user) также игнорирует символы.
А вот Object.assign, в отличие от цикла for..in , копирует и строковые, и символьные свойства:
let user = < [id]: 123 >; let clone = Object.assign(<>, user); alert( clone[id] ); // 123Здесь нет никакого парадокса или противоречия. Так и задумано. Идея заключается в том, что, когда мы клонируем или объединяем объекты, мы обычно хотим скопировать все свойства (включая такие свойства с ключами-символами, как, например, id в примере выше).
Глобальные символы
Итак, как мы видели, обычно все символы уникальны, даже если их имена совпадают. Но иногда мы наоборот хотим, чтобы символы с одинаковыми именами были одной сущностью. Например, разные части нашего приложения хотят получить доступ к символу «id» , подразумевая именно одно и то же свойство.
Для этого существует глобальный реестр символов. Мы можем создавать в нём символы и обращаться к ним позже, и при каждом обращении нам гарантированно будет возвращаться один и тот же символ.
Для чтения (или, при отсутствии, создания) символа из реестра используется вызов Symbol.for(key) .
Он проверяет глобальный реестр и, при наличии в нём символа с именем key , возвращает его, иначе же создаётся новый символ Symbol(key) и записывается в реестр под ключом key .
// читаем символ из глобального реестра и записываем его в переменную let // если символа не существует, он будет создан // читаем его снова и записываем в другую переменную (возможно, из другого места кода) let idAgain = Symbol.for("id"); // проверяем -- это один и тот же символ alert( idAgain ); // trueСимволы, содержащиеся в реестре, называются глобальными символами. Если вам нужен символ, доступный везде в коде – используйте глобальные символы.
Похоже на Ruby
В некоторых языках программирования, например, Ruby, на одно имя (описание) приходится один символ, и не могут существовать разные символы с одинаковым именем.
В JavaScript, как мы видим, это утверждение верно только для глобальных символов.
Symbol.keyFor
Для глобальных символов, кроме Symbol.for(key) , который ищет символ по имени, существует обратный метод: Symbol.keyFor(sym) , который, наоборот, принимает глобальный символ и возвращает его имя.
// получаем символ по имени let sym = Symbol.for("name"); let sym2 = Symbol.for("id"); // получаем имя по символу alert( Symbol.keyFor(sym) ); // name alert( Symbol.keyFor(sym2) ); // idВнутри метода Symbol.keyFor используется глобальный реестр символов для нахождения имени символа. Так что этот метод не будет работать для неглобальных символов. Если символ неглобальный, метод не сможет его найти и вернёт undefined .
Впрочем, для любых символов доступно свойство description .
let globalSymbol = Symbol.for("name"); let localSymbol = Symbol("name"); alert( Symbol.keyFor(globalSymbol) ); // name, глобальный символ alert( Symbol.keyFor(localSymbol) ); // undefined для неглобального символа alert( localSymbol.description ); // nameСистемные символы
Существует множество «системных» символов, использующихся внутри самого JavaScript, и мы можем использовать их, чтобы настраивать различные аспекты поведения объектов.
Эти символы перечислены в спецификации в таблице Well-known symbols:
- Symbol.hasInstance
- Symbol.isConcatSpreadable
- Symbol.iterator
- Symbol.toPrimitive
- …и так далее.
В частности, Symbol.toPrimitive позволяет описать правила для объекта, согласно которым он будет преобразовываться к примитиву. Мы скоро увидим его применение.
С другими системными символами мы тоже скоро познакомимся, когда будем изучать соответствующие возможности языка.
Итого
Символ (symbol) – примитивный тип данных, использующийся для создания уникальных идентификаторов.
Символы создаются вызовом функции Symbol() , в которую можно передать описание (имя) символа.
Даже если символы имеют одно и то же имя, это – разные символы. Если мы хотим, чтобы одноимённые символы были равны, то следует использовать глобальный реестр: вызов Symbol.for(key) возвращает (или создаёт) глобальный символ с key в качестве имени. Многократные вызовы команды Symbol.for с одним и тем же аргументом возвращают один и тот же символ.
Символы имеют два основных варианта использования:
- «Скрытые» свойства объектов. Если мы хотим добавить свойство в объект, который «принадлежит» другому скрипту или библиотеке, мы можем создать символ и использовать его в качестве ключа. Символьное свойство не появится в for..in , так что оно не будет нечаянно обработано вместе с другими. Также оно не будет модифицировано прямым обращением, так как другой скрипт не знает о нашем символе. Таким образом, свойство будет защищено от случайной перезаписи или использования. Так что, используя символьные свойства, мы можем спрятать что-то нужное нам, но что другие видеть не должны.
- Существует множество системных символов, используемых внутри JavaScript, доступных как Symbol.* . Мы можем использовать их, чтобы изменять встроенное поведение ряда объектов. Например, в дальнейших главах мы будем использовать Symbol.iterator для итераторов, Symbol.toPrimitive для настройки преобразования объектов в примитивы и так далее.
Технически символы скрыты не на 100%. Существует встроенный метод Object.getOwnPropertySymbols(obj) – с его помощью можно получить все свойства объекта с ключами-символами. Также существует метод Reflect.ownKeys(obj), который возвращает все ключи объекта, включая символьные. Так что они не совсем спрятаны. Но большинство библиотек, встроенных методов и синтаксических конструкций не используют эти методы.
Типы данных в JavaScript. Инструкция для начинающих
Как и любая компьютерная программа, JavaScript нуждается в наведении порядка в данных, в их структурировании. На языке JavaScript мы говорим, что если данные имеют одинаковую структуру, то они имеют одинаковый тип. Внутренняя организация данных может быть простой, как ДА или НЕТ, а может быть весьма замысловатой, как дерево HTML-элементов или маршруты на карте навигатора.
Примитивные типы. JavaScript предлагает разработчику несколько простых, примитивных, типов. Среди них: boolean , number , string . Примитивными эти типы называют за то, что значения этих типов нельзя поменять. Их можно клонировать, встроить в другие значения. Через минуту увидим как это происходит
Работа с наборами. Дополнительно JavaScript предлагает несколько типов объектов для работы с наборами — массивы, словари, множества. Это очень кстати, попробуй вспомни, как эффективно реализовать сортировку.
Готовые структуры для хранения информации на все случаи жизни не напасешься, поэтому JavaScript предоставляет разработчику полную свободу в этих вопросах, и разработчик может создавать самостоятельно бесконечное разнообразие типов для собственных нужд.
Давайте обсудим детали разных типов данных, как примитивных, так и встроенных. Поговорим и о кастомных типах.
Примитивные типы
JavaScript различает семь типов:
undefined — обозначает тип значения переменной, которую объявили, но не инициализировали. Этот тип для данных, которых нет.
boolean — принимает только два значения «истина» и «ложь».
number — попытка научиться записывать любое число в ограниченную память компьютера (провалилась).
string — последовательность символов. JavaScript умеет преобразовывать значение любого примитивного типа в строку. Значение типа string в JavaScript неизменяемое, нельзя изменить одну букву в слове.
symbol — специальный тип данных. Задача значений типа symbol — служить специальными именами для обозначения специальных свойств объектов. Так много слов «специально», что мы дадим разъяснения ниже.
bigint — низкий поклон в сторону крипро- валют, токенов, контрактов. Они оперируют большими числами, и там этот тип данных очень пригождается. Скорее всего, не пригодится в вашем обычном фронтенде.
object — кроме примитивных типов в JavaScript есть структурные типы (объекты), а среди объектов особо выделяют функции. Обратите внимание, что в JavaScript функции это тоже объекты.
А как же null ? null — ещё одна возможность обозначить данные, которых нет. Да, в JavaScript есть целых два способа для обозначения ситуации, когда в переменной нет значения. Обратите внимание, что в JavaScript null это значение, а не тип данных и это значение может содержаться в JSON, а undefined в JSON не бывает.
Тип undefined
Когда вы объявляете переменную, но не присваиваете ей значение, переменная получает значение «по умолчанию» и это значение undefined , которое имеет тип undefined.
let buffer;Переменная buffer объявлена с ключевым словом let , но до тех пока она не получила явного значения ее значение undefined .
Хитрость JavaScript в том, что иногда разработчик присваивает переменной результат работы функции, но переменная все равно остается undefined . В следующем примере buffer получает значение при объявлении. Можете ли вы догадаться почему buffer всё равно undefined ?
function trickyComputation () < let a = 0; while(a<3)< a+=2; >> let buffer = trickyComputation();Все дело в том, что trickyComputation не возвращает значения. Попробуйте рассуждать о том, что цель функции — «присвоить значение» ключевому слову return . В вышеприведенном примере нет return , и этот самый воображаемый return остается неинициализированным, отсюда и undefined .
Будьте внимательны к функциям.
Тип boolean
В JavaScript про любое значение можно задать вопрос «похоже ли оно на правду?». Значение true — правда, false — ложь. Пустое значение обычно значит false.
Посмотрим на практике. Для проверки правдивости будем пользоваться вот таким методом showTrueness .
const showTrueness = (n,value)=> console.log(`$ - $`);// эти значения похожи на ложь showTrueness(1,false); // false showTrueness(2,''); // false showTrueness(3,0) // falseКогда значение отличается от банально-начального, оно обычно истинно.
showTrueness(1, true); // true showTrueness(2, 42); // true showTrueness(3, 'false') // trueКак видите, слово false получает приговор — истина! Но это еще не самый курьёзный случай с boolean :
showTrueness(1, []); // true showTrueness(2, <>); // true showTrueness(3, [] === []); // false showTrueness(4, <> === <>); // falseХотя обычно пустые значения это ложь, пустой массив (1) и пустой объект (2) оказались истиной. А ещё в следующем отрывке JavaScript рассматривает число 0 эквивалентной строке ‘0’.
Видите 0==’0′ показывает true . При этом по отдельности число 0 и строка ‘0’ относятся к разным типам.
showTrueness(1, 0 == '0'); // true showTrueness(2, 0); // false showTrueness(3, '0'); // trueЭто происходит из-за способности JavaScript преобразовывать значения из одного типа в другой. Если невнимательно следить за такими преобразованиями, могут возникнуть ошибки, которые очень тяжело найти.
Для избежания сюрпризов используйте явное преобразования значений между типами и строгое сравнение (===) (! ==). В этом вам поможет набор правил eslinter от академии.
Тип number
Тип значения number предназначен для моделирования действительных чисел. Действительных чисел очень много — бесконечно много — а компьютерная память ограничена. Инженерам из IEEE пришлось даже выпустить отдельный международный стандарт для чисел с плавающей точкой — IEEE 754.
Во многих случаях числа с плавающей точкой ведут себя нормально. Сумма чисел 1 и 2 равна 3, а сумма чисел 0.1 и 0.2 равна сумме чисел 0.2 и 0.1. Но это не всегда так, и 0.1 + 0.2 может быть не равно 0.3. Если в школе вам говорили, что от перемены мест сумма не меняется, то в JavaScript это не всегда верно.
И если будете делать выписку в крипто-банке, два раза подумайте, в каком порядке вы хотите складывать числа. Потому что значение переменной currentBalanceV1 больше значения переменной currentBalanceV2 , а всё потому что величины складываемых чисел сильно отличаются.
showTrueness(1, 1+2 === 3); // true const lastMonthCredits = Array.from (,()=>Number.EPSILON/10); const previousMonthBalance = 0.8; const currentBalanceV1 = previousMonthBalance + lastMonthCredits.reduce((a,b)=>a+b,0); const currentBalanceV2 = 0 + lastMonthCredits.reduce((a,b)=>a+b,previousMonthBalance); showTrueness(2, currentBalanceV1>currentBalanceV2); // true showTrueness(3, 0.1+0.2 === 0.2+0.1) // trueСоветы при работе с числами
- При работе с числами старайтесь сначала делать действия над числами сравнимой величины
- При работе с числами старайтесь сравнивать их порядок, (что больше, а что меньше), а не равенство.
- Изучите назначение предопределенных констант Number.MAX_SAFE_INTEGER , Number.EPSILON и других.
Задание для самопроверки. На сколько отличаются currentBalanceV1 , currentBalanceV2 и previousMonthBalance ?
Тип string
Строки в JavaScript — это неизменяемые цепочки букв. Вы можете добавлять строки одну к другой, брать нужную букву по порядку. Нумерация букв в строке начинается с нуля, поэтому вы видите в примере (-1).
const show = (value)=>console.log(value); const alfavit = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'; show(alfavit); // "абвгдеёжзийклмнопрстуфхцчшщъыьэюя" show(alfavit[1-1]); // "а" show(alfavit[33-1]); //"я"Поскольку строка неизменяемая, вы не можете просто взять и поменять букву, забить звездочками часть номера банковской карты не получится.
alfavit[2-1]='*'; show(alfavit); // "абвгдеёжзийклмнопрстуфхцчшщъыьэюя"В виде строк можно хранить любой вид данных, нужно только договориться. Однако мы не рекомендуем вам изобретать велосипед. Лучше используйте общепринятые способы преобразования данных в строку и обратно: JSON.stringify , JSON.parse (для превращения в JSON и обратно), Intl.NumberFormat , Intl.DateFormat .
Оператор typeof
Вы можете использовать оператор typeof для определения типа значения. Этот оператор воздействует на переменную и возвращает имя типа (если знает)
const show = (value, type)=>console.log(`typeof ($) is $`); const whatIsMyTypeName = (value)=> < switch(typeof value)< case 'boolean': case 'number': case 'string': return show(value, typeof value); default: show(value, 'выясним позднее') >> whatIsMyTypeName(true); // typeof (true) is boolean" whatIsMyTypeName(42); // "typeof (42) is number" whatIsMyTypeName('миру-мир!'); // "typeof (миру-мир!) is string" whatIsMyTypeName(<>); // "typeof ([object Object]) is выясним позднее" whatIsMyTypeName(window); // "typeof ([object Window]) is выясним позднее"Встроенные типы
В JavaScript вам доступно много типов данных — ими могут быть элементы браузера, документа, видео и изображения и многое другое. Полный набор зависит от окружения. Но почти наверняка вам будут доступны массивы, словари и множества.
Array (массивы)
Сразу обратите внимание, что слово Array мы употребляем с большой буквы, в то время как boolean , number , string — с маленькой.
Если вы разработчик на JavaScript, то методы работы с массивами нужно знать и уметь вспомнить, даже если вас разбудили посреди ночи. Проверьте, что вы знаете о существовании методов массива length, from, map, sort, reduce, filter, find, indexOf, findIndex
Вы можете получить значение из массива по индексу, а можете заменить это значение другим. Индексом значения в массиве выступает число. Индексы идут от 0 в сторону увеличения.
JavaScript не будет следить за тем, обращаетесь ли вы по индексу правомерно или выходите за пределы массива. Вы даже можете положить новое значение по индексу «минус один» и получить его обратно. Однако вас ждут сюрпризы, если вы не будете контролировать размеры массива и значения индекса.
const show = (index, value)=> console.log(`$: $`); const items = [ 'Chrome', 'Opera', 'Edge' ]; // Предоставляет элемент show(1,items[1]) // "1: Opera" // Ошибки нет, но и элемента тоже show(100, items[100]); "100: undefined" // Как, впрочем, и здесь show(-1, items[-1]);// "-1: undefined" items[12] = 'safari'; items[-1] = 'IE'; // Ошибки нет, а элемент появился show(12, items[12]);//"12: safari" show(-1, items[-1]);// "-1: IE" // а где IE? show('all', items)"all: Chrome,Opera,Edge. safari" // А тут всё ещё есть show(-1, items[-1]);// "-1: IE"Совет: контролируйте индексы, знайте размер массивов, с которыми работаете.
Set
Тип данных Set позволяет вам хранить набор уникальных элементов. Этим он отличается от массива. При работе с массивом вам придется предпринимать специальные усилия для поддержания уникальности элементов, Set сделает это за вас. В отличии от массива Set не позволяет произвольный доступ к элементу. Вы можете проверить наличие и получить список в порядке вставки.
const items = new Set(Array.from (,(_,ix)=>ix)); const str = (value)=>`$`; const compare = (left,right)=>leftcompare(left,right); const byStringValue =(left,right)=>compare(str(left),str(right)); console.log([. items].sort(byNumbericalValue)); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] console.log([. items].sort(byStringValue)); // [0, 1, 10, 11, 2, 3, 4, 5, 6, 7, 8, 9] items.add(100); items.add(20); //порядок, в котором JavaScript отдает содержимое Set, зависит от порядка добавления элементов console.log([. items].sort(byNumbericalValue)); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 20, 100] console.log([. items].sort(byStringValue)); // [0, 1, 10, 100, 11, 2, 20, 3, 4, 5, 6, 7, 8, 9] console.log([. items]); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 100, 20]Совет: для вывода элементов из Set в нужном порядке позаботьтесь о функции сортировки.
Кастомные объекты <> — ассоциативный массив
JavaScript объекты — это словари, где строковому ключу поставлен в соответствие элемент-значение. Возможны два синтаксиса доступа к значению ключа
const dictionary = < 'language':'javascript', 'type': 'structured', 'age': 25 >console.log(dictionary.language); // javascript console.log(dictionary['language']); // javascript const key = 'language'; console.log(dictionary[key]); // javascriptВажно! Ключ в кастомных объектах — это строковое значение. JavaScript неявно преобразует значение к строковому типу перед использованием его в качестве ключа.
Это ограничение не позволяет использовать объекты для создания связей между HTML-элементами и дополнительными данным, нужными для работы программы. Для этого используйте тип Map .
Map
По логическому устройству Map очень похож на ассоциативный массив, но свободен от ограничения на вид ключа.
const DictionaryCO = <>; // инициализируем вариант ассоциативного массива на основе обычного объекта const DictionaryMap = new Map(); // инициализируем вариант ассоциативного массива на основе встроенного типа Map // Первый эксперимент будет связывать ключ 'k1' и значение 'v1' const aKey = 'k1'; const aValue = 'v1'; // Второй эксперимент будет связывать ключ в виде объекта и значение - строку // такой вариант в жизни встречается если для нескольких HTML элементов нужно ввести дополнительные данные, которые нельзя положить в data-xxx атрибут const bKey = ; const bValue = 'v2'; // Третий эксперимент покажет нам недостатки ассоциативного массива на основе обычного объекта. Мы используем в качестве ключа - другой объект const cKey = ; const cValue = 'v3'; // заполняем первый ассоциативный массив (который в виде объекта) DictionaryCO[aKey] = aValue; DictionaryCO[bKey] = bValue; DictionaryCO[cKey] = cValue; // заполняем второй ассоциативный массива (которы в виде Map) DictionaryMap.set(aKey, aValue); DictionaryMap.set(bKey, bValue); DictionaryMap.set(cKey, cValue); console.log('a object', DictionaryCO[aKey]); // "a object", "v1" console.log('b object', DictionaryCO[bKey]); // "b object", "v2" // хотя мы пытаемся получить значение по ключу cKey, почему-то // мы получаем значение, которое связали с ключом bKey console.log('c object, oops!', DictionaryCO[cKey]); // "c object, oops!", "v2" console.log('a map', DictionaryMap.get(aKey)); // "a map", "v1" console.log('b map', DictionaryMap.get(bKey)); // "b map", "v2" // в этом варианте ассоциативного массива все сработало так, как мы и ожидали console.log('c map', DictionaryMap.get(cKey)); // "c map", "v3"Собственные структуры данных
Утиная типизация
Программисты работают с лозунгом «Algorithms + Data Structures = Programs». При работе с JavaScript разработчик преобразует требования заказчика в алгоритмы и структуры данных. Примитивных типов данных, доступных в JavaScript явно недостаточно для всего разнообразия бизнес задач, поэтому приходится использовать кастомные структуры. Для приложения электронной очереди потребуется структура с номером и временем, для умного дома — структура с данными о температуре чайника, заполненности холодильника и т. п.
let cusomerInQueue = < numberOnScreen: 'A42', timeTaken: '11:40' >let smartHouseState = < kettler: 80, fridge:< milk: true, banana: false, >, >В вашей программе вы получаете сведения о заполнении холодильника, потому что ожидаете наличия поля fridge в структуре объекта, а не потому, что переменная называется smartHouseState . Выяснив, что в структуре объекта нет поля fridge вы смело можете сказать, что значение переменной cusomerInQueue не связано с управлением умным домом, и наоборот наличие такого поля в переменной smartHouseState подсказывает вам, что её значение описывает умный дом. Такой подход, когда о назначении значения вы судите по его структуре, называется структурной типизацией.
Иногда вы знаете структуру значения (потому что знаете), а иногда вам приходится как-то догадываться. Но как, ведь typeof в этой ситуации не поможет?
Object.keys и другие шпионские средства
JavaScript предоставляет вам ряд средств и сервисов, с помощью которых вы можете исследовать значение и принимать решение, как его обрабатывать.
Основными помощниками в исследовании полученных данных вам будут
- Статический метод Object.keys
- Оператор typeof
- Оператор instanceof
- Статический метод Object.hasOwn
const someValue = < title: 'Cruella', release: new Date(2021,05,03), empty: undefined, >console.log('all keys', Object.keys(someValue)); // "all keys", ["title", "release", "empty"] console.log('typeof key release', typeof someValue.release); // "typeof key release", "object" console.log('instance of Date', someValue.release instanceof Date) // "instance of Date", true console.log('missing value', typeof someValue.empty); // "missing value", "undefined" console.log('missing key', Object.hasOwn(someValue, 'missing')); // "missing key", falseПроблемы типов JavaScript
Динамическая структурная типизация JavaScript имеет давнюю историю и восходит к идее создания небольших обработчиков событий в статических HTML страницах. С тех пор все поменялось, и динамическая типизация становится серьезным риском при постепенном и постоянном совершенствовании приложения.
Например, у нас была функция transform . Мы обнаружили, что она ломается при получении значения null .
const transform = (value) => value.replace('с', 'б'); let data = 'соль' console.log(transform(data).toUpperCase()) // "БОЛЬ" data = null; try < console.log(transform(data).toUpperCase()) // не выполнится >catch (err) < console.log(err.message) // "Cannot read properties of null (reading 'replace')" >Исправили программу в одном месте — она начала ломаться в другом. Как разорвать замкнутый круг?
const transform2 = (value) => < if (typeof value === 'string') < return value.replace('с', 'б'); >> data = 'соль'; console.log(transform2(data).toUpperCase()) // "БОЛЬ" data = null try < console.log(transform2(data).toUpperCase()) >catch (err) < console.log(err.message) // "Cannot read properties of undefined (reading 'toUpperCase')" >Вам поможет TypeScript! Но об этом в следующий раз.
Узнать больше
- Что нужно знать фронтендеру, кроме JavaScript
- 12 полезных книг по JavaScript
- Зачем фронтендерам React, если есть JavaScript
- TypeScript. Зачем он нужен и почему так популярен
«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.