На самом деле боксинг срабатывает, даже если мы обращаемся к примитивному типу напрямую
'hello'.length
Но важно понимать, что у строки нет свойств, прозрачно для нас происходит оборачивание в объект и работает это примерно так: new String('hello')
.
Обычно, изменение объекта происходит так:
const obj = { key: 'value' };
obj.key = 'another value';
Но что делать, если свойство заранее неизвестно? Тогда можно воспользоваться таким способом:
const name = 'key';
const obj = { key: 'value' };
obj[name] = 'another value';
Вычислимые свойства встречаются достаточно часто на этапе создания объекта, по этой причине появился специальный синтаксис, позволяющий задавать подобные свойства в литерале. Дополнительный бонус в том, что при таком подходе в коде пропадают лишние изменения (мутации) и код становится более функциональным:
const name = 'key2';
const obj = { key: 'value', [name]: 'another value' };
// { key: 'value', key2: 'another value' }
Единственное отличие от стандартного синтаксиса в том, что ключ - это переменная (а не имя), взятая в квадратные скобки.
Еще одна интересная возможность объектов в JS - сокращенный синтаксис создания объектов при использовании переменных или констант. Обычное создание выглядит так:
const name = 'Mike';
const user = { name: name };
В примере выше, имя свойства совпадает с именем переменной, в которой хранится нужное значение. JS позволяет написать этот код лаконичнее. Можно просто опустить часть name:
. И получится:
const name = 'Mike';
const user = { name };
То же самое работает и для нескольких переменных:
const name = 'Mike';
const surname = 'Smith';
const user = { name, surname };
Можно даже мешать разные стили в рамках одного объекта:
const name = 'Mike';
const user = { name, surname: 'Smith' };
--
Теперь, когда мы уже хорошо понимаем в чём соль объектно-ориентированного программирования, мы можем познакомиться с настоящими нативными объектами, которые реализованы прямо в JS.
Разберём простой пример:
// primitive type
const str = 'hello, world';
https://repl.it/@hexlet/js-ddp-native-objects-properties-and-methods
Записываем в константу строчку и после этого используем известный уже для нас синтаксис доступа к свойствам, только не через модуль, а через объект. Это очень похожий механизм, когда мы вызывали методы или передавали сообщение. Например, с помощью length
, мы можем посмотреть длину строчки:
// property
str.length; // 12
Метод toUpperCase
возвращает нам новую строчку (он не мутирует текущую), которая переводит строчку в верхний регистр:
// method
str.toUpperCase(); // HELLO, WORLD
Если мы не будем делать вызов после toUpperCase
, а просто сделаем обращение к свойству и распечатаем, то что лежит внутри, мы увидим, что там лежит function
:
// actually, just property
str.toUpperCase; // [Function: toUpperCase]
В большинстве языков принято разделять понятия свойства и методы и говорить об этом явно. В JavaScript нет методов, есть только свойства, просто в некоторых свойствах могут лежать функции, что довольно просто, потому что с функциями мы уже довольно хорошо знакомы: их можно куда угодно записывать, можно передавать как аргументы, получать как значения.
С объектом примерно также. Есть некоторый объект внутри переменной или константы, точка – вызов какого-то свойства, которое может быть функцией (тогда ставятся скобки).
Давайте договоримся, что в будущем мы будем говорить слово «методы», просто потому что так принято и все так делают во всем мире, на всех языках. При этом мы понимаем, что в JS – это просто функция, которая записана и лежит внутри определённого свойства с тем же именем.
JavaScript не является простым языком, когда мы говорим о понимании его внутренностей и глубоком понимании того, как он работает. На самом деле строчка – это примитивный тип, а не объект. Но если мы обратимся к строке напрямую, то все отработает также, как если бы она была записана в константу или переменную:
'hello'.length // 12
Почему нет ошибки и строка начинает вести себя, как объект?
Если простыми словами, то происходит какой-то некий процесс, который делает для вас прозрачным трансформацию в объект. Этот процесс называется boxing. То есть в тот момент, когда вы складываете в константу какой-то примитивный тип и после этого через точку к нему обращаетесь, то в этот момент JS за вас оборачивает этот примитивный тип в соответствующий объект, который связан с текущим примитивным типом и он начинает работать таким образом. После этого происходит его unboxing (разворачивание) и внутри всё равно оказывается примитивный тип. Этот механизм следует определённым целям и попозже мы разберёмся с ним чуть глубже.
В JavaScript есть отдельный тип данных, который называется объект и по сути любой объект, с которым мы работаем, является этим типом данных (точнее его подтипом, но об этом мы будем говорить позже, потому что сейчас мы сосредоточены больше на базовом понимании и связи с ООП, чем на особенностях работы JS).
Как же работает этот тип данных:
const card = {
name: 'percent card',
key: 'value',
};
Он представляет собой вот такую структуру, в которой мы задаём ограничители (фигурные скобки в начале и в конце), после этого описываем ключ и значение.
Значением может быть всё что угодно. На вход ожидается любое выражение, после этого ставится запятая и дальше опять пара ключ-значение и тд. После этого в card
у нас оказывается объект.
Здесь начинается некоторая путаница, которую вводит JS и люди, которые знакомы только с этим языком часто смешивают это в одно и постоянно употребляют в разговоре. В свою очередь люди из других языков это не совсем понимают, потому что в других языках такая запись обозначает несколько иную вещь. К этому надо просто привыкнуть и всегда внутри у себя дифференцировать о чём мы говорим: о типе данных объект или объекте, как мы понимаем это с точки зрения ООП, хотя в каком-то смысле разница в JS стирается.
Поскольку этот тип данных (объект) является и настоящим объектом в том числе, поэтому обращение к его ключам идёт, как обращение к обычным свойствам в объекте:
const card = {
name: 'percent card',
key: 'value',
};
card.key; // value
Мы пишем card.key
и получаем значение, всё довольно просто.
Если мы обратимся к неизвестному свойству, то мы получим undefined
.
card.wrongKey; // undefined
Имеется в виду, что свойство не определено, хотя оно может быть определено и равно undefined
, но обычно так не делают.
Мы уже говорили, что определять самостоятельно undefined
– неправильно, потому что вы просто не отличите определено оно или нет и что важно – вы не получаете ошибку. То есть, если вы где-то случайно (в JS легко это сделать, поскольку это динамический язык) допустите синтаксическую ошибку, то ваш код может продолжить работать, но продолжит работать с ошибками и при этом не упадёт. Это довольно опасная вещь, поэтому за этим надо следить.
У объектов в JS есть другой синтаксис. Вы всегда можете обратиться через квадратные скобки определив ключ, как строчку:
// map syntax
card['key']; // value
card['wrongKey']; // undefined
https://repl.it/@hexlet/js-ddp-native-objects-object
Это позволяет, например, обращаться к свойствам, которые названы чуть сложнее, чем просто имена состоящие из алфавита английского языка. Например, если вы используете целое предложение, пробелы или какие-то специальные символы, то используйте такой способ обращения.
Это не частый кейс, но такое бывает когда у вас объект динамический, то есть вы откуда-то собираете информацию – это будет достаточно удобно. Этот синтаксис ведёт себя точно также, если мы ошибаемся с ключом, то получаем undefined
.
Есть одна интересная деталь, которая уже может вводить в заблуждение. До этого момента мы привыкли, что константа обозначает реально константу и она никогда не меняется, но посмотрите внимательно на этот пример:
const card = {
name: 'percent card',
key: 'value',
};
card.wrongKey; // undefined
card.wrongKey = 'something';
card.wrongKey; // something
// TypeError: Assignment to constant variable.
card = {};
https://repl.it/@hexlet/js-ddp-native-objects-constant
Мы пишем card.wrongKey
, который естественно пишет undefined
, потому что он не определён, но после этого мы делаем присваивание (ничего не происходит, всё срабатывает молча) и после этого мы делаем снова вызов и получаем это значение.
И вот тут возникает 2 интересных момента:
Возникает вопрос, а почему это константа?
Константа она только по той простой причине, что вы по имени card
не можете записать какой-то объект целиком, то есть вы не можете заменить card
полностью.
Если вы перезапишите card
чем-то новым (динамический язык позволяет это) – вы получите ошибку:
// TypeError: Assignment to constant variable.
card = {};
Кстати, так можно определять пустой объект
const obj = {};
Но если вы меняете внутренности этого объекта, который вы положили в card
, то всё будет работать.
У вас может оставаться какое-то странное ощущение, что константа не совсем константа, но какая-то определенная логика в этом тоже есть и к этому нужно просто привыкнуть. Этот момент будет детальнее разобран в следующем курсе JS: Коллекции.
Давайте посмотрим, как использовать наши объекты вместе с функциями.
Функции – это объекты первого рода.
В данном случае под словом "объект" имеется в виду, как некоторая сущность в обычном понимании этого слова. Важно не путать это с ООП.
В объект (тип данных) мы можем записать любую функцию и пользоваться ей.
const card = {
name: 'percent card',
damage: health => Math.round(health * (80 / 100)),
};
card.name; // percent card
card.damage(10); // 8
https://repl.it/@hexlet/js-ddp-native-objects-object-using
Создаём карту и после этого вызываем name
и damage
.
name
– статичное свойство, которое возвращает имя карты
damage
– вызов метода (на самом деле это свойство, внутри которого записана функция) куда передаётся значение здоровья.
Как видите, такой синтаксис выглядит приятнее, потому что это уже встроенный механизм, но по своему смыслу, семантике и результату действия – это абсолютно то же самое, что мы делали самостоятельно.
Наши конструкторы и наши объекты превращаются в стандартный механизм, с которым мы работали.
// percentCard.js
export const make = (name, percent) => {
return {
name: name,
damage: health =>
Math.round(health * (percent / 100)),
}
};
У нас есть файл, который представляет из себя модуль. Он экспортирует некий конструктор, внутри которого мы теперь чуть-чуть проще реализуем всю нашу логику.
Мы просто возвращаем объект, как тип данных, внутрь которого мы записываем переданные параметры: в name
– имя, а в функцию damage
мы передаём проценты исходя из которых будет считаться урон.
С точки зрения интерфейсов ничего не меняется кроме варианта использования. То есть мы теперь не просто вызываем функцию и передаём туда строчку, а берём константу, пишем точку и обращаемся к какому-то свойству.
Используя стандартный механизм мы получаем диспетчеризацию.
const card = percentCard.make('percent card', 60)
// const card = simpleCard.make('simple card', 3)
card.name; // percent card
card.damage(10); // 6
Диспетчеризация происходит по имени свойства, потому что здесь выбор строится на основе объекта. То есть объект уже сам связан с какой-то конкретной функцией.
На самом деле этот механизм чуть сложнее, просто сейчас нет смысла в это погружаться.
В JS существует крайне важная концепция, он сильно отличается от многих ООП языков – это прототипы. Об этом мы поговорим позже, чтобы не вводить сейчас слишком много новой информации. Тем более она не имеет никакого значения к решаемым нами проблемам.
Вам ответят команда поддержки Хекслета или другие студенты.
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
Наши выпускники работают в компаниях:
Зарегистрируйтесь или войдите в свой аккаунт