٢٥ مارس ٢٠٢١

السلاسل النصية

تُخَزَّن النصوص في JavaScript كسلاسل نصية أي سلاسل من المحارف (string of charecter). لا يوجد نوع بيانات مستقل للحرف الواحد (char).

الصيغة الداخلية للنصوص هي دائمًا UTF-16,ولا تكون مرتبطة بتشفير الصفحة.

علامات التنصيص “”

لنراجع أنواع علامات التنصيص (الاقتباس).

يمكن تضمين النصوص إما في علامات الاقتباس الأحادية، أو الثنائية أو الفاصلة العليا المائلة:

let single = 'single-quoted';
let double = "double-quoted";

let backticks = `backticks`;

علامات التنصيص الفردية والثنائية تكون متماثلة. أما الفاصلة العليا المائلة، فَتُتيح لنا تضمين أي تعبير في السلسلة النصية، عبر تضمينها في ‎${…}‎:

function sum(a, b) {
  return a + b;
}

alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3.

الميزة الأخرى لاستخدام الفاصلة العلوية المائلة هي إمكانية فصل السلسلة النصية إلى عدة أسطر:

let guestList = `Guests:
 * John
 * Pete
 * Mary
`;

alert(guestList); // قائمة بالضيوف في أسطر منفصلة

يبدو الأمر طبيعيًا أليس كذلك؟ لكن علامات التنصيص الفردية والثنائية لا تعمل بهذه الطريقة. إن حاولنا استخدامها في نص متعدد الأسطر، سنحصل على خطأ:

let guestList = "Guests: // Error: Unexpected token ILLEGAL
  * John";

أتى استخدام علامات الاقتباس الفردية والثنائية في أوقات مبكرة من إنشاء اللغة، عندما لم يُؤخَذ بالحسبان الحاجة إلى نص متعدد الأسطر. ظهرت الفاصلة العلوية المائلة مؤخرًا ولذا فإنها متعددة الاستعمالات.

Backticks also allow us to specify a “template function” before the first backtick. The syntax is: func`string`. The function func is called automatically, receives the string and embedded expressions and can process them. This is called “tagged templates”. This feature makes it easier to implement custom templating, but is rarely used in practice. You can read more about it in the manual.

الرموز الخاصة

ما زال بالإمكان كتابة نصوص متعددة الأسطر باستخدام علامات الاقتباس الأحادية والثنائية باستخدام ما يسمى ب “رمز السطر الجديد”، والذي يُكتَب ‎\n، ويرمز لسطر جديد:

let guestList = "Guests:\n * John\n * Pete\n * Mary";

alert(guestList);  // قائمة متعددة الأسطر بالضيوف

مثلًا، السطرين التاليين متماثلان، لكنهما مكتوبين بطريقة مختلفة:

let str1 = "Hello\nWorld"; // سطران باستخدام "رمز السطر الجديد"

// سطران باستخدام سطر جديد عادي والفواصل العليا المائلة
let str2 = `Hello
World`;

alert(str1 == str2); // true

يوجد رموز خاصة أخرى أقل انتشارًا.

هذه القائمة كاملة:

المحرف الوصف
‎\n محرف السطر الجديد (Line Feed).
‎\r محرف العودة إلى بداية السطر (Carriage Return)، ولا يستخدم بمفرده. تستخدم ملفات ويندوز النصية تركيبة من رمزين ‎\r\n لتمثيل سطر جديد.
'\ , "\ علامة اقتباس مزدوجة ومفردة.
\\ شرطة مائلة خلفية
‎\t مسافة جدولة “Tab”
\b, \f, \v فراغ خلفي (backspace)، محرف الانتقال إلى صفحة جديد (Form Feed)، مسافة جدولة أفقية (Vertical Tab) على التوالي – تُستعمَل للتوافق، ولم تعد مستخدمة.
‎\xXX صيغة رمز يونيكود مع عدد ست عشري مُعطى XX، مثال: '‎ \x7A' هي نفسها 'z'.
‎\uXXXX صيغة رمز يونيكود مع عدد ست عشرية XXXX في تشفير UTF-16، مثلًا، ‎\u00A9 – هو اليونيكود لرمز حقوق النسخ ©. يجب أن يكون مكون من 6 خانات ست عشرية.
\u{X…XXXXXX}‎ (1 إلى 6 أحرف ست عشرية) رمز يونيكود مع تشفير UTF-32 المعطى. تُشَفَّر بعض الرموز الخاصة برمزي يونيكود، فتأخذ 4 بايت. هكذا يمكننا إدخال شيفرات طويلة.
Character Description
\n New line
\r Carriage return: not used alone. Windows text files use a combination of two characters \r\n to represent a line break.
\', \" Quotes
\\ Backslash
\t Tab
\b, \f, \v Backspace, Form Feed, Vertical Tab – kept for compatibility, not used nowadays.
\xXX Unicode character with the given hexadecimal Unicode XX, e.g. '\x7A' is the same as 'z'.
\uXXXX A Unicode symbol with the hex code XXXX in UTF-16 encoding, for instance \u00A9 – is a Unicode for the copyright symbol ©. It must be exactly 4 hex digits.
\u{X…XXXXXX} (1 to 6 hex characters) A Unicode symbol with the given UTF-32 encoding. Some rare characters are encoded with two Unicode symbols, taking 4 bytes. This way we can insert long codes.

