Все мы знаем что в JavaScript есть ссылочные (Object), присваивающиеся по ссылке и примитивные типы данных (String, Number, Null и тд), присваивающиеся по значению. Но так ли это на самом деле? В этой статье с помощью небольшого эксперимента мы убедимся, что это не совсем так и посмотрим как "примитивные" типы данных на самом деле хранятся в памяти.

Создадим небольшой HTML файл:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<a class="la1"></a>
<body>
  <script>
    var word = "word".repeat(1000000);
    document.querySelector('.a1').innerHTML = word;
  </script>
</body>
</html>

Мы создаем переменную word в которой хранится строка, содержащая 1 миллион повторений слова "word"
Сделаем snapshot во вкладке Memory в браузере Chrome и посмотрим на память

Мы видим что наша огромная строка занимает 4Мб памяти

Далее добавим в наш HTML файл немного кода

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<a class="a1"></a>
<a class="a2"></a>
<body>
  <script>
    var word = "word".repeat(1000000);
    // Добавляем переменную word2
    var word2 = word;
    document.querySelector('.a1').innerHTML = word;
    // Отрисовываем ее значение во вторую ссылку 
    document.querySelector('.a2').innerHTML = word;
  </script>
</body>
</html>

Как мы все много раз слышали, примитивные типы передаются по значению, соответственно в переменную word2 должна попасть полная копия значения, которое лежит в переменной word и составляет 4Мб. В итоге мы ожидаем увидеть увеличение памяти в 2 раза.

Давайте смотреть!

Но как так! Память, занимаемая строками не то что не увеличилась в 2 раза, она вообще не изменилась! И мы не наблюдаем второй строки, переменная word2 ссылается на тот же участок памяти, что и переменная word. Никакой копии не произошло. Попробуем по-другому.

Давайте создадим массив и добавим туда несколько значений

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<a class="a1"></a>
<body>
  <script>
    var word = "word".repeat(1000000)
    var arr = [word, word, word, word, word]
    for (let i = 0; i < arr.length - 1; i++) {
        document.querySelector('.a1').innerHTML += word;
    }
  </script>
</body>
</html>

Как мы видим, снова ничего не изменилось. Все 5 элементов массива ссылаются на один и тот же участок памяти.

Мы можем взглянуть на сам массив и увидеть его Shallow Size - объем памяти, непосредственно занимаемый объектом, который не включает память, занимаемую другими объектами, на которые этот объект ссылается.

Вывод

JavaScript движки оптимизируют использование памяти, храня данные в одном месте и создавая ссылки на одну и ту же строку для всех переменных, которые на неё ссылаются.
Однако, если одна из переменных, ссылающихся на одну и ту же строку, изменяется, движок создаст новую строку, которая будет занимать соответствующий объем памяти.
Это позволяет эффективно использовать память и избегать её ненужного увеличения.

