Obiekt `String`

Ten artykuł wyjaśnia obiekt String.

Wyjaśnienie obejmuje wszystko od podstaw po zaawansowane techniki, w tym pułapki związane z Unicode i wyrażeniami regularnymi, krok po kroku i w łatwy do zrozumienia sposób.

YouTube Video

Obiekt String

Łańcuchy znaków w JavaScript są jednym z najczęściej używanych typów w codziennym programowaniu.

Różnica między prostymi łańcuchami znaków a obiektami String

Proste łańcuchy znaków (takie jak "hello") zachowują się inaczej niż obiekty otaczające, takie jak new String("hello"). Zazwyczaj należy używać typów prostych, a potrzeba użycia formy obiektowej jest znikoma.

1// Primitive string
2const a = "hello";
3
4// String wrapper object
5const b = new String("hello");
6
7console.log(typeof a); // "string"
8console.log(typeof b); // "object"
9console.log(a === b);  // false — wrapper objects are not strictly equal
  • Ten kod pokazuje różnicę typu między typem prostym a obiektem opakowującym oraz jak zachowują się przy ścisłym porównaniu. W większości przypadków unikaj używania new String() i korzystaj z typów prostych.

Sposoby tworzenia łańcuchów znaków (literały i template literals)

Template literals są przydatne do wstawiania zmiennych i pisania wielowierszowych łańcuchów znaków. Możesz intuicyjnie wstawiać zmienne oraz oceniać wyrażenia.

1const name = "Alice";
2const age = 30;
3
4// Template literal
5const greeting = `Name: ${name}, Age: ${age + 1}`;
6
7console.log(greeting); // "Name: Alice, Age: 31"
  • Template literals są bardzo czytelne i idealne do budowania złożonych łańcuchów znaków, w tym wielowierszowych.

Typowe metody (wyszukiwanie i wyodrębnianie podłańcuchów)

Obiekt String posiada wiele podstawowych metod.

 1const text = "Hello, world! Hello again.";
 2
 3// search
 4console.log(text.indexOf("Hello"));       // 0
 5console.log(text.indexOf("Hello", 1));    // 13
 6console.log(text.includes("world"));      // true
 7console.log(text.startsWith("Hello"));    // true
 8console.log(text.endsWith("again."));     // true
 9
10// slice / substring
11console.log(text.slice(7, 12));           // "world"
12console.log(text.substring(7, 12));       // "world"
  • slice i substring są podobne, ale inaczej obsługują ujemne indeksy. slice interpretuje wartości ujemne jako pozycje liczone od końca. Upewnij się, której z nich chcesz użyć.

Dzielenie i łączenie (split / join)

Często dzieli się łańcuch na tablicę do przetwarzania, a następnie łączy z powrotem.

1const csv = "red,green,blue";
2const arr = csv.split(","); // ["red","green","blue"]
3
4console.log(arr);
5console.log(arr.join(" | ")); // "red | green | blue"
  • Popularnym wzorcem jest użycie split do podziału łańcucha, przetworzenie wynikowej tablicy za pomocą map lub filter, a następnie połączenie jej z powrotem za pomocą join.

Zastępowanie i wyrażenia regularne

replace zastępuje tylko pierwsze dopasowanie. Jeśli chcesz zastąpić wszystkie dopasowania, użyj flagi g z wyrażeniem regularnym. Przekazując funkcję jako zamiennik, możesz również wykonywać dynamiczne zamiany.

 1const s = "foo 1 foo 2";
 2
 3// replace first only
 4console.log(s.replace("foo", "bar")); // "bar 1 foo 2"
 5
 6// replace all using regex
 7console.log(s.replace(/foo/g, "bar")); // "bar 1 bar 2"
 8
 9// replace with function
10const r = s.replace(/\d+/g, (match) => String(Number(match) * 10));
11console.log(r);    // "foo 10 foo 20"
  • Korzystając z dynamicznej zamiany za pomocą funkcji, możesz zwięźle napisać kod, który analizuje i przekształca dopasowane fragmenty.

Zmiana wielkości liter i normalizacja

