أساسيات JavaScript

التعبيرات النمطية في JavaScript

45 دقيقة الدرس 31 من 60

ما هي التعبيرات النمطية؟

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

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

إنشاء التعبيرات النمطية: الصيغة الحرفية مقابل المُنشئ

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

مثال: إنشاء التعبيرات النمطية

// الطريقة 1: الصيغة الحرفية (تُجمع عند تحميل السكربت)
const pattern1 = /hello/;
const pattern2 = /hello/gi;

// الطريقة 2: مُنشئ RegExp (يُجمع أثناء التشغيل)
const pattern3 = new RegExp('hello');
const pattern4 = new RegExp('hello', 'gi');

// المُنشئ مفيد عندما يكون النمط ديناميكيًا
const userInput = 'مصطلح البحث';
const dynamicPattern = new RegExp(userInput, 'i');

// كلاهما ينشئ نفس نوع الكائن
console.log(typeof pattern1); // "object"
console.log(pattern1 instanceof RegExp); // true
console.log(pattern3 instanceof RegExp); // true
ملاحظة: عند استخدام مُنشئ RegExp، يجب عليك مضاعفة ترميز الشرطات المائلة العكسية لأن السلسلة النصية نفسها تفسر مستوى واحدًا من الترميز. على سبيل المثال، لمطابقة رقم باستخدام \d، تكتب new RegExp('\\d') في المُنشئ، ولكن ببساطة /\d/ مع الصيغة الحرفية.

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

مثال: الأنماط الديناميكية مع المُنشئ

function highlightWord(text, word) {
    // ترميز أحرف regex الخاصة في كلمة المستخدم
    const escaped = word.replace(/[.*+?^${}()|[\]\]/g, '\\$&');
    const regex = new RegExp(`(${escaped})`, 'gi');
    return text.replace(regex, '<mark>$1</mark>');
}

console.log(highlightWord('Hello world! Hello again!', 'hello'));
// "<mark>Hello</mark> world! <mark>Hello</mark> again!"

أعلام التعبيرات النمطية

تعدّل الأعلام كيفية معالجة محرك التعبيرات النمطية للنمط. توضع بعد الشرطة المائلة الختامية في الصيغة الحرفية أو تُمرر كمعامل ثانٍ لمُنشئ RegExp. تدعم JavaScript عدة أعلام تتحكم في سلوك المطابقة.

مثال: جميع أعلام التعبيرات النمطية في JavaScript

// g - عام: إيجاد جميع المطابقات وليس الأولى فقط
const globalMatch = 'cat bat sat'.match(/[a-z]at/g);
console.log(globalMatch); // ["cat", "bat", "sat"]

// i - غير حساس لحالة الأحرف: تجاهل الحالة عند المطابقة
const caseInsensitive = 'Hello HELLO hello'.match(/hello/gi);
console.log(caseInsensitive); // ["Hello", "HELLO", "hello"]

// m - متعدد الأسطر: ^ و $ تطابق حدود السطر وليس حدود السلسلة فقط
const multiline = `Line 1
Line 2
Line 3`;
const lineStarts = multiline.match(/^Line/gm);
console.log(lineStarts); // ["Line", "Line", "Line"]

// s - DotAll: يجعل . تطابق أحرف السطر الجديد أيضًا
const dotAll = 'Hello\nWorld'.match(/Hello.World/s);
console.log(dotAll[0]); // "Hello\nWorld"

// u - يونيكود: يفعّل مطابقة يونيكود الكاملة
const unicode = '\u{1F600}'.match(/./u);
console.log(unicode[0]); // "\u{1F600}" (حرف إيموجي)

// d - HasIndices: يتضمن فهارس البداية والنهاية للمجموعات الملتقطة
const indices = 'hello world'.match(/(?<greeting>hello)/d);
console.log(indices.indices.groups.greeting); // [0, 5]

// دمج الأعلام
const combined = /pattern/gims;
نصيحة احترافية: علم u ضروري عند التعامل مع نص قد يحتوي على إيموجي أو أحرف خارج المستوى الأساسي متعدد اللغات. بدونه، تعامل JavaScript هذه الأحرف كوحدتي كود منفصلتين، مما يؤدي إلى نتائج مطابقة غير متوقعة.

دوال RegExp والسلاسل النصية الأساسية

