Jaka jest różnica między prymitywami łańcuchowymi a obiektami typu String w JavaScript?

116

Pochodzą z MDN

Literały łańcuchowe (oznaczone podwójnymi lub pojedynczymi cudzysłowami) i ciągi zwracane przez wywołania String w kontekście innym niż konstruktor (tj. Bez użycia słowa kluczowego new) są ciągami pierwotnymi. JavaScript automatycznie konwertuje prymitywy na obiekty typu String, dzięki czemu możliwe jest użycie metod obiektów typu String dla łańcuchów pierwotnych. W kontekstach, w których metoda ma być wywoływana na łańcuchu pierwotnym lub następuje wyszukiwanie właściwości, JavaScript automatycznie zawinie łańcuch pierwotny i wywoła metodę lub przeprowadzi wyszukiwanie właściwości.

Pomyślałem więc, że (logicznie) operacje (wywołania metod) na prymitywach łańcuchowych powinny być wolniejsze niż operacje na obiektach łańcuchowych, ponieważ każdy prymityw ciągu jest konwertowany na ciąg Object (dodatkowa praca) przed method zastosowaniem na łańcuchu.

Ale w tym przypadku testowym wynik jest odwrotny. W polu kodu-1 działa szybciej niż kod blokowy-2 , oba bloki kodu podano poniżej:

blok kodu 1:

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

blok kodu 2:

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

Wyniki różnią się w przeglądarkach, ale blok kodu 1 jest zawsze szybszy. Czy ktoś może to wyjaśnić, dlaczego blok kodu 1 jest szybszy niż blok kodu 2 .

Alpha
źródło
6
Użycie new Stringwprowadza kolejną przezroczystą warstwę zawijania obiektów . typeof new String(); //"object"
Paul S.
o czym '0123456789'.charAt(i)?
Yuriy Galanter
@YuriyGalanter, to nie problem, ale pytam, dlaczego code block-1jest szybszy?
Alpha
2
Obiekty ciągów są stosunkowo rzadkie w kontekście rzeczywistego życia, dlatego nie jest zaskakujące, że interpretery optymalizują literały ciągów. W dzisiejszych czasach kod nie jest po prostu interpretowany , istnieje wiele warstw optymalizacji, które mają miejsce za kulisami.
Fabrício Matté
2
To dziwne: wersja 2
hjpotter92

Odpowiedzi:

149

JavaScript ma dwie główne kategorie typów, prymitywne i obiekty.

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

Wzorce pojedynczego cudzysłowu / podwójnego cudzysłowu są identyczne pod względem funkcjonalności. Poza tym zachowanie, które próbujesz nazwać, nazywa się automatycznym boksowaniem. Tak więc w rzeczywistości dzieje się tak, że prymityw jest konwertowany na swój typ opakowania, gdy wywoływana jest metoda typu opakowania. Mówiąc prosto:

var s = 'test';

Jest prymitywnym typem danych. Nie ma żadnych metod, jest niczym innym jak wskaźnikiem do surowego odniesienia do pamięci danych, co wyjaśnia znacznie większą prędkość dostępu swobodnego.

Więc co się dzieje, kiedy s.charAt(i)na przykład to robisz ?

Ponieważ snie jest to instancja String, JavaScript będzie automatycznie-box s, który ma typeof stringswój typ opakowania String, z typeof objectlub dokładniej s.valueOf(s).prototype.toString.call = [object String].

Zachowanie auto-boxingu rzutuje w stę iz powrotem do swojego typu opakowania w razie potrzeby, ale standardowe operacje są niewiarygodnie szybkie, ponieważ masz do czynienia z prostszym typem danych. Jednak auto-boxing iObject.prototype.valueOf mają różne efekty.

Jeśli chcesz wymusić automatyczne boksowanie lub rzutowanie prymitywu na jego typ opakowania, możesz użyć Object.prototype.valueOf, ale zachowanie jest inne. Bazując na szerokiej gamie scenariuszy testowych, auto-boxing stosuje tylko „wymagane” metody, bez zmiany pierwotnego charakteru zmiennej. Dlatego uzyskujesz lepszą prędkość.

flawian
źródło
33

Jest to raczej zależne od implementacji, ale zrobię zdjęcie. Podam przykład z V8, ale zakładam, że inne silniki stosują podobne podejście.

Prymityw łańcuchowy jest przetwarzany na v8::Stringobiekt. W związku z tym metody mogą być wywoływane bezpośrednio na nim, jak wspomniano w jfriend00 .

Z drugiej strony obiekt String jest analizowany do postaci v8::StringObjectrozszerzającej się Objecti oprócz tego, że jest pełnoprawnym obiektem, służy jako opakowanie dlav8::String .

