В чем разница между строковыми примитивами и объектами String в JavaScript?

116

Взято из MDN

Строковые литералы (обозначаемые двойными или одинарными кавычками) и строки, возвращаемые из вызовов String в контексте, не являющемся конструктором (т. Е. Без использования ключевого слова new), являются примитивными строками. JavaScript автоматически преобразует примитивы в объекты String, так что можно использовать методы объекта String для примитивных строк. В контекстах, где метод должен быть вызван для примитивной строки или происходит поиск свойства, JavaScript автоматически оборачивает строковый примитив и вызывает метод или выполняет поиск свойства.

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

Но в этом тестовом примере результат противоположный. В блок 1-код работает быстрее , чем код блока-2 , оба кодовых блоков приведены ниже:

кодовый блок-1:

var s = '0123456789';
for (var i = 0; i < s.length; i++) {
  s.charAt(i);
}

кодовый блок-2:

var s = new String('0123456789');
for (var i = 0; i < s.length; i++) {
    s.charAt(i);
}

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

Альфа
источник
6
Использование new Stringвводит еще один прозрачный слой обтекания объекта . typeof new String(); //"object"
Paul S.
о чем '0123456789'.charAt(i)?
Юрий Галантер
@YuriyGalanter, это не проблема, но я спрашиваю, почему code block-1быстрее?
Альфа
2
Строковые объекты относительно редко можно увидеть в реальном контексте, поэтому для интерпретаторов неудивительно, что они оптимизируют строковые литералы. В настоящее время ваш код не просто интерпретируется , существует множество уровней оптимизации, которые происходят за кулисами.
Fabrício Matté
2
Это странно: ревизия 2
hjpotter92

Ответы:

149

В JavaScript есть две основные категории типов: примитивы и объекты.

var s = 'test';
var ss = new String('test');

Шаблоны одинарных / двойных кавычек идентичны с точки зрения функциональности. Помимо этого, поведение, которое вы пытаетесь назвать, называется автоматическим боксом. Итак, что на самом деле происходит, так это то, что примитив преобразуется в свой тип оболочки при вызове метода типа оболочки. Поставьте просто:

var s = 'test';

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

Так что же происходит, например, когда вы это делаете s.charAt(i)?

Так как sэто не является экземпляром String, JavaScript будет автоматически поле s, которое имеет typeof stringего тип обертки, Stringс typeof objectили более точно s.valueOf(s).prototype.toString.call = [object String].

Поведение автобокса sпо мере необходимости приводит к типу оболочки, но стандартные операции невероятно быстрые, поскольку вы имеете дело с более простым типом данных. Однако автобокс и Object.prototype.valueOfэффекты имеют разные.

Если вы хотите принудительно установить автоматическую упаковку или привести примитив к его типу оболочки, вы можете использовать Object.prototype.valueOf, но поведение будет другим. Основываясь на большом количестве сценариев тестирования, автоматическая упаковка применяет только «необходимые» методы, не изменяя примитивный характер переменной. Вот почему вы получаете лучшую скорость.

Флавиан
источник
33

Это скорее зависит от реализации, но я попробую. Я приведу пример с V8, но предполагаю, что другие движки используют аналогичные подходы.

Строковый примитив разбирается на v8::Stringобъект. Следовательно, методы могут быть вызваны непосредственно на нем, как указано в jfriend00 .

С другой стороны, объект String анализируется до v8::StringObjectрасширяемого Objectобъекта и, помимо того, что он является полноценным объектом, служит оболочкой для v8::String.

Теперь это логично, вызов new String('').method()должен распаковывать это v8::StringObject«S v8::Stringперед выполнением методы, поэтому он медленнее.


Во многих других языках примитивные значения не имеют методов.

То, как это выражается в MDN, кажется самым простым способом объяснить, как работает автоматическая упаковка примитивов (как также упоминалось в ответе flav ), то есть как значения примитивов y в JavaScript могут вызывать методы.

Однако интеллектуальный движок не будет преобразовывать строковый примитив-y в объект String каждый раз, когда вам нужно вызвать метод. Это также информативно упоминается в аннотированной спецификации ES5. что касается разрешения свойств (и "методов" ¹) примитивных значений:

ПРИМЕЧАНИЕ . Объект, который может быть создан на шаге 1, недоступен вне описанного выше метода. Реализация может решить избежать фактического создания объекта. [...]

На очень низком уровне строки чаще всего реализуются как неизменяемые скалярные значения. Пример структуры оболочки:

StringObject > String (> ...) > char[]