توفر JavaScript عدة دوال على كائن RegExp وكائن String تعمل مع التعبيرات النمطية. فهم متى تستخدم كل دالة أمر حاسم لكتابة كود مطابقة أنماط فعال وصحيح.

مثال: test() -- التحقق مما إذا كان النمط يتطابق

// test() تعيد true أو false
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

console.log(emailPattern.test('user@example.com'));  // true
console.log(emailPattern.test('invalid-email'));      // false
console.log(emailPattern.test('user@.com'));           // false

// مفيدة في العبارات الشرطية
const input = 'user@example.com';
if (emailPattern.test(input)) {
    console.log('صيغة بريد إلكتروني صالحة');
} else {
    console.log('صيغة بريد إلكتروني غير صالحة');
}

مثال: match() -- إيجاد المطابقات في سلسلة نصية

const text = 'السعر هو $45.99 والضريبة هي $3.50';

// بدون علم g: تعيد أول مطابقة مع التفاصيل
const firstMatch = text.match(/\$[\d.]+/);
console.log(firstMatch[0]);     // "$45.99"
console.log(firstMatch.index);  // 10
console.log(firstMatch.input);  // السلسلة الأصلية

// مع علم g: تعيد مصفوفة بجميع المطابقات
const allMatches = text.match(/\$[\d.]+/g);
console.log(allMatches); // ["$45.99", "$3.50"]

// عدم وجود مطابقة يعيد null
const noMatch = text.match(/\u20AC[\d.]+/g);
console.log(noMatch); // null (لا توجد مبالغ باليورو)

مثال: matchAll() -- التكرار على جميع المطابقات مع التفاصيل

const text = 'التاريخ: 2024-01-15، التحديث: 2024-03-20';
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/g;

// matchAll() تعيد مكرر كائنات المطابقة
const matches = [...text.matchAll(dateRegex)];

for (const match of matches) {
    console.log('المطابقة الكاملة:', match[0]);
    console.log('السنة:', match[1]);
    console.log('الشهر:', match[2]);
    console.log('اليوم:', match[3]);
    console.log('الفهرس:', match.index);
    console.log('---');
}
// المطابقة الكاملة: 2024-01-15، السنة: 2024، الشهر: 01، اليوم: 15
// المطابقة الكاملة: 2024-03-20، السنة: 2024، الشهر: 03، اليوم: 20

مثال: search() -- إيجاد فهرس أول مطابقة

const text = 'JavaScript is awesome!';

// search() تعيد فهرس أول مطابقة أو -1
console.log(text.search(/awesome/));    // 14
console.log(text.search(/python/i));    // -1
console.log(text.search(/java/i));      // 0
console.log(text.search(/script/i));    // 4

مثال: replace() و replaceAll() مع التعبيرات النمطية

const text = 'Hello World! Hello JavaScript!';

// replace() مع regex -- تستبدل أول مطابقة (أو الكل مع علم g)
console.log(text.replace(/Hello/, 'Hi'));
// "Hi World! Hello JavaScript!"

console.log(text.replace(/Hello/g, 'Hi'));
// "Hi World! Hi JavaScript!"

// استخدام مجموعات الالتقاط في الاستبدال
const date = '2024-01-15';
const formatted = date.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1');
console.log(formatted); // "01/15/2024"

// استخدام دالة كمستبدل
const prices = 'تكلفة العناصر $10 و $25';
const doubled = prices.replace(/\$(\d+)/g, (match, amount) => {
    return '$' + (parseInt(amount) * 2);
});
console.log(doubled); // "تكلفة العناصر $20 و $50"

فئات الأحرف

تسمح فئات الأحرف بمطابقة أي حرف واحد من مجموعة محددة. توفر JavaScript فئات أحرف مختصرة للأنماط الشائعة وفئات أحرف مخصصة باستخدام الأقواس المربعة. هذه هي اللبنات الأساسية لكل نمط تعبير نمطي تقريبًا.

مثال: فئات الأحرف المدمجة والمخصصة

// \d -- تطابق أي رقم (0-9)
console.log('abc123'.match(/\d+/g));  // ["123"]

// \D -- تطابق أي حرف غير رقمي
console.log('abc123'.match(/\D+/g));  // ["abc"]

// \w -- تطابق أحرف الكلمة (حروف وأرقام وشرطة سفلية)
console.log('hello_world 123!'.match(/\w+/g));  // ["hello_world", "123"]