Dla obsługi wielu języków i porównywania, oprócz toLowerCase i toUpperCase, ważna jest także normalizacja Unicode (normalize). Jest to szczególnie konieczne przy porównywaniu znaków z akcentami.

 1// Case conversion example:
 2// "\u00DF" represents the German letter "ß".
 3// In some locales, converting "ß" to uppercase becomes "SS".
 4// JavaScript follows this behavior.
 5console.log("\u00DF");
 6console.log("\u00DF".toUpperCase()); // "SS"
 7
 8// Unicode normalization example:
 9// "e\u0301" is "e" + a combining acute accent.
10// "\u00e9" is the precomposed character "é".
11// These two look the same but are different code point sequences.
12const a = "e\u0301";
13const b = "\u00e9";
14
15console.log(a === b);   // false: different underlying code points
16console.log(a.normalize() === b.normalize()); // true: normalized to the same form
  • Różne reprezentacje Unicode, takie jak ligatury i znaki łączone, nie będą równe w tej formie, dlatego należy użyć normalize() przed porównaniem.

Unicode i punkty kodowe (obsługa par surrogatów)

Łańcuchy znaków w JavaScript są sekwencjami jednostek kodowych UTF-16, więc niektóre znaki, takie jak emotikony, mogą zajmować dwie jednostki kodowe dla jednego znaku. Aby obsłużyć rzeczywiste jednostki znakowe, użyj Array.from, operatora spread lub for...of.

 1// Emoji composed with multiple code points:
 2// "\u{1F469}" = woman, "\u{200D}" = Zero Width Joiner (ZWJ),
 3// "\u{1F4BB}" = laptop. Combined, they form a single emoji: 👩‍💻
 4const s = "\u{1F469}\u{200D}\u{1F4BB}";
 5console.log(s);
 6
 7// Length in UTF-16 code units (not actual Unicode characters):
 8// Because this emoji uses surrogate pairs + ZWJ, the length may be > 1.
 9console.log("Length:", s.length);
10
11// Iterate by Unicode code points (ES6 for...of iterates code points):
12// Each iteration gives a full Unicode character, not UTF-16 units.
13for (const ch of s) {
14  console.log(ch);
15}
16
17// Convert to an array of Unicode characters:
18console.log(Array.from(s));
  • length zwraca liczbę jednostek kodowych, więc możesz nie otrzymać oczekiwanej liczby przy emotikonach lub ligaturach. for...of i Array.from obsługują coś zbliżonego do wyświetlanych znaków (klastrów grafemowych), ale jeśli potrzebujesz pełnej obsługi grafemów, rozważ użycie specjalistycznej biblioteki.

Bezpieczna zamiana z użyciem wyrażeń regularnych (przy obsłudze danych użytkownika)

Jeśli zapomnisz o ucieczce danych wejściowych użytkownika podczas osadzania ich w wyrażeniu regularnym, może to prowadzić do niespodziewanych zachowań i luk w zabezpieczeniach. Zawsze stosuj ucieczkę znaków danych wejściowych użytkownika przed użyciem ich we wzorcu.

1function escapeRegex(s) {
2  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3}
4
5const userInput = "a+b";
6const pattern = new RegExp(escapeRegex(userInput), "g");
7console.log("a+b a+b".replace(pattern, "X")); // "X X"
  • Nigdy nie używaj bezpośrednio ciągów znaków od użytkownika w wyrażeniach regularnych; zawsze je ucieczkuj przed zbudowaniem wyrażenia.

Porady dotyczące wydajności: konkatenacja i template strings

Łącząc wiele małych łańcuchów znaków w sekwencji, umieszczenie ich w tablicy i użycie join może być wydajniejsze. Z drugiej strony, template strings są bardzo czytelne i wystarczająco szybkie w większości przypadków.

 1// concatenation in loop (less ideal)
 2let s = "";
 3for (let i = 0; i < 1000; i++) {
 4  s += i + ",";
 5}
 6
 7// using array + join (often faster for many pieces)
 8const parts = [];
 9for (let i = 0; i < 1000; i++) {
10  parts.push(i + ",");
11}
12const s2 = parts.join("");
  • Nowoczesne silniki JavaScript są bardzo zoptymalizowane, więc nie musisz się martwić o wydajność przy niewielkiej liczbie konkatenacji. Jednakże, jeśli musisz wykonać dziesiątki tysięcy konkatenacji, użycie join może być wydajniejsze.