Чем дальше вы от примитива, тем больше времени потребуется, чтобы добраться до него. На практике Stringпримитивы встречаются гораздо чаще, чем StringObjects, поэтому для движков не удивительно добавлять методы к классу соответствующих (интерпретируемых) объектов строковых примитивов вместо преобразования туда и обратно между Stringи, StringObjectкак предлагает объяснение MDN.


¹ В JavaScript «метод» - это просто соглашение об именах для свойства, которое преобразуется в значение типа function.

Фабрисио Матте
источник
1
Пожалуйста. =]Теперь мне интересно, существует ли объяснение MDN только потому, что это, кажется, самый простой способ понять автобокс, или есть ли какая-либо ссылка на него в спецификации ES .. Читая всю спецификацию в данный момент, чтобы проверить, не забудьте обновите ответ, если найду ссылку.
Fabrício Matté
Отличное понимание реализации V8. Добавлю, что бокс нужен не только для решения этой функции. Здесь также можно передать ссылку this в метод. Я не уверен, пропускает ли V8 это для встроенных методов, но если вы добавите свое собственное расширение, чтобы сказать String.prototype, вы будете получать коробочную версию строкового объекта каждый раз, когда он вызывается.
Бен
17

В случае строкового литерала мы не можем назначать свойства

var x = "hello" ;
x.y = "world";
console.log(x.y); // this will print undefined

Тогда как в случае String Object мы можем назначать свойства

var x = new String("hello");
x.y = "world";
console.log(x.y); // this will print world
рефакторинг
источник
1
Наконец, кто-то мотивирует, почему нам Stringтоже нужны предметы. Спасибо!
Ciprian Tomoiagă
1
Зачем кому-то это нужно?
Aditya
11

Строковый литерал:

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

var a = 's';
var b = 's';

a==b Результатом будет «истина», обе строки ссылаются на один и тот же объект.

Строковый объект:

Здесь создаются два разных объекта, и у них разные ссылки:

var a = new String("s");
var b = new String("s");

a==b результат будет ложным, потому что у них разные ссылки.

Ваджахат Али Куреши
источник
1
Является ли строковый объект неизменным?
Ян Ван
@YangWang, это глупый язык, потому что оба a& bпытаются назначить, a[0] = 'X'он будет выполнен успешно, но не будет работать, как вы могли ожидать
ruX
Вы написали: «var a = 's'; var b = 's'; a == b результат будет« true », обе строки ссылаются на один и тот же объект». Это неверно: a и b не относятся к одному и тому же объекту, результат верен, потому что они имеют одинаковое значение. Эти значения хранятся в разных ячейках памяти, поэтому, если вы измените одно, другое не изменится!
SC1000
9

Если вы используете new , вы явно заявляете, что хотите создать экземпляр объекта . Таким образом, new Stringпроизводит в объект Обертывания строки примитива, что означает любое действие на нем включает дополнительный слой работы.

typeof new String(); // "object"
typeof '';           // "string"

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

Пол С.
источник
5

Когда вы заявляете:

var s = '0123456789';

вы создаете строковый примитив. У этого строкового примитива есть методы, которые позволяют вам вызывать на нем методы без преобразования примитива в объект первого класса. Итак, ваше предположение, что это будет медленнее, потому что строка должна быть преобразована в объект, неверно. Его не нужно преобразовывать в объект. Сам примитив может вызывать методы.

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

jfriend00
источник
Почему строковый примитив наследует все свойства прототипа, включая пользовательские String.prototype?
Юрий Галантер
1
var s = '0123456789';это примитивное значение, как это значение может иметь методы, я запутался!
Альфа
2
@SheikhHeera - примитивы встроены в языковую реализацию, поэтому интерпретатор может наделить их особыми полномочиями.
jfriend00
1
@SheikhHeera - Я не понимаю ваш последний комментарий / вопрос. Строковый примитив сам по себе не поддерживает добавление к нему собственных свойств. Для этого в javascript также есть объект String, который имеет все те же методы, что и строковый примитив, но представляет собой полноценный объект, с которым вы можете обращаться как с объектом во всех отношениях. Эта двойная форма кажется немного запутанной, но я подозреваю, что это было сделано в качестве компромисса для производительности, поскольку в 99% случае используются примитивы, и они, вероятно, могут быть быстрее и эффективнее с точки зрения памяти, чем строковые объекты.
jfriend00
1
@SheikhHeera «преобразованный в строковый объект» - это то, как MDN выражает это, чтобы объяснить, как примитивы могут вызывать методы. Они буквально не конвертируются в строковые объекты.
Fabrício Matté,
4

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

По сути, еще одно различие между ними заключается в использовании eval. eval ('1 + 1') дает 2, тогда как eval (new String ('1 + 1')) дает '1 + 1', поэтому, если определенный блок кода может выполняться как "нормально", так и с eval, он может привести к странным результатам