// \W -- تطابق الأحرف غير الكلمية
console.log('hello world!'.match(/\W+/g));  // [" ", "!"]

// \s -- تطابق المسافات البيضاء (مسافة وتبويب وسطر جديد إلخ)
console.log('hello\tworld\n!'.match(/\s/g));  // ["\t", "\n"]

// \S -- تطابق غير المسافات البيضاء
console.log('hello world'.match(/\S+/g));  // ["hello", "world"]

// . -- تطابق أي حرف ما عدا السطر الجديد (إلا مع علم s)
console.log('hat hot hit'.match(/h.t/g));  // ["hat", "hot", "hit"]

// فئات أحرف مخصصة مع []
console.log('gray grey'.match(/gr[ae]y/g));  // ["gray", "grey"]

// فئة أحرف منفية مع [^]
console.log('abc123'.match(/[^a-z]+/g));  // ["123"]

// نطاق في فئة الأحرف
console.log('A1b2C3'.match(/[A-Za-z]/g)); // ["A", "b", "C"]

المُحددات الكمية

تحدد المُحددات الكمية عدد المرات التي يجب أن يظهر فيها حرف أو مجموعة للمطابقة. تدعم JavaScript عدة أنواع من المُحددات الكمية، من التكرار البسيط إلى عناصر التحكم في النطاق الدقيق. بشكل افتراضي، المُحددات الكمية جشعة، مما يعني أنها تطابق أكبر قدر ممكن من النص. يمكنك جعلها كسولة بإضافة علامة استفهام بعدها، مما يجعلها تطابق أقل قدر ممكن من النص.

مثال: المُحددات الكمية عمليًا

// + تطابق واحد أو أكثر
console.log('aabbb'.match(/b+/));  // ["bbb"]

// * تطابق صفر أو أكثر
console.log('aac'.match(/ab*/));   // ["a"] (صفر b مقبول)

// ? تطابق صفر أو واحد
console.log('color colour'.match(/colou?r/g));  // ["color", "colour"]

// {n} تطابق بالضبط n مرة
console.log('1234567'.match(/\d{3}/g));  // ["123", "456"]

// {n,} تطابق n مرات أو أكثر
console.log('aabbbcccc'.match(/c{2,}/g));  // ["cccc"]

// {n,m} تطابق بين n و m مرة
console.log('aaabbbcccc'.match(/a{1,2}/g));  // ["aa", "a"]

// المُحددات الجشعة مقابل الكسولة
const html = '<div>Hello</div><div>World</div>';

// جشع (افتراضي) -- يطابق أكبر قدر ممكن
console.log(html.match(/<div>.*<\/div>/)[0]);
// "<div>Hello</div><div>World</div>" (السلسلة بأكملها)

// كسول (مع ?) -- يطابق أقل قدر ممكن
console.log(html.match(/<div>.*?<\/div>/)[0]);
// "<div>Hello</div>" (أول مطابقة فقط)
خطأ شائع: استخدام المُحددات الكمية الجشعة مع .* داخل أنماط تشبه HTML غالبًا ما يطابق نصًا أكثر بكثير مما هو مقصود. فكر دائمًا في استخدام المُحددات الكمية الكسولة .*? أو أنماط أكثر تحديدًا مثل [^<]* لتجنب المطابقة عبر حدود العناصر.

المراسي والحدود

المراسي لا تطابق الأحرف نفسها؛ بدلاً من ذلك، تطابق المواضع في السلسلة. تُستخدم للتأكيد على أن المطابقة تحدث في موقع محدد، مثل بداية السلسلة أو نهايتها أو عند حد الكلمة. المراسي ضرورية لضمان أن أنماطك تتطابق بالضبط حيث تتوقع.

مثال: المراسي ومطابقات الحدود

// ^ تطابق بداية السلسلة (أو السطر مع علم m)
console.log(/^Hello/.test('Hello World'));  // true
console.log(/^Hello/.test('Say Hello'));    // false

// $ تطابق نهاية السلسلة (أو السطر مع علم m)
console.log(/World$/.test('Hello World'));  // true
console.log(/World$/.test('World Cup'));    // false

// \b تطابق حد الكلمة
console.log('cat concatenate'.match(/\bcat\b/g));  // ["cat"]
// تطابق "cat" ككلمة كاملة فقط وليس داخل "concatenate"