Praktyczne przydatne techniki: padding, trim i repeat

trim, padStart, padEnd oraz repeat to wygodne metody, które są szczególnie przydatne w codziennym przetwarzaniu ciągów znaków. Często są używane w praktycznych sytuacjach, takich jak formatowanie wartości wejściowych lub standaryzacja formatów wyjściowych.

1console.log("  hello  ".trim());       // "hello"
2console.log("5".padStart(3, "0"));     // "005"
3console.log("x".repeat(5));            // "xxxxx"
  • Metody te można wykorzystać do normalizacji danych wejściowych lub generowania danych o stałej szerokości.

Porównywanie łańcuchów znaków (porównanie z lokalizacją)

localeCompare jest skuteczne do porównywania łańcuchów znaków według kolejności słownikowej w różnych językach. Możesz określić język oraz opcje czułości (na przykład rozróżnianie wielkości znaków).

1console.log(
2  "\u00E4".localeCompare("z", "de")
3); // may be -1 or other depending on locale
4
5console.log(
6  "a".localeCompare("A", undefined, { sensitivity: "base" })
7); // 0
  • Do zlokalizowanych porównań używaj localeCompare i określ odpowiednią lokalizację oraz opcje.

Praktyczny przykład: konwersja wiersza CSV do obiektu (praktyczny przebieg)

Powszechnym przypadkiem użycia jest parsowanie pojedynczego wiersza CSV na obiekt przy użyciu połączenia split, trim i map. Dla pól w cudzysłowach lub złożonych plików CSV używaj dedykowanego parsera CSV.

 1// simple CSV parse (no quotes handling)
 2function parseCsvLine(line, headers) {
 3  const values = line.split(",").map(v => v.trim());
 4  const obj = {};
 5  headers.forEach((h, i) => obj[h] = values[i] ?? null);
 6  return obj;
 7}
 8
 9const headers = ["name", "age", "city"];
10const line = " Alice , 30 , New York ";
11console.log(parseCsvLine(line, headers));
12// { name: "Alice", age: "30", city: "New York" }
  • Ta metoda działa dla prostych plików CSV, ale pamiętaj, że nie poradzi sobie z przecinkiem wewnątrz pola w cudzysłowie.

Częste pułapki

Istnieją łatwe do przeoczenia specyfikacje i zachowania w obsłudze łańcuchów znaków w JavaScript. Aby uniknąć nieoczekiwanych błędów, ważne jest, aby pamiętać o poniższych kwestiach.

  • Użycie new String() może prowadzić do niepoprawnych wyników przy sprawdzaniu typu lub porównaniach. W większości przypadków wystarczają proste typy łańcuchów znaków.
  • W Unicode pojedynczy widoczny znak może się składać z wielu jednostek kodowych. Wartość zwracana przez length może nie odpowiadać faktycznej liczbie wyświetlanych znaków.
  • Gdy wstawiasz dane użytkownika do wyrażenia regularnego, zawsze najpierw je ucieczkuj.
  • String.prototype.replace() domyślnie zastępuje tylko pierwsze dopasowanie. Jeśli chcesz zastąpić wszystkie wystąpienia, użyj flagi /g w swoim wyrażeniu regularnym.
  • Łańcuchy znaków są niezmienne, więc operacje zawsze zwracają nowy łańcuch. Ważne jest, aby zawsze przypisywać zwracaną wartość.

Podsumowanie

Chociaż łańcuchy znaków w JavaScript mogą wydawać się proste, ważne jest, aby rozumieć ich cechy związane z Unicode i niezmiennością. Opanowując podstawy, możesz znacznie poprawić niezawodność i czytelność przetwarzania łańcuchów znaków.

Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.

YouTube Video