Комментарии (20)


  1. Skipy
    26.06.2024 07:26
    +4

    А с каких пор строки стали примитивами?


    1. Lazytech
      26.06.2024 07:26
      +3

      Такова терминология JavaScript.

      https://learn.javascript.ru/primitives-methods
      ЦИТАТА:
      Есть 7 примитивных типов: string, number, boolean, symbol, null, undefined и bigint.

      https://developer.mozilla.org/en-US/docs/Glossary/Primitive
      ЦИТАТА:
      There are 7 primitive data types:
      string
      number
      bigint
      boolean
      undefined
      symbol
      null


    1. AcckiyGerman
      26.06.2024 07:26

      В JS как всегда всё странно. Строки в JS бывают примитивами или объектами. Интерфейс у них при этом одинаковый:

      на строковых примитивах возможно использовать методы объекта String. В контекстах, когда на примитивной строке вызывается метод или происходит поиск свойства, JavaScript автоматически оборачивает строковый примитив объектом и вызывает на нём метод или ищет в нём свойство.

      Но проблемы всё же бывают:

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

      Авторский код "word".repeat(1000000); очевидно возвращает объект, который далее передаётся по ссылке. Чтобы получить примитив строки из объекта строки, нужно использовать метод valueOf() - "word".repeat(1000000).valueOf().

      @Vindrix попробуйте начать читать мануалы.


      1. Vindrix Автор
        26.06.2024 07:26

        Метод repeat возвращает примитивную строку, поэтому valueOf() было бы актуально к

        let objStr = new String("word");
        let primStr = objStr.valueOf();


      1. Alexandroppolus
        26.06.2024 07:26

        Нет, это совсем другое. Вы говорите про "объектные обертки" к примитивным типам. А данная статья именно о примитивных типах, которые под капотом нифига не примитивные.

        "word".repeat(1000000); возвращает именно строку, а не объектную обертку, и valueOf тут ничего не поменяет.


      1. Avangardio
        26.06.2024 07:26

        Ну вообще не стоит создавать инстансы строк через new, а то можно на тех же проверках типов полететь.


  1. ARad
    26.06.2024 07:26

    Это что? Незнание того что существуют неизменяемые типы? Строка это неизменяемый ссылочный тип.


    1. Stawros
      26.06.2024 07:26

      Выглядит как заметка об иммутабельности строк. В c#, например, интернирование строк тема достаточно интересная, но большая часть темы уже касается не простого запихивания одних и тех же констант в массив, а о поведении при операциях со строками, вроде склеивания (var word ="word" и var word2 = "wo" + "rd" - это и то же?).


      1. ViktorVovk
        26.06.2024 07:26

        Нет не тоже, в таком случае в памяти heap создастся три значения


  1. Avangardio
    26.06.2024 07:26

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

    Строки как объекты

    Почти всё в JavaScript является объектами. Когда вы создаёте строку, например:

    let string = 'This is my string';
    

    ваша переменная становится строковым объектом, и, как результат, ей доступно множество свойств и методов.


  1. senfiaj
    26.06.2024 07:26

    Так по мне семантически это верно. Думаю так легче новичкам объяснять чем заморачивать им такими деталями. Хотя очень полезно такое знать.


    1. Avangardio
      26.06.2024 07:26

      Новичкам возможно и не стоит заморачиваться помимо не использования new String(…), а потом вместе с теми же скрытыми классами узнать, когда будет интересно.


  1. Alexandroppolus
    26.06.2024 07:26

    То что строки на самом деле объекты, в общем-то интуитивно понятно. Меня больше удивило, что даже числа могут быть объектами. "Истинных" примитивов в JS немного, вроде только целые 32-разрядные числа со знаком


    1. senfiaj
      26.06.2024 07:26

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

      Я пробовал это
      Number.prototype.myFunc = function () {return this}
      var t = 2
      t.myFunc() === t.myFunc() // false


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


      1. rock
        26.06.2024 07:26

        Попробуйте в строгом режиме.


  1. Gary_Ihar
    26.06.2024 07:26
    +1

    Вывод некорректный по итогу. А эксперимент годный. Я как раз недавно пытался по новой погрузиться в эту тему(удалось не до конца, пока на паузе).

    Стоит заметить, что сразу можно откинуть источники инфы, которые пользуются определением "ссылочный/примитивный". Видимо это тянется с ES 5 https://262.ecma-international.org/5.1/#sec-4.3.2

    В новой спеке используются понятия " Values without identity/ Values with identity " и то ...
    https://tc39.es/ecma262/multipage/notational-conventions.html#sec-identity

    Внимание, ниже мой вывод, который может быть вообще некорректный и требует валидации.

    Как я понял каждое значение имеет некие характеристики, которым можно описывать само значение(в том числе ссылка на значение в памяти). И если с так называемыми примитивами эти характеристики легко получить, то с объектами - нет.

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


    1. sergeygolovan
      26.06.2024 07:26

      Согласно спецификации JS не существует таких понятий как примитивный или ссылочный тип данных (привет @demimurych).

      "JavaScript движки оптимизируют использование памяти"
      Это верно. Но здесь, с большой вероятностью, речь идет о применении оптимизационного механизма на стадии компиляции кода внутри движка (к сожалению, наименования оптимизационного правила на вскидку не припомню).


  1. dom1n1k
    26.06.2024 07:26
    +4

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

    Кстати, мне недавно попадалась информация, что вроде бы V8 по каким-то своим причинам эмулирует floating point арифметику! То есть это не нативные операции CPU, а числа на самом деле тоже как бы объекты. Сам я это утверждение пока не проверял, руки не дошли. Если кто-то в курсе как на самом деле - сообщите.


  1. ViktorVovk
    26.06.2024 07:26
    +1

    Забавная статья, для джуна сойдет)) вообще ссылаться на learnjs (это было в коментах, а не в статье), это плохой пример убедить читателей в компетентности. В спеке ecma, если открыть про строки, тут же прочитаете, что они имутабельны. Из этого можно сделать вывод, а для чего в памяти хранить копии одного значения, если каждое есть имутабельно? Подумав, можно придти к выводу, что смысла нет) а значит как то они все же передаются по ссылке. Тут стоит еще капнуть глубже и понять, что в js не существует переменных как таковых, вы не найдете такого термина в спецификации. Все, что вы называете переменными в js на самом деле называется идентификаторами которые всегда по ссылке соединены со значением. Исключениями, в рамках реализации различных рантаймов, могут быть числа. В v8 числа до 2^31-1 хранятся в smi а не в heap. Вы можете ознакомится, как например можно запустить node js с флагом --allow-natives-syntax, и с помощью команды %DebugPrint посмотреть на что и как ссылаются ваши идентификаторы. Это перевернет Ваш мир)


  1. winkyBrain
    26.06.2024 07:26

    https://habr.com/ru/articles/774548/ более подробный разбор