luanped
источник
Спасибо за ваш вклад :-)
Альфа
Вау, это действительно странное поведение. Вы должны добавить небольшую встроенную демонстрацию в свой комментарий, чтобы продемонстрировать это поведение - это чрезвычайно открывает глаза.
EyuelDK
это нормально, если задуматься. new String("")возвращает объект, а eval оценивает только строку, а все остальное возвращает как есть
Феликс Брюне
3

Существование объекта имеет мало общего с фактическим поведением String в движках ECMAScript / JavaScript, поскольку корневая область видимости будет просто содержать объекты функций для этого. Таким образом, функция charAt (int) в случае строкового литерала будет найдена и выполнена.

С реальным объектом вы добавляете еще один слой, на котором метод charAt (int) также ищется на самом объекте до того, как сработает стандартное поведение (как и выше). Видимо, здесь проделана на удивление большая работа.

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

чистая вода
источник
3

Самая большая разница между строковым примитивом и строковым объектом заключается в том, что объекты должны следовать этому правилу для ==оператора :

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

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

"hello" == "hello"
-> true
new String("hello") == new String("hello") // beware!
-> false

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

personal_cloud
источник
Спасибо за добавление ценности к вопросу после довольно долгого времени :-)
Альфа
1

Код оптимизируется перед запуском движком javascript. В общем, микротесты могут вводить в заблуждение, потому что компиляторы и интерпретаторы переупорядочивают, изменяют, удаляют и выполняют другие трюки с частями вашего кода, чтобы заставить его работать быстрее. Другими словами, написанный код сообщает, какова цель, но компилятор и / или среда выполнения решают, как достичь этой цели.

Блок 1 быстрее в основном из-за: var s = '0123456789'; всегда быстрее, чем var s = new String ('0123456789'); из-за накладных расходов на создание объекта.

Часть цикла не является причиной замедления, потому что chartAt () может быть встроен интерпретатором. Попробуйте удалить петлю и перезапустите тест, вы увидите, что соотношение скоростей будет таким же, как если бы петля не была удалена. Другими словами, для этих тестов блоки цикла во время выполнения имеют точно такой же байт-код / ​​машинный код.

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

дуга
источник
1
Спасибо за Ваш ответ.
Альфа,
0

В Javascript примитивные типы данных, такие как строка, являются несоставным строительным блоком. Это означает, что это просто ценности, не более того: let a = "string value"; по умолчанию нет встроенных методов, таких как toUpperCase, toLowerCase и т. Д.

Но, если вы попытаетесь написать:

console.log( a.toUpperCase() ); or console.log( a.toLowerCase() );

Это не вызовет никаких ошибок, вместо этого они будут работать так, как должны.

Что произошло ? Что ж, когда вы пытаетесь получить доступ к свойству строки, aJavascript приводит строку к объекту, new String(a);известному как объект-оболочка .

Этот процесс связан с концепцией, называемой конструкторами функций в Javascript, где функции используются для создания новых объектов.

Когда вы вводите new String('String value');здесь String - это конструктор функции, который принимает аргумент и создает пустой объект внутри области функции, этот пустой объект назначается этому и в этом случае String предоставляет все известные встроенные функции, о которых мы упоминали ранее. и как только операция завершена, например, операция в верхнем регистре, объект оболочки отбрасывается.

Чтобы доказать это, давайте сделаем следующее:

let justString = 'Hello From String Value';
justString.addNewProperty = 'Added New Property';
console.log( justString );

Здесь вывод будет неопределенным. Зачем ? В этом случае Javascript создает объект-оболочку String, устанавливает новое свойство addNewProperty и немедленно отбрасывает объект-оболочку. вот почему вы получаете undefined. Псевдокод будет выглядеть так:

let justString = 'Hello From String Value';
let wrapperObject = new String( justString );
wrapperObject.addNewProperty = 'Added New Property'; //Do operation and discard
Александр Гарибашвили
источник
0

мы можем определить String тремя способами

  1. var a = "первый способ";
  2. var b = String («второй путь»);
  3. var c = new String («третий путь»);

// также мы можем создавать используя 4. var d = a + '';

Проверить тип созданных строк с помощью оператора typeof

  • typeof // "строки"
  • typeof b // "строка"
  • typeof c // "объект"


когда вы сравниваете a и b var a==b ( // yes)


когда вы сравниваете объект String

var StringObj = new String("third way")
var StringObj2 = new String("third way")
StringObj  == StringObj2 // no result will be false, because they have different references
СоуМитя чаухан
источник