Examples with Unicode:

alert( "\u00A9" ); // ©
alert( "\u{20331}" ); // 佫, a rare Chinese hieroglyph (long Unicode)
alert( "\u{1F60D}" ); // 😍, a smiling face symbol (another long Unicode)

// (رمز نادر من الهيروغليفية الصينية (يونيكود طويل alert( “\u{20331}” ); // 佫

// (رمز وجه مبتسم (يونيكود طويل آخر alert( “\u{1F60D}” ); // 😍

لاحظ بدء جميع الرموز الخاصة بشرطة مائلة خلفية `\`. تدعى أيضا ب "محرف التهريب" (escape character). يمكننا استخدامها أيضًا إن أردنا تضمين علامة اقتباس في النص: مثلًا:

```js run
alert( 'I\'m the Walrus!' ); // I'm the Walrus!

يجب إلحاق علامة الاقتباس الداخلية بالشرطة المائلة الخلفية ‎\'‎، وإلا فستُعتَبر نهاية السلسلة النصية. لاحظ أن الشرطة المائلة الخلفية \ تعمل من أجل تصحيح قراءة السلسلة النصية بواسطة JavaScript. ومن ثم تختفي، لذا فإن النص في الذاكرة لا يحتوي على \. يمكننا رؤية ذلك بوضوح باستخدام alert على المثال السابق.

Of course, only the quotes that are the same as the enclosing ones need to be escaped. So, as a more elegant solution, we could switch to double quotes or backticks instead:

alert( `I'm the Walrus!` ); // I'm the Walrus!

لكن ماذا إن أردنا عرض شرطة مائلة خلفية ضمن النص؟ يمكن ذلك، لكننا نحتاج إلى تكرارها هكذا \\:

alert( `The backslash: \\` ); // The backslash: \

طول النص

تحمل الخاصية length طول النص:

alert( `My\n`.length ); // 3

لاحظ أن n\ هو رمز خاص، لذا يكون طول السلسلة الفعلي هو 3.

length هي خاصية

يُخطِئ بعض الأشخاص ذوي الخلفيات بلغات برمجية أخرى و يستدعون str.length()‎ بدلًا من استدعاء str.length فقط. لذا لا يعمل هذا التابع لعدم وجوده. فلاحظ أن str.length هي خاصية عددية، وليس تابعًا ولا حاجة لوضع قوسين بعدها.

الوصول إلى محارف سلسلة

للحصول على حرف في مكان معين من السلسلة النصية pos، استخدم الأقواس المعقوفة [pos] أو استدعِ التابع str.charAt(pos). يبدأ أول حرف في الموضع رقم صفر:

let str = `Hello`;

// the first character
alert( str[0] ); // H
alert( str.charAt(0) ); // H

// the last character
alert( str[str.length - 1] ); // o

الأقواس المعقوفة هي طريقة جديدة للحصول على حرف، بينما التابع charAt موجود لأسباب تاريخية. الاختلاف الوحيد بينهما هو إن لم تجد الأقواس المربعة [] الحرف تُرجِع القيمة undefined بينما يُرجِع charAt نصًا فارغًا:

let str = `Hello`;

alert( str[1000] ); // undefined
alert( str.charAt(1000) ); // '' (سلسلة نصية فارغ)

يمكننا أيضا التنقل خلال جميع محارف سلسلة باستخدام for..of:

for (let char of "Hello") {
  alert(char); // H,e,l,l,o (char becomes "H", then "e", then "l" etc)
}

