كن مطوّر واجهات أمامية محترفا
6 دقيقة قراءة

JavaScript الوظيفية: الدوال النقية والثباتية والتركيب

دليل عملي للبرمجة الوظيفية في JavaScript — الدوال النقية والآثار الجانبية، والثباتية وكيف تحدّث البيانات بلا تغيير، والدوال من الدرجة الأولى وعالية الرتبة، وmap/filter/reduce، والتركيب والـ currying، ولماذا يقوم React على هذا الأسلوب — مع تمارين عملية وحلولها.

البرمجة الوظيفية ليست نمطًا غريبًا عليك أن تتحوّل إليه كليًّا — بل مجموعة عادات تجعل JavaScript أكثر توقّعًا وأسهل اختبارًا. الدوال النقية، والتحديثات الثابتة، والتركيب هي الأفكار نفسها التي بُني عليها React وRedux وخطوط البيانات الحديثة. لا تحتاج نظرية الفئات؛ تحتاج حفنة تقنيات تقلّل الأخطاء بإزالة ما يسبّب معظمها: التغيّر غير المنضبط. (يبني على الدوال وتوابع المصفوفات من أساسيات JavaScript.)

الوعد الجوهري: دالّة نقية بـ بيانات ثابتة تفعل الشيء نفسه دائمًا للمُدخَل نفسه ولا تغيّر شيئًا آخر. الشيفرة المبنية هكذا قابلة للاختبار ببساطة، وآمنة لإعادة الاستخدام، وخالية من فئة أخطاء "شيء في مكان آخر غيّر بياناتي".

الدوال النقية

الدالّة نقية إن حقّقت قاعدتين: خرجها يعتمد فقط على مُدخَلاتها، ولا تسبّب آثارًا جانبية (لا تغيير حالة خارجية، لا إدخال/إخراج، لا تسجيل، لا تغييرات DOM). نفس المُدخَل، نفس الخرج، في كل مرّة:

// نقية — تعتمد فقط على وسائطها، لا تغيّر شيئًا
function add(a, b) { return a + b; }
function fullName(user) { return `${user.first} ${user.last}`; }

// غير نقية — تقرأ/تكتب حالة خارجية
let total = 0;
function addToTotal(n) { total += n; }   // أثر جانبي: يغيّر `total`
function now() { return Date.now(); }     // الخرج يعتمد على الساعة

الدوال النقية متوقّعة، قابلة للاختبار منعزلةً (بلا إعداد، بلا محاكاة)، قابلة للتخزين المؤقّت، وآمنة للتشغيل بأي ترتيب. لا تستطيع جعل كل شيء نقيًّا — تحتاج التطبيقات إدخال/إخراج في مكان ما — لكنك تستطيع دفع الأجزاء غير النقية إلى الحوافّ وإبقاء المنطق الأساسي نقيًّا.

الآثار الجانبية، معزولة

الأثر الجانبي أي شيء تفعله الدالّة أبعد من إرجاع قيمة: الكتابة إلى متغيّر خارجها، استدعاء API، لمس DOM، التسجيل. إنها ضرورية، لكن العادة الوظيفية أن تعزلها كي تبقى معظم شيفرتك نقية:

// ❌ المنطق والأثر متشابكان
function applyDiscount(cart) {
  cart.total = cart.total * 0.9;          // يغيّر المُدخَل
  localStorage.setItem("cart", JSON.stringify(cart)); // أثر جانبي ممزوج
}

// ✅ حساب نقيّ، الأثر منفصل
function withDiscount(cart) {
  return { ...cart, total: cart.total * 0.9 };  // يُرجِع سلّة جديدة
}
localStorage.setItem("cart", JSON.stringify(withDiscount(cart))); // الأثر على الحافّة

الثباتية

