الدوال عالية الرتبة والبرمجة الوظيفية
الدوال مواطنة من الدرجة الاولى
في الجافاسكريبت، الدوال هي مواطنة من الدرجة الاولى. هذه واحدة من اقوى ميزات اللغة وهي اساس البرمجة الوظيفية في الجافاسكريبت. كونها "من الدرجة الاولى" يعني ان الدوال تُعامل تماما مثل اي قيمة اخرى. يمكنك تعيينها لمتغيرات وتخزينها في مصفوفات وكائنات وتمريرها كوسائط لدوال اخرى واعادتها من دوال. هذا يختلف جذريا عن اللغات التي تكون فيها الدوال بنى خاصة لا يمكن الا الاعلان عنها واستدعاؤها.
مثال: الدوال كقيم
// تعيين دالة لمتغير
const greet = function(name) {
return 'Hello, ' + name + '!';
};
// تخزين الدوال في مصفوفة
const operations = [
function(a, b) { return a + b; },
function(a, b) { return a - b; },
function(a, b) { return a * b; },
];
// تخزين الدوال في كائن
const validators = {
isPositive: function(n) { return n > 0; },
isEven: function(n) { return n % 2 === 0; },
isString: function(val) { return typeof val === 'string'; },
};
// تمرير دالة كوسيط
function executeOperation(fn, a, b) {
return fn(a, b);
}
console.log(executeOperation(operations[0], 5, 3)); // 8
console.log(executeOperation(operations[2], 5, 3)); // 15
// اعادة دالة من دالة
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(10)); // 20
console.log(triple(10)); // 30
دالة createMultiplier هي مصنع ينتج دوالا جديدة. كل دالة مُعادة "تتذكر" قيمة factor من خلال الاغلاق (closure). هذا النمط من اعادة الدوال هو اساس العديد من تقنيات البرمجة الوظيفية التي سنستكشفها في هذا الدرس.
ما هي الدوال عالية الرتبة؟
الدالة عالية الرتبة (HOF) هي دالة تقوم بواحد على الاقل مما يلي: تقبل دالة واحدة او اكثر كوسائط، او تعيد دالة كنتيجة لها. الدوال عالية الرتبة هي الالية الاساسية للتجريد في البرمجة الوظيفية. تسمح لك بفصل "ماذا" عن "كيف" -- تصف ما يجب ان يحدث (الاستدعاء الراجع)، وتتعامل الدالة عالية الرتبة مع آليات كيفية حدوثه.
مثال: الدوال عالية الرتبة تقبل وتعيد الدوال
// دالة عالية الرتبة تقبل دالة
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log);
// 0
// 1
// 2
repeat(3, function(i) {
console.log('Iteration ' + (i + 1) + ' of 3');
});
// دالة عالية الرتبة تعيد دالة
function unless(test, thenFn) {
return function(...args) {
if (!test(...args)) {
return thenFn(...args);
}
};
}
const logUnlessNegative = unless(
(n) => n < 0,
(n) => console.log(n)
);
logUnlessNegative(5); // 5
logUnlessNegative(-3); // (لا شيء)
logUnlessNegative(10); // 10
addEventListener وsetTimeout وsetInterval وArray.prototype.forEach -- كل هذه تقبل دوال استدعاء راجع كوسائط وبالتالي هي دوال عالية الرتبة.الدوال عالية الرتبة المدمجة: map
دالة map تنشئ مصفوفة جديدة بتطبيق دالة تحويل على كل عنصر من عناصر المصفوفة الاصلية. لا تعدل المصفوفة الاصلية -- بل تعيد مصفوفة جديدة تماما. هذه واحدة من اكثر دوال المصفوفات استخداما في الجافاسكريبت الحديثة وحجر اساس في نمط البرمجة الوظيفية.
مثال: تحويل البيانات باستخدام map
const numbers = [1, 2, 3, 4, 5];
// مضاعفة كل رقم
const doubled = numbers.map(function(num) {
return num * 2;
});
console.log(doubled); // [2, 4, 6, 8, 10]
// صيغة دالة السهم (اكثر شيوعا في الممارسة)
const squared = numbers.map(num => num ** 2);
console.log(squared); // [1, 4, 9, 16, 25]
// تحويل الكائنات
const users = [
{ firstName: 'Alice', lastName: 'Johnson', age: 28 },
{ firstName: 'Bob', lastName: 'Smith', age: 34 },
{ firstName: 'Carol', lastName: 'Williams', age: 22 },
];
const fullNames = users.map(user => user.firstName + ' ' + user.lastName);
console.log(fullNames);
// ['Alice Johnson', 'Bob Smith', 'Carol Williams']
// map تمرر ثلاث وسائط: العنصر والفهرس والمصفوفة
const withIndex = numbers.map((num, index) => {
return { value: num, position: index };
});
console.log(withIndex);
// [{value:1,position:0}, {value:2,position:1}, ...]
الدوال عالية الرتبة المدمجة: filter
دالة filter تنشئ مصفوفة جديدة تحتوي فقط على العناصر التي تجتاز اختبارا تنفذه الدالة المقدمة. يجب ان يعيد الاستدعاء الراجع true للاحتفاظ بالعنصر او false لاستبعاده. مثل map، لا تغير filter المصفوفة الاصلية.
مثال: تصفية البيانات
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]
const greaterThanFive = numbers.filter(n => n > 5);
console.log(greaterThanFive); // [6, 7, 8, 9, 10]
// تصفية الكائنات
const products = [
{ name: 'Laptop', price: 999, inStock: true },
{ name: 'Phone', price: 699, inStock: false },
{ name: 'Tablet', price: 449, inStock: true },
{ name: 'Monitor', price: 299, inStock: true },
{ name: 'Keyboard', price: 79, inStock: false },
];
const availableProducts = products.filter(p => p.inStock);
console.log(availableProducts.length); // 3
const affordableAvailable = products.filter(p => p.inStock && p.price < 500);
console.log(affordableAvailable);
// [{name:'Tablet',price:449,...}, {name:'Monitor',price:299,...}]
// ازالة القيم الكاذبة من مصفوفة
const mixed = [0, 'hello', '', null, 42, undefined, 'world', false, NaN];
const truthy = mixed.filter(Boolean);
console.log(truthy); // ['hello', 42, 'world']
Boolean كاستدعاء راجع لـ filter هو نمط شائع لازالة جميع القيم الكاذبة (0 و'' وnull وundefined وfalse وNaN) من مصفوفة. يعمل لان Boolean(value) يعيد true للقيم الصادقة وfalse للقيم الكاذبة.الدوال عالية الرتبة المدمجة: reduce
دالة reduce هي اكثر دوال المصفوفات تنوعا. تعالج كل عنصر من عناصر المصفوفة وتراكم نتيجة واحدة. يستقبل الاستدعاء الراجع مراكما (المجموع الجاري او النتيجة) والعنصر الحالي ويعيد قيمة المراكم الجديدة. يمكنك تنفيذ map وfilter واي تحويل مصفوفة اخر تقريبا باستخدام reduce وحده، ولهذا يُعتبر اللبنة الاساسية لمعالجة المصفوفات.
مثال: تقليص المصفوفات الى قيم مفردة
const numbers = [1, 2, 3, 4, 5];
// جمع كل الارقام
const sum = numbers.reduce((accumulator, current) => {
return accumulator + current;
}, 0);
console.log(sum); // 15
// ايجاد القيمة القصوى
const max = numbers.reduce((acc, curr) => {
return curr > acc ? curr : acc;
}, -Infinity);
console.log(max); // 5
// عد تكرارات كل عنصر
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const counts = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(counts); // { apple: 3, banana: 2, orange: 1 }
// تجميع الكائنات حسب خاصية
const people = [
{ name: 'Alice', department: 'Engineering' },
{ name: 'Bob', department: 'Marketing' },
{ name: 'Carol', department: 'Engineering' },
{ name: 'Dave', department: 'Marketing' },
{ name: 'Eve', department: 'Design' },
];
const byDepartment = people.reduce((groups, person) => {
const dept = person.department;
if (!groups[dept]) {
groups[dept] = [];
}
groups[dept].push(person);
return groups;
}, {});
console.log(byDepartment);
// { Engineering: [{...}, {...}], Marketing: [{...}, {...}], Design: [{...}] }
// تسطيح المصفوفات المتداخلة
const nested = [[1, 2], [3, 4], [5, 6]];
const flat = nested.reduce((acc, arr) => acc.concat(arr), []);
console.log(flat); // [1, 2, 3, 4, 5, 6]
reduce. بدونها، تستخدم reduce العنصر الاول كمراكم ابتدائي وتبدا التكرار من العنصر الثاني. هذا قد يؤدي الى نتائج غير متوقعة، خاصة مع المصفوفات الفارغة حيث تثير TypeError. قدم دائما قيمة ابتدائية لجعل كودك قابلا للتنبؤ وامنا.الدوال عالية الرتبة المدمجة: sort
دالة sort ترتب عناصر المصفوفة في مكانها وتعيد المصفوفة المرتبة. تقبل دالة مقارنة اختيارية. بدون دالة مقارنة، تحول sort العناصر الى سلاسل نصية وترتبها حسب ترتيب نقاط كود UTF-16 مما يؤدي الى نتائج مفاجئة مع الارقام. يجب ان تعيد دالة المقارنة رقما سالبا اذا كان يجب ان ياتي a قبل b، ورقما موجبا اذا كان يجب ان ياتي a بعد b، وصفرا اذا كانا متساويين.
مثال: الترتيب بدوال المقارنة
// بدون دالة مقارنة -- يرتب كسلاسل نصية!
const nums = [10, 9, 8, 100, 20, 3];
console.log(nums.sort()); // [10, 100, 20, 3, 8, 9] -- خطا للارقام!
// ترتيب رقمي صحيح
const sorted = [10, 9, 8, 100, 20, 3].sort((a, b) => a - b);
console.log(sorted); // [3, 8, 9, 10, 20, 100]
// ترتيب تنازلي
const descending = [10, 9, 8, 100, 20, 3].sort((a, b) => b - a);
console.log(descending); // [100, 20, 10, 9, 8, 3]
// ترتيب الكائنات حسب خاصية
const students = [
{ name: 'Alice', grade: 92 },
{ name: 'Bob', grade: 85 },
{ name: 'Carol', grade: 98 },
{ name: 'Dave', grade: 78 },
];
const byGrade = [...students].sort((a, b) => b.grade - a.grade);
console.log(byGrade.map(s => s.name));
// ['Carol', 'Alice', 'Bob', 'Dave']
// ترتيب السلاسل النصية ابجديا (غير حساس لحالة الاحرف)
const names = ['banana', 'Apple', 'cherry', 'Date'];
const alphabetical = [...names].sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
console.log(alphabetical); // ['Apple', 'banana', 'cherry', 'Date']
map وfilter، تغير sort المصفوفة الاصلية. لتجنب التغيير، استخدم عامل الانتشار لانشاء نسخة اولا: [...array].sort(compareFn). بدلا من ذلك، استخدم Array.prototype.toSorted() وهي نسخة اجدد لا تغير المصفوفة متاحة في بيئات الجافاسكريبت الحديثة.تسلسل الدوال عالية الرتبة
واحد من اقوى الانماط في الجافاسكريبت الوظيفية هو تسلسل map وfilter وreduce معا. لان map وfilter تعيدان مصفوفات جديدة، يمكنك استدعاء دالة اخرى على النتيجة فورا. هذا ينشئ خط انابيب لمعالجة البيانات يسهل قراءته والتفكير فيه.
مثال: بناء خط انابيب للبيانات
const transactions = [
{ id: 1, type: 'sale', amount: 250, currency: 'USD' },
{ id: 2, type: 'refund', amount: 50, currency: 'USD' },
{ id: 3, type: 'sale', amount: 175, currency: 'EUR' },
{ id: 4, type: 'sale', amount: 320, currency: 'USD' },
{ id: 5, type: 'refund', amount: 80, currency: 'USD' },
{ id: 6, type: 'sale', amount: 410, currency: 'USD' },
];
// ايجاد اجمالي الايرادات من مبيعات USD فقط
const totalUsdRevenue = transactions
.filter(t => t.type === 'sale') // الاحتفاظ بالمبيعات فقط
.filter(t => t.currency === 'USD') // الاحتفاظ بـ USD فقط
.map(t => t.amount) // استخراج المبالغ
.reduce((sum, amount) => sum + amount, 0); // جمعها
console.log(totalUsdRevenue); // 980
// تحويل وتلخيص البيانات
const summary = transactions
.filter(t => t.currency === 'USD')
.reduce((acc, t) => {
if (t.type === 'sale') acc.sales += t.amount;
if (t.type === 'refund') acc.refunds += t.amount;
return acc;
}, { sales: 0, refunds: 0 });
summary.net = summary.sales - summary.refunds;
console.log(summary); // { sales: 980, refunds: 130, net: 850 }
انشاء دوال عالية الرتبة مخصصة
كتابة دوالك عالية الرتبة الخاصة تساعدك في انشاء تجريدات قابلة لاعادة الاستخدام. بدلا من تكرار منطق مشابه في اماكن متعددة، تستخرج النمط المشترك في دالة عالية الرتبة وتمرر السلوك المتغير كاستدعاء راجع.
مثال: دوال عالية الرتبة مخصصة
// اعادة محاولة دالة N مرات قبل الاستسلام
function retry(fn, maxAttempts, delay = 1000) {
return async function(...args) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn(...args);
} catch (error) {
if (attempt === maxAttempts) throw error;
console.log('Attempt ' + attempt + ' failed. Retrying...');
await new Promise(r => setTimeout(r, delay));
}
}
};
}
const fetchWithRetry = retry(fetch, 3, 2000);
// fetchWithRetry('https://api.example.com/data')
// ازالة الارتداد: تاخير تنفيذ الدالة حتى يتوقف الادخال
function debounce(fn, wait) {
let timeoutId = null;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, wait);
};
}
const handleSearch = debounce(function(query) {
console.log('Searching for: ' + query);
}, 300);
// الخنق: تحديد تنفيذ الدالة مرة واحدة لكل فترة زمنية
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
return fn.apply(this, args);
}
};
}
const handleScroll = throttle(function() {
console.log('Scroll position: ' + window.scrollY);
}, 200);
تركيب الدوال
تركيب الدوال هو عملية دمج دالتين او اكثر لانتاج دالة جديدة. مخرجات دالة واحدة تصبح مدخلات الدالة التالية. هذا مفهوم اساسي في البرمجة الوظيفية يتيح لك بناء تحويلات معقدة من قطع بسيطة قابلة لاعادة الاستخدام.
مثال: تركيب الدوال يدويا
// دوال نقية بسيطة
const add10 = x => x + 10;
const multiply3 = x => x * 3;
const subtract5 = x => x - 5;
// تركيب يدوي
const transform = x => subtract5(multiply3(add10(x)));
console.log(transform(5)); // subtract5(multiply3(15)) = subtract5(45) = 40
// القراءة من الداخل للخارج صعبة. دعنا نبني ادوات مساعدة.
ادوات pipe و compose
ادوات pipe وcompose تؤتمت تركيب الدوال. pipe تطبق الدوال من اليسار لليمين (ترتيب القراءة الطبيعي)، بينما compose تطبقها من اليمين لليسار (الاصطلاح الرياضي). كلتاهما مبنيتان باستخدام reduce.
مثال: بناء pipe و compose
// pipe: تركيب من اليسار لليمين
function pipe(...fns) {
return function(initialValue) {
return fns.reduce((value, fn) => fn(value), initialValue);
};
}
// compose: تركيب من اليمين لليسار
function compose(...fns) {
return function(initialValue) {
return fns.reduceRight((value, fn) => fn(value), initialValue);
};
}
const add10 = x => x + 10;
const multiply3 = x => x * 3;
const subtract5 = x => x - 5;
const toString = x => 'Result: ' + x;
// pipe تُقرا بشكل طبيعي: add10، ثم multiply3، ثم subtract5، ثم toString
const processWithPipe = pipe(add10, multiply3, subtract5, toString);
console.log(processWithPipe(5)); // 'Result: 40'
// compose تُقرا رياضيا: toString(subtract5(multiply3(add10(x))))
const processWithCompose = compose(toString, subtract5, multiply3, add10);
console.log(processWithCompose(5)); // 'Result: 40'
// مثال واقعي: خط انابيب معالجة النصوص
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const replaceSpaces = str => str.replace(/\s+/g, '-');
const removeSpecialChars = str => str.replace(/[^a-z0-9-]/g, '');
const slugify = pipe(trim, toLowerCase, replaceSpaces, removeSpecialChars);
console.log(slugify(' Hello World! This is a Test '));
// 'hello-world-this-is-a-test'
pipe على compose لانها تُقرا بترتيب اليسار لليمين الطبيعي. استخدم pipe لخطوط انابيب تحويل البيانات وcompose عندما تريد مطابقة الترميز الرياضي f(g(x)). كلتاهما تنتجان نتائج متطابقة -- فقط ترتيب الوسائط يختلف.التطبيق الجزئي
التطبيق الجزئي هو تقنية تثبت فيها (تملا مسبقا) بعض وسائط دالة وتعيد دالة جديدة تقبل الوسائط المتبقية. هذا يختلف عن الكاري (الذي يحول دالة من N وسيط الى N دالة كل منها بوسيط واحد). التطبيق الجزئي يتيح لك انشاء نسخ متخصصة من الدوال العامة.
مثال: التطبيق الجزئي
// اداة تطبيق جزئي عامة
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
// دالة تسجيل عامة
function log(level, timestamp, message) {
console.log('[' + level + '] ' + timestamp + ': ' + message);
}
// انشاء مسجلات متخصصة
const logError = partial(log, 'ERROR');
const logWarning = partial(log, 'WARNING');
const logInfo = partial(log, 'INFO');
const now = new Date().toISOString();
logError(now, 'Database connection failed');
// [ERROR] 2025-01-15T10:30:00.000Z: Database connection failed
logInfo(now, 'Server started successfully');
// [INFO] 2025-01-15T10:30:00.000Z: Server started successfully
// استخدام bind للتطبيق الجزئي
const multiply = (a, b) => a * b;
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// التطبيق الجزئي مع الاعدادات
function createApiClient(baseUrl, headers) {
return function(endpoint) {
return fetch(baseUrl + endpoint, { headers });
};
}
const githubApi = createApiClient('https://api.github.com', {
'Accept': 'application/vnd.github.v3+json',
});
// لاحقا، مرر فقط نقطة النهاية
// githubApi('/users/octocat')
التخزين المؤقت (Memoization)
التخزين المؤقت هو تقنية تحسين في البرمجة الوظيفية تخزن نتائج استدعاءات الدوال المكلفة في ذاكرة مؤقتة. عندما تُستدعى دالة مخزنة مؤقتا بنفس الوسائط مرة اخرى، تعيد النتيجة المخزنة بدلا من اعادة الحساب. يعمل هذا بشكل صحيح فقط مع الدوال النقية (الدوال التي تعيد دائما نفس المخرجات لنفس المدخلات وليس لها اثار جانبية).
مثال: تنفيذ التخزين المؤقت
// اداة تخزين مؤقت عامة
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit for: ' + key);
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// حساب مكلف
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// بدون تخزين مؤقت: fibonacci(40) يستغرق عدة ثوان
// مع تخزين مؤقت: فوري تقريبا بعد الاستدعاء الاول
const memoFib = memoize(function fib(n) {
if (n <= 1) return n;
return memoFib(n - 1) + memoFib(n - 2);
});
console.time('memoized');
console.log(memoFib(40)); // 102334155
console.timeEnd('memoized'); // ~1ms
// تخزين مؤقت لاستدعاءات API
const memoizedFetch = memoize(async function(url) {
const response = await fetch(url);
return response.json();
});
// تخزين مؤقت بحجم ذاكرة محدود (بنمط LRU)
function memoizeWithLimit(fn, maxSize = 100) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
const value = cache.get(key);
// نقل الى النهاية (الاكثر استخداما مؤخرا)
cache.delete(key);
cache.set(key, value);
return value;
}
const result = fn.apply(this, args);
cache.set(key, result);
// ازالة اقدم مدخل اذا تجاوز الحد
if (cache.size > maxSize) {
const oldestKey = cache.keys().next().value;
cache.delete(oldestKey);
}
return result;
};
}
الدوال النقية
الدالة النقية هي دالة تستوفي شرطين: تعيد دائما نفس المخرج لنفس المدخل (حتمية)، ولا تنتج اثارا جانبية (لا تعدل الحالة الخارجية ولا تجري عمليات ادخال/اخراج ولا تغير وسائطها). الدوال النقية هي اللبنات الاساسية للبرمجة الوظيفية لانها قابلة للتنبؤ وقابلة للاختبار وقابلة للتخزين المؤقت وامنة للتشغيل بالتوازي.
مثال: الدوال النقية مقابل غير النقية
// نقية: نفس المدخل ينتج دائما نفس المخرج، بدون اثار جانبية
function add(a, b) {
return a + b;
}
function calculateTax(price, rate) {
return price * rate;
}
function formatName(first, last) {
return first.charAt(0).toUpperCase() + first.slice(1) + ' ' +
last.charAt(0).toUpperCase() + last.slice(1);
}
// غير نقية: تعتمد على حالة خارجية
let discount = 0.1;
function calculatePrice(amount) {
return amount * (1 - discount); // تعتمد على discount الخارجي
}
// غير نقية: تعدل الحالة الخارجية (اثر جانبي)
let total = 0;
function addToTotal(amount) {
total += amount; // تغير المتغير الخارجي
return total;
}
// غير نقية: تعدل مدخلاتها (اثر جانبي)
function addItem(cart, item) {
cart.push(item); // تغير مصفوفة cart
return cart;
}
// نسخة نقية من addItem
function addItemPure(cart, item) {
return [...cart, item]; // تعيد مصفوفة جديدة
}
// غير نقية: تجري عمليات ادخال/اخراج
function logAndReturn(value) {
console.log(value); // اثر جانبي: ادخال/اخراج
return value;
}
انماط عدم القابلية للتغيير
عدم القابلية للتغيير يعني عدم تغيير البيانات بعد انشائها. بدلا من تعديل الكائنات او المصفوفات الموجودة، تنشئ كائنات جديدة بالتغييرات المطلوبة. هذا يمنع الاخطاء الناتجة عن الحالة المشتركة القابلة للتغيير ويجعل كودك اكثر قابلية للتنبؤ. توفر الجافاسكريبت عدة ادوات للعمل مع البيانات غير القابلة للتغيير.
مثال: تقنيات عدم القابلية للتغيير
// Object.freeze: يمنع تعديل الكائن (سطحي)
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
});
config.timeout = 10000; // يفشل بصمت (يثير خطا في الوضع الصارم)
console.log(config.timeout); // لا يزال 5000
// تنبيه: Object.freeze سطحي
const user = Object.freeze({
name: 'Alice',
preferences: { theme: 'dark', language: 'en' },
});
user.preferences.theme = 'light'; // هذا يعمل -- الكائنات المتداخلة غير مجمدة
console.log(user.preferences.theme); // 'light'
// اداة تجميد عميق
function deepFreeze(obj) {
Object.freeze(obj);
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null && !Object.isFrozen(obj[key])) {
deepFreeze(obj[key]);
}
});
return obj;
}
// عامل الانتشار للتحديثات غير القابلة للتغيير
const original = { name: 'Alice', age: 28, city: 'NYC' };
const updated = { ...original, age: 29 }; // كائن جديد، العمر تغير
console.log(original.age); // 28 (لم يتغير)
console.log(updated.age); // 29
// عمليات المصفوفات غير القابلة للتغيير
const numbers = [1, 2, 3, 4, 5];
// بدلا من push، استخدم spread
const withSix = [...numbers, 6]; // [1, 2, 3, 4, 5, 6]
// بدلا من splice، استخدم filter
const withoutThree = numbers.filter(n => n !== 3); // [1, 2, 4, 5]
// بدلا من التعديل بالفهرس، استخدم map
const doubleThird = numbers.map((n, i) => i === 2 ? n * 2 : n);
// [1, 2, 6, 4, 5]
// structuredClone للنسخ العميقة (جافاسكريبت حديثة)
const complex = {
users: [
{ name: 'Alice', scores: [95, 87, 92] },
{ name: 'Bob', scores: [78, 85, 90] },
],
metadata: { version: 2, lastUpdated: new Date() },
};
const deepCopy = structuredClone(complex);
deepCopy.users[0].scores.push(100);
console.log(complex.users[0].scores.length); // 3 (لم يتغير)
console.log(deepCopy.users[0].scores.length); // 4
= لـ "نسخ" الكائنات او المصفوفات ينشئ مرجعا وليس نسخة. كلا المتغيرين يشيران الى نفس البيانات في الذاكرة. تعديل واحد يعدل الاخر. استخدم دائما صيغة الانتشار {...obj} و[...arr] او structuredClone() للنسخ الحقيقية. استخدم structuredClone عندما يكون لديك كائنات متداخلة تحتاج نسخا عميقا.النمط التصريحي مقابل الحتمي
البرمجة الحتمية تصف كيفية فعل شيء خطوة بخطوة. البرمجة التصريحية تصف ما تريد ان تكون النتيجة، تاركة تفاصيل التنفيذ للدوال المجردة. البرمجة الوظيفية تفضل النمط التصريحي لانه اكثر قابلية للقراءة والايجاز واقل عرضة للاخطاء.
مثال: الحتمي مقابل التصريحي
const orders = [
{ product: 'Laptop', quantity: 2, unitPrice: 999 },
{ product: 'Mouse', quantity: 5, unitPrice: 25 },
{ product: 'Monitor', quantity: 1, unitPrice: 450 },
{ product: 'Keyboard', quantity: 3, unitPrice: 75 },
{ product: 'Webcam', quantity: 2, unitPrice: 89 },
];
// حتمي: تعليمات خطوة بخطوة
let imperativeResult = [];
for (let i = 0; i < orders.length; i++) {
const total = orders[i].quantity * orders[i].unitPrice;
if (total > 200) {
imperativeResult.push({
product: orders[i].product,
total: total,
});
}
}
imperativeResult.sort(function(a, b) {
return b.total - a.total;
});
// تصريحي: صف ما تريد
const declarativeResult = orders
.map(order => ({
product: order.product,
total: order.quantity * order.unitPrice,
}))
.filter(order => order.total > 200)
.sort((a, b) => b.total - a.total);
console.log(declarativeResult);
// [
// { product: 'Laptop', total: 1998 },
// { product: 'Monitor', total: 450 },
// { product: 'Keyboard', total: 225 },
// ]
النسخة التصريحية تُقرا كوصف للتحويل: "حوّل كل طلب الى منتج واجمالي، صفِّ تلك التي تتجاوز 200، رتب حسب الاجمالي تنازليا." النسخة الحتمية تتطلب منك تتبع متغيرات الحلقة والشروط والتغييرات ذهنيا لفهم النتيجة.
اعادة الهيكلة الوظيفية العملية
دعنا ناخذ دالة حتمية من العالم الحقيقي ونعيد هيكلتها خطوة بخطوة الى نمط وظيفي نظيف. هذا يوضح كيف تحسن البرمجة الوظيفية قابلية قراءة وصيانة الكود في الممارسة العملية.
مثال: اعادة الهيكلة من الحتمي الى الوظيفي
// قبل: معالجة حتمية للمستخدمين
function processUsersImperative(users) {
let result = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
let fullName = users[i].firstName + ' ' + users[i].lastName;
let user = {
id: users[i].id,
name: fullName.toUpperCase(),
email: users[i].email.toLowerCase(),
yearsSinceJoining: new Date().getFullYear() - users[i].joinYear,
};
if (user.yearsSinceJoining >= 2) {
result.push(user);
}
}
}
result.sort(function(a, b) {
return b.yearsSinceJoining - a.yearsSinceJoining;
});
return result;
}
// بعد: اعادة هيكلة وظيفية بدوال صغيرة قابلة لاعادة الاستخدام
const isActive = user => user.active;
const toDisplayUser = user => ({
id: user.id,
name: (user.firstName + ' ' + user.lastName).toUpperCase(),
email: user.email.toLowerCase(),
yearsSinceJoining: new Date().getFullYear() - user.joinYear,
});
const hasMinYears = minYears => user => user.yearsSinceJoining >= minYears;
const byYearsDescending = (a, b) => b.yearsSinceJoining - a.yearsSinceJoining;
function processUsersFunctional(users) {
return users
.filter(isActive)
.map(toDisplayUser)
.filter(hasMinYears(2))
.sort(byYearsDescending);
}
// اختبار مع بيانات نموذجية
const users = [
{ id: 1, firstName: 'alice', lastName: 'johnson', email: 'Alice@Email.COM', joinYear: 2020, active: true },
{ id: 2, firstName: 'bob', lastName: 'smith', email: 'Bob@Work.COM', joinYear: 2024, active: true },
{ id: 3, firstName: 'carol', lastName: 'williams', email: 'Carol@Mail.COM', joinYear: 2019, active: false },
{ id: 4, firstName: 'dave', lastName: 'brown', email: 'Dave@Corp.COM', joinYear: 2021, active: true },
];
console.log(processUsersFunctional(users));
// [
// { id: 1, name: 'ALICE JOHNSON', email: 'alice@email.com', yearsSinceJoining: 5 },
// { id: 4, name: 'DAVE BROWN', email: 'dave@corp.com', yearsSinceJoining: 4 },
// ]
لاحظ كيف تقسم النسخة الوظيفية المشكلة الى دوال صغيرة مسماة. كل دالة تفعل شيئا واحدا: isActive تصفي المستخدمين النشطين، toDisplayUser تحول الشكل، hasMinYears تنشئ مرشح الاقدمية (باستخدام التطبيق الجزئي)، وbyYearsDescending تحدد ترتيب الفرز. كل من هذه قابلة للاختبار واعادة الاستخدام بشكل مستقل. دالة processUsersFunctional تُقرا كوصف لخط الانابيب: صفِّ المستخدمين النشطين، حوّل الى تنسيق العرض، صفِّ حسب الحد الادنى للسنوات، رتب حسب السنوات تنازليا.
مثال: التركيب الوظيفي في تطبيق حقيقي
// بناء خط انابيب تحقق باستخدام التركيب
const createValidator = (rules) => (value) => {
const errors = rules
.map(rule => rule(value))
.filter(error => error !== null);
return {
isValid: errors.length === 0,
errors,
};
};
// قواعد تحقق فردية (دوال نقية)
const required = (value) =>
value === '' || value === null || value === undefined
? 'This field is required'
: null;
const minLength = (min) => (value) =>
typeof value === 'string' && value.length < min
? 'Must be at least ' + min + ' characters'
: null;
const maxLength = (max) => (value) =>
typeof value === 'string' && value.length > max
? 'Must be no more than ' + max + ' characters'
: null;
const matchesPattern = (regex, message) => (value) =>
typeof value === 'string' && !regex.test(value)
? message
: null;
// تركيب المتحققات لحقول محددة
const validateUsername = createValidator([
required,
minLength(3),
maxLength(20),
matchesPattern(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores allowed'),
]);
const validateEmail = createValidator([
required,
matchesPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Invalid email format'),
]);
console.log(validateUsername('ab'));
// { isValid: false, errors: ['Must be at least 3 characters'] }
console.log(validateUsername('alice_smith'));
// { isValid: true, errors: [] }
console.log(validateEmail('not-an-email'));
// { isValid: false, errors: ['Invalid email format'] }
يوضح نظام التحقق هذا عدة مبادئ للبرمجة الوظيفية تعمل معا: الدوال النقية كقواعد تحقق، والتطبيق الجزئي لقواعد قابلة للتكوين (مثل minLength(3))، والدوال عالية الرتبة لانشاء المتحقق، وmap مع filter للمعالجة. كل قاعدة تحقق هي دالة صغيرة قابلة للاختبار واعادة الاستخدام تتركب بنظافة في خطوط انابيب تحقق اكبر.
map/filter/reduce على الحلقات الحتمية، وتجنب تغيير وسائط الدوال، واستخدم التركيب لبناء سلوك معقد من اجزاء بسيطة. حتى التبني الجزئي للانماط الوظيفية سيجعل كودك اكثر قابلية للصيانة بشكل كبير.تمرين عملي
ابنِ وحدة معالجة بيانات كاملة باستخدام تقنيات البرمجة الوظيفية. ابدا بمصفوفة بيانات الموظفين هذه:
const employees = [
{ id: 1, name: 'Alice', department: 'Engineering', salary: 95000, performance: 4.5, yearsExp: 7 },
{ id: 2, name: 'Bob', department: 'Marketing', salary: 72000, performance: 3.8, yearsExp: 4 },
{ id: 3, name: 'Carol', department: 'Engineering', salary: 110000, performance: 4.9, yearsExp: 10 },
{ id: 4, name: 'Dave', department: 'Design', salary: 85000, performance: 4.2, yearsExp: 6 },
{ id: 5, name: 'Eve', department: 'Engineering', salary: 88000, performance: 3.5, yearsExp: 3 },
{ id: 6, name: 'Frank', department: 'Marketing', salary: 68000, performance: 4.0, yearsExp: 5 },
{ id: 7, name: 'Grace', department: 'Design', salary: 92000, performance: 4.7, yearsExp: 8 },
];
اكمل المهام التالية باستخدام تقنيات البرمجة الوظيفية فقط (بدون حلقات، بدون تغيير): (1) انشئ دالة pipe. (2) اكتب دوال تصفية نقية: byDepartment(dept) وbyMinPerformance(min) وbyMinExperience(years). (3) اكتب دالة تحويل نقية calculateBonus تضيف حقل bonus يساوي الراتب مضروبا في الاداء مقسوما على 5. (4) استخدم pipe لانشاء خط انابيب يصفي موظفي الهندسة ذوي الاداء فوق 4.0، ويحسب مكافاتهم، ويرتب حسب المكافاة تنازليا. (5) اكتب دالة memoize وغلف خط الانابيب بها. (6) انشئ دالة عالية الرتبة groupBy باستخدام reduce واستخدمها لتجميع جميع الموظفين حسب القسم. (7) اكتب دالة compose واستخدمها مع دوالك النقية لانشاء مولد تقارير رواتب قابل لاعادة الاستخدام ياخذ الموظفين ويصفي حسب الحد الادنى للخبرة 5 سنوات ويحول الى كائنات بالاسم والقسم والراتب ويرتب حسب الراتب تنازليا.