٣ يوليو ٢٠٢٠

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

تُخَزَّن النصوص في 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";

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

تتيح لنا أيضا الفاصلة العلوية المائلة تحديد “دالة كنموذج” قبل الفاصلة العلوية المائلة الأولى. تكون الصيغة كما يلي: ```funcstring````. تُستَدعى الدالةfunc` تلقائيًا، وتستقبل النص والتعابير المُضَمَّنة وتعالجها. يسمى هذا ب “القوالب الملحقة”. تجعل هذه الميزة من السهل تضمين قوالب مخصصة، لكنها تستخدم بشكل نادر عمليًا. يمكنك قراءة المزيد عنها في هذا الدليل.

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

ما زال بالإمكان كتابة نصوص متعددة الأسطر باستخدام علامات الاقتباس الأحادية والثنائية باستخدام ما يسمى ب “رمز السطر الجديد”، والذي يُكتَب ‎\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 بايت. هكذا يمكننا إدخال شيفرات طويلة.

أمثلة باستخدام حروف يونيكود:

alert( "\u00A9" ); // ©

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

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

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

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

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

يجب استخدام محرف التهريب في حالة استخدام علامة الاقتباس المحيطة بالنص نفسها، لذا فإن الحل الأمثل هو استخدام علامات اقتباس مزدوجة أو فواصل عليا مائلة في مثل هذه الحالة:

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)

يتيح لنا المُعامِل الثاني الاختياري البحث من الموضع المُعطَى. مثلًا في الحالة الثالثة، أول ظهور ل "id" هو في الموضع 1. لِلبحث عن الظهور التالي له نبدأ البحث من الموضع 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!
}

خدعة NOT على مستوى البِت

إحدى الخدع القديمة هي لعامل الثنائي ~ الذي تعمل على مستوى البِت. فهو يُحَوِّل العدد إلى عدد صحيح بصيغة 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(...))‎ يعمل بالصيغة «إن وُجِد».

حتى نكون دقيقين، عندما تُحَوَّل الأرقام إلى صيغة 32-بِت باستخدام المعامل ~ يوجد أعداد أخرى تُعطي القيمة 0، أصغر هذه الأعداد هي ‎~4294967295 == 0. ما يجعل هذا الفحص صحيحًا في حال النصوص القصيرة فقط.

لا نجد هذه الخدعة حاليًا سوى في الشيفرات القديمة، وذلك لأن 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
```
يمكننا إضافة حرف يونيكود باستخدام رمزه بواسطة `‎\u` متبوعة بالرمز الست عشري:

```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.

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

الخوارزمية الصحيحة لموازنة النصوص أكثر تعقيدًا مما يبدو عليه الأمر، لأن الأحرف تختلف باختلاف اللغات. لذا، يحتاج المتصفح لمعرفة اللغة لموازنة نصوصها موازنةً صحيحة.

لحسن الحظ، تدعم جميع المتصفحات الحديثة المعيار العالمي 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

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

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

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

لدعم التراكيب الأساسية، تتيح لنا UTF-16 استخدام العديد من حروف يونيكود: الحرف الرئيسي متبوعًا بعلامة أو أكثر لتشكيله. مثلًا، إن كان لدينا S متبوعًا بالرمز الخاص “النقطة العلوية” (التي رمزها ‎ \u0307). فسيُعرَض ك Ṡ.

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

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

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

هذا يوفر مرونة كبيرة، لكن مشكلة كبيرة أيضًا: قد يظهر حرفان بالشكل ذاته، لكن يمثلان بتراكيب يونيكود مختلفة. مثلًا:

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

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

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

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

لحل ذلك، يوجد خوارزمية تدعى “توحيد ترميز اليونيكود” (unicode normalization) والتي تُعيد كل نص إلى الصيغة الطبيعية المستقلة له.

هذه الخوارزمية مُضَمَّنة في التابع 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 في الجدول الرئيسي وأعطوه رمزًا خاصًا.

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

المُلخص

  • يوجد 3 أنواع لِعلامات الاقتباس. تسمح الشرطات العلوية المائلة للنص بالتوسع لأكثر من سطر وتضمين التعبير ‎${…}‎.
  • النصوص في JavaScript مُشَفَّرة بواسطة UTF-16.
  • يمكننا استخدام أحرف خاصة مثل ‎ \n وإدخال أحرف باستخدام رمز يونيكود الخاص بها باستخدام ‎\u...‎.
  • استخدم [] للحصول على حرف ضمن سلسلة نصية.
  • للحصول على جزء من النص، استخدم: slice أو substring.
  • للتحويل من أحرف كبيرة/صغيرة، استخدم: toLowerCase أو toUpperCase.
  • للبحث عن جزء من النص، استخدم: indexOf، أو includes أو startsWith أو endsWith للفحص البسيط.
  • لموازنة النصوص وفقًا للغة، استخدم: localeCompare، وإلا فستوازن برموز الحروف.

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

  • 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 بالإختبارات.

الطول الكلي هو maxlength، لذا فإننا نحتاج لقص النص إلى أقصر من ذلك بقليل لإعطاء مساحة للنقط "…". لاحظ أن هناك حرف يونيكود واحد للحرف "…". وليست ثلاث نقاط.

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.

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

التعليقات

إقرأ هذا قبل أن تضع تعليقًا…
  • إذا كان لديك اقتراحات أو تريد تحسينًا - من فضلك من فضلك إفتح موضوعًا فى جيتهاب أو شارك بنفسك بدلًا من التعليقات.
  • إذا لم تستطع أن تفهم شيئّا فى المقال - وضّح ماهو.
  • إذا كنت تريد عرض كود استخدم عنصر <code> ، وللكثير من السطور استخدم <pre>، ولأكثر من 10 سطور استخدم (plnkr, JSBin, codepen…)