// \B تطابق غير حد الكلمة
console.log('cat concatenate'.match(/\Bcat/g));  // ["cat"] (من concatenate)

// دمج المراسي لمطابقة السلسلة الكاملة
const isExactMatch = /^hello$/i;
console.log(isExactMatch.test('hello'));       // true
console.log(isExactMatch.test('hello world')); // false
console.log(isExactMatch.test('say hello'));    // false

// سلوك المراسي متعدد الأسطر
const text = `first line
second line
third line`;
console.log(text.match(/^\w+ line$/gm));
// ["first line", "second line", "third line"]

المجموعات والالتقاط

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

مثال: مجموعات الالتقاط والتبديل

// مجموعة التقاط أساسية
const dateMatch = '2024-01-15'.match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(dateMatch[0]); // "2024-01-15" (المطابقة الكاملة)
console.log(dateMatch[1]); // "2024" (المجموعة الأولى)
console.log(dateMatch[2]); // "01" (المجموعة الثانية)
console.log(dateMatch[3]); // "15" (المجموعة الثالثة)

// مجموعة غير التقاطية (?:...)
const nonCapturing = 'http://example.com'.match(/(?:http|https):\/\/(.+)/);
console.log(nonCapturing[1]); // "example.com" (النطاق فقط يُلتقط)

// التبديل مع |
const pets = 'لدي قطة وكلب';
console.log('I have a cat and a dog'.match(/cat|dog/g)); // ["cat", "dog"]

// التجميع مع المُحددات الكمية
const repeated = 'abcabcabc'.match(/(abc){2,}/);
console.log(repeated[0]); // "abcabcabc"

مثال: المجموعات المسماة

// مجموعات الالتقاط المسماة تستخدم (?<name>...)
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2024-01-15'.match(dateRegex);

console.log(match.groups.year);  // "2024"
console.log(match.groups.month); // "01"
console.log(match.groups.day);   // "15"

// المجموعات المسماة في replace()
const formatted = '2024-01-15'.replace(
    /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
    '$<day>/$<month>/$<year>'
);
console.log(formatted); // "15/01/2024"

// المجموعات المسماة مع matchAll()
const text = 'من 2024-01-15 إلى 2024-12-31';
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;

for (const m of text.matchAll(regex)) {
    console.log(`${m.groups.month}/${m.groups.day}/${m.groups.year}`);
}
// "01/15/2024"
// "12/31/2024"

المراجع الخلفية

تسمح المراجع الخلفية بالإشارة إلى مجموعة تم التقاطها سابقًا ضمن نفس التعبير النمطي. هذا مفيد لمطابقة الأنماط المتكررة وإيجاد الكلمات المكررة أو التأكد من تطابق محددات الفتح والإغلاق. المراجع الخلفية المرقمة تستخدم \1 و \2 وهكذا، بينما المراجع الخلفية المسماة تستخدم \k<name>.

مثال: المراجع الخلفية

// مرجع خلفي مرقم: \1 يشير إلى المجموعة الأولى الملتقطة
const duplicateWords = /\b(\w+)\s+\1\b/gi;
const text = 'The the quick brown fox fox jumped';
console.log(text.match(duplicateWords));
// ["The the", "fox fox"]

