Методы примитивов
JavaScript позволяет нам работать с примитивными типами данных – строками, числами и т.д., как будто они являются объектами. У них есть и методы. Мы изучим их позже, а сначала разберём, как это всё работает, потому что, конечно, примитивы – не объекты.
Давайте взглянем на ключевые различия между примитивами и объектами.
- Это – значение «примитивного» типа.
- Есть 7 примитивных типов: string , number , boolean , symbol , null , undefined и bigint .
- Может хранить множество значений как свойства.
- Объявляется при помощи фигурных скобок <> , например: . В JavaScript есть и другие виды объектов: например, функции тоже являются объектами.
Одна из лучших особенностей объектов – это то, что мы можем хранить функцию как одно из свойств объекта.
let roma = < name: "Рома", sayHi: function() < alert("Привет, дружище!"); >>; roma.sayHi(); // Привет, дружище!
Здесь мы создали объект roma с методом sayHi .
Существует множество встроенных объектов. Например, те, которые работают с датами, ошибками, HTML-элементами и т.д. Они имеют различные свойства и методы.
Однако у этих возможностей есть обратная сторона!
Объекты «тяжелее» примитивов. Они нуждаются в дополнительных ресурсах для поддержания внутренней структуры.
Примитив как объект
Вот парадокс, с которым столкнулся создатель JavaScript:
- Есть много всего, что хотелось бы сделать с примитивами, такими как строка или число. Было бы замечательно, если бы мы могли обращаться к ним при помощи методов.
- Примитивы должны быть лёгкими и быстрыми насколько это возможно.
Выбранное решение, хотя выглядит оно немного неуклюже:
- Примитивы остаются примитивами. Одно значение, как и хотелось.
- Язык позволяет осуществлять доступ к методам и свойствам строк, чисел, булевых значений и символов.
- Чтобы это работало, при таком доступе создаётся специальный «объект-обёртка», который предоставляет нужную функциональность, а после удаляется.
Каждый примитив имеет свой собственный «объект-обёртку», которые называются: String , Number , Boolean , Symbol и BigInt . Таким образом, они имеют разный набор методов.
К примеру, существует метод str.toUpperCase(), который возвращает строку в верхнем регистре.
Вот, как он работает:
let str = "Привет"; alert( str.toUpperCase() ); // ПРИВЕТ
Очень просто, не правда ли? Вот, что на самом деле происходит в str.toUpperCase() :
- Строка str – примитив. В момент обращения к его свойству, создаётся специальный объект, который знает значение строки и имеет такие полезные методы, как toUpperCase() .
- Этот метод запускается и возвращает новую строку (показывается в alert ).
- Специальный объект удаляется, оставляя только примитив str .
Получается, что примитивы могут предоставлять методы, и в то же время оставаться «лёгкими».
Движок JavaScript сильно оптимизирует этот процесс. Он даже может пропустить создание специального объекта. Однако, он всё же должен придерживаться спецификаций и работать так, как будто он его создаёт.
Число имеет собственный набор методов. Например, toFixed(n) округляет число до n знаков после запятой.
let num = 1.23456; alert( num.toFixed(2) ); // 1.23
Более подробно с различными свойствами и методами мы познакомимся в главах Числа и Строки.
Конструкторы String/Number/Boolean предназначены только для внутреннего пользования
Некоторые языки, такие как Java, позволяют явное создание «объектов-обёрток» для примитивов при помощи такого синтаксиса как new Number(1) или new Boolean(false) .
В JavaScript, это тоже возможно по историческим причинам, но очень не рекомендуется. В некоторых местах последствия могут быть катастрофическими.
alert( typeof 0 ); // "число" alert( typeof new Number(0) ); // "object"!
Объекты в if всегда дают true , так что в нижеприведённом примере будет показан alert :
let zero = new Number(0); if (zero) < // zero возвращает "true", так как является объектом alert( "zero имеет «истинное» значение. " ); >
С другой стороны, использование функций String/Number/Boolean без оператора new – вполне разумно и полезно. Они превращают значение в соответствующий примитивный тип: в строку, в число, в булевый тип.
К примеру, следующее вполне допустимо:
let num = Number("123"); // превращает строку в число
null/undefined не имеют методов
Особенные примитивы null и undefined являются исключениями. У них нет соответствующих «объектов-обёрток», и они не имеют никаких методов. В некотором смысле, они «самые примитивные».
Попытка доступа к свойствам такого значения возвратит ошибку:
alert(null.test); // ошибка
LocalStorage, sessionStorage
Объекты веб-хранилища localStorage и sessionStorage позволяют хранить пары ключ/значение в браузере.
Что в них важно – данные, которые в них записаны, сохраняются после обновления страницы (в случае sessionStorage ) и даже после перезапуска браузера (при использовании localStorage ). Скоро мы это увидим.
Но ведь у нас уже есть куки. Зачем тогда эти объекты?
- В отличие от куки, объекты веб-хранилища не отправляются на сервер при каждом запросе. Именно поэтому мы можем хранить гораздо больше данных. Большинство современных браузеров могут выделить как минимум 5 мегабайтов данных (или больше), и этот размер можно поменять в настройках.
- Ещё одно отличие от куки – сервер не может манипулировать объектами хранилища через HTTP-заголовки. Всё делается при помощи JavaScript.
- Хранилище привязано к источнику (домен/протокол/порт). Это значит, что разные протоколы или поддомены определяют разные объекты хранилища, и они не могут получить доступ к данным друг друга.
Объекты хранилища localStorage и sessionStorage предоставляют одинаковые методы и свойства:
- setItem(key, value) – сохранить пару ключ/значение.
- getItem(key) – получить данные по ключу key .
- removeItem(key) – удалить данные с ключом key .
- clear() – удалить всё.
- key(index) – получить ключ на заданной позиции.
- length – количество элементов в хранилище.
Как видим, интерфейс похож на Map ( setItem/getItem/removeItem ), но также позволяет получить доступ к элементу по индексу – key(index) .
Давайте посмотрим, как это работает.
Демо localStorage
Основные особенности localStorage :
- Этот объект один на все вкладки и окна в рамках источника (один и тот же домен/протокол/порт).
- Данные не имеют срока давности, по которому истекают и удаляются. Сохраняются после перезапуска браузера и даже ОС.
Например, если запустить этот код…
localStorage.setItem('test', 1);
…И закрыть/открыть браузер или открыть ту же страницу в другом окне, то можно получить данные следующим образом:
alert( localStorage.getItem('test') ); // 1
Нам достаточно находиться на том же источнике (домен/протокол/порт), при этом URL-путь может быть разным.
Объект localStorage доступен всем окнам из одного источника, поэтому, если мы устанавливаем данные в одном окне, изменения становятся видимыми в другом.
Доступ как к обычному объекту
Также можно получать/записывать данные, как в обычный объект:
// установить значение для ключа localStorage.test = 2; // получить значение по ключу alert( localStorage.test ); // 2 // удалить ключ delete localStorage.test;
Это возможно по историческим причинам и, как правило, работает, но обычно не рекомендуется, потому что:
-
Если ключ генерируется пользователем, то он может быть каким угодно, включая length или toString или другой встроенный метод localStorage . В этом случае getItem/setItem сработают нормально, а вот чтение/запись как свойства объекта не пройдут:
let key = 'length'; localStorage[key] = 5; // Ошибка, невозможно установить length
Перебор ключей
Методы, которые мы видим, позволяют читать/писать/удалять данные. А как получить все значения или ключи?
К сожалению, объекты веб-хранилища нельзя перебрать в цикле, они не итерируемы.
Но можно пройти по ним, как по обычным массивам:
for(let i=0; i: $`); >
Другой способ – использовать цикл, как по обычному объекту for key in localStorage .
Здесь перебираются ключи, но вместе с этим выводятся несколько встроенных полей, которые нам не нужны:
// bad try for(let key in localStorage) < alert(key); // покажет getItem, setItem и другие встроенные свойства >
…Поэтому нам нужно либо отфильтровать поля из прототипа проверкой hasOwnProperty :
for(let key in localStorage) < if (!localStorage.hasOwnProperty(key)) < continue; // пропустит такие ключи, как "setItem", "getItem" и так далее >alert(`$: $`); >
…Либо просто получить «собственные» ключи с помощью Object.keys, а затем при необходимости вывести их при помощи цикла:
let keys = Object.keys(localStorage); for(let key of keys) < alert(`$: $`); >
Последнее работает, потому что Object.keys возвращает только ключи, принадлежащие объекту, игнорируя прототип.
Только строки
Обратите внимание, что ключ и значение должны быть строками.
Если мы используем любой другой тип, например число или объект, то он автоматически преобразуется в строку:
localStorage.user = ; alert(localStorage.user); // [object Object]
Мы можем использовать JSON для хранения объектов:
localStorage.user = JSON.stringify(); // немного позже let user = JSON.parse( localStorage.user ); alert( user.name ); // John
Также возможно привести к строке весь объект хранилища, например для отладки:
// для JSON.stringify добавлены параметры форматирования, чтобы объект выглядел лучше alert( JSON.stringify(localStorage, null, 2) );
sessionStorage
Объект sessionStorage используется гораздо реже, чем localStorage .
Свойства и методы такие же, но есть существенные ограничения:
- sessionStorage существует только в рамках текущей вкладки браузера.
- Другая вкладка с той же страницей будет иметь другое хранилище.
- Но оно разделяется между ифреймами на той же вкладке (при условии, что они из одного и того же источника).
Давайте посмотрим на это в действии.
Запустите этот код…
sessionStorage.setItem('test', 1);…И обновите страницу. Вы всё ещё можете получить данные:
alert( sessionStorage.getItem('test') ); // после обновления: 1…Но если вы откроете ту же страницу в другой вкладке и попробуете получить данные снова, то код выше вернёт null , что значит «ничего не найдено».
Так получилось, потому что sessionStorage привязан не только к источнику, но и к вкладке браузера. Поэтому sessionStorage используется нечасто.
Событие storage
Когда обновляются данные в localStorage или sessionStorage , генерируется событие storage со следующими свойствами:
- key – ключ, который обновился ( null , если вызван .clear() ).
- oldValue – старое значение ( null , если ключ добавлен впервые).
- newValue – новое значение ( null , если ключ был удалён).
- url – url документа, где произошло обновление.
- storageArea – объект localStorage или sessionStorage , где произошло обновление.
Важно: событие срабатывает на всех остальных объектах window , где доступно хранилище, кроме того окна, которое его вызвало.
Представьте, что у вас есть два окна с одним и тем же сайтом. Хранилище localStorage разделяется между ними.
Вы можете открыть эту страницу в двух окнах браузера, чтобы проверить приведённый ниже код.
Теперь, если оба окна слушают window.onstorage , то каждое из них будет реагировать на обновления, произошедшие в другом окне.
// срабатывает при обновлениях, сделанных в том же хранилище из других документов window.onstorage = event => < // можно также использовать window.addEventListener('storage', event =>< if (event.key != 'now') return; alert(event.key + ':' + event.newValue + " at " + event.url); >; localStorage.setItem('now', Date.now());Обратите внимание, что событие также содержит: event.url – url-адрес документа, в котором данные обновились.
Также event.storageArea содержит объект хранилища – событие одно и то же для sessionStorage и localStorage , поэтому event.storageArea ссылается на то хранилище, которое было изменено. Мы можем захотеть что-то записать в ответ на изменения.
Это позволяет разным окнам одного источника обмениваться сообщениями.
Современные браузеры также поддерживают Broadcast channel API специальный API для связи между окнами одного источника, он более полнофункциональный, но менее поддерживаемый. Существуют библиотеки (полифилы), которые эмулируют это API на основе localStorage и делают его доступным везде.
Итого
Объекты веб-хранилища localStorage и sessionStorage позволяют хранить пары ключ/значение в браузере.
- key и value должны быть строками.
- Лимит 5 Мб+, зависит от браузера.
- Данные не имеют «времени истечения».
- Данные привязаны к источнику (домен/протокол/порт).
localStorage sessionStorage Совместно используется между всеми вкладками и окнами с одинаковым источником Разделяется в рамках вкладки браузера, среди ифреймов из того же источника «Переживает» перезапуск браузера «Переживает» перезагрузку страницы (но не закрытие вкладки) - setItem(key, value) – сохранить пару ключ/значение.
- getItem(key) – получить данные по ключу key .
- removeItem(key) – удалить значение по ключу key .
- clear() – удалить всё.
- key(index) – получить ключ на заданной позиции.
- length – количество элементов в хранилище.
- Используйте Object.keys для получения всех ключей.
- Можно обращаться к ключам как к обычным свойствам объекта, в этом случае событие storage не срабатывает.
- Срабатывает при вызове setItem , removeItem , clear .
- Содержит все данные об произошедшем обновлении ( key/oldValue/newValue ), url документа и объект хранилища storageArea .
- Срабатывает на всех объектах window , которые имеют доступ к хранилищу, кроме того, где оно было сгенерировано (внутри вкладки для sessionStorage , глобально для localStorage ).
Задачи
Автосохранение поля формы
Создайте поле textarea , значение которого будет автоматически сохраняться при каждом его изменении.
Когда пользователь закроет страницу и потом откроет её заново он должен увидеть последнее введённое значение.
Объекты
Как мы знаем из главы Типы данных, в JavaScript существует 8 типов данных. Семь из них называются «примитивными», так как содержат только одно значение (будь то строка, число или что-то другое).
Объекты же используются для хранения коллекций различных значений и более сложных сущностей. В JavaScript объекты используются очень часто, это одна из основ языка. Поэтому мы должны понять их, прежде чем углубляться куда-либо ещё.
Объект может быть создан с помощью фигурных скобок с необязательным списком свойств. Свойство – это пара «ключ: значение», где ключ – это строка (также называемая «именем свойства»), а значение может быть чем угодно.
Мы можем представить объект в виде ящика с подписанными папками. Каждый элемент данных хранится в своей папке, на которой написан ключ. По ключу папку легко найти, удалить или добавить в неё что-либо.
Пустой объект («пустой ящик») можно создать, используя один из двух вариантов синтаксиса:
let user = new Object(); // синтаксис "конструктор объекта" let user = <>; // синтаксис "литерал объекта"Обычно используют вариант с фигурными скобками <. >. Такое объявление называют литералом объекта или литеральной нотацией.
Литералы и свойства
При использовании литерального синтаксиса <. >мы сразу можем поместить в объект несколько свойств в виде пар «ключ: значение»:
let user = < // объект name: "John", // под ключом "name" хранится значение "John" age: 30 // под ключом "age" хранится значение 30 >;У каждого свойства есть ключ (также называемый «имя» или «идентификатор»). После имени свойства следует двоеточие «:» , и затем указывается значение свойства. Если в объекте несколько свойств, то они перечисляются через запятую.
В объекте user сейчас находятся два свойства:
- Первое свойство с именем «name» и значением «John» .
- Второе свойство с именем «age» и значением 30 .
Можно сказать, что наш объект user – это ящик с двумя папками, подписанными «name» и «age».
Мы можем в любой момент добавить в него новые папки, удалить папки или прочитать содержимое любой папки.
Для обращения к свойствам используется запись «через точку»:
// получаем свойства объекта: alert( user.name ); // John alert( user.age ); // 30Значение может быть любого типа. Давайте добавим свойство с логическим значением:
user.isAdmin = true;Для удаления свойства мы можем использовать оператор delete :
delete user.age;Имя свойства может состоять из нескольких слов, но тогда оно должно быть заключено в кавычки:
let user = < name: "John", age: 30, "likes birds": true // имя свойства из нескольких слов должно быть в кавычках >;Последнее свойство объекта может заканчиваться запятой:
let user =
Это называется «висячая запятая». Такой подход упрощает добавление, удаление и перемещение свойств, так как все строки объекта становятся одинаковыми.
Объект, объявленный как константа, может быть изменён
Объект, объявленный через const , может быть изменён.
const user = < name: "John" >; user.name = "Pete"; // (*) alert(user.name); // PeteМожет показаться, что строка (*) должна вызвать ошибку, но нет, здесь всё в порядке. Дело в том, что объявление const защищает от изменений только саму переменную user , а не её содержимое.
Определение const выдаст ошибку только если мы присвоим переменной другое значение: user=. .
Есть ещё один способ сделать константами свойства объекта, который мы рассмотрим в главе Флаги и дескрипторы свойств.
Квадратные скобки
Для свойств, имена которых состоят из нескольких слов, доступ к значению «через точку» не работает:
// это вызовет синтаксическую ошибку user.likes birds = trueJavaScript видит, что мы обращаемся к свойству user.likes , а затем идёт непонятное слово birds . В итоге синтаксическая ошибка.
Точка требует, чтобы ключ был именован по правилам именования переменных. То есть не имел пробелов, не начинался с цифры и не содержал специальные символы, кроме $ и _ .
Для таких случаев существует альтернативный способ доступа к свойствам через квадратные скобки. Такой способ сработает с любым именем свойства:
let user = <>; // присваивание значения свойству user["likes birds"] = true; // получение значения свойства alert(user["likes birds"]); // true // удаление свойства delete user["likes birds"];Сейчас всё в порядке. Обратите внимание, что строка в квадратных скобках заключена в кавычки (подойдёт любой тип кавычек).
Квадратные скобки также позволяют обратиться к свойству, имя которого может быть результатом выражения. Например, имя свойства может храниться в переменной:
let key = "likes birds"; // то же самое, что и user["likes birds"] = true; user[key] = true;Здесь переменная key может быть вычислена во время выполнения кода или зависеть от пользовательского ввода. После этого мы используем её для доступа к свойству. Это даёт нам большую гибкость.
let user = < name: "John", age: 30 >; let key = prompt("Что вы хотите узнать о пользователе?", "name"); // доступ к свойству через переменную alert( user[key] ); // John (если ввели "name")Запись «через точку» такого не позволяет:
let user = < name: "John", age: 30 >; let key = "name"; alert( user.key ); // undefinedВычисляемые свойства
Мы можем использовать квадратные скобки в литеральной нотации для создания вычисляемого свойства.
let fruit = prompt("Какой фрукт купить?", "apple"); let bag = < [fruit]: 5, // имя свойства будет взято из переменной fruit >; alert( bag.apple ); // 5, если fruit="apple"Смысл вычисляемого свойства прост: запись [fruit] означает, что имя свойства необходимо взять из переменной fruit .
И если посетитель введёт слово «apple» , то в объекте bag теперь будет лежать свойство .
По сути, пример выше работает так же, как и следующий пример:
let fruit = prompt("Какой фрукт купить?", "apple"); let bag = <>; // имя свойства будет взято из переменной fruit bag[fruit] = 5;…Но первый пример выглядит лаконичнее.
Мы можем использовать и более сложные выражения в квадратных скобках:
let fruit = 'apple'; let bag = < [fruit + 'Computers']: 5 // bag.appleComputers = 5 >;Квадратные скобки дают намного больше возможностей, чем запись через точку. Они позволяют использовать любые имена свойств и переменные, хотя и требуют более громоздких конструкций кода.
Подведём итог: в большинстве случаев, когда имена свойств известны и просты, используется запись через точку. Если же нам нужно что-то более сложное, то мы используем квадратные скобки.
Свойство из переменной
В реальном коде часто нам необходимо использовать существующие переменные как значения для свойств с тем же именем.
function makeUser(name, age) < return < name: name, age: age // . другие свойства >; > let user = makeUser("John", 30); alert(user.name); // JohnВ примере выше название свойств name и age совпадают с названиями переменных, которые мы подставляем в качестве значений этих свойств. Такой подход настолько распространён, что существуют специальные короткие свойства для упрощения этой записи.
Вместо name:name мы можем написать просто name :
function makeUser(name, age) < return < name, // то же самое, что и name: name age // то же самое, что и age: age // . >; >Мы можем использовать как обычные свойства, так и короткие в одном и том же объекте:
let user = < name, // тоже самое, что и name:name age: 30 >;Ограничения на имена свойств
Как мы уже знаем, имя переменной не может совпадать с зарезервированными словами, такими как «for», «let», «return» и т.д.
Но для свойств объекта такого ограничения нет:
// эти имена свойств допустимы let obj = < for: 1, let: 2, return: 3 >; alert( obj.for + obj.let + obj.return ); // 6Иными словами, нет никаких ограничений к именам свойств. Они могут быть в виде строк или символов (специальный тип для идентификаторов, который будет рассмотрен позже).
Все другие типы данных будут автоматически преобразованы к строке.
Например, если использовать число 0 в качестве ключа, то оно превратится в строку «0» :
let obj = < 0: "Тест" // то же самое что и "0": "Тест" >; // обе функции alert выведут одно и то же свойство (число 0 преобразуется в строку "0") alert( obj["0"] ); // Тест alert( obj[0] ); // Тест (то же свойство)Есть небольшой подводный камень, связанный со специальным свойством __proto__ . Мы не можем установить его в необъектное значение:
let obj = <>; obj.__proto__ = 5; // присвоим число alert(obj.__proto__); // [object Object], значение - это объект, т.е. не то, что мы ожидалиКак мы видим, присвоение примитивного значения 5 игнорируется.
Мы более подробно исследуем особенности свойства __proto__ в следующих главах Прототипное наследование, а также предложим способы исправления такого поведения.
Проверка существования свойства, оператор «in»
В отличие от многих других языков, особенность JavaScript-объектов в том, что можно получить доступ к любому свойству. Даже если свойства не существует – ошибки не будет!
При обращении к свойству, которого нет, возвращается undefined . Это позволяет просто проверить существование свойства:
let user = <>; alert( user.noSuchProperty === undefined ); // true означает "свойства нет"Также существует специальный оператор «in» для проверки существования свойства в объекте.
"key" in objectКак сохранить объект в localstorage js
Хранить в localStorage объекты нельзя, но можно хранить строки. Поэтому для хранения объекта можно преобразовать его в строку, например через JSON.stringify() . А при извлечении из хранилища преобразовывать строку обратно в объект с помощью JSON.parse() .
// создаём объект const user = name: 'Ivan', age: 23 >; // преобразовываем объект в строку и сохраняем в localStorage localStorage.setItem('user', JSON.stringify(user)); // извлекаем данные и преобразовываем в объект const savedUser = JSON.parse(localStorage.getItem('user'));