الثباتية (immutability) تعني عدم تغيير البيانات في مكانها — بدل تغيير كائن أو مصفوفة، تنشئ واحدة جديدة بالتغيير. هذا أكبر مصدر للأمان، لأن الكائنات تُشارَك بالمرجع: تغيير واحدة قد يفاجئ كل مكان آخر يحمل المرجع نفسه.

// ❌ تغيير — يؤثّر على الجميع حاملي هذه المصفوفة/الكائن
arr.push(4);
user.age = 30;

// ✅ أنشئ قيمًا جديدة بمعامل النشر
const arr2 = [...arr, 4];
const user2 = { ...user, age: 30 };

// المصفوفات: فضّل التوابع التي تُرجِع مصفوفات جديدة
const doubled = nums.map(n => n * 2);          // لا حلقة تغيّر
const active  = users.filter(u => u.active);   // مصفوفة جديدة
const without = arr.filter((_, i) => i !== 2); // أزل الفهرس 2 بثبات

تحديث بيانات متداخلة بثبات يعني النشر عند كل مستوى تغيّره:

const next = {
  ...state,
  user: { ...state.user, name: "Ada" },   // كائن user جديد داخل state جديد
};

هذا النمط بالضبط هو كيف تحدّث حالة React — لا تغيّر أبدًا، أرجِع كائنًا جديدًا دائمًا — ولهذا تنتقل العادات الوظيفية مباشرةً إلى الأُطر الحديثة.

الدوال من الدرجة الأولى وعالية الرتبة

الدوال قيم في JavaScript: تستطيع تخزينها وتمريرها وإرجاعها. والدالّة عالية الرتبة هي التي تأخذ أو تُرجِع دالّة — أساس map/filter/reduce والتجريدات القابلة لإعادة الاستخدام:

// تأخذ دالّة
[1, 2, 3].map(n => n * 2);

// تُرجِع دالّة
function multiplyBy(factor) {
  return (n) => n * factor;
}
const triple = multiplyBy(3);
triple(5); // 15

// تأخذ وتُرجِع — غلاف قابل لإعادة الاستخدام
function withLogging(fn) {
  return (...args) => {
    console.log("استدعاء بـ", args);
    return fn(...args);
  };
}

map / filter / reduce كخطّ أنابيب

الثلاثي الوظيفي يعبّر عن معظم تحويلات البيانات تعريفيًّا — ماذا تريد، لا آلية الحلقة. تتسلسل إلى خطوط أنابيب مقروءة:

const totalActiveSpend = users
  .filter(u => u.active)            // أبقِ بعضًا
  .map(u => u.totalSpent)           // حوّل كلًّا
  .reduce((sum, n) => sum + n, 0);  // اطوِ إلى قيمة واحدة

reduce هي العامّة — تطوي قائمة إلى أي نتيجة واحدة (مجموع، كائن، خريطة مجمَّعة):

// جمّع العناصر حسب الفئة
const byCategory = items.reduce((acc, item) => {
  (acc[item.category] ??= []).push(item);
  return acc;
}, {});

التركيب والـ Currying

التركيب (composition) يبني عملية معقّدة بتسلسل دوال صغيرة — خرج إحداها يغذّي التالية:

const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);

const trim    = (s) => s.trim();
const lower   = (s) => s.toLowerCase();
const slugify = compose((s) => s.replace(/\s+/g, "-"), lower, trim);

slugify("  Hello World  "); // "hello-world"

الـ Currying يحوّل دالّة متعدّدة الوسائط إلى سلسلة دوال أحادية الوسيط، ما يجعل الدوال سهلة التهيئة المسبقة والتركيب:

const add = (a) => (b) => a + b;
const add10 = add(10);   // مطبَّق مسبقًا
add10(5); // 15

كلاهما يتيح بناء السلوك من قطع صغيرة مسمّاة قابلة لإعادة الاستخدام بدل دالّة واحدة كبيرة — أسهل قراءةً واختبارًا وإعادة تركيب.