// مرجع خلفي مسمى: \k<name>
const matchQuoted = /(?<quote>['"]).*?\k<quote>/g;
const code = `She said "hello" and 'goodbye' today`;
console.log(code.match(matchQuoted));
// ["\"hello\"", "'goodbye'"]

// مرجع خلفي لمطابقة وسوم HTML
const tagMatch = /<(\w+)>.*?<\/\1>/g;
const html = '<b>bold</b> and <i>italic</i>';
console.log(html.match(tagMatch));
// ["<b>bold</b>", "<i>italic</i>"]

تأكيدات النظر للأمام والنظر للخلف

تأكيدات النظر للأمام والنظر للخلف هي تأكيدات صفرية العرض -- تتحقق مما إذا كان النمط موجودًا أمام أو خلف الموضع الحالي دون تضمينه في المطابقة. هذه التأكيدات قوية لمطابقة الأنماط المعقدة حيث تحتاج سياقًا حول المطابقة دون استهلاك الأحرف.

مثال: النظر للأمام والنظر للخلف

// نظر إيجابي للأمام (?=...) -- يطابق إذا متبوع بالنمط
const prices = '100 USD 200 EUR 300 USD';
const usdPrices = prices.match(/\d+(?= USD)/g);
console.log(usdPrices); // ["100", "300"]

// نظر سلبي للأمام (?!...) -- يطابق إذا غير متبوع بالنمط
const nonUsd = prices.match(/\d+(?! USD)(?= \w+)/g);
console.log(nonUsd); // ["200"]

// نظر إيجابي للخلف (?<=...) -- يطابق إذا مسبوق بالنمط
const text = 'السعر: $100، التكلفة: $200';
const amounts = text.match(/(?<=\$)\d+/g);
console.log(amounts); // ["100", "200"]

// نظر سلبي للخلف (?<!...) -- يطابق إذا غير مسبوق بالنمط
const words = 'unhappy happy'.match(/(?<!un)happy/g);
console.log(words); // ["happy"]

// عملي: التحقق من قوة كلمة المرور مع نظرات أمامية متعددة
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%]).{8,}$/;
console.log(strongPassword.test('Passw0rd!'));  // true
console.log(strongPassword.test('password'));    // false
console.log(strongPassword.test('Pass1!'));      // false (قصيرة جدًا)
نصيحة احترافية: تأكيدات النظر للأمام والنظر للخلف مثالية للتحقق من كلمات المرور لأنه يمكنك التحقق من شروط متعددة في وقت واحد. كل (?=.*condition) يتحقق بشكل مستقل من متطلب واحد، ويجب أن تنجح جميعها حتى يتطابق النمط العام.

أنماط التحقق الشائعة

تُستخدم التعبيرات النمطية بشكل شائع للتحقق من المدخلات. إليك أنماط مجربة جيدًا للتحقق من تنسيقات البيانات الشائعة. تذكر أن التحقق القائم على regex يجب أن يكمل التحقق من جانب الخادم وليس بديلاً عنه.

مثال: التحقق من البريد الإلكتروني

// نمط التحقق الأساسي من البريد الإلكتروني
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

console.log(emailRegex.test('user@example.com'));      // true
console.log(emailRegex.test('user.name@domain.co.uk')); // true
console.log(emailRegex.test('user@.com'));              // false
console.log(emailRegex.test('@example.com'));           // false
console.log(emailRegex.test('user@com'));               // false

// التفصيل:
// ^[a-zA-Z0-9._%+-]+   -- واحد أو أكثر من أحرف الجزء المحلي الصالحة
// @                      -- رمز @ حرفي
// [a-zA-Z0-9.-]+        -- واحد أو أكثر من أحرف النطاق الصالحة
// \.[a-zA-Z]{2,}$       -- نقطة متبوعة بحرفين أو أكثر TLD

مثال: التحقق من رقم الهاتف

// رقم هاتف أمريكي: (123) 456-7890 أو 123-456-7890 أو 1234567890
const usPhoneRegex = /^(\+1\s?)?(\(\d{3}\)|\d{3})[\s.-]?\d{3}[\s.-]?\d{4}$/;

console.log(usPhoneRegex.test('(123) 456-7890')); // true
console.log(usPhoneRegex.test('123-456-7890'));    // true
console.log(usPhoneRegex.test('1234567890'));      // true
console.log(usPhoneRegex.test('+1 123-456-7890')); // true
console.log(usPhoneRegex.test('12345'));            // false

// هاتف دولي مع رمز الدولة
const intlPhone = /^\+\d{1,3}\s?\d{4,14}$/;
console.log(intlPhone.test('+44 7911123456'));  // true
console.log(intlPhone.test('+966 501234567'));  // true

مثال: التحقق من عناوين URL

// نمط التحقق من URL
const urlRegex = /^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&\/=]*)$/;

console.log(urlRegex.test('https://example.com'));           // true
console.log(urlRegex.test('http://www.example.com/path'));   // true
console.log(urlRegex.test('www.example.com'));               // true
console.log(urlRegex.test('example.com/page?id=1'));         // true
console.log(urlRegex.test('not a url'));                     // false

// استخراج مكونات URL مع المجموعات المسماة
const urlParts = /^(?<protocol>https?:\/\/)?(?<domain>[^\/\s]+)(?<path>\/[^\s]*)?$/;
const parsed = 'https://example.com/path/page'.match(urlParts);
console.log(parsed.groups.protocol); // "https://"
console.log(parsed.groups.domain);   // "example.com"
console.log(parsed.groups.path);     // "/path/page"

