В какой-то момент своей карьеры разработчики сталкиваются с задачей обмена значениями. Большую часть времени мы пользуемся классическим методом с использованием дополнительной переменной. Ах, если бы был способ лучше. Но подождите-ка! Такой способ есть, и не один. В моменты отчаяния мы серфим интернет в поисках решений, находим одно, копируем его без какого-либо понимания, как работает этот кусочек кода. К счастью для вас, сейчас самое время понять, как поменять местами два значения просто и эффективно.
1) Использование временной переменной
Сразу отсечем самое очевидное решение.
function swapWithTemp(num1,num2){
console.log(num1,num2)
var temp = num1;
num1 = num2;
num2 = temp;
console.log(num1,num2)
}
swapWithTemp(2.34,3.45)
2) Использование сложения и вычитания
Да, вы прочитали правильно. Мы можем использовать немного магии математики, чтобы обменять парочку значений.
function swapWithPlusMinus(num1,num2){
console.log(num1,num2)
num1 = num1+num2;
num2 = num1-num2;
num1 = num1-num2;
console.log(num1,num2)
}
swapWithPlusMinus(2.34,3.45)
Что-о-о!? Ага, а теперь давайте посмотрим, как это работает. Мы получаем сумму двух чисел на 4 строке. Сейчас, если мы вычтем одно число из суммы, то получим другое число. Это как раз то, что мы делаем на 5 строке. Вычитание num2 из суммы, которая находится в num1, дает нам изначальное значение num1, которое помещается в num2. Аналогично мы получаем num2 и помещаем его в num1 на 6 строке.
Осторожно: на просторах интернета гуляет еще один метод обмена в одну строчку лишь с операцией сложения.
Вот как он выглядит:
function swapWithPlusMinusShort(num1,num2){
console.log(num1,num2)
num2 = num1+(num1=num2)-num2;
console.log(num1,num2)
}
swapWithPlusMinusShort(2,3)
Код выше дает ожидаемый результат. Выражение внутри () хранит num2 в num1, затем мы вычитаем num1 - num2, а это ноль, так как num2 - num2 = 0. Следовательно, мы получаем нужный результат. Но когда мы используем числа с плавающей точкой, иногда можем увидеть неожиданный результат.
Поиграйтесь в консоли с дробными входными параметрами, как в коде ниже.
function swapWithPlusMinusShort(num1,num2){
console.log(num1,num2)
num2 = num1+(num1=num2)-num2;
console.log(num1,num2)
}
swapWithPlusMinusShort(2,3.1)
3) Использование сложения или вычитания
Мы можем получить тот же результат, используя только операцию сложения.
Посмотрим, как этого добиться:
function swapWithPlus(num1,num2){
console.log(num1,num2)
num2 = num1 + (num1=num2, 0)
console.log(num1,num2)
}
//Попробуйте реализовать это с помощью только вычитания
swapWithPlus(2.3,3.4)
Эта программа работает, но ее читабельность явно страдает. На 4 строке в скобках мы присваиваем num1 num2 и возвращаем 0. По сути наша строка выглядит так:
num2 = num1 + 0 => num2 = num1
Отсюда мы и получаем наш результат.
Замечание: некоторые движки JavaScript могут выполнять свои собственные оптимизации приведенного выше кода, который игнорирует + 0.
4) Использование умножения и деления
Еще немного магии при помощи операторов умножения и деления.
Принцип тот же, что и в предыдущем методе, но с парочкой "причудов".
function swapWithMulDiv(num1,num2){
console.log(num1,num2)
num1 = num1*num2;
num2 = num1/num2;
num1 = num1/num2;
console.log(num1,num2)
}
swapWithMulDiv(2.34,3.45)
Собственно, здесь все похоже. Получаем произведения двух чисел и храним его в одном из чисел. Это то, что происходит на 4 строке. Затем делим произведение на второе число и получаем первое, потом повторяем процесс и получаем второе число.
Вы сделали это! Да вы "математический фокусник".
Погодите! Что насчет тех "причудов"?
Давайте попробуем запустить следующий код.
function swapWithMulDiv(num1,num2){
console.log(num1,num2)
num1 = num1*num2;
num2 = num1/num2;
num1 = num1/num2;
console.log(num1,num2)
}
//Попробуйте обменять вот таки числа и посмотрите, что выйдет
swapWithMulDiv(2.34,0)
Наши значения не поменяются местами, и вместо этого мы получим NaN. Если вы посещали уроки математики, то помните, что вам всегда говорили не делить на 0, потому что получится неопределенность. Причина кроется в работе ограничений и в некоторых других вещах, в которые мы погружаться не будем. Сейчас же мы посмотрим на еще одну проблему этого метода.
Рассмотрим пример:
function swapWithMulDiv(num1,num2){
console.log(num1,num2)
num1 = num1*num2;
num2 = num1/num2;
num1 = num1/num2;
console.log(num1,num2)
}
//Попробуйте обменять вот таки числа и посмотрите, что выйдет
swapWithMulDiv(2.34,Infinity)
Да, вновь NaN. Потому что мы не можем делить на бесконечность, это также неопределенность.
Хотите увидеть еще одну причуду? Я так и думал!
function swapWithMulDiv(num1,num2){
console.log(num1,num2)
num1 = num1*num2;
num2 = num1/num2;
num1 = num1/num2;
console.log(num1,num2)
}
//Попробуйте обменять вот таки числа и посмотрите, что выйдет
swapWithMulDiv(2.34,-Infinity)
-бесконечность будет возвращать то же значение, как в прошлом примере, по той же причине.
Даже "математический фокусник" при всех своих силах не может сделать невозможного.
Ниже представлена краткая версия обмена с умножением и делением с теми же проблемами:
function swapWithMulDivShort(num1,num2){
console.log(num1,num2)
num2 = num1*(num1=num2)/num2;
console.log(num1,num2)
}
swapWithMulDivShort(2.3,3.4)
Этот код аналогичен краткой версии обмена со сложением и вычитанием. Мы присваиваем num1 num2, а потом 4 строка выглядит так:
num2 = num1 * num2 / num2
=> num2 = num1
Вуаля! Наши значения поменялись местами.
5) Использование умножения или деления
function swapWithMul(num1,num2){
console.log(num1,num2)
num2 = num1 * (num1=num2, 1)
console.log(num1,num2)
}
//попробуйте сделать обмен через деление и возведение в степень
swapWithMul(2.3,3.4)
Эта программа работает, но ее читабельность явно страдает. На 4 строке в скобках мы присваиваем num1 num2 и возвращаем 1. На деле наша строка выглядит так:
num2 = num1 * 1 => num2 = num1
И мы вновь получаем требуемый результат.
6) Использование побитового исключающего ИЛИ
XOR манипулирует битами. Возвращает 1, когда значения переменных различны, и 0 в противном случае.
X |
Y |
X^Y |
1 |
1 |
0 |
1 |
0 |
1 |
0 |
1 |
1 |
0 |
0 |
0 |
Теперь поймем, как это работает.
function swapWithXOR(num1,num2){
console.log(num1,num2)
num1 = num1^num2;
num2 = num1^num2;
num1 = num1^num2;
console.log(num1,num2)
}
// протестируйте также отрицательные значения
swapWithXOR(10,1)
4-ех битное представление 10 -> 1010
4-ех битное представление 1 -> 0001
Тогда:
На 4 строке: num1 = num1 ^ num2 => 1010 ^ 0001 => 1011 => 7
На 5 строке: num2 = num1 ^ num2 => 1011 ^ 0001 => 1010 => 10
На 6 строке: num1 = num1 ^ num2 => 1011 ^ 1010 => 0001 => 1
Вуаля! Мы вновь поменяли значения местами.
Посмотрим другой пример.
function swapWithXOR(num1,num2){
console.log(num1,num2)
num1 = num1^num2;
num2 = num1^num2;
num1 = num1^num2;
console.log(num1,num2)
}
swapWithXOR(2.34,3.45)
Хм, где же обмен? Мы получили только целую часть числа. И в этом проблема такого способа. XOR предполагает, что входные параметры это целые числа, и уже в соответствии с этим производит вычисления. Но числа с плавающей точкой не являются целыми и представлены стандартом IEEE 754, согласно которому число разбивается на 3 части: знаковый бит, группа битов, представляющих показатель степени, еще одна группа битов, представляющая число между 1 (включая) и 2 (не включая), то есть мантиссу. Поэтому мы и получаем не тот результат.
Еще один пример:
function swapWithXOR(num1,num2){
console.log(num1,num2)
num1 = num1^num2;
num2 = num1^num2;
num1 = num1^num2;
console.log(num1,num2)
}
//Поэксперементируйте с целыми числами и бесконечностью.
swapWithXOR(-Infinity,Infinity)
И снова не тот результат, который мы ожидали. Все потому, что бесконечность и -бесконечность это числа с плавающей точкой, а, как мы уже поняли, для XOR это проблема.
7) Использование побитового исключающего НЕ ИЛИ
Эта операция противоположна исключающему ИЛИ. XNOR возвращает 0, если значения различны, и 1 в противном случае. В JavaScript нет специального оператора для XNOR, так что мы используем NOT оператор, чтобы инвертировать XOR.
X |
Y |
XNOR |
1 |
1 |
1 |
1 |
0 |
0 |
0 |
1 |
0 |
0 |
0 |
1 |
Давайте поймем, как это работает!
function swapWithXNOR(num1,num2){
console.log(num1,num2)
num1 = ~(num1^num2);
num2 = ~(num1^num2);
num1 = ~(num1^num2);
console.log(num1,num2)
}
//Попробуйте отрицательные значения
swapWithXNOR(10,1)
4-ех битное представление 10 -> 1010
4-ех битное представление 1 -> 0001
На 4 строчке:
num1 = ~(num1 ^ num2) => ~(1010 ^ 0001) =>~(1011) => ~11 => -12
Так как мы получили отрицательное значение, нам необходимо конвертировать его в двоичное, инвертировать биты и прибавить единицу:
-12 => 1100 => 0011 + 1 => 0100
На 5 строчке:
num2 = ~(num1 ^ num2) => ~(0100 ^ 0001) => ~(0101) => ~5 => -6
-6 => 0110 => 1001 + 1 => 1010 => 10
На 6 строчке:
num1 = ~(num1 ^ num2) => ~(0100^ 1010) => ~(1110) => ~14 => -15
-15 => 1111 => 0000 + 1 => 0001 => 1
Это займет некоторое время, но мы получим обмен значений. Но, к сожалению, у этого способа будут те же проблемы, что и у метода с XOR. Все плохо с числами с плавающей точкой и бесконечностями.
Попробуйте поэкспериментировать со значениями, как в коде ниже.
function swapWithXNOR(num1,num2){
console.log(num1,num2)
num1 = ~(num1^num2);
num2 = ~(num1^num2);
num1 = ~(num1^num2);
console.log(num1,num2)
}
swapWithXNOR(2.3,4.5)
8) Использование присваивания внутри массива
Это трюк в одну строку. Правда! Вам просто нужна одна строка для обмена и, что более важно, никакой математики. Понадобятся лишь базовые знания о массивах. Может выглядеть странно, но оно работает.
Посмотрим на этот метод в действии!
function swapWithArray(num1,num2){
console.log(num1,num2)
num2 = [num1, num1 = num2][0];
console.log(num1,num2)
}
swapWithArray(2.3,Infinity)
В нулевом индексе массива хранится num1, в 1 индексе мы присваиваем num1 num2 и, соответственно, храним num2. Затем мы просто обращаемся к 0 индексу, чтобы получить num1 и присвоить это значения num2. Таким образом мы может менять и целые числа, и числа с плавающей точкой, и бесконечности, и даже строки. Выглядит довольно аккуратно, но мало читабельно. Взглянем на похожий способ.
9) Использование деструктуризации
Это нововведение, появившееся в ES6. И оно очень простое. В одну линию мы можем поменять значения местами:
let num1 = 23.45;
let num2 = 45.67;
console.log(num1,num2);
[num1,num2] = [num2,num1];
console.log(num1,num2);
10) Использование немедленно вызываемой функции (IIFE)
Еще один странный вариант обмена. IIFE - это функция, которая вызывается сразу после ее объявления.
Посмотрим, как мы можем ее использовать:
function swapWithIIFE(num1,num2){
console.log(num1,num2)
num1 = (function (num2){ return num2; })(num2, num2=num1)
console.log(num1,num2)
}
swapWithIIFE(2.3,3.4)
В этом примере мы сразу же вызываем функцию на 4 строке. Скобки в конце это аргументы. Во втором аргументе мы присваиваем num2 num1, а первый просто возвращается из функции. Наши значения вновь поменялись местами. Но имейте в виду, этот способ не эффективный.
Заключение
В этой статье мы рассмотрели несколько способов обмена значениями в JavaScript. Надеюсь, вы подчерпнули для себя что-то новое. Спасибо за прочтение!
Комментарии (18)
orekh
27.03.2022 11:25+2Только 1, 8, 9 и 10 можно использовать без опасений что-то сломать. Только 1-й не создает массивов или функций, так что он быстр. Но 9-й способ побеждает в лаконичности.
Saiv46
27.03.2022 13:379-й способ будет быстрым когда допилят оптимизации в движке, ибо тут создаётся массив и сразу же из неё берутся значения.
KhodeN
27.03.2022 17:13Он и так быстр. За исключением обмена больших строк. В остальных случаях это либо простое скалярное значение, либо ссылки на объекты. Массив из двух ссылок - очень легковесная штука.
Но классический вариант с временной переменной все же лучше. В том числе для читаемости.
xxxphilinxxx
27.03.2022 13:29+2Все описанное сводится к двум идеям:
(memory-bound) копирование через дополнительную память: полноценную переменную или временное значение при вычислении выражений. Разница в синтаксисе и дополнительных операциях => нагрузке на проц и выделении памяти.
(CPU-bound) математические операции над данными как над числами, которые накладывают дополнительные ограничения даже на базовые числовые типы, а уж при работе с объектами вовсе никуда не годятся: потребуют работы с сырой памятью (в JS недоступно) и могут нарушать целостность данных (например, перемещать данные, на которые ссылаются указатели). Можно было бы обмен указателями на эти объекты производить (в JS недоступно), но опять-таки ограничения как на числовые типы, да еще и особенности арифметики указателей в языке могут выстрелить.
В итоге обычно (кроме случаев редких просчитанных оптимизаций, имхо нетипичных для JS) все-таки выбирается классический обмен через память в любом удобном синтаксическом выражении: простой, понятный, быстрый, безопасный.
demimurych
27.03.2022 16:01Можно было бы обмен указателями на эти объекты производить (в JS недоступно),
в javascript все идентификаторы являются указателями. Что соответсвует как официальной спецификации, так и реализации ее в случае v8.
исключением являются smi, значение которых, позволяет поместиться в 31 бит.
копирование через дополнительную память: полноценную переменную или временное значение при вычислении выражений. Разница в синтаксисе и дополнительных операциях => нагрузке на проц и выделении памяти.
в javascript, все данные имутабельны. потому, все попытки что либо куда либо переместить и поменять, фактически сводятся к тому, что в конкретном идентификаторе, меняется сама ссылка на данные. Но сами данные, даже те, на которые больше не осталось ни единой ссылки - лежат себе ровно в тех же местах где и были размещены в момент выполнения литерала.
и единтсвенная возможность увидеть изменение адреса у данных, это ситуация когда начнет работать gc при большой фрагментации. Но тут уже не о сылках нужно думать.
xxxphilinxxx
27.03.2022 18:12Можно было бы обмен указателями на эти объекты производить (в JS недоступно)
в javascript все идентификаторы являются указателями
Немного поспорю здесь с вами :) при наличии указателей, обмен сложными типами с помощью математики можно было бы проводить, оперируя значениями указателей (=адресами) как числами. Но в JS это тоже недоступно, т.к. указатели (pointer), о которых вы говорите, так-то являются ссылками (reference), а из ссылки нельзя получить адрес - вот, что тут важно. Получение адреса из ссылки возвращало бы как раз указатель, которых в JS нет.
Их, бывает, путают, но это не одно и то же. И указатель, и ссылка предоставляют множественный доступ к области памяти, но указатель - это тип данных, содержащий непосредственно адрес значения, а ссылка - механизм связывания переменной с областью памяти, уже использующейся другой переменной.А обмен ссылками через память так же неинтересен для разбора, как и числами.
в javascript, все данные имутабельны. потому, все попытки что либо куда либо переместить и поменять, фактически сводятся к тому, что в конкретном идентификаторе, меняется сама ссылка на данные
Кроме нескольких примитивных типов - вы же сами о SMI пишете. С ними будет изменение самих данных, а не жонглирование ссылками. С остальным согласен.
Razoomnick
27.03.2022 22:02Поддержу, именно такие мысли возникли и у меня.
Добавлю только, что обзор способов поменять значения переменных местами плох тем, что уровень материала рассчитан на новичка, а у новичка недостаточно опыта, чтобы понять, что в реальных проектах можно использовать только один-два из них.
Будь посыл другим, типа давайте посмотрим, как работают некоторые аспекты языка, и почему работают именно так, и в качестве мета-шутки будем менять значения переменных местами - было бы намного лучше. По сути ведь речь идет про это, хоть и не всегда развернуто.
pqbd
27.03.2022 15:32+1Когда я был студентом, меня как-то попросили на собеседовании поменять два числа местами без введения новой переменной. Я сделал через сложение и вычитание… Мне всячески подсказывали вариант через XOR, но я как-то даже не вспомнил. Меня не взяли. Зато, любопытства ради, потом придумал свой велосипед (так что спасибо добрым людям за пинок). Язык на собеседовании был PHP, но можно и на JS...
Применять на практике никому не рекомендую, как и поднимать этот вопрос на собеседованиях (спрашивайте что-нибудь доброе, светлое) :)
Короче...
съешь ещё этих мягких французских булок, да выпей чаю:
a = b + (0 & (b = a))
если а и б строки, то можно что-то типаa = b + ((0 & (b = a)) || '')
Скорость хромает… Нужно не хуже, чем через известный XOR
a = b ^ (0 & (b = a))
а можно и быстрее на пару процентов (* по мнению https://jsbench.me/)a = b >> (0 & (b = a))
pqbd
29.03.2022 17:15а кому понравились варианты 8 и 9, можно ещё вот так:
a = [b][0 & (b = a)]
что, по мнению того же jsbench, быстрее эдак на 15% и 30% соответственно (:
Format-X22
28.03.2022 01:37Самый простой вариант на самом деле будет такой:
function swap(num1,num2){ console.log(num1,num2) console.log(num2,num1) } swap(2.34,3.45)
Это мета-логический свап, в котором мы используем нашу свободу к применении нужной переменной в нужном месте без расхода вычислительных ресурсов. Подумайте-покрутите эту мысль в голове ;-)
Delagen
29.03.2022 11:31Заголовок про обмен "значениями", но 90% описанных методов подходит только для "чисел" и то с оговорками.... Смысл статьи?
thenonsense
Примечательно, что вариант через строки даже не рассматривался.
danilovmy
написал реальному автору.
На мой вгляд, сваппинг - это признак плохого алгоритма, поскольку в коде не происходит ничего полезного. Да-да, если я пишу сортировку пузырьковым методом вручную, скорее всего, на собеседовании, то можно конечно подумать. Да и то, это только если я собеседуюсь на ассемблере под RISC процессоры с урезанным набором команд, спойлер, там я сделаю свап через доп реестр, потому, как это быстрее.
Мне было бы интересно увидеть пример, где свап реально необходим и неизбежен.
thenonsense
Я сколько игровых прототипов не программировал - вот именно чистый свап понадобился примерно около 0 раз (через временную переменную обычно что-то ещё происходит, а не обмен значениями просто так). Но статья так-то про варианты, а не целесообразность.
dom1n1k
Это бывает делают для упорядочивания входных параметров.
Типа есть someMethod(a, b) и код предполагает, что a >= b.
Если это не так — меняем местами.