الأخطاء الشائعة

  • وصف map/filter بـ "وظيفي" مع تغيير شيء داخل ردّ النداء (أثر جانبي).
  • استخدام forEach لبناء نتيجة بالدفع إلى مصفوفة خارجية — map/reduce يعبّران عنه نقيًّا.
  • الظنّ أن النشر يصنع نسخة عميقة — إنه سطحيّ؛ الكائنات المتداخلة ما زالت مشتركة.
  • تغيير حالة React (أو أي حالة) مباشرةً بدل إرجاع كائن جديد — يكسر كشف التغيير.
  • sort() وreverse() تغيّران في المكان — انسخ أولًا ([...arr].sort()) إن احتجت الثباتية.
  • الإفراط في التجريد بـ currying/composition حيث دالّة عادية أوضح.
  • نسيان القيمة الابتدائية لـ reduce، التي تسيء التصرّف مع المصفوفات الفارغة.

تمارين

جرّب كلًّا منها قبل فتح الحل.

تمرين 1 — اجعلها نقية

أعد كتابة هذا ليكون نقيًّا (بلا تغيير، بلا حالة خارجية):

let count = 0;
function tag(item) { count++; item.tagged = true; }
إظهار الحل
function tag(item) {
  return { ...item, tagged: true };   // كائن جديد، بلا count خارجي
}

النسخة النقية تُرجِع عنصرًا موسومًا جديدًا وتترك العدّ للمستدعي (مثلًا items.map(tag).length)، مزيلةً التغيير وcount الخارجي معًا.

تمرين 2 — تحديث ثابت

بمعطى const state = { user: { name: "A", age: 20 } }، أنتِج حالة جديدة بعمر 21، دون تغيير state.

إظهار الحل
const next = { ...state, user: { ...state.user, age: 21 } };

انشر عند كل مستوى تغيّره: كائن state جديد يحوي كائن user جديدًا — الأصل state سليم.

تمرين 3 — خطّ أنابيب واحد

من orders (كلٌّ { paid, amount })، احسب مجموع الطلبات المدفوعة باستخدام filter/map/reduce.

إظهار الحل
const total = orders
  .filter(o => o.paid)
  .map(o => o.amount)
  .reduce((sum, a) => sum + a, 0);

كل خطوة نقية وتُرجِع مصفوفة (أو قيمة) جديدة، فيُقرأ الخطّ كوصف للنتيجة بدل مسك دفاتر حلقة.

تمرين 4 — ركّب دالّتين

اكتب shout = compose(exclaim, upper) كي يكون shout("hi") يساوي "HI!".

إظهار الحل
const compose = (f, g) => (x) => f(g(x));
const upper = (s) => s.toUpperCase();
const exclaim = (s) => s + "!";
const shout = compose(exclaim, upper);
shout("hi"); // "HI!"

compose(f, g) يطبّق g أولًا ثم f — فيُكبَّر النصّ، ثم يُضاف !.

النموذج الذهني الذي تحتفظ به

JavaScript الوظيفية عن إزالة التغيّر غير المنضبط. اكتب دوالًّا نقية — الخرج يعتمد فقط على المُدخَل، بلا آثار جانبية — وادفع الآثار التي لا مفرّ منها (إدخال/إخراج، DOM، تخزين) إلى الحوافّ. عامِل البيانات كـ ثابتة: حدّث بإنشاء قيم جديدة بالنشر وتوابع المصفوفات التي تُرجِع مصفوفات جديدة، لا بتغيير مراجع مشتركة (هكذا تعمل حالة React بالضبط). اتّكئ على الدوال عالية الرتبة وخطّ map/filter/reduce لقول ماذا تريد تعريفيًّا، وركّب السلوك من قطع صغيرة بـ التركيب والـ currying. لست مضطرًّا لأن تكون متشدّدًا — مجرّد سحب منطقك الأساسي نحو دوال نقية ثابتة يزيل فئة كاملة من الأخطاء ويجعل كل شيء أسهل اختبارًا.