В какой-то момент своей карьеры разработчики сталкиваются с задачей обмена значениями. Большую часть времени мы пользуемся классическим методом с использованием дополнительной переменной. Ах, если бы был способ лучше. Но подождите-ка! Такой способ есть, и не один. В моменты отчаяния мы серфим интернет в поисках решений, находим одно, копируем его без какого-либо понимания, как работает этот кусочек кода. К счастью для вас, сейчас самое время понять, как поменять местами два значения просто и эффективно.

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)


  1. thenonsense
    27.03.2022 11:15

    Примечательно, что вариант через строки даже не рассматривался.


    1. danilovmy
      27.03.2022 11:39
      +2

      написал реальному автору.

      На мой вгляд, сваппинг - это признак плохого алгоритма, поскольку в коде не происходит ничего полезного. Да-да, если я пишу сортировку пузырьковым методом вручную, скорее всего, на собеседовании, то можно конечно подумать. Да и то, это только если я собеседуюсь на ассемблере под RISC процессоры с урезанным набором команд, спойлер, там я сделаю свап через доп реестр, потому, как это быстрее.

      Мне было бы интересно увидеть пример, где свап реально необходим и неизбежен.


      1. thenonsense
        27.03.2022 13:35

        Я сколько игровых прототипов не программировал - вот именно чистый свап понадобился примерно около 0 раз (через временную переменную обычно что-то ещё происходит, а не обмен значениями просто так). Но статья так-то про варианты, а не целесообразность.


      1. dom1n1k
        27.03.2022 15:03
        +3

        Это бывает делают для упорядочивания входных параметров.
        Типа есть someMethod(a, b) и код предполагает, что a >= b.
        Если это не так — меняем местами.


  1. orekh
    27.03.2022 11:25
    +2

    Только 1, 8, 9 и 10 можно использовать без опасений что-то сломать. Только 1-й не создает массивов или функций, так что он быстр. Но 9-й способ побеждает в лаконичности.


    1. Saiv46
      27.03.2022 13:37

      9-й способ будет быстрым когда допилят оптимизации в движке, ибо тут создаётся массив и сразу же из неё берутся значения.


      1. KhodeN
        27.03.2022 17:13

        Он и так быстр. За исключением обмена больших строк. В остальных случаях это либо простое скалярное значение, либо ссылки на объекты. Массив из двух ссылок - очень легковесная штука.

        Но классический вариант с временной переменной все же лучше. В том числе для читаемости.


  1. xxxphilinxxx
    27.03.2022 13:29
    +2

    Все описанное сводится к двум идеям:

    • (memory-bound) копирование через дополнительную память: полноценную переменную или временное значение при вычислении выражений. Разница в синтаксисе и дополнительных операциях => нагрузке на проц и выделении памяти.

    • (CPU-bound) математические операции над данными как над числами, которые накладывают дополнительные ограничения даже на базовые числовые типы, а уж при работе с объектами вовсе никуда не годятся: потребуют работы с сырой памятью (в JS недоступно) и могут нарушать целостность данных (например, перемещать данные, на которые ссылаются указатели). Можно было бы обмен указателями на эти объекты производить (в JS недоступно), но опять-таки ограничения как на числовые типы, да еще и особенности арифметики указателей в языке могут выстрелить.

    В итоге обычно (кроме случаев редких просчитанных оптимизаций, имхо нетипичных для JS) все-таки выбирается классический обмен через память в любом удобном синтаксическом выражении: простой, понятный, быстрый, безопасный.


    1. demimurych
      27.03.2022 16:01

      Можно было бы обмен указателями на эти объекты производить (в JS недоступно),

      в javascript все идентификаторы являются указателями. Что соответсвует как официальной спецификации, так и реализации ее в случае v8.

      исключением являются smi, значение которых, позволяет поместиться в 31 бит.

      копирование через дополнительную память: полноценную переменную или временное значение при вычислении выражений. Разница в синтаксисе и дополнительных операциях => нагрузке на проц и выделении памяти.

      в javascript, все данные имутабельны. потому, все попытки что либо куда либо переместить и поменять, фактически сводятся к тому, что в конкретном идентификаторе, меняется сама ссылка на данные. Но сами данные, даже те, на которые больше не осталось ни единой ссылки - лежат себе ровно в тех же местах где и были размещены в момент выполнения литерала.

      и единтсвенная возможность увидеть изменение адреса у данных, это ситуация когда начнет работать gc при большой фрагментации. Но тут уже не о сылках нужно думать.


      1. xxxphilinxxx
        27.03.2022 18:12

        Можно было бы обмен указателями на эти объекты производить (в JS недоступно)

        в javascript все идентификаторы являются указателями

        Немного поспорю здесь с вами :) при наличии указателей, обмен сложными типами с помощью математики можно было бы проводить, оперируя значениями указателей (=адресами) как числами. Но в JS это тоже недоступно, т.к. указатели (pointer), о которых вы говорите, так-то являются ссылками (reference), а из ссылки нельзя получить адрес - вот, что тут важно. Получение адреса из ссылки возвращало бы как раз указатель, которых в JS нет.
        Их, бывает, путают, но это не одно и то же. И указатель, и ссылка предоставляют множественный доступ к области памяти, но указатель - это тип данных, содержащий непосредственно адрес значения, а ссылка - механизм связывания переменной с областью памяти, уже использующейся другой переменной.

        А обмен ссылками через память так же неинтересен для разбора, как и числами.

        в javascript, все данные имутабельны. потому, все попытки что либо куда либо переместить и поменять, фактически сводятся к тому, что в конкретном идентификаторе, меняется сама ссылка на данные

        Кроме нескольких примитивных типов - вы же сами о SMI пишете. С ними будет изменение самих данных, а не жонглирование ссылками. С остальным согласен.


    1. Razoomnick
      27.03.2022 22:02

      Поддержу, именно такие мысли возникли и у меня.

      Добавлю только, что обзор способов поменять значения переменных местами плох тем, что уровень материала рассчитан на новичка, а у новичка недостаточно опыта, чтобы понять, что в реальных проектах можно использовать только один-два из них.

      Будь посыл другим, типа давайте посмотрим, как работают некоторые аспекты языка, и почему работают именно так, и в качестве мета-шутки будем менять значения переменных местами - было бы намного лучше. По сути ведь речь идет про это, хоть и не всегда развернуто.


  1. 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))


    1. pqbd
      29.03.2022 17:15

      а кому понравились варианты 8 и 9, можно ещё вот так:
      a = [b][0 & (b = a)]
      что, по мнению того же jsbench, быстрее эдак на 15% и 30% соответственно (:


  1. Format-X22
    28.03.2022 01:37

    Самый простой вариант на самом деле будет такой:

    function swap(num1,num2){
      console.log(num1,num2)
      console.log(num2,num1)
    }
    
    swap(2.34,3.45)

    Это мета-логический свап, в котором мы используем нашу свободу к применении нужной переменной в нужном месте без расхода вычислительных ресурсов. Подумайте-покрутите эту мысль в голове ;-)


  1. seycom
    28.03.2022 09:21

    [ картинка про троллейбус из буханки ]


  1. makar_crypt
    28.03.2022 09:23
    -1

    [num1,num2] = [num2,num1];

    самый нужный )


  1. Delagen
    29.03.2022 11:31

    Заголовок про обмен "значениями", но 90% описанных методов подходит только для "чисел" и то с оговорками.... Смысл статьи?


  1. dzhiharev
    29.03.2022 21:07

    Держите еще в копилку бесполезных знаний :)
    {a, b} = {a:b, b:a}