Пытаясь переписать часть разрозненной кодовой базы, Кирилл заново открыл для себя силу объектов JavaScript. Он снова и снова возвращался к шаблону, который сохранял ясность кода и убирал ненужные операции. Приближение обычных объектов к примитивным значениям изменило ситуацию. Так были открыты Примитивные объекты. В первой части вы увидите, чем отличаются объекты и примитивные значения и как их сблизить.
Кажется естественным использовать строки для различения вещей. Вполне вероятно, что в вашей кодовой базе есть объекты со свойствами name, id или label, которые используются для определения того, является ли объект тем, который вы ищете.
if (element.label === "title") {
make_bold(element);
}
В определенный момент ваш проект растет (в размере, важности, популярности или во всем сразу). Ему требуется больше строк, поскольку появляется больше вещей, которые нужно отличать друг от друга. Строки становятся длиннее, как и стоимость опечаток или, скажем, изменения соглашения об именовании ярлыков. Теперь вам нужно найти все экземпляры этих строк и заменить их. Следовательно, коммит для этого изменения становится намного больше, чем должен быть. Это позволяет вам выглядеть лучше в глазах несведущих. Одновременно это делает вашу жизнь несчастной, поскольку теперь гораздо труднее найти причину регрессии в истории git.
Строки плохо подходят для идентификации. Вы должны учитывать уникальность и опечатки; ваш редактор или IDE не проверит, та ли это строка, которую вы имели в виду. Это плохо. Я слышу, как кто-то говорит: ”Просто поместите их в переменную, да”. Это хорошее предложение, и оно снимает часть моих опасений. Но посмотрите на Джона Смита:
const john_smith_a_person = "John Smith";
const john_smith_a_company = "John Smith";
// Do they have the same name?
john_smith_a_person === john_smith_a_company; // true
// Are they the same thing?
john_smith_a_person === john_smith_a_company; // true
Случилось так, что Джон носит имя одной компании. Что если я скажу вам, что у меня есть лучшее решение? То, которое устранит все проблемы и добавит больше ценности - позволит вам достичь большего. Что бы вы ответили? Ну, я не буду переписывать статью только потому, что ваш ответ не вписывается в мое изложение. Ответ - объекты. Вы используете сами объекты, чтобы понять, является ли объект тем, что вы ищете.
// Do they have a same name?
john_smith_a_person.name === john_smith_a_company.name; // true
// Are they the same thing?
john_smith_a_person === john_smith_a_company; // false
Это делает намерение более ясным. Позвольте мне привести пример получше. Допустим, в вашем приложении есть ярлыки. Они локализованы, поэтому строка ярлыка определяется библиотекой локализации, которую вы используете, и процессом перевода вашей команды. Вы храните свои ярлыки в модуле, где все они аккуратно организованы и курируются. Когда вам нужно сделать что-то особенное для определенных меток, вы можете сравнить их непосредственно с теми, которые у вас есть.
import React from "react";
import labels from "./labels.js";
const render_label(label) => (
<Label
className={label === labels.title ? "bold" : "plain"}
icon={label.icon}
text={label.text}
/>
)
function TableOfContents({ items }) {
return (
<ul className="my-menu">
{items.map(render_label(item.label)}
</ul>
);
}
Видите, как много еще можно сделать с объектами? В модуле меток я выделил заголовок метки, который в данном случае должен быть выделен жирным шрифтом. Кроме того, будучи объектом, моя метка может содержать строку локализации (образно называемую текстом) и иконку. Все это аккуратно организовано заранее, что позволяет сохранить чистоту логики пользовательского интерфейса.
Но это только часть картины. Я знаю, что мы повсюду используем объекты, и нет ничего нового в том, чтобы группировать вещи в них. Но я готов поспорить, что вы не используете их именно так. Я редко вижу, чтобы два объекта сравнивали подобным образом, потому что вы никогда не знаете, что там внутри и откуда оно взялось. Объекты постоянно создаются и изменяются. Скорее всего, их будут сравнивать по значениям их свойств, чем по самим объектам. И причина этого в том, что объекты не подходят для такого использования. Они слишком многофункциональны. Чтобы разрешить этот и многие другие случаи использования, нам придется, с одной стороны, уменьшить некоторые возможности объектов, а с другой - реализовать еще несколько. И в итоге мы получим то, что я называю примитивными объектами. Th… решение al… некоторых проблем.
В первой части серии я хочу рассказать о некоторых аспектах JavaScript, которые помогают приблизить объекты к примитивным значениям, что в свою очередь позволит нам воспользоваться общими возможностями языка, которые обычно не ассоциируются с объектом, например, сравнением и арифметическими операторами. В следующей части мы подробно рассмотрим практические примеры и инструменты для работы с такими объектами. А сейчас давайте посмотрим, что представляют собой объекты в JavaScript.
Во-первых, давайте определим нашу цель. Давайте нарисуем картину того, где мы хотели бы оказаться впоследствии. Какими свойствами примитивных значений мы хотим, чтобы обладали наши объекты?
Неизменяемость
Примитивные значения доступны только для чтения. Мы хотим, чтобы наши объекты не могли быть отредактированы никем после их создания. Вспомните предыдущий пример. Что толку от ярлыка, если какой-то не зависящий от нас код изменил его текст или значок? Как только объект определен, его следует закрепить. Работайте с операторами.
Выражения с определенными операторами возвращают соответствующий тип. Арифметические операторы возвращают числа. Сравнение дает булевы числа. Используйте литеральный синтаксис.
Литералы для примитивов дают точное значение, точнее, объект, представляющий это значение. Такие объекты создаются один раз для каждого значения. Каждый раз, когда в вашем коде есть “hello”, вы получаете один и тот же объект.Иметь типы.
Оператор typeof говорит вам, с чем вы имеете дело (за исключением null). Мы не всегда знаем, какой тип объекта мы получаем. Поэтому, прежде чем тыкать в его свойства, было бы неплохо знать, с чем мы имеем дело.
Я перечислил их по степени непосредственной полезности. По счастливой случайности они также упорядочены по легкости получения. В этой статье я рассмотрю первое и часть второго. Мы увидим, как сделать объекты неизменяемыми. Мы также определим их представление в примитивных значениях, что позволит нам использовать некоторые операторы над ними. Переход от объектов к примитивным значениям прост, так как примитивные значения сами являются объектами - вроде как.
Объекты до самого конца, даже если это вроде как не так
Я помню свое замешательство, когда впервые увидел {} === {}; // false. Что это за язык, который даже не может различить две одинаковые вещи? Это казалось таким нелепым и забавным. Гораздо позже я узнал, что в JavaScript есть детали гораздо хуже, после чего перестал смеяться, наблюдая за разговором на wat.
Объект - это одна из фундаментальных вещей в JavaScript. Возможно, вы слышали, что в JavaScript все является объектом. Это действительно так. За исключением некоторых нижних значений, все примитивы являются объектами. Хотя технически это более тонко, с точки зрения нашего кода это правда. На самом деле, это достаточно верно, чтобы считать, что все является объектами, может быть полезной ментальной моделью. Но давайте сначала попробуем понять, что происходит с этим сравнением объекта с объектом, которое так забавляло младшего меня.
Синтаксис объектного литерала используется для создания новых объектов. Он позволяет нам объявить и инициировать объект в одном выражении.
// Instead of this.
const my_object = new Object();
my_object.first_property = "First property";
my_object.nth_property = "Next property";
// You can do this.
const my_object = {
first_property: "First property",
nth_property: "Next property"
};
Намного чище, верно? Но теперь я думаю, что отсутствие строки инициализации объекта - это то, что заставило меня запутаться в этих двух пустых выражениях равенства объектов. Казалось, что оно показывает, что язык пытается распознать кажущееся равенство. Но на самом деле в этом выражении происходит следующее:
new Object() === new Object(); // false
Теперь очевидно, что они не равны. Вы сравниваете два разных объекта, которые вы только что создали. Ожидать обратного - то же самое, что ожидать, что 5 === 3 вернет true. В обоих случаях это разные вещи.
Давайте проведем проверку на вменяемость. Будут ли две переменные, ссылающиеся на один и тот же объект, считаться равными?
const my_object = {};
const other_thing = my_object;
my_object === other_thing; // true
В данном случае только в первой строке есть выражение, создающее объект. Во второй строке мы заставляем переменную other_thing ссылаться на только что созданный объект. Теперь две переменные ссылаются на один и тот же объект. Сравнивать их - все равно что сравнивать два одинаковых числа, не так ли?
Почему это важно? Потому что это дает нам возможность проверить, относится ли переменная к искомому объекту. И если мы подумаем об этом в контексте ”все есть объект”, то именно так работают числа и строки. Когда вы сравниваете две переменные, содержащие строки, движку не нужно проверять, одинаков ли каждый символ в этих строках. Достаточно сравнить, ссылаются ли переменные на один и тот же объект. Это происходит благодаря самому существенному различию между регулярными объектами и примитивными значениями - неизменяемости.
Как приблизить регулярные объекты к примитивным значениям
В JavaScript примитивные значения неизменяемы. Вы не можете изменить ни одного символа в строке, так же как не можете превратить число пять в шесть. Если вы используете const для инициализации переменной и поместите в нее примитивное значение, оно всегда будет оставаться неизменным. Никто не сможет изменить значение; оно неизменяемо. Никто не сможет переназначить переменную; она была создана с помощью const.
Давайте рассмотрим, как работают числа. Вы можете получить шесть из пяти, увеличив его на единицу, но это ничего не изменит в числе пять.
const five = 5;
const six = 5 + 1;
five === 5; // true
Кто-то может сказать, что использование let изменит это. Но посмотрите, оно не может изменить пять:
const five = 5;
let result = 5;
result++;
result === 6; // true
five === 5; // true
Пять - это все равно пять. Это потому, что ++ - это просто сокращение для += 1. Видите знак равенства? Произошло то, что я присвоил новое значение переменной result - значение, которое я получил из выражения result + 1 (именно так сокращенно обозначается += 1). Ключевое слово const предотвращает переназначение переменной. В приведенном выше примере именно оно дает мне возможность знать, что пять всегда ссылается на объект 5.
Можно предположить, что единственный способ изменения примитивных значений в JavaScript - это присваивание, что означает, что на самом деле мы изменяем то, на что ссылается переменная. Таким образом, изменяются именно переменные, а не значения. По крайней мере, не примитивные. Но как это работает с объектами?
После инициализации объекта вы можете изменить его свойства: удалить их, добавить новые и переназначить старые. Все мы знаем, как это делается. Но кроме этого, он ведет себя так же, как и примитивные значения. На самом деле, если вы привыкнете к модели, в которой объекты и примитивные значения - это одно и то же, вы будете по-другому смотреть на всевозможные проблемы в JavaScript.
Возможно, вы наткнулись на вопрос о том, как переменные передаются в функцию. Люди спрашивают, передаются ли переменные по значению или по ссылке. Обычный ответ - примитивные значения передаются по значению, а объекты - по ссылке. Но с той ментальной моделью, которую я навязываю вам здесь, вы, возможно, уже знаете, что я скажу по этому поводу. Перед этим позвольте мне показать вам, как этот вопрос не имеет особого смысла в JavaScript. Я также раскрою вам хитрость, которую используют многие статьи и учебники.
Когда вы передаете переменные в качестве параметров вызова функции, они присваиваются аргументам функции. Аргументы являются локальными переменными в области видимости функции и не имеют обратной связи с исходными переменными, что вполне логично. Если вы передаете выражение в функцию, вам нужно куда-то поместить его результат, не так ли?
Посмотрите на две следующие функции. Они делают одно и то же - передают значение, но одна из них определена с одним параметром, а другая - без него. Вторая демонстрирует, что происходит с параметром, который мы передали.
function single(arg) {
return arg;
}
function none() {
// The first parameter is assigned to a variable `arg`.
// Notice the `let`; it will be significant later.
let arg = arguments[0];
return arg;
}
single("hi"); // "hi"
none(5); // 5
Вы видите, что они оба работают одинаково. Помня о том, как работают аргументы функции, давайте попробуем изменить некоторые значения. У нас будет функция, которая изменяет свой единственный аргумент и возвращает его. Я также создам несколько переменных, которые буду передавать в функцию по очереди. Попробуйте предсказать, что будет выведено в консоль. (Ответ находится во втором предложении следующего абзаца).
function reassign(arg) {
arg = "OMG";
}
const unreassignable = "What";
let reassignable = "is";
let non_primitive = { val: "happening" };
reassign(unreassignable);
reassign(reassignable);
reassign(non_primitive);
console.log(unreassignable, reassignable, non_primitive.val, "😱");
Было ли в вашем предположении хоть одно “OMG”? Не должно быть, так как консоль покажет ”Что происходит 😱”. Независимо от того, что передается в функцию в JavaScript, переназначение изменяет только переменную аргумента. Таким образом, ни const, ни let здесь ничего не меняют, потому что функция не получает саму переменную. Но что произойдет, если мы попробуем изменить свойства аргумента?
Я создал еще одну функцию, которая пытается изменить свойство val своего аргумента. Попробуйте угадать, какое сообщение появится в консоли на этот раз.
function change_val_prop(arg) {
try {
arg.val = "OMG";
} catch (ignore) {}
}
const a_string = "What";
const a_number = 15;
const non_primitive = { val: "happening" };
const non_primitive_read_only = Object.freeze({ my_string: "here" });
change_val_prop(a_string);
change_val_prop(a_number);
change_val_prop(non_primitive);
change_val_prop(non_primitive_read_only);
console.log(
a_string.val,
a_number.val,
non_primitive.val,
non_primitive_read_only.val,
"😱"
);
В вашей догадке теперь есть “OMG”? Отлично, сообщение “undefined undefined OMG undefined 😱”. Единственный раз, когда функция могла изменить свойство, это с общим объектом. О чем это нам говорит? Есть ли разница между тем, как передаются примитивные значения и как объекты? Неужели передача замороженного объекта внезапно меняет его на pass-by-value? Я думаю, что полезнее рассматривать их как равные.
Теперь о той ловкости рук, о которой я упоминал. Практически все ресурсы делают такую вещь, когда они говорят, что примитивы и объекты передаются по-разному, а затем сразу же приводят пример, где они обрабатываются по-разному. Посмотрите на описание функций в MDN. На момент написания этой статьи она описывалась так (выделение мое):
Аргументы могут передаваться по значению (в случае примитивных значений) или по ссылке (в случае объектов). Это означает, что если функция переназначит параметр примитивного типа, его значение не изменится за пределами функции. В случае с параметром объектного типа, если его свойства будут изменены, это изменение отразится за пределами функции.
Я только что показал, что переназначение также не изменит объект. Вы не можете изменить свойства примитивов, потому что они доступны только для чтения, что также относится к замороженным объектам. И большинство примеров, которые вы найдете, делают то же самое. Сначала они указывают разницу между двумя значениями, а затем демонстрируют ее, используя различные методы для каждого значения.
Я не пытаюсь критиковать, не поймите меня неправильно. Возможно, это было сделано потому, что это объясняет причуды JavaScript более привычным способом. Просто имейте в виду, что иногда объяснение дает вам модель мышления о проблеме. Но эта модель никогда не бывает полностью верной природе проблемы.
Взгляд на эту проблему с точки зрения того, что примитивы - это такие же застывшие объекты, поможет вам понять, что происходит на самом деле. Альтернативные решения становятся нелогичными. А теперь, открыв это понятие объекта-примитива, который никто не может изменить, давайте сделаем их более дружественными для остальной части вашей программы.
Преобразование
Примитивные значения стоят сами по себе; любая программа знает, как с ними работать. Объекты могут быть чем угодно. И даже если вы назовете их примитивными, этого недостаточно, чтобы они вдруг стали гражданами первого класса. Чтобы добиться этого, нам нужно проделать определенную работу.
Вы можете определить способ преобразования объектов в примитивные значения, такие как строки или числа. Например, давайте создадим объект, представляющий оценку от нуля до пяти. Нам нужно иметь возможность работать с числовым представлением для сравнения и сортировки. Нам также нужно иметь возможность выводить его в виде текста.
Существуют определенные методы, которые вы можете определить для описания представления объекта. Помните [объект Object]? Это то, что получается, когда вы пытаетесь превратить ваш объект в строку:
String({}); // "[object Object]"
Давайте изменим это.
Представление строки
Этот вывод происходит из метода по умолчанию toString, определенного в прототипе объекта. Но вы можете переписать его, определив его в своем собственном объекте.
String({ toString: () => "hello there" }); // "hello there"
Это то, что мы будем использовать для наших объектов рейтинга. Чтобы это было удобно, создадим функцию, которая инициализирует и замораживает такие объекты. Она также будет проверять, находится ли значение в диапазоне от нуля до пяти, и возвращать неопределенное значение в противном случае.
function new_rating(value) {
const max = 5;
// That symbol forces textual representation (who needs emoji anyway 🙄).
const text_only = "\ufe0e";
const star = "⭑" + text_only;
const no_star = "⭐" + text_only;
if (
!Number.isSafeInteger(value) ||
(value < 0 || value > max)
) {
return undefined;
}
return Object.freeze({
value,
toString: () => star.repeat(value) + no_star.repeat(max - value)
});
}
Теперь давайте оценим что-нибудь. Есть ручка, которая мне нравится. Она довольно замечательная, и я бы дал ей пять звезд.
const ratings = new WeakMap();
ratings.set(jetstream_pen, new_rating(5));
Этот WeakMap для рейтингов - это способ присвоения свойств объектам без их фактического изменения. Теперь, когда мы хотим получить рейтинг, мы можем преобразовать оба наших объекта в строки.
if (ratings.has(jetstream_pen)) {
console.log(`${jetstream_pen} ${ratings.get(jetstream_pen)}`);
// "Uni-Ball Jetstream 0.5 ⭑︎⭑︎⭑︎⭑︎⭑︎"
}
Обернув оба объекта в литерал шаблона string, я использовал метод toString. В противном случае вы могли бы просто вызвать функцию String, как я сделал в начале этого раздела.
Для любителей чисел
Для чисел существует метод valueOf, который вызывается при каждой попытке преобразования в сравнение чисел или математические операторы (кроме +). Давайте добавим его в нашу функцию new_rating:
function new_rating(value) {
// ...
return Object.freeze({
value,
toValue: () => value,
toString: () => star.repeat(value) + no_star.repeat(max - value)
});
}
Теперь может показаться излишним возвращать свойство value напрямую. Но помните, что никто, кроме нас, не знает, что оно есть. Возврат его из toValue - это универсальный способ получить числовое представление.
Допустим, у нас снова есть объект pen. И допустим, что рейтинг теперь является его свойством (просто для упрощения примера). Теперь мы можем отфильтровать товары с менее чем четырьмя звездами:
articles.filter((item) => item.rating > 3);
// [ { name: "Uni-Ball Jetstream 0.5", ... } ]
Аналогичным образом мы можем сортировать элементы по рейтингу. Мы можем сделать это с помощью метода сортировки массивов. Вероятно, у вас уже есть любимая функция сортировки, которую вы хотели бы использовать, например, эта:
function sorter(first, second) {
return second.rating - first.rating;
}
const sorted_by_rating = array_of.sort(sorter);
Теперь в sorted_by_rating хранится массив самых лучших товаров.
Заключение
Я редко смотрел на объекты как на что-то, что может расширить возможности JavaScript. С примитивными объектами это то, что я пытаюсь исследовать. Есть вещи, которые мы все еще не можем добавить, например, новые операторы или литеральный синтаксис, но все же с помощью примитивных объектов мы можем определить новые типы значений.
В этой первой части серии ”Примитивные объекты” я попытался дать общее представление о том, как сделать объекты похожими на некоторые свойства примитивов. Вы замораживаете их, чтобы сделать доступными только для чтения. Вы также можете определить представление в примитивах, число или строку, чтобы заставить их работать с арифметическими операторами или выводить их в текст.
В следующих частях, которые появятся на следующей неделе, я постараюсь привести больше примеров использования и сравнения с другими подходами, с которыми я сталкивался. Вы увидите, как упростить создание примитивных объектов и превратить их в структуры.
В этой серии я стараюсь затронуть те возможности JavaScript, на которые можно положиться. Даже если не все из этого имеет смысл, я надеюсь, что, рассмотрев некоторые из приведенных здесь примеров, вы узнаете что-то полезное, что позволит сделать работу с JavaScript менее хрупкой без ненужного обращения к дополнительным инструментам.