النصوص ثابتة

لا يمكن تغيير النصوص في JavaScript، فمن المستحيل تغيير حرف داخل سلسلة نصية فقط. لنجرب الأمر للتأكد من أنه لن يعمل:

let str = 'Hi';

str[0] = 'h'; // خطأ
alert( str[0] ); // لا تعمل

الطريقة المعتادة هي إنشاء نص جديد وإسناده للمتغير str بدلًا من النص السابق. مثلًا:

let str = 'Hi';

str = 'h' + str[1]; // تستبدل كامل السلسلة النصية

alert( str ); // hi

سنرى المزيد من الأمثلة عن ذلك في الأجزاء التالية.

تغيير حالة الأحرف الأجنبية

يقوم التابع toLowerCase() والتابع toUpperCase() بِتغيير حالة الأحرف الأجنبية:

alert( 'Interface'.toUpperCase() ); // INTERFACE
alert( 'Interface'.toLowerCase() ); // interface

أو إن أردنا بتغيير حالة حرف واحد فقط:

alert( 'Interface'[0].toLowerCase() ); // 'i'

البحث عن جزء من النص

يوجد العديد من الطرق للبحث عن جزء من النص ضمن السلسلة النصية.

str.indexOf

التابع الأول هو str.indexOf(substr, pos).

يبحث التابع عن substr في str بدءًا من الموضع المحدد pos، ثم يُرجِع الموضع الذي تطابق مع النص أو يُرجِع ‎ -1 إن لم تعثر على تطابق. مثلًا:

let str = 'Widget with id';

alert( str.indexOf('Widget') ); // 0, because 'Widget' is found at the beginning
alert( str.indexOf('widget') ); // -1, not found, the search is case-sensitive

alert( str.indexOf("id") ); // 1, "id" is found at the position 1 (..idget with id)

The optional second parameter allows us to start searching from a given position.

For instance, the first occurrence of "id" is at position 1. To look for the next occurrence, let’s start the search from position 2:

let str = 'Widget with id';

alert( str.indexOf('id', 2) ) // 12

إن كنت مهتمًا بجميع المواضع التي يظهر فيها نص معين، يمكنك استخدام indexOf في حلقة. يتم كل استدعاء جديد من الموضِع التالي لِلموضع السابق الذي تطابق مع النص:

let str = 'As sly as a fox, as strong as an ox';

let target = 'as'; // let's look for it

let pos = 0;
while (true) {
  let foundPos = str.indexOf(target, pos);
  if (foundPos == -1) break;

  alert( `Found at ${foundPos}` );
  pos = foundPos + 1; // continue the search from the next position
}

يمكن تقصير الخوارزمية:

let str = "As sly as a fox, as strong as an ox";
let target = "as";

let pos = -1;
while ((pos = str.indexOf(target, pos + 1)) != -1) {
  alert( pos );
}
str.lastIndexOf(substr, position)

يوجد أيضًا تابع مشابه str.lastIndexOf(substr, position) والذي يبدأ البحث من نهاية السلسلة النصية حتى بدايتها. أي أنه يعيد موضع ظهور النص المبحوث عنه انطلاقًا من نهاية السلسلة.

يوجد خلل طفيف عند استخدام indexOf في if. فلا يمكن وضعها بداخل if بالطريقة التالية:

let str = "Widget with id";

if (str.indexOf("Widget")) {
    alert("We found it");  // لا تعمل!
}

لا يتحقق الشرط في المثال السابق لأن str.indexOf("Widget")‎ يُرجِع 0 (ما يعني وجود تطابق في الموضع الأول) رغم عثور التابع على الكلمة، لكن if تعد القيمة 0 على أنها false. لذا يجب أن نفحص عدم وجود القيمة -‎ 1 هكذا:

let str = "Widget with id";

if (str.indexOf("Widget") != -1) {
    alert("We found it"); // works now!
}

The bitwise NOT trick

One of the old tricks used here is the bitwise NOT ~ operator. It converts the number to a 32-bit integer (removes the decimal part if exists) and then reverses all bits in its binary representation.

In practice, that means a simple thing: for 32-bit integers ~n equals -(n+1).

