อ็อบเจกต์ `String`

อ็อบเจกต์ `String`

บทความนี้อธิบายเกี่ยวกับอ็อบเจกต์ String

คำอธิบายนี้ครอบคลุมตั้งแต่พื้นฐานจนถึงเทคนิคขั้นสูง รวมถึงข้อควรระวังที่เกี่ยวข้องกับ Unicode และ regular expressions ทีละขั้นตอนอย่างเข้าใจง่าย

YouTube Video

อ็อบเจกต์ String

String ใน JavaScript เป็นชนิดข้อมูลที่ใช้งานบ่อยมากในการพัฒนาในชีวิตประจำวัน

ความแตกต่างระหว่าง Primitive String กับ String Object

Primitive string (เช่น "hello") ทำงานแตกต่างจากอ็อบเจกต์ Wrapper เช่น new String("hello") โดยปกติควรใช้ Primitive string และไม่ค่อยจำเป็นต้องใช้อ็อบเจกต์รูปแบบ Object

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
  • โค้ดนี้แสดงความแตกต่างของประเภทระหว่าง Primitive กับ Wrapper และแสดงวิธีการทำงานเมื่อเปรียบเทียบแบบ strict ในกรณีส่วนใหญ่ควรหลีกเลี่ยงการใช้ new String() และใช้แบบ Primitive จะดีกว่า

วิธีสร้าง String (Literals และ Template Literals)

Template literal มีประโยชน์เมื่อต้องแทรกตัวแปรหรือเขียนข้อความหลายบรรทัด คุณสามารถแทรกตัวแปรและประเมินผลนิพจน์ได้อย่างเข้าใจง่าย

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 literal อ่านเข้าใจง่าย เหมาะกับการสร้าง string ซับซ้อนหรือ multi-line string

เมธอดที่ใช้บ่อย (การค้นหาและการตัดข้อความย่อย)

อ็อบเจกต์ String มีเมธอดพื้นฐานจำนวนมาก

 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 กับ substring มีลักษณะคล้ายกัน แต่จะจัดการกับ index ลบต่างกัน slice จะมองค่าลบเป็นตำแหน่งนับจากท้าย ควรแน่ใจว่าเลือกใช้แบบใดให้เหมาะสม

การแยกและรวมข้อความ (split / join)

มักจะนำ string มาแยกเป็น array เพื่อประมวลผล และรวมกลับเป็น string ด้วย join

1const csv = "red,green,blue";
2const arr = csv.split(","); // ["red","green","blue"]
3
4console.log(arr);
5console.log(arr.join(" | ")); // "red | green | blue"
  • รูปแบบที่พบบ่อยคือใช้ split แยก string จากนั้นใช้ map หรือ filter ประมวลผล แล้วใช้ join รวมกลับเป็น string

การแทนที่และ Regular Expressions

replace จะทำการแทนที่เฉพาะรายการที่พบเป็นครั้งแรกเท่านั้น ถ้าต้องการแทนที่ทั้งหมด ให้ใช้ Regular Expression พร้อม flag g ถ้าส่งฟังก์ชันเป็นค่าที่จะแทนที่ จะสามารถแทนที่แบบไดนามิกได้

 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"
  • การแทนที่ด้วยฟังก์ชันช่วยให้สามารถเขียนโค้ดที่วิเคราะห์และปรับเปลี่ยนข้อความได้อย่างกระชับ

การแปลงตัวพิมพ์และการปกติ (Normalization)

สำหรับการรองรับและเปรียบเทียบหลายภาษา นอกเหนือจาก toLowerCase และ toUpperCase แล้ว การปรับมาตรฐาน Unicode (normalize) ก็มีความสำคัญเช่นกัน สิ่งนี้จำเป็นมากเมื่อต้องเปรียบเทียบตัวอักษรที่มีสัญลักษณ์พิเศษ (accent)

 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
  • รูปแบบ Unicode ที่ต่างกัน เช่น ligature และ Combining Character จะไม่เท่ากันโดยตรง ดังนั้นควรใช้ normalize() ก่อนเปรียบเทียบ

Unicode และ Code Point (การจัดการ Surrogate Pair)

String ใน JavaScript คือชุดของโค้ดยูนิต UTF-16 ดังนั้นตัวอักษรบางชนิด (เช่น อีโมจิ) อาจใช้โค้ดยูนิต 2 ตัวสำหรับ 1 ตัวอักษร หากต้องการจัดการกับตัวอักษรแต่ละตัวจริง ๆ ให้ใช้ Array.from, spread operator, หรือ 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 จะคืนค่าจำนวนโค้ดยูนิต ดังนั้นการนับกับอีโมจิหรือ ligature อาจไม่ตรงตามที่คาดหวัง for...of กับ Array.from จะรับมือกับกลุ่มตัวอักษรที่ใกล้เคียงกับตัวที่ปรากฏจริง (grapheme cluster) แต่หากต้องการรองรับ grapheme สมบูรณ์ ควรเลือกใช้ไลบรารีเฉพาะทาง

การแทนที่ด้วย Regular Expression อย่างปลอดภัย (เมื่อจัดการข้อมูลที่ผู้ใช้ป้อน)

