Васильев Андрей Михайлович, 2022
Версии презентации
Операции над товаром:
Операции с корзиной:
Каждую операцию с переменными можно разделить по операциям чтения и записи
Проведём разделение для операций над корзиной
Мы уже видели и знаем как реализовать последние три операции с помощью техники копирование при записи, чтобы обеспечить неизменяемость данных
В языке JavaScript структуры данных изменяемы по умолчанию, поэтому необходимо выполнять дополнительные явные действия, чтобы поддержать неизменность
Языки с неизменяемыми данными по умолчанию: Clojure, Elm, Haskell, Elixir, Erlang, PureScript
Техника копирование при записи (КПЗ) состоит всего из трёх шагов. Если применить технику КПЗ ко всем операциям при работе с корзиной, то она будет неизменяемой
Шаги техники копирования при записи:
function add_last(array, elem) {
var new_array =
array.slice();
new_array.push(elem);
return new_array;
}
Данный метод не модифицирует переданные данные, возвращает информацию, значит это операция чтения. С помощью техники КПЗ можно преобразовать запись данных в чтение.
Рассмотрим следующий метод, который удаляет товар из корзины:
function remove_item_by_name(cart, name) {
var index = null;
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
index = i;
}
if(idx !== null)
cart.splice(index, 1); // Модификация
}
Метод Array.splice()
позволяет удалять элементы из массива, она модифицирует массив, на котором была вызвана
Если ей передать в качестве аргумента глобальный shopping_cart
, то эта глобальная переменная станет изменяемой
// Оригинал
function remove_item_by_name(cart, name) {
var index = null;
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
index = i;
}
if(idx !== null)
cart.splice(index, 1);
}
// Шаг №1: создание копии данных
function remove_item_by_name(cart, name) {
var new_cart = cart.slice();
var index = null;
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
index = i;
}
if(idx !== null)
cart.splice(index, 1);
}
// Шаг №2: модификация копии
function remove_item_by_name(cart, name) {
var new_cart = cart.slice();
var index = null;
for(var i = 0; i < new_cart.length; i++) {
if(new_cart[i].name === name)
index = i;
}
if(idx !== null)
new_cart.splice(index, 1);
}
// Шаг №3: Возвращение копии
function remove_item_by_name(cart, name) {
var new_cart = cart.slice();
var index = null;
for(var i = 0; i < new_cart.length; i++) {
if(new_cart[i].name === name)
index = i;
}
if(idx !== null)
new_cart.splice(index, 1);
return new_cart;
}
Сам метод теперь работает согласно подходу копирование при записи
Осталось только изменить все места вызова данного метода
Рассмотрим работу обработчика события нажатия на кнопку удаления товара
// Оригинал
function delete_handler(item_name) {
remove_item_by_name(shopping_cart, item);
var total = calc_total(shopping_cart);
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
}
// Вызов изменённой функции
function delete_handler(item_name) {
shopping_cart = remove_item_by_name(shopping_cart, item);
var total = calc_total(shopping_cart);
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
}
Действие по удалению элемента из массива часто будет использоваться в коде приложения, поэтому мы можем выделить общий метод для решения этой задачи
function removeItems(array, index, count) {
var copy = array.slice();
copy.splice(index, count);
return copy
}
// Применим данный метод
function remove_item_by_name(cart, name) {
var new_cart = cart.slice();
var index = null;
for(var i = 0; i < new_cart.length; i++) {
if(new_cart[i].name === name)
index = i;
}
if(idx !== null)
new_cart.splice(index, 1);
return new_cart;
}
function remove_item_by_name(cart, name) {
var index = null;
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
index = i;
}
if(idx !== null)
return removeItems(cart, index, 1);
return cart;
}
Массивы в JavaScript описываются с помощью класса Array
[index]
#
Получает ссылку на элемент, хранящийся по индексу. Номера начинаются с нуля
> var array = [1, 2, 3, 4];
> array[2]
3
[] =
#
Оператор присваивания заменяет объект по индексу, изменяет массив
> var array = [1, 2, 3, 4];
> array[2] = "abc"
> array
[1, 2, "abc", 4]
.length
#
Содержит количество элементов в массиве, является свойством, а не методом
> var array = [1, 2, 3, 4];
> array.length
4
.slice()
#
Создаёт и возвращает неглубокую копию массива
> var array = [1, 2, 3, 4];
> array.slice()
[1, 2, 3, 4]
.push(element)
#
Изменяет массив, добавляя элемент в его конец, возвращает новую длину массива
> var array = [1, 2, 3, 4];
> array.push(10)
5
> array
[1, 2, 3, 4, 5]
.pop()
#
Изменяет массив, удаляя у него последний элемент. Возвращает значение удалённого элемента
> var array = [1, 2, 3, 4];
> array.pop();
4
> array
[1, 2, 3]
.unshift(el)
#
Изменяет массив, добавляя элемент в его начало. Возвращает новую длину
> var array = [1, 2, 3, 4];
> array.unshift(10);
5
> array
[5, 1, 2, 3, 4]
.shfit()
#
Изменяет массив, удаляет первый элемент массива. Возвращает значение удалённого элемента
> var array = [1, 2, 3, 4];
> array.shift()
1
> array
[2, 3, 4]
.splice(idx, num)
#
Изменяет массив, удаляя указанные num
элементов, начиная с номера idx
. Возвращает удалённые элементы
> var array = [1, 2, 3, 4, 5, 6];
> array.splice(2, 3);
[3, 4, 5]
> array
[1, 2, 6]
Рассмотрим операцию добавления нового почтового адреса в список контактов
var mailing_list = [];
function add_contact(email) {
mailing_list.push(email);
}
function submit_form_handler(event) {
var form = event.target;
var email = form.element["email"].value;
add_contact(email);
}
Преобразуйте код add_contact, чтобы он использовал подход КПЗ
var mailing_list = [];
function add_contact(mailing_list, email) {
var list_copy = mailing_list.slice();
list_copy.push(email);
return list_copy;
}
function submit_from_handler(event) {
var form = event.target;
var email = form.elements["email"].value;
mailing_list = add_contact(mailing_list, email);
}
Иногда функция выполняет одновременно две роли: она модифицирует значение и возвращает значение. Пример такой функции: .shift()
var array = [1, 2, 3, 4];
var element = array.shift();
Данный метод модифицирует массив и возвращает значение
Для его преобразования можно воспользоваться двумя подходами:
Рассмотрим оба данных подхода, однако первый является предпочтительным
Данный подход состоит из двух этапов: выделение чтения из функции и преобразование записи в операцию копирования при записи
.shift()
#
Логика чтения в методе .shift()
заключается в получении первого элемента массива
function first_elemnet(array) {
return array[0];
}
function drop_first(array) {
var array_copy = array.slice();
array_copy.shift();
return array_copy;
}
Данный подход тоже состоит из двух шагов: оборачивание метода в функцию, которую мы можем изменять, с последующим КПЗ-преобразованием
function shift(array) {
return array.shift();
}
function shift(array) {
var array_copy = array.slice();
var first = array_copy.shift();
return {
first: first,
array: array_copy
};
}
В первом подходе мы разделили операции чтения и записи в два метода. Если нам потребовался метод, который одновременно удаляет первый элемент и возвращает его, то его легко реализовать на примитивах:
function shift(array) {
return {
first: first_element(array),
array: drop_first(array)
}
}
Преобразуйте метод pop()
в метод только для чтения двумя способами, которые были рассмотрены для метода shift()
function last_element(array) {
return array[array.length - 1];
}
function drop_last(array) {
var array_copy = array.slice();
array_copy.pop();
return array_copy;
}
function pop(array) {
var array_copy = array.slice();
var last = array_copy.pop();
return {
last: last,
array: array_copy
};
}
Данный метод применяет технику КПЗ, не модифицирует свои аргументы и возвращает новый результат на их основании. Этот метод является методом-чтением (вычислением) по определению
Да, в данном случае это верно
Однако в рамках примеров мы продолжим использовать массивы, так как многие части существующего кода знают, что корзина — это массив
В современном мире распределённых вычислений и многопоточного выполнения приложений использование неизменяемых данных позволяет снизить риск возникновения проблем
Язык JavaScript действительно плохо приспособлен к использованию таких данных: нет поддержки ни на уровне языка, ни на уровне среды исполнения
Потенциально можно было бы уменьшить количество функций, применяя техники сразу во всех методах. Такой подход в конечном итоге приведёт к увеличению общей работы из-за повтора кода
Также сейчас много работы уходит из-за эффекта низкой базы. После создания необходимой базы на её поддержание и расширение не будет уходить много времени
Реализуйте КПЗ-версию метода .push()
для массива. Данный метод добавляет элемент в конец массива
Используйте реализованный метод push в коде по добавлению контакта
function add_contact(mailing_list, email) {
var list_copy = mailing_list.slice();
list_copy.push(email);
return list_copy;
}
Реализуйте КПЗ-метод, который позволит устанавливать значения элементам массива
function arraySet(array, index, value) {
...
}
Предположим, что мы сможем преобразовать все операции записи в операции чтения. В таком случае встаёт вопрос: если все данные стали неизменными, то как приложение может отслеживать изменения, которые происходят? Как пользователь может добавить товар в корзину, если ничего не изменяется?
. . .
В приложении нам потребуется место для хранения изменяемых данных
В примере с корзиной таким местом является глобальная переменная shopping_cart
Значение данной переменной подменяется на новое после его вычисления
shopping_cart = add_item(shopping_cart, shoes);
shopping_cart = remove_item_by_name(shopping_cart, "shirt");
Шаблон подмены значения переменной часто применяется в ФЯП. Он позволяет реализовывать сложные операции, например операции отмены действия
В общем случае применение неизменяемых структур данных потребует большего объёма оперативной памяти и будет работать медленнее по сравнению с применением изменяемых структур данных
Однако на ЯП, использующих неизменяемые структуры данных, реализовано много высоконагруженных приложений, что доказывает применимость данного подхода для промышленного применения
На ранних этапах разработки зачастую трудно узнать о проблемных с точки зрения производительности местах. Это верно вне зависимости от используемого подхода к разработке ПО
В большинстве промышленных ФЯП можно при необходимости выполнить оптимизацию с помощью изменяемых данных
В большинстве сред выполнения языков программирования была проведена большая работа по улучшению работы сборщиков мусора. Слушателям рекомендуется самостоятельно изучить выработанные техники
Если мы рассмотрим функции, реализующие КПЗ, то увидим, что объём копирования на самом деле не большой. Например при работе над корзиной из 100 товаров при копировании происходит только создание массива со 100 ссылками
При неглубоком копировании создаются структуры данных, которые содержат много общего. Такой подход называется структурной общностью
Текущие методы опираются на систему типов JavaScript, используя самую простую реализацию. Для приложения-примера этого достаточно
В специализированных языках применяются специально разработанные методы, которые эффективно используют оперативную память для реализации шаблона КПЗ
Мы рассмотрели вариант реализации КПЗ для массивов в JavaScript. Однако нам необходимо выполнять операции над товарами, которые представлены в виде объектов
Общая структура действий остаётся одинаковой: сделать копию, модифицировать копию, возвратить копию
Для решения задачи с массивами мы использовали метод .slice()
. Для объектов такого метода нет, поэтому мы воспользуемся другим подходом - копированием всех ключей и значений
var object = {a: 1, b: 2};
var object_copy = Object.assign({}, object);
Для изменения цены товара реализуем метод setPrice
function setPrice(item, new_price) {
var item_copy = Object.assign({}, item);
item_copy.price = new_price;
return item_copy;
}
При поверхностном копировании мы делаем копии только первого слоя вложенных структур данных. При поверхностном копировании массива из объектов происходит копирование только ссылок на объекты внутри массива
Данные массивы будут совместно использовать объекты, на которые они ссылаются. Если оба массива остаются неизменяемыми, то разделение не является проблемой
Объекты в JavaScript представляют собой ассоциативные массивы, которые содержат пары ключ-значение и у который ключи являются уникальными
[key]
#
Данный метод получает значение по ключу. Если ключа нет, то возвращает undefined
> var object = {a: 1, b: 2};
> object["a"]
1
.key
#
Альтернативный синтаксис для получения значения
> var object = {a: 1, b: 2};
> object.a
1
.key =
или [key] =
#
Можно установить значение по ключу, изменив объект. Если ключ уже существует, то старое значение будет заменено
> var object = {a: 1, b: 2};
> object["a"] = 7;
7
> object
{a: 7, b: 2}
> object.c = 10;
10
> object
{a: 7, b: 2, c: 10}
delete
#
Данный метод изменяет объект. После его вызова указанная пара ключ-значение удаляется из указанного объекта
> var object = {a: 1, b: 2};
> delete object["a"];
1
> object
{b: 2}
Object.assign(a, b)
#
Данный метод копирует все пары ключ-значения из объекта b
в объект a
, изменяя его
> var object = {x: 1, y: 2};
> Object.assign({}, object);
{x: 1, y: 2}
Object.keys()
#
Данный метод удобно применять, если мы хотим пройти по всем парам объекта. Сначала получаем массив ключей, а затем используем значения из него, чтобы получить все значения
> var object = {a: 1, b: 2};
> Object.keys(object);
["a", "b"]
Реализуйте КПЗ-методы для рассмотренных ранее методов модифицирования
Установка значения: function objectSet(object, key, value)
Используйте метод для установки цены для объекта:
function setPrice(item, new_price) {
var item_copy = Object.assign({}, item);
item_copy.price = new_price;
return item_copy;
}
И для метода установки количества:
function setQuantity(item, new_quantity) {}
Реализуйте КПЗ-метод для удаления пары ключ-значение
function objectDelete(object, key) {}
Даже после применения всех созданных методов операция изменения цены осталась операцией-записью. Она изменяет объект, вложенный в массив корзины
У нас уже есть КПЗ-опреация для вложенного объекта, осталось её применить
function setPriceByName(cart, name, price) {
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name) {
cart[i].price = price;
}
}
}
function setPriceByName(cart, name, price) {
var cartCopy = cart.slice();
for(var i = 0; i < cartCopy.length; i++) {
if(cartCopy[i].name === name) {
cartCopy[i] = setPrice(cartCopy[i], price);
}
}
return cartCopy;
}
Операции над вложенными данными работают по схожей технологии: сделать копию, модифицировать её, вернуть копию
Единственное отличие - выполнение КПЗ над вложенной структурой данных
Предположим, что у нас есть три элемента в корзине: футболка, носки и ботинки. В результате у нас есть 1 массив и 3 объекта
Мы установим цену для футболки в 700 рублей, вызываем метод setPriceByName
function setPriceByName(cart, name, price) {
var cartCopy = cart.slice(); // Копируем массив
for(var i = 0; i < cartCopy.length; i++) {
if(cartCopy[i].name === name) {
cartCopy[i] = setPrice(cartCopy[i], price); // Копируем объект
}
}
return cartCopy;
}
Были скопированы только массив и объект, так как делаются поверхностные копии
Предположим, что корзина состоит из четырёх элементов:
var shopping_cart = [
{name: "shoes", price: 10},
{name: "socks", price: 3},
{name: "pants", price: 27},
{name: "t-shirt", price: 7}
]
И мы выполним следующий код:
setPriceByName(shopping_cart, "socks", 2);
Какие объекты будут скопированы? Какие будут заменены?
Преобразуйте следующий метод с помощью техники КПЗ
function setQuantityByName(cart, name, quantity) {
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
cart[i].quantity = quantity;
}
}