إحدى الخدع القديمة هي لعامل الثنائي ~ الذي تعمل على مستوى البِت. فهو يُحَوِّل العدد إلى عدد صحيح بصيغة 32-بِت (يحذف الجزء العشري إن وجد) ثم يُحوِّل جميع البتات إلى تمثيلها الثنائي. عمليًا، يعني ذلك شيئًا بسيطًا: بالنسبة للأعداد الصحيحة بصيغة 32-بِت ‎~n تساوي ‎-(n+1)‎. مثلًا:

alert( ~2 ); // -3, the same as -(2+1)
alert( ~1 ); // -2, the same as -(1+1)
alert( ~0 ); // -1, the same as -(0+1)
alert( ~-1 ); // 0, the same as -(-1+1)

كما نرى، يكون ‎~‎n صفرًا فقط عندما تكون n == -1 (وذلك لأي عدد صحيح n ذي إشارة). لذا، يكون ناتج الفحص if ( ~str.indexOf("...") )‎ صحيحًا إذا كانت نتيجة indexOf لا تساوي ‎-1. بمعنى آخر تكون القيمة true إذا وُجِد تطابق.

الآن، يمكن استخدام هذه الحيلة لتقصير الفحص باستخدام indexOf:

let str = "Widget";

if (~str.indexOf("Widget")) {
  alert( 'Found it!' ); // works
}

لا يكون من المستحسن غالبًا استخدام ميزات اللغة بطريقة غير واضحة، لكن هذه الحيلة تُستخدم بكثرة في الشيفرات القديمة، لذا يجب أن نفهمها.

تذكر أن الشرط if (~str.indexOf(...))‎ يعمل بالصيغة «إن وُجِد».

To be precise though, as big numbers are truncated to 32 bits by ~ operator, there exist other numbers that give 0, the smallest is ~4294967295=0. That makes such check correct only if a string is not that long.

لا نجد هذه الخدعة حاليًا سوى في الشيفرات القديمة، وذلك لأن JavaScript وفرت التابع ‎.includes (ستجدها في الأسفل).

includes, startsWith, endsWith

يُرجِع التابع الأحدث str.includes(substr, pos) القيمة المنطقية true أو false وفقًا لما إن كانت السلسلة النصية str تحتوي على السلسلة النصية الفرعية substr. هذه هي الطريقة الصحيحة في حال أردنا التأكد من وجود تطابق جزء من سلسلة ضمن سلسلة أخرى، ولا يهمنا موضعه:

alert( "Widget with id".includes("Widget") ); // true

alert( "Hello".includes("Bye") ); // false

المُعامِل الثاني الاختياري للتابع str.includes هو الموضع المراد بدء البحث منه:

alert( "Widget".includes("id") ); // true
alert( "Widget".includes("id", 3) ); // false, from position 3 there is no "id"

يعمل التابعان str.startsWith و str.endsWith بما هو واضح من مسمياتهما، “سلسلة نصية تبدأ بـ”، و “سلسلة نصية تنتهي بـ” على التوالي:

alert( "Widget".startsWith("Wid") ); // true, "Widget" starts with "Wid"
alert( "Widget".endsWith("get") ); // true, "Widget" ends with "get"

جلب جزء من نص

There are 3 methods in JavaScript to get a substring: substring, substr and slice.

str.slice(start [, end])‎

يُرجِع جزءًا من النص بدءًا من الموضع start وحتى الموضع end (بما لا يتضمن end).

مثلًا:

```js run
let str = "stringify";
alert( str.slice(0, 5) ); // 'strin', the substring from 0 to 5 (not including 5)
alert( str.slice(0, 1) ); // 's', from 0 to 1, but not including 1, so only character at 0
```

إن لم يكن هناك مُعامل ثانٍ، فسيقتطع التابع`slice` الجزء المحدد من الموضع `start` وحتى نهاية النص:

```js run
let str = "st*!*ringify*/!*";
alert( str.slice(2) ); // 'ringify', from the 2nd position till the end
```

يمكن أيضًا استخدام عدد سالبًا مع `start` أو `end`، وذلك يعني أن الموضع يُحسَب بدءًا من نهاية السلسلة النصية:

```js run
let str = "strin*!*gif*/!*y";

// start at the 4th position from the right, end at the 1st from the right
alert( str.slice(-4, -1) ); // 'gif'
```

str.substring(start [, end])‎

يُرجِع هذا التابع جزءًا من النص الواقع بين الموضع start والموضع end.

`. يشبه هذا التابع تقريبًا التابع `slice`، لكنه يسمح بكون المعامل `start` أكبر من `end`.

مثلًا:

```js run

let str = "stringify";