Teraz jest to tylko logiczne, wywołanie new String('').method()musi unbox to v8::StringObject„s v8::Stringprzed wykonaniem metody, więc jest wolniejszy.


W wielu innych językach wartości pierwotne nie mają metod.

Sposób, w jaki MDN to ujmuje, wydaje się najprostszym sposobem wyjaśnienia, jak działa auto-boksowanie prymitywów (jak również wspomniano w odpowiedzi flava ), czyli w jaki sposób prymitywne wartości -y JavaScript mogą wywoływać metody.

Jednak inteligentny silnik nie konwertuje łańcucha znaków pierwotnych-y na obiekt String za każdym razem, gdy trzeba wywołać metodę. Jest to również informacyjnie wspomniane w opisanej specyfikacji ES5. w odniesieniu do rozwiązywania właściwości (i „metod” ¹) wartości pierwotnych:

UWAGA Obiekt, który może zostać utworzony w kroku 1, nie jest dostępny poza powyższą metodą. Implementacja może zdecydować się na uniknięcie faktycznego tworzenia obiektu. […]

Na bardzo niskim poziomie Ciągi są najczęściej implementowane jako niezmienne wartości skalarne. Przykładowa struktura opakowania:

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

Im dalej jesteś od prymitywu, tym dłużej zajmie ci dotarcie do niego. W praktyce Stringprymitywy występują znacznie częściej niż StringObjects, dlatego nie jest zaskoczeniem, że silniki dodają metody do klasy odpowiadających (zinterpretowanych) obiektów prymitywów typu String zamiast konwertowania w tę iz powrotem między StringiStringObject jak sugeruje wyjaśnienie MDN.


¹ W JavaScript „metoda” jest po prostu konwencją nazewnictwa właściwości, która jest tłumaczona na wartość typu function.

Fabrício Matté
źródło
1
Nie ma za co. =]Teraz zastanawiam się, czy wyjaśnienie MDN jest tam tylko dlatego, że wydaje się być najłatwiejszym sposobem zrozumienia auto-boksu, czy też jest jakieś odniesienie do niego w specyfikacji ES .. Czytając całą specyfikację w tym momencie, aby sprawdzić, pamiętam, aby zaktualizuj odpowiedź, jeśli kiedykolwiek znajdę odniesienie.
Fabrício Matté
Świetny wgląd w implementację V8. Dodam, że boks służy nie tylko rozwiązaniu tej funkcji. Jest tam również, aby przekazać to odniesienie do metody. Teraz nie jestem pewien, czy V8 pomija to dla wbudowanych metod, ale jeśli dodasz własne rozszerzenie, aby powiedzieć String.prototype, otrzymasz pudełkową wersję obiektu ciągu za każdym razem, gdy zostanie wywołany.
Ben
17

W przypadku literału tekstowego nie możemy przypisać właściwości

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

Natomiast w przypadku obiektu typu String możemy przypisać właściwości

var x = new String("hello");
x.y = "world";
console.log(x.y); // this will print world
refaktor
źródło
1
Wreszcie ktoś motywuje, dlaczego potrzebujemy również Stringprzedmiotów. Dziękuję Ci!
Ciprian Tomoiagă
1
Dlaczego ktoś miałby to robić?
Aditya,
11

Literał ciągu:

Literały łańcuchowe są niezmienne, co oznacza, że ​​po utworzeniu ich stan nie może zostać zmieniony, co sprawia, że ​​są one również bezpieczne dla wątków.

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

a==b wynikiem będzie „prawda”, oba ciągi odwołują się do tego samego obiektu.

Obiekt łańcuchowy:

Tutaj tworzone są dwa różne obiekty, które mają różne odniesienia:

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

a==b wynik będzie fałszywy, ponieważ mają różne odniesienia.

Wajahat Ali Qureshi
źródło
1
Czy obiekt typu string jest również niezmienny?
Yang Wang,
@YangWang to głupi język, dla obu ai bspróbuj go przypisać a[0] = 'X', zostanie wykonany pomyślnie, ale nie zadziała tak, jak można się spodziewać
ruX
Napisałeś "var a = 's'; var b = 's'; a == b wynik będzie 'true', oba ciągi odnoszą się do tego samego obiektu." To nie jest poprawne: a i b nie odnoszą się do żadnego tego samego obiektu, wynik jest prawdziwy, ponieważ mają tę samą wartość. Te wartości są przechowywane w różnych lokalizacjach pamięci, dlatego zmiana jednej nie zmienia się!
SC1000
9

Jeśli używasz new, wyraźnie oświadczasz, że chcesz utworzyć wystąpienie Object . Dlatego new Stringtworzy obiekt opakowujący prymityw String , co oznacza, że ​​każda akcja na nim wymaga dodatkowej warstwy pracy.

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