أمثلة عملية من الواقع

دعنا نفحص عدة سيناريوهات واقعية حيث تحل التعبيرات النمطية تحديات التطوير الشائعة. توضح هذه الأمثلة كيف تجمع أنماط regex المفاهيم التي غطيناها في حلول عملية.

مثال: استخراج البيانات من السلاسل النصية

// استخراج جميع الهاشتاغات من منشور وسائل التواصل الاجتماعي
const post = 'تعلم #JavaScript و #RegExp ممتع! #WebDev';
const hashtags = post.match(/#\w+/g);
console.log(hashtags); // ["#JavaScript", "#RegExp", "#WebDev"]

// استخراج جميع الأرقام (بما في ذلك الكسور العشرية) من النص
const report = 'نمت المبيعات 15.5% إلى $1,234.56 مليون في الربع الثالث';
const numbers = report.match(/\d+\.?\d*/g);
console.log(numbers); // ["15.5", "1", "234.56"]

// تحليل سطر CSV مع احترام الحقول بين علامات الاقتباس
const csvLine = 'John,"Doe, Jr.",30,"New York"';
const fields = [...csvLine.matchAll(/(?:"([^"]*)"|([^,]+))/g)]
    .map(m => m[1] !== undefined ? m[1] : m[2]);
console.log(fields); // ["John", "Doe, Jr.", "30", "New York"]

مثال: تحويل وتنظيف النصوص

// تحويل camelCase إلى kebab-case
function toKebabCase(str) {
    return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
console.log(toKebabCase('backgroundColor')); // "background-color"
console.log(toKebabCase('fontSize'));         // "font-size"

// إزالة وسوم HTML من سلسلة نصية
function stripHtml(html) {
    return html.replace(/<[^>]*>/g, '');
}
console.log(stripHtml('<p>مرحبا <b>بالعالم</b></p>'));
// "مرحبا بالعالم"

// تطبيع المسافات البيضاء
function normalizeSpaces(text) {
    return text.replace(/\s+/g, ' ').trim();
}
console.log(normalizeSpaces('  Hello   world  \n  !'));
// "Hello world !"

// إخفاء البيانات الحساسة
function maskEmail(email) {
    return email.replace(/^(.).+(@.+)$/, '$1****$2');
}
console.log(maskEmail('john.doe@example.com')); // "j****@example.com"

// إخفاء رقم بطاقة الائتمان
function maskCard(number) {
    return number.replace(/\d(?=\d{4})/g, '*');
}
console.log(maskCard('4111222233334444')); // "************4444"

مثال: تعقيم المدخلات وتنسيقها

// تنسيق رقم الهاتف أثناء الكتابة
function formatPhone(input) {
    const digits = input.replace(/\D/g, '');
    if (digits.length <= 3) return digits;
    if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
    return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
}
console.log(formatPhone('1234567890')); // "(123) 456-7890"

// التحقق من slug وتنسيقه
function createSlug(title) {
    return title
        .toLowerCase()
        .replace(/[^\w\s-]/g, '')   // إزالة الأحرف غير الكلمية
        .replace(/\s+/g, '-')       // استبدال المسافات بشرطات
        .replace(/-+/g, '-')        // دمج الشرطات المتعددة
        .replace(/^-|-$/g, '');     // قص الشرطات البادئة/الختامية
}
console.log(createSlug('Hello World! This is a Test.'));
// "hello-world-this-is-a-test"

// تلوين بناء الجملة لمقاطع الكود (مبسط)
function highlightSyntax(code) {
    return code
        .replace(/\b(const|let|var|function|return|if|else)\b/g,
            '<span class="keyword">$1</span>')
        .replace(/(\/\/.*$)/gm,
            '<span class="comment">$1</span>')
        .replace(/('[^']*'|"[^"]*")/g,
            '<span class="string">$1</span>');
}
console.log(highlightSyntax('const name = "world"; // greeting'));
خطأ شائع: لا تستخدم regex أبدًا لتحليل HTML بالكامل. التعبيرات النمطية لا يمكنها التعامل مع البنى المتداخلة أو التعقيد الكامل لـ HTML. استخدم محلل DOM (DOMParser) لتحليل HTML. regex مناسب لإزالة الوسوم البسيطة أو استخراج البيانات من أجزاء HTML معروفة ومتحكم بها.

مثال: العمل مع دالة split()

// التقسيم بمحددات متعددة
const data = 'apple, banana; cherry  grape';
const fruits = data.split(/[,;\s]+/);
console.log(fruits); // ["apple", "banana", "cherry", "grape"]

// التقسيم مع الاحتفاظ بالمحدد
const sentence = 'Hello! How are you? I am fine.';
const parts = sentence.split(/([!?.])\s*/);
console.log(parts);
// ["Hello", "!", "How are you", "?", "I am fine", ".", ""]

// تقسيم camelCase إلى كلمات
const camel = 'backgroundColor';
const words = camel.split(/(?=[A-Z])/);
console.log(words); // ["background", "Color"]

اعتبارات الأداء

بينما التعبيرات النمطية قوية، يمكن للأنماط المكتوبة بشكل سيء أن تسبب مشاكل أداء شديدة، بما في ذلك التراجع الكارثي الذي يمكن أن يجمد تطبيقك. فهم هذه المخاطر يساعدك على كتابة أنماط regex فعالة.

مثال: تجنب مشاكل الأداء

// سيء: تراجع كارثي مع مُحددات كمية متداخلة
// هذا النمط يمكن أن يستغرق وقتًا أسيًا على السلاسل غير المتطابقة
// const bad = /^(a+)+$/;  // لا تستخدم

// جيد: مكافئ مبسط بدون مُحددات كمية متداخلة
const good = /^a+$/;

// سيء: مطابقة جشعة على سلاسل كبيرة
// const badHtml = /<div>.*<\/div>/;  // يمسح السلسلة بأكملها

// جيد: استخدم فئة أحرف منفية محددة أو مُحدد كمي كسول
const goodHtml = /<div>[^<]*<\/div>/;

// جمّع regex خارج الحلقات لأداء أفضل
const regex = /\d+/g;  // يُجمع مرة واحدة
const data = ['abc123', 'def456', 'ghi789'];

// جيد: إعادة استخدام regex المُجمع
const results = data.map(str => str.match(regex));

// نصيحة: أعد تعيين lastIndex عند إعادة استخدام regex عام
const globalRegex = /test/g;
console.log(globalRegex.test('test')); // true
console.log(globalRegex.lastIndex);    // 4
globalRegex.lastIndex = 0;             // إعادة التعيين قبل إعادة الاستخدام
console.log(globalRegex.test('test')); // true
ملاحظة: عند استخدام regex مع علم g في حلقة مع test() أو exec()، تتقدم خاصية lastIndex مع كل استدعاء. أعد دائمًا تعيين lastIndex إلى 0 قبل إعادة استخدام regex على سلسلة جديدة، وإلا قد تحصل على نتائج غير متوقعة.

تمرين عملي

ابنِ وحدة تحقق كاملة للنماذج باستخدام التعبيرات النمطية. أنشئ دوال تحقق للحقول التالية: (1) اسم مستخدم يجب أن يكون من 3 إلى 20 حرفًا يحتوي فقط على أحرف وأرقام وشرطات سفلية ويجب أن يبدأ بحرف؛ (2) كلمة مرور تتطلب 8 أحرف على الأقل مع حرف كبير واحد على الأقل وحرف صغير واحد ورقم واحد وحرف خاص واحد؛ (3) عنوان بريد إلكتروني بتنسيق قياسي؛ (4) عنوان URL يجب أن يبدأ بـ http أو https؛ (5) تاريخ بتنسيق YYYY-MM-DD حيث يكون الشهر بين 01 و 12 واليوم بين 01 و 31. اكتب دالة تأخذ أي سلسلة نصية وتحدد جميع التواريخ المضمنة وتستخرجها إلى مصفوفة من الكائنات مع خصائص السنة والشهر واليوم باستخدام المجموعات المسماة. أيضًا اكتب دالة تحويل نص تحول كتلة من النص إلى حالة العنوان (كتابة الحرف الأول من كل كلمة بحرف كبير) مع الحفاظ على حروف الجر الشائعة بأحرف صغيرة. اختبر جميع دوالك بمدخلات صالحة وغير صالحة واعرض النتائج في وحدة التحكم.