หากคุณลืมเอสเคปข้อมูลที่ผู้ใช้ป้อนเมื่อนำไปฝังในนิพจน์ปกติ อาจทำให้เกิดพฤติกรรมที่ไม่คาดคิดและช่องโหว่ด้านความปลอดภัยได้ ควร escape ข้อมูลจากผู้ใช้ก่อนนำไปใช้ในแพทเทิร์นเสมอ

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"
  • อย่าใช้ string ที่ผู้ใช้ป้อนโดยตรงใน Regular Expression ควร escape ก่อนสร้าง regex เสมอ

เคล็ดลับประสิทธิภาพ: การต่อข้อความและ Template Strings

เมื่อต้องต่อ string เล็ก ๆ หลายตัวเข้าด้วยกัน การนำไปใส่ array แล้วใช้ join อาจจะรวดเร็วกว่า ในทางกลับกัน template string ทั้งอ่านง่ายและเร็วจนเพียงพอสำหรับเกือบทุกกรณี

 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("");
  • เอนจิน JavaScript สมัยใหม่ได้รับการปรับปรุงประสิทธิภาพสูง จึงไม่จำเป็นต้องกังวลกับประสิทธิภาพเมื่อ concat น้อยครั้ง แต่ถ้าจำเป็นต้อง concat หลายหมื่นครั้ง การใช้ join จะมีประสิทธิภาพมากกว่า

เทคนิคที่มีประโยชน์: Padding, Trim และ Repeat

trim, padStart, padEnd และ repeat เป็นเมธอดที่สะดวกและมีประโยชน์อย่างยิ่งในการประมวลผลสตริงในชีวิตประจำวัน มักถูกนำไปใช้จริงในสถานการณ์ต่างๆ เช่น การจัดรูปแบบค่าที่รับเข้ามาหรือการทำให้รูปแบบผลลัพธ์เป็นมาตรฐาน

1console.log("  hello  ".trim());       // "hello"
2console.log("5".padStart(3, "0"));     // "005"
3console.log("x".repeat(5));            // "xxxxx"
  • เมธอดเหล่านี้ใช้สำหรับปรับข้อมูลฟอร์มให้เหมาะสมหรือสร้างข้อความความกว้างคงที่

การเปรียบเทียบ String (Locale Comparison)

localeCompare มีประสิทธิภาพเมื่อเปรียบเทียบ string ตามลำดับพจนานุกรมในแต่ละภาษา คุณสามารถระบุภาษาและตั้งค่าความละเอียดอ่อน (เช่น การแยกแยะตัวพิมพ์)

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
  • สำหรับการเปรียบเทียบข้ามภาษา ให้ใช้ localeCompare และระบุ locale และ option ที่เหมาะสม

ตัวอย่างจริง: การแปลงแถวใน CSV เป็นอ็อบเจกต์ (เวิร์กโฟลว์จริง)

ตัวอย่างการใช้งานที่พบบ่อย คือการแปลงแถวเดียวใน CSV เป็นอ็อบเจกต์ โดยใช้ split, trim และ map หากเจอฟิลด์ที่มีคำพูดหรือไฟล์ CSV ที่ซับซ้อน ควรใช้ไลบรารีสำหรับ 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" }
  • วิธีนี้ใช้สำหรับ CSV รูปแบบง่าย แต่ไม่สามารถจัดการกับกรณีที่มีเครื่องหมายจุลภาคในฟิลด์ที่มีเครื่องหมายคำพูด

ข้อควรระวังที่พบบ่อย

มีข้อกำหนดและพฤติกรรมบางอย่างในการจัดการ String ใน JavaScript ที่มักถูกมองข้าม เพื่อหลีกเลี่ยงบั๊กที่ไม่คาดคิด ควรจดจำประเด็นต่อไปนี้

  • การใช้ new String() อาจทำให้การตรวจสอบประเภทหรือการเปรียบเทียบผิดพลาดได้ ในกรณีส่วนใหญ่ การใช้ primitive string ก็เพียงพอแล้ว
  • ใน Unicode ตัวอักษรที่มองเห็นเพียงตัวเดียวอาจประกอบด้วยโค้ดยูนิตหลายตัว ดังนั้นค่าที่ได้จาก length อาจไม่ตรงกับจำนวนตัวอักษรที่แสดงจริง
  • เมื่อนำค่าที่ผู้ใช้ป้อนมาใช้ใน Regular Expression ควร escape ก่อนเสมอ
  • String.prototype.replace() จะเปลี่ยนแปลงเพียงรายการแรกที่พบโดยค่าเริ่มต้น หากต้องการแทนที่ทุกกรณีที่พบ ให้ใช้แฟล็ก /g ในนิพจน์ปกติของคุณ
  • String ไม่สามารถเปลี่ยนแปลงได้ (immutable) ดังนั้นทุกการดำเนินการจะคืนค่า string ใหม่เสมอ ควรนำค่าผลลัพธ์ไปเก็บไว้เสมอ

สรุป

ถึงแม้ String ใน JavaScript จะดูเหมือนง่าย แต่ควรเข้าใจลักษณะเฉพาะทั้งเรื่อง Unicode และความเป็น immutable ด้วย หากเข้าใจพื้นฐานดีแล้ว จะช่วยเพิ่มความน่าเชื่อถือและอ่านโค้ด string ได้ง่ายมากยิ่งขึ้น

คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย

YouTube Video