Ponieważ są one różnych typów, Twój interpreter JavaScript może również je optymalizować w inny sposób, jak wspomniano w komentarzach .

Paul S.
źródło
5

Kiedy deklarujesz:

var s = '0123456789';

tworzysz łańcuch pierwotny. Ten prymityw łańcuchowy ma metody, które umożliwiają wywoływanie metod bez konwertowania prymitywu na obiekt pierwszej klasy. Więc twoje przypuszczenie, że będzie to wolniejsze, ponieważ łańcuch musi zostać przekonwertowany na obiekt, nie jest poprawne. Nie trzeba go konwertować na obiekt. Sam prymityw może wywołać metody.

Przekształcenie go w pełnowymiarowy obiekt (co pozwala na dodanie do niego nowych właściwości) jest dodatkowym krokiem i nie przyspiesza powstawania ciągu (w rzeczywistości twój test pokazuje, że spowalnia go).

jfriend00
źródło
W jaki sposób prymityw łańcuchowy dziedziczy wszystkie właściwości prototypu, w tym niestandardowe String.prototype?
Yuriy Galanter
1
var s = '0123456789';jest prymitywną wartością, jak ta wartość może mieć metody, jestem zdezorientowany!
Alpha
2
@SheikhHeera - prymitywy są wbudowane w implementację języka, więc interpreter może nadać im specjalne uprawnienia.
jfriend00
1
@SheikhHeera - Nie rozumiem twojego ostatniego komentarza / pytania. Sama prymityw łańcuchowy nie obsługuje dodawania do niego własnych właściwości. Aby na to pozwolić, javascript ma również obiekt String, który ma wszystkie te same metody, co prymityw łańcuchowy, ale jest pełnowymiarowym obiektem, który można traktować jak obiekt pod każdym względem. Ta podwójna forma wydaje się być nieco niechlujna, ale podejrzewam, że została wykonana jako kompromis w zakresie wydajności, ponieważ przypadek 99% polega na użyciu prymitywów i prawdopodobnie mogą być szybsze i bardziej wydajne w pamięci niż obiekty łańcuchowe.
jfriend00
1
@SheikhHeera „przekonwertowany na ciąg Object” to sposób wyrażenia w MDN w celu wyjaśnienia, w jaki sposób prymitywy mogą wywoływać metody. Nie są dosłownie konwertowane na obiekty typu string.
Fabrício Matté
4

Widzę, że to pytanie zostało rozwiązane dawno temu, istnieje jeszcze jedno subtelne rozróżnienie między literałami strunowymi a obiektami strunowymi, ponieważ nikt nie zdawał się tego dotykać, pomyślałem, że napiszę to tylko dla kompletności.

Zasadniczo inną różnicą między nimi jest użycie eval. eval ('1 + 1') daje 2, podczas gdy eval (new String ('1 + 1')) daje '1 + 1', więc jeśli pewien blok kodu może być wykonany zarówno 'normalnie', jak i z eval, może prowadzą do dziwnych wyników

luanped
źródło
Dzięki za wkład :-)
Alpha
Wow, to naprawdę dziwne zachowanie. Powinieneś dodać małe demo w linii w swoim komentarzu, aby pokazać to zachowanie - to niezwykle otwiera oczy.
EyuelDK,
to normalne, jeśli się nad tym zastanowić. new String("")zwraca obiekt, a eval ocenia tylko ciąg znaków, et zwraca wszystko inne takie, jakie jest
Félix Brunet
3

Istnienie obiektu ma niewiele wspólnego z rzeczywistym zachowaniem String w silnikach ECMAScript / JavaScript, ponieważ zakres główny będzie zawierał po prostu obiekty funkcji do tego celu. Zatem funkcja charAt (int) w przypadku literału łańcuchowego zostanie przeszukana i wykonana.

W przypadku prawdziwego obiektu dodajesz jeszcze jedną warstwę, w której metoda charAt (int) jest również przeszukiwana w samym obiekcie, zanim zacznie się standardowe zachowanie (tak samo jak powyżej). Najwyraźniej w tym przypadku wykonano zaskakująco dużo pracy.

Swoją drogą, nie sądzę, aby prymitywy były faktycznie konwertowane na obiekty, ale silnik skryptów po prostu oznaczy tę zmienną jako typ łańcuchowy i dlatego może znaleźć wszystkie dostarczone dla niej funkcje, więc wygląda na to, że wywołujesz obiekt. Nie zapominaj, że jest to środowisko wykonawcze skryptu, które działa na innych zasadach niż środowisko wykonawcze OO.