// substring الأمرين التاليين متماثلين بالنسبة لـ 
alert( str.substring(2, 6) ); // "ring"
alert( str.substring(6, 2) ); // "ring"

// slice لكن ليس مع
alert( str.slice(2, 6) ); // "ring" (نفس النتيجة السابقة)
alert( str.slice(6, 2) ); // "" (نص فارغ)


```

بعكس `slice`، القيم السالبة غير مدعومة ضمن المعاملات، وتقيَّم إلى `0` إن مُرِّرت إليه.

str.substr(start [, length])‎

يُرجِع هذا التابع الجزء المطلوب من النص، بدءًا من start وبالطول length المُعطى

 بعكس التوابع السابقة، يتيح لنا هذا التابع تحديد طول النص المطلوب بدلًا من موضع نهايته:

```js run

let str = "stringify";

// خذ 4 أحرف من الموضع 2
alert( str.substr(2, 4) ); // ring

```

يمكن أن يكون المُعامِل الأول سالبًا لتحديد الموضع بدءًا من النهاية:

```js run
let str = "strin*!*gi*/!*fy";
alert( str.substr(-4, 2) ); // حرفين ابتداءًا من الموضع الرابع
```

لِنُلَخِّص هذه التوابع لتجنب الخلط بينها:

التابع يقتطع … المواضع السالبة
slice(start, end)‎ من الموضع start إلى الموضع end (بما لا يتضمن end) مسموحة لكلا المعاملين
substring(start, end)‎ ما بين الموضع start والموضع end غير مسموحة وتصبح 0
substr(start, length)‎ أرجع الأحرف بطول length بدءًا من start مسموحة للمعامل start
أيهما تختار؟

يمكن لجميع التوابع تنفيذ الغرض المطلوب. لدى التابع substr قصور بسيط رسميًا: فهو غير ذكورة في توثيق JavaScript الرسمي، بل في Annex B والذي يغطي ميزات مدعومة في المتصفحات فقط لأسباب تاريخية، لذا فإن أي بيئة لا تعمل على المتصفح ستفشل في دعم هذا التابع، لكنه يعمل عمليًا في كل مكان.

ما بين الخيارين الآخرين، slice هو أكثر مرونة، فهو يسمح بتمرير مُعامِلات سالبة كما أنه أقصر في الكتابة. لذا، من الكافِ تذكر slice فقط من هذه التوابع الثلاث.

موازنة النصوص

توازن السلاسل النصية حرفًا حرفًا بترتيب أبجدي كما عرفنا في فصل المقارنات, strings are compared character-by-character in alphabetical order.

بالرغم من ذلك، يوجد بعض الحالات الشاذة.

1- الحرف الأجنبي الصغير دائما أكبر من الحرف الكبير:

```js run
alert( 'a' > 'Z' ); // true
```

2- الأحرف المُشَكَلَة خارج النطاق:

```js run
alert( 'Österreich' > 'Zealand' ); // true
```

قد يقود ذلك إلى نتائج غريبة إن رتبنا مثلًا بين أسماء بلدان، فيتوقع الناس دائمًا أن Zealand تأتي بعد Österreich في القائمة وأن تونس تأتي قبل سوريا وهكذا. لفهم ما يحدث، لنراجع تمثيل النصوص الداخلي في JavaScript.

جميع النصوص مشفرة باستخدام UTF-16. يعني أن: لكل حرف رمز عددي مقابل له. يوجد دوال خاصة تسمح بالحصول على الحرف من رمزه والعكس.

str.codePointAt(pos)‎

يُرجِع هذا التابع الرمز العددي الخاص بالحرف المعطى في الموضع pos:

```js run
// لدى الأحرف المختلفة في الحالة رموز مختلفة
alert( "z".codePointAt(0) ); // 122
alert( "Z".codePointAt(0) ); // 90
```

String.fromCodePoint(code)‎

يُنشِئ حرفًا من رمزه العددي code:

```js run
alert( String.fromCodePoint(90) ); // Z
```

We can also add Unicode characters by their codes using `\u` followed by the hex code:

```js run
// يُمثَّل العدد العشري 90 بالعدد 5a في النظام الست عشري.
alert( '\u005a' ); // Z
```

لنرَ الآن الأحرف ذات الرموز 65..220 (الأحرف اللاتينية وأشياء إضافية) عبر إنشاء نصوص منها:

let str = '';

for (let i = 65; i <= 220; i++) {
  str += String.fromCodePoint(i);
}
alert( str );
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„
// ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜ

تبدأ الأحرف الكبيرة كما ترى، ثم أحرف خاصة، ثم الأحرف الصغيرة، ثم Ö بالقرب من نهاية المخرجات.

يصبح الآن واضحًا لم a > Z. أي توازن الأحرف بواسطة قيمها العددية. فالرمز العددي الأكبر يعني أن الحرف أكبر. الرمز للحرف a هو 97‎ وهو أكبر من الرمز العددي للحرف Z الذي هو 90.

  • تأتي الأحرف الصغيرة بعد الأحرف الكبيرة دائمًا لأن رموزها العددية دائمًا أكبر.
  • تكون بعض الأحرف مثل Ö بعيدة عن الأحرف الهجائية. هنا، قيمة الحرف هذا أكبر من أي حرف بين a و z.

موازنات صحيحة

Correct comparisons

لحسن الحظ، تدعم جميع المتصفحات الحديثة المعيار العالمي ECMA 402(IE10- الذي يتطلب المكتبة الاضافية Intl.JS)، إذ يوفر تابعًا خاصًا لموازنة النصوص بلغات متعددة، وفقًا لقواعدها.

يُرجِع استدعاء التابع str.localeCompare(str2)‎ عددًا يحدد ما إن كان النص str أصغر، أو يساوي، أو أكبر من النص str2 وفقًا لقواعد اللغة المحلية:

  • يُرجِع قيمة سالبة إن كان str أصغر من str2.
  • يُرجِع قيمة موجبة إن كان str أكبر من str2.
  • يُرجِع 0 إن كانا متساويين.

إليك المثال التالي:

alert( 'Österreich'.localeCompare('Zealand') ); // -1

mdn:js/String/localeCompare

في الحقيقة، لهذه الدالة مُعامِلين إضافيين كما في توثيقها على MDN، إذ يسمح هذان المُعاملان بتحديد اللغة (تؤخذ من بيئة العمل تلقائيًا، ويعتمد ترتيب الأحرف على اللغة) بالإضافة إلى إعداد قواعد أخرى مثل الحساسية تجاه حالة الأحرف، أو ما إن كان يجب معاملة "a" و "á" بالطريقة نفسها …الخ.

ما خلف الستار، يونيكود

معلومات متقدمة

يتعمق الجزء التالي في ما يقبع خلف ستار النصوص التي تراها، وهذه المعلومات ستكون قيمة إن كنت تخطط للتعامل مع الرموز التعبيرية، أو الأحرف الرياضية النادرة أو الهيروغليفية أو أي رموز نادرة أخرى. يمكنك تخطي هذا الجزء إن لم تكن مهتمًا به.

أزواج بديلة (Surrogate pairs)

لكل الأحرف المستخدمة بكثرة رموز عددية (code) مؤلفة من 2-بايت. لدى أحرف اللغات الأوروبية، والأرقام، وحتى معظم الرموز الهيروغليفية تمثيل من 2-بايت.

لكن، نحصل من 2-بايت 65536 على تركيبًا فقط وذلك غير كافٍ لكل الرموز (symbol) المُحتَمَلَة، لذا فإن الرموز (symbol) النادرة مرمزة بزوج من المحارف بحجم 2-بايت يسمى “أزواج بديلة” (Surrogate pairs).

طول كل رمز هو 2:

// في الرياضيات X الحرف
alert( '𝒳'.length ); // 2

// وجه ضاحك بدموع
alert( '😂'.length ); // 2

// حرف صيني هيروغليفي نادر
alert( '𩷶'.length ); // 2

لاحظ أن الأزواج البديلة لم تكن موجودة منذ إنشاء JavaScript، ولذا لا تعالج بشكل صحيح بواسطة اللغة. في النصوص السابقة لدينا رمز واحد فقط، لكن طول النص length ظهر على أنه 2.

التابعان String.fromCodePoint و str.codePointAt نادران وقليلا الاستخدام، إذ يتعاملان مع الأزواج البديلة بصحة. وقد ظهرت مؤخرًا في اللغة. في السابق كان هنالك التابعان String.fromCharCode و str.charCodeAt فقط. هذان التابعان يشبهان fromCodePoint و codePointAt، لكنهما لا يتعاملان مع الأزواج البديلة.

قد يكون الحصول على رمز (symbol) واحد صعبًا، لأن الأزواج البديلة تُعامَل معاملة حرفين:

alert( '𝒳'[0] ); // رموز غريبة.
alert( '𝒳'[1] ); // أجزاء من الزوج البديل

لاحظ أن أجزاء الزوج البديل لا تحمل أي معنى إذا كانت منفصلة عن بعضها البعض. لذا فإن ما يعرضه مر alert في الأعلى هو شيء غير مفيد.

يمكن تَوَقُّع الأزواج البديلة عمليًا بواسطة رموزها: إن كان الرمز العددي لحرف يقع في المدى 0xd800..0xdbff، فإنه الجزء الأول من الزوج البديل. أما الجزء الثاني فيجب أن يكون في المدى 0xdc00..0xdfff. هذا المدى محجوز للأزواج البديلة وفقًا للمعايير المتبعة.

وفقًا للحالة السابقة، سنستعمل التابع charCodeAt الذي :

// لا يتعامل مع الأزواج البديلة، لذا فإنه يُرجِع أجزاء الرمز

alert( '𝒳'.charCodeAt(0).toString(16) ); // d835, between 0xd800 and 0xdbff
alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3, between 0xdc00 and 0xdfff

ستجد المزيد من الطرق للتعامل مع الأزواج البديلة لاحقًا في الفصل Iterables. يوجد أيضًا مكاتب خاصة لذلك، لكن لا يوجد شيء شهير محدد لِاقتراحه هنا.

علامات التشكيل وتوحيد الترميز

يوجد حروف مركبة في الكثير من اللغات والتي تتكون من الحرف الرئيسي مع علامة فوقه/تحته. مثلًا، يمكن للحرف a أن يكون أساسًا للأحرف التالية: àáâäãåā. لدى معظم الحروف المركبة رمزها الخاص بها في جدول UTF-16. لكن ليس جميعها، وذلك لوجود الكثير من الاحتمالات.

To support arbitrary compositions, UTF-16 allows us to use several Unicode characters: the base character followed by one or many “mark” characters that “decorate” it.

alert( 'S\u0307' ); // Ṡ

إن احتجنا إلى رمز آخر فوق أو تحت الحرف فلا مشكلة، أضِف العلامة المطلوبة فقط. مثلًا، إن ألحقنا حرف “نقطة بالأسفل” (رمزها ‎ \u0323)، فسنحصل على “S بنقاط فوقه وتحته”، Ṩ:

alert( 'S\u0307\u0323' ); // Ṩ

This provides great flexibility, but also an interesting problem: two characters may visually look the same, but be represented with different Unicode compositions.

For instance:

// S + نقطة في الأعلى + نقطة في الأسفل
let s1 = 'S\u0307\u0323'; // Ṩ

// S + نقطة في الأسفل + نقطة في الأعلى
let s2 = 'S\u0323\u0307'; // Ṩ,

alert( `s1: ${s1}, s2: ${s2}` );

alert( s1 == s2 ); // خطأ بالرغم من أن الحرفين متساويان ظاهريًا

To solve this, there exists a “Unicode normalization” algorithm that brings each string to the single “normal” form.

هذه الخوارزمية مُضَمَّنة في التابع str.normalize().

alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true

من المضحك في حالتنا أن normalize()‎ تجمع سلسلة من 3 أحرف مع بعضها بعضًا إلى حرف واحد: ‎ \u1e68 (الحرف S مع النقطتين).

alert( "S\u0307\u0323".normalize().length ); // 1

alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true

في الواقع، هذه ليست الحالة دائمًا. وذلك لأن الرمز متعارف بكثرة، فضَمَّنّهُ مُنشِئوا UTF-16 في الجدول الرئيسي وأعطوه رمزًا خاصًا.

إن أردت تعلم المزيد عن قواعد التوحيد واختلافاتها – فستجدها في ملحق معايير اليونيكود: نماذج توحيد ترميز اليونيكود, لكن للأغراض العملية المتعارفة فالمعلومات السابقة تفي بالغرض.

المُلخص

  • There are 3 types of quotes. Backticks allow a string to span multiple lines and embed expressions ${…}.
  • Strings in JavaScript are encoded using UTF-16.
  • We can use special characters like \n and insert letters by their Unicode using \u....
  • To get a character, use: [].
  • To get a substring, use: slice or substring.
  • To lowercase/uppercase a string, use: toLowerCase/toUpperCase.
  • To look for a substring, use: indexOf, or includes/startsWith/endsWith for simple checks.
  • To compare strings according to the language, use: localeCompare, otherwise they are compared by character codes.

يوجد الكثير من التوابع الأخرى المفيدة في النصوص:

  • str.trim()‎ تحذف (“تقتطع”) المسافات الفارغة من بداية ونهاية النص.
  • str.repeat(n)‎ تُكرِّر النص n مرة.
  • والمزيد، يمكن الاطلاع عليها في manual.

هنالك توابع أخرى للنصوص أيضًا تعمل على البحث/الاستبدال مع التعابير النمطية (regular expressions). لكن ذلك موضوع كبير، لذا فقد شُرِحَ في فصل مستقل، Regular expressions.

مهمه

الأهمية: 5

حول الحرف الأول إلى حرف كبير

اكتب دالة باسم ucFirst(str)‎ تُرجِع النص str مع تكبير أول حرف فيه، مثلًا:

ucFirst("john") == "John";

افتح sandbox بالإختبارات.

لا يمكننا استبدال الحرف الأول، لأن النصوص في JavaScript غير قابلة للتعديل. لكن، يمكننا إنشاء نص جديد وفقًا للنص الموجود، مع تكبير الحرف الأول:

let newStr = str[0].toUpperCase() + str.slice(1);

لكن، يوجد مشكلة صغيرة، وهي إن كان str فارغًا، فسيصبح str[0]‎ قيمة غير معرفة undefined، ولأن undefined لا يملك الدالة toUpperCase()‎ فسيظهر خطأ.

يوجد طريقتين بديلتين هنا: 1- استخدام str.charAt(0)‎، لأنها تُرجِع نصًا دائمًا (ربما نصًا فارغًا). 2- إضافة اختبار في حال كان النص فارغًا.

هنا الخيار الثاني:

function ucFirst(str) {
  if (!str) return str;

  return str[0].toUpperCase() + str.slice(1);
}

alert( ucFirst("john") ); // John

افتح الحل الإختبارات في sandbox.

الأهمية: 5

فحص وجود شيء مزعج

اكتب دالة باسم checkSpam(str)‎ تُرجِع true إن كان str يحوي ‘viagra’ أو ‘XXX’، وإلا فتُرجِع false. يجب أن لا تكون الدالة حساسة لحالة الأحرف:

checkSpam('buy ViAgRA now') == true
checkSpam('free xxxxx') == true
checkSpam("innocent rabbit") == false

افتح sandbox بالإختبارات.

لجعل البحث غير حساس لحالة الأحرف، نحوِّل النص إلى أحرف صغيرة ومن ثم نبحث فيه على النص المطلوب:

function checkSpam(str) {
  let lowerStr = str.toLowerCase();

  return lowerStr.includes('viagra') || lowerStr.includes('xxx');
}

alert( checkSpam('buy ViAgRA now') );
alert( checkSpam('free xxxxx') );
alert( checkSpam("innocent rabbit") );

افتح الحل الإختبارات في sandbox.

الأهمية: 5

قص النص

انشئ دالة باسم truncate(str, maxlength)‎ تفحص طول النص str وتستبدل نهايته التي تتجاوز الحد maxlength بالرمز "…" لجعل طولها يساوي maxlength بالضبط. يجب أن تكون مخرجات الدالة النص المقصوص (في حال حدث ذلك). مثلًا:

truncate("What I'd like to tell on this topic is:", 20) = "What I'd like to te…"

truncate("Hi everyone!", 20) = "Hi everyone!"

افتح sandbox بالإختبارات.

The maximal length must be maxlength, so we need to cut it a little shorter, to give space for the ellipsis.

Note that there is actually a single Unicode character for an ellipsis. That’s not three dots.

function truncate(str, maxlength) {
  return str.length > maxlength ? str.slice(0, maxlength - 1) + '…' : str;
}

افتح الحل الإختبارات في sandbox.

الأهمية: 4

استخراج المال

لدينا قيمة بالشكل "‎ $120"، إذ علامة الدولار تأتي أولًا ومن ثم العدد. أنشِئ دالة باسم extractCurrencyValue(str)‎ تستخرج القيمة العددية من نصوص مشابهة وإرجاعها. مثال:

alert( extractCurrencyValue('$120') === 120 ); // true

افتح sandbox بالإختبارات.

function extractCurrencyValue(str) {
  return +str.slice(1);
}

افتح الحل الإختبارات في sandbox.

خريطة الدورة التعليمية