czysta woda
źródło
3

Największą różnicą między obiektem typu string a obiektem typu string jest to, że obiekty muszą przestrzegać tej reguły dla ==operatora :

Wyrażenie porównujące obiekty jest prawdziwe tylko wtedy, gdy operandy odwołują się do tego samego obiektu.

Tak więc, podczas gdy prymitywy łańcuchowe mają wygodny sposób ==porównywania wartości, nie masz szczęścia, jeśli chodzi o sprawianie, że jakikolwiek inny niezmienny typ obiektu (w tym obiekt ciągu) zachowuje się jak typ wartości.

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

(Inni zauważyli, że obiekt typu string jest technicznie zmienny, ponieważ można do niego dodawać właściwości. Ale nie jest jasne, do czego jest to przydatne; sama wartość ciągu nie jest zmienna).

personal_cloud
źródło
Dzięki za dodanie wartości do pytania po dość długim czasie :-)
Alfa
1

Kod jest optymalizowany przed uruchomieniem przez silnik javascript. Ogólnie rzecz biorąc, mikro testy porównawcze mogą wprowadzać w błąd, ponieważ kompilatory i interpretery przestawiają, modyfikują, usuwają i wykonują inne sztuczki na częściach kodu, aby działał szybciej. Innymi słowy, napisany kod mówi, jaki jest cel, ale kompilator i / lub środowisko wykonawcze zadecydują, jak go osiągnąć.

Blok 1 jest szybszy głównie z powodu: var s = '0123456789'; jest zawsze szybszy niż var s = new String ('0123456789'); ze względu na koszty tworzenia obiektów.

Część pętli nie jest tą, która powoduje spowolnienie, ponieważ funkcja chartAt () może być wstawiana przez interpreter. Spróbuj usunąć pętlę i ponownie uruchom test, zobaczysz, że współczynnik prędkości będzie taki sam, jak gdyby pętla nie została usunięta. Innymi słowy, w przypadku tych testów bloki pętli w czasie wykonywania mają dokładnie ten sam kod bajtowy / kod maszynowy.

W przypadku tego typu mikro-benchmarków, spojrzenie na kod bajtowy lub kod maszynowy zapewni jaśniejszy obraz.

Łuk
źródło
1
Dziękuję za odpowiedź.
Alpha Alpha
0

W JavaScript prymitywne typy danych, takie jak ciąg, są niezłożonymi blokami konstrukcyjnymi. Oznacza to, że są to tylko wartości, nic więcej: let a = "string value"; domyślnie nie ma wbudowanych metod, takich jak toUpperCase, toLowerCase itp ...

Ale jeśli spróbujesz napisać:

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

Nie spowoduje to żadnego błędu, zamiast tego będą działać tak, jak powinny.

Co się stało ? Cóż, kiedy próbujesz uzyskać dostęp do właściwości łańcucha, aJavaScript wymusza ciąg znaków na obiekt za pomocą new String(a);znanego jako obiekt opakowania .

Ten proces jest powiązany z koncepcją zwaną konstruktorami funkcji w Javascript, gdzie funkcje są używane do tworzenia nowych obiektów.

Kiedy wpiszesz new String('String value');tutaj Ciąg jest konstruktorem funkcji, który przyjmuje argument i tworzy pusty obiekt w zakresie funkcji, ten pusty obiekt jest przypisywany do this aw tym przypadku String dostarcza wszystkie znane wbudowane funkcje, o których wspomnieliśmy wcześniej. a gdy tylko operacja zostanie zakończona, na przykład operacja wielkimi literami, obiekt opakowania jest odrzucany.

Aby to udowodnić, zróbmy to:

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

Tutaj wynik będzie niezdefiniowany. Czemu ? W tym przypadku JavaScript tworzy opakowujący obiekt String, ustawia nową właściwość addNewProperty i natychmiast odrzuca opakowujący obiekt. dlatego stajesz się niezdefiniowany. Pseudo kod wyglądałby tak:

let justString = 'Hello From String Value';
let wrapperObject = new String( justString );
wrapperObject.addNewProperty = 'Added New Property'; //Do operation and discard
Alexander Gharibashvili
źródło
0

możemy zdefiniować String na 3 sposoby

  1. var a = "pierwsza droga";
  2. var b = String („druga droga”);
  3. var c = new String („trzecia droga”);

// możemy również tworzyć używając 4. var d = a + '';

Sprawdź typ ciągów utworzonych za pomocą operatora typeof

  • typ // „string”
  • typeof b // "string"
  • typeof c // "obiekt"


kiedy porównujesz a i b var a==b ( // yes)


kiedy porównujesz obiekt 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
SouMitya chauhan
źródło