الدوال
الدوال الحديثة في JavaScript
JavaScript الحديث: الدوال المتقدمة
تقع الدوال في صميم JavaScript، وهي من أقوى جوانبها. سنستكشف في هذا الدرس الميزات الحديثة للدوال في JavaScript، ولا سيما الدوال السهمية، والإغلاقات (closures)، والدوال عالية المرتبة، وأهم الأنماط الوظيفية.
كثيرًا ما توصف JavaScript بأنها لغة ذات “دوال من الدرجة الأولى”، أي أن الدوال تُعامَل كأي متغير آخر. تمنح هذه الخاصية مرونة كبيرة وتتيح أنماط برمجة قوية.
مراجعة لأساسيات الدوال
قبل استكشاف المفاهيم المتقدمة، لنراجع الطرق المختلفة لتعريف الدوال في JavaScript.
تصريح الدالة
function saluer(nom) {
return `Bonjour, ${nom} !`;
}
console.log(saluer("Marie")); // "Bonjour, Marie !"
تعبير الدالة
const saluer = function(nom) {
return `Bonjour, ${nom} !`;
};
console.log(saluer("Pierre")); // "Bonjour, Pierre !"
الدالة السهمية (ES6)
const saluer = (nom) => {
return `Bonjour, ${nom} !`;
};
// نسخة موجزة للدوال البسيطة
const saluerConcis = nom => `Bonjour, ${nom} !`;
console.log(saluerConcis("Jean")); // "Bonjour, Jean !"
الدوال السهمية (Arrow Functions)
تقدم الدوال السهمية، التي أُدخلت مع ES6، صياغة موجزة وسلوكيات خاصة تجعلها مفيدة جدًا في سياقات معينة.
صياغة مبسطة
// دالة عادية
const double = function(x) {
return x * 2;
};
// الدالة السهمية المكافئة
const doubleArrow = (x) => {
return x * 2;
};
// نسخة موجزة جدًا (بدون أقواس معقوفة ولا return صريحة)
const doubleConc = x => x * 2;
console.log(doubleConc(5)); // 10
الأقواس والمعاملات
// بدون معاملات - الأقواس إلزامية
const salut = () => "Bonjour !";
// معامل واحد - الأقواس اختيارية
const carre = x => x * x;
// عدة معاملات - الأقواس إلزامية
const somme = (a, b) => a + b;
// إرجاع كائن - الأقواس مطلوبة حول الكائن
const creerPersonne = (nom, age) => ({ nom, age });
console.log(creerPersonne("Alice", 30)); // { nom: "Alice", age: 30 }
سلوك this
الفرق الوظيفي الرئيسي بين الدوال السهمية والدوال التقليدية هو طريقة تعاملها مع الكلمة المفتاحية this.
// في الدالة التقليدية، يعتمد this على سياق الاستدعاء
function FonctionClassique() {
this.valeur = 42;
setTimeout(function() {
console.log(this.valeur); // undefined (this يشير إلى الكائن العام)
}, 1000);
}
// في الدالة السهمية، this معجمي (موروث من السياق الأب)
function FonctionAvecFleche() {
this.valeur = 42;
setTimeout(() => {
console.log(this.valeur); // 42 (this هو نفسه في FonctionAvecFleche)
}, 1000);
}
متى نستخدم الدوال السهمية
تناسب الدوال السهمية بشكل خاص:
- الدوال القصيرة والبسيطة
- دوال callback، خصوصًا في دوال المصفوفات (map، filter، إلخ)
- الدوال المجهولة
- الحالات التي ترغب فيها في الحفاظ على سياق
this
ولا تناسب:
- دوال الكائنات (التي يجب أن تشير إلى الكائن عبر
this) - الدوال البانية (لا يمكن استخدام
newمع الدوال السهمية) - الدوال التي تستخدم
arguments(فالدوال السهمية لا تملك كائنarguments)
// مثال على الاستخدام مع دوال المصفوفات
const nombres = [1, 2, 3, 4, 5];
const carres = nombres.map(x => x * x); // [1, 4, 9, 16, 25]
// لا تُستخدم مع دوال الكائنات
const objet = {
valeur: 42,
// أسلوب سيئ
mauvaiseFonction: () => {
console.log(this.valeur); // undefined، this هو الكائن العام
},
// أسلوب جيد
bonneFonction() {
console.log(this.valeur); // 42
}
};
معاملات الدوال المتقدمة
أدخلت ES6 والإصدارات اللاحقة عدة ميزات للتعامل مع المعاملات بطريقة أكثر مرونة وتعبيرًا.
المعاملات الافتراضية
function saluer(nom = "Utilisateur", message = "Bonjour") {
return `${message}, ${nom} !`;
}
console.log(saluer()); // "Bonjour, Utilisateur !"
console.log(saluer("Marie")); // "Bonjour, Marie !"
console.log(saluer("Pierre", "Bienvenue")); // "Bienvenue, Pierre !"
// تعبيرات كقيم افتراضية
function getDateFormatee(date = new Date()) {
return date.toLocaleDateString();
}
// استخدام معاملات سابقة كقيم افتراضية
function calculer(a = 1, b = a * 2) {
return a + b;
}
console.log(calculer()); // 3 (1 + 2)
معاملات الباقي (Rest Parameters)
function somme(...nombres) {
return nombres.reduce((total, n) => total + n, 0);
}
console.log(somme(1, 2, 3, 4, 5)); // 15
// الجمع مع معاملات عادية
function formatMessage(prefix, suffix, ...messages) {
return messages.map(msg => `${prefix}${msg}${suffix}`);
}
console.log(formatMessage("<<", ">>", "Info", "Erreur", "Avertissement"));
// ["<<Info>>", "<<Erreur>>", "<<Avertissement>>"]
تفكيك المعاملات (Destructuring)
// تفكيك كائن في المعاملات
function afficherUtilisateur({ nom, age, email = "Non fourni" }) {
console.log(`Nom: ${nom}, Age: ${age}, Email: ${email}`);
}
afficherUtilisateur({ nom: "Alice", age: 30 });
// "Nom: Alice, Age: 30, Email: Non fourni"
// تفكيك مصفوفة
function afficherCoordonnees([x, y, z = 0]) {
console.log(`X: ${x}, Y: ${y}, Z: ${z}`);
}
afficherCoordonnees([10, 20]);
// "X: 10, Y: 20, Z: 0"
الإغلاقات (Closures)
تُعد الإغلاقات من أقوى المفاهيم في JavaScript، فهي تتيح للدالة أن “تتذكر” بيئتها المعجمية حتى بعد أن تنهي الدالة الأم تنفيذها.
فهم الإغلاقات
function creerCompteur() {
let compte = 0; // متغير خاص
return function() {
compte++; // الوصول إلى متغير النطاق الأب
return compte;
};
}
const compteur = creerCompteur();
console.log(compteur()); // 1
console.log(compteur()); // 2
console.log(compteur()); // 3
// المتغير compte "مُغلَّف" داخل الإغلاق
// ولا يمكن الوصول إليه من الخارج
تطبيقات عملية للإغلاقات
تغليف البيانات
function creerBanque(soldeInitial) {
let solde = soldeInitial; // متغير خاص
return {
deposer(montant) {
solde += montant;
return solde;
},
retirer(montant) {
if (montant <= solde) {
solde -= montant;
return true;
}
return false;
},
getSolde() {
return solde;
}
};
}
const monCompte = creerBanque(100);
console.log(monCompte.getSolde()); // 100
monCompte.deposer(50);
console.log(monCompte.getSolde()); // 150
monCompte.retirer(30);
console.log(monCompte.getSolde()); // 120
// solde لا يمكن الوصول إليه مباشرة: console.log(monCompte.solde) // undefined
إنشاء دوال مخصصة
function multiplierPar(facteur) {
return function(nombre) {
return nombre * facteur;
};
}
const double = multiplierPar(2);
const triple = multiplierPar(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
إدارة السياق غير المتزامن
function fetchData(url) {
return function(callback) {
fetch(url)
.then(response => response.json())
.then(data => callback(data))
.catch(error => console.error(error));
};
}
const getUserData = fetchData('https://api.example.com/users');
// لاحقًا في الشيفرة
getUserData(data => {
console.log("Données reçues:", data);
});
المشكلة الكلاسيكية للحلقات والإغلاقات
تظهر مشكلة شائعة مع الإغلاقات عند استخدام متغيرات الحلقات داخل دوال غير متزامنة:
// المشكلة
function probleme() {
const fonctions = [];
for (var i = 0; i < 3; i++) {
fonctions.push(function() {
console.log(i);
});
}
// في هذه المرحلة، أصبحت قيمة i تساوي 3
return fonctions;
}
const fns = probleme();
fns[0](); // 3 (وليس 0 كما قد نتوقع)
fns[1](); // 3
fns[2](); // 3
// الحل باستخدام let (نطاق الكتلة)
function solution() {
const fonctions = [];
for (let i = 0; i < 3; i++) {
// كل تكرار ينشئ i جديدًا
fonctions.push(function() {
console.log(i);
});
}
return fonctions;
}
const fnsSolution = solution();
fnsSolution[0](); // 0
fnsSolution[1](); // 1
fnsSolution[2](); // 2
// الحل باستخدام إغلاق صريح (مع var)
function solutionClassique() {
const fonctions = [];
for (var i = 0; i < 3; i++) {
// IIFE (Immediately Invoked Function Expression)
(function(index) {
fonctions.push(function() {
console.log(index);
});
})(i);
}
return fonctions;
}
الدوال عالية المرتبة (Higher-Order Functions)
الدوال عالية المرتبة هي دوال تأخذ دوال أخرى كوسائط أو تُرجع دوالَّ. وهي أساسية للبرمجة الوظيفية في JavaScript.
دوال تقبل callbacks
// دالة عالية المرتبة تأخذ دالة كمعامل
function appliquerOperation(nombre, operation) {
return operation(nombre);
}
// دوال يمكن استخدامها كـ callbacks
const double = x => x * 2;
const carre = x => x * x;
const absolu = x => Math.abs(x);
console.log(appliquerOperation(5, double)); // 10
console.log(appliquerOperation(-3, absolu)); // 3
console.log(appliquerOperation(4, carre)); // 16
دوال تُرجع دوالَّ
// Currying - تحويل دالة ذات عدة وسائط
// إلى سلسلة من الدوال ذات الوسيط الواحد
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
} else {
return function(...args2) {
return curried(...args, ...args2);
};
}
};
}
function ajouter(a, b, c) {
return a + b + c;
}
const ajouterCurry = curry(ajouter);
console.log(ajouterCurry(1)(2)(3)); // 6
console.log(ajouterCurry(1, 2)(3)); // 6
console.log(ajouterCurry(1)(2, 3)); // 6
// تركيب الدوال
const compose = (f, g) => x => f(g(x));
const ajouterUn = x => x + 1;
const multiplierParDeux = x => x * 2;
const ajouterPuisMultiplier = compose(multiplierParDeux, ajouterUn);
console.log(ajouterPuisMultiplier(5)); // (5 + 1) * 2 = 12
أمثلة على دوال عالية المرتبة مدمجة
تتضمن JavaScript عدة دوال عالية المرتبة مدمجة خاصة بالمصفوفات:
const nombres = [1, 2, 3, 4, 5, 6];
// map - تحوّل كل عنصر
const doubles = nombres.map(n => n * 2); // [2, 4, 6, 8, 10, 12]
// filter - تختار بعض العناصر
const pairs = nombres.filter(n => n % 2 === 0); // [2, 4, 6]
// reduce - تجمع القيم
const somme = nombres.reduce((acc, n) => acc + n, 0); // 21
// find - تجد أول عنصر يحقق شرطًا
const premierPair = nombres.find(n => n % 2 === 0); // 2
// every - تتحقق من أن جميع العناصر تحقق شرطًا
const tousSontPositifs = nombres.every(n => n > 0); // true
// some - تتحقق من أن عنصرًا واحدًا على الأقل يحقق شرطًا
const auMoinsUnGrand = nombres.some(n => n > 4); // true
أنماط وظيفية متقدمة
تتيح JavaScript تطبيق العديد من الأنماط المستمدة من البرمجة الوظيفية.
الدالة النقية (Pure Function)
الدالة النقية ليس لها آثار جانبية، وتُرجع دائمًا النتيجة نفسها للمدخلات نفسها:
// دالة نقية
function ajouter(a, b) {
return a + b;
}
// دالة غير نقية (أثر جانبي)
let total = 0;
function ajouterAuTotal(valeur) {
total += valeur; // تعدّل حالة خارجية
return total;
}
عدم القابلية للتغيير (Immutability)
يقوم مبدأ عدم القابلية للتغيير على عدم تعديل البيانات أبدًا، بل إنشاء نسخ جديدة منها:
// أسلوب غير قابل للتغيير
const array1 = [1, 2, 3];
const array2 = [...array1, 4]; // ينشئ مصفوفة جديدة
// array1 تبقى [1, 2, 3]
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // كائن جديد
// obj1 يبقى { a: 1, b: 2 }
الحفظ المؤقت للنتائج (Memoization)
الحفظ المؤقت للنتائج (Memoization) تقنية تحسين تقوم على تخزين نتائج استدعاءات الدالة في ذاكرة مؤقتة:
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
// دالة تعاودية مكلفة
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// نسخة محسّنة باستخدام memoization
const fiboMemo = memoize(function(n) {
if (n <= 1) return n;
return fiboMemo(n - 1) + fiboMemo(n - 2);
});
console.time("sans memo");
console.log(fibonacci(35)); // بطيئة جدًا
console.timeEnd("sans memo");
console.time("avec memo");
console.log(fiboMemo(35)); // أسرع بكثير
console.timeEnd("avec memo");
سياق التنفيذ وthis
إدارة سياق this أمر بالغ الأهمية في JavaScript، وهي مصدر لكثير من الالتباس.
التحكم في سياق this
توفر JavaScript عدة دوال للتحكم في سياق التنفيذ:
const personne = {
nom: "Alice",
saluer() {
return `Bonjour, je suis ${this.nom}`;
}
};
// الاستخدام العادي
console.log(personne.saluer()); // "Bonjour, je suis Alice"
// فقدان السياق
const fonctionSaluer = personne.saluer;
console.log(fonctionSaluer()); // "Bonjour, je suis undefined"
// الحل 1: bind
const fonctionLiee = personne.saluer.bind(personne);
console.log(fonctionLiee()); // "Bonjour, je suis Alice"
// الحل 2: call
console.log(personne.saluer.call({ nom: "Bob" })); // "Bonjour, je suis Bob"
// الحل 3: apply (مثل call لكنها تأخذ مصفوفة من الوسائط)
console.log(personne.saluer.apply({ nom: "Charlie" })); // "Bonjour, je suis Charlie"
الفروق بين call و apply و bind
function presenter(prefix, suffix) {
return `${prefix} ${this.nom} ${suffix}`;
}
const user = { nom: "Alice" };
// call: يستدعي الدالة فورًا مع وسائط منفصلة
console.log(presenter.call(user, "Bonjour", "!")); // "Bonjour Alice !"
// apply: يستدعي الدالة فورًا مع وسائط ضمن مصفوفة
console.log(presenter.apply(user, ["Salut", "..."])); // "Salut Alice ..."
// bind: ينشئ دالة جديدة مرتبطة بالسياق دون استدعائها
const presenterUtilisateur = presenter.bind(user, "Bienvenue");
console.log(presenterUtilisateur("!")); // "Bienvenue Alice !"
الدوال ذاتية التنفيذ وIIFE
IIFE (Immediately Invoked Function Expression)
الـ IIFE هي دوال تُنفَّذ مباشرة بعد تعريفها:
// Syntaxe classique
(function() {
const message = "Je m'exécute immédiatement";
console.log(message);
})();
// Avec des paramètres
(function(nom) {
console.log(`Bonjour, ${nom} !`);
})("Alice");
// Avec des fonctions fléchées
(() => {
console.log("IIFE avec flèche");
})();
فوائد IIFE
- عزل المتغيرات: تجنّب تلويث النطاق العام (global scope)
- تغليف البيانات: إنشاء وحدات (modules) بسيطة
- تنفيذ الشيفرة فورًا: تهيئة التطبيقات
- إنشاء closures: التقاط قيم من البيئة المحيطة
// Exemple de module avec IIFE
const compteur = (function() {
let compte = 0; // Variable privée
return {
incrementer() {
return ++compte;
},
decrementer() {
return --compte;
},
valeur() {
return compte;
}
};
})();
console.log(compteur.incrementer()); // 1
console.log(compteur.incrementer()); // 2
console.log(compteur.decrementer()); // 1
الدوال المولِّدة (Generators) وasync/await
تُعد الدوال المولِّدة وasync/await آليات قوية لإدارة مسارات التنفيذ المعقدة.
الدوال المولِّدة
يمكن للدوال المولِّدة تعليق تنفيذها ثم استئنافه:
function* compterJusqua(n) {
for (let i = 1; i <= n; i++) {
yield i; // يعلّق التنفيذ ويعيد القيمة
}
}
const compteur = compterJusqua(5);
console.log(compteur.next().value); // 1
console.log(compteur.next().value); // 2
console.log(compteur.next().value); // 3
console.log(compteur.next().value); // 4
console.log(compteur.next().value); // 5
console.log(compteur.next().value); // undefined
// Itération avec for...of
for (const valeur of compterJusqua(3)) {
console.log(valeur); // 1, 2, 3
}
الدوال async وawait
تبسّط async/await التعامل مع الشيفرة غير المتزامنة:
// Fonction qui retourne une Promise
function attendre(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Fonction asynchrone utilisant await
async function processAsync() {
console.log("Début");
await attendre(1000);
console.log("Après 1 seconde");
await attendre(1000);
console.log("Après 2 secondes");
return "Terminé";
}
// Utilisation
processAsync().then(resultat => {
console.log(resultat); // "Terminé" après 2 secondes
});
// Gestion d'erreurs
async function fetchData(url) {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error("Erreur lors de la récupération:", error);
throw error; // إعادة رمي الخطأ لمعالجته لاحقًا
}
}
تمارين عملية
لترسيخ معارفك، جرّب هذه التمارين:
-
تصفية متقدمة: اكتب دالة من الرتبة العليا باسم
filtrerParتستقبل معيارًا (وهو دالة) وتعيد دالة تصفّي مصفوفة وفقًا لهذا المعيار. -
Debounce: أنشئ دالة
debounceتحدّ من وتيرة تنفيذ دالة ما. وهي مفيدة لمعالجات الأحداث مثل البحث الفوري. -
Pipeline: نفّذ دالة
pipelineتستقبل عدة دوال وتشغّلها بالتسلسل، مع تمرير نتيجة كل دالة إلى التي تليها. -
Memoization مخصصة: حسّن الدالة
memoizeبحيث تأخذ في الاعتبار دالة مخصصة لتوليد المفاتيح. -
مدير الوعود: أنشئ دالة تستقبل مصفوفة من الوعود (promises) وتعيد وعدًا جديدًا يُحَل بمصفوفة من النتائج بالترتيب نفسه.
مثال كامل: محلّل البيانات
إليك مثالًا يطبّق عدة مفاهيم عرضناها:
// Simuler un appel API
const fetchData = () => Promise.resolve([
{ id: 1, nom: "Alice", age: 25, scores: [85, 90, 78] },
{ id: 2, nom: "Bob", age: 30, scores: [92, 85, 90] },
{ id: 3, nom: "Charlie", age: 22, scores: [70, 65, 80] },
{ id: 4, nom: "David", age: 28, scores: [95, 88, 92] }
]);
// Fonctions pures pour les opérations
const calculerMoyenne = scores =>
scores.reduce((total, score) => total + score, 0) / scores.length;
const evaluerPerformance = moyenne => {
if (moyenne >= 90) return "Excellent";
if (moyenne >= 80) return "Très bien";
if (moyenne >= 70) return "Bien";
return "Passable";
};
// Fonction d'ordre supérieur pour filtrer
const filtrerPar = critere => tableau =>
tableau.filter(critere);
// Closure pour garder un cache des résultats
function createAnalyseur() {
const cache = new Map();
return {
async analyserEtudiants(filtre = null) {
// Vérifier le cache
const cacheKey = filtre ? filtre.toString() : "tous";
if (cache.has(cacheKey)) {
console.log("Utilisation du cache pour", cacheKey);
return cache.get(cacheKey);
}
// Récupérer les données
const etudiants = await fetchData();
// Appliquer le filtre si nécessaire
const donneesFiltrees = filtre
? filtrerPar(filtre)(etudiants)
: etudiants;
// Transformer les données
const resultat = donneesFiltrees.map(etudiant => {
const moyenne = calculerMoyenne(etudiant.scores);
return {
id: etudiant.id,
nom: etudiant.nom,
age: etudiant.age,
moyenne: parseFloat(moyenne.toFixed(2)),
performance: evaluerPerformance(moyenne)
};
});
// Sauvegarder dans le cache et retourner
cache.set(cacheKey, resultat);
return resultat;
},
// Effacer le cache
clearCache() {
cache.clear();
}
};
}
// Utilisation
async function demoAnalyse() {
const analyseur = createAnalyseur();
// Analyse de tous les étudiants
console.log("Tous les étudiants:");
const tous = await analyseur.analyserEtudiants();
console.table(tous);
// Filtre par performance
console.log("\nÉtudiants excellents:");
const excellents = await analyseur.analyserEtudiants(
etudiant => calculerMoyenne(etudiant.scores) >= 90
);
console.table(excellents);
// Filtre par âge
console.log("\nÉtudiants de moins de 25 ans:");
const jeunes = await analyseur.analyserEtudiants(
etudiant => etudiant.age < 25
);
console.table(jeunes);
// Démonstration du cache
console.log("\nSeconde demande (depuis le cache):");
const deuxiemeAppel = await analyseur.analyserEtudiants();
console.table(deuxiemeAppel);
}
// Exécution de la démo
demoAnalyse().catch(error => console.error(error));
الخاتمة
استكشفنا في هذا الدرس الميزات المتقدمة للدوال في JavaScript الحديثة:
- الطرق المختلفة لتعريف الدوال، ولا سيما الدوال السهمية (arrow functions)
- الـ closures وقدرتها على تغليف البيانات
- الدوال من الرتبة العليا التي تتعامل مع دوال أخرى
- الأنماط الوظيفية مثل عدم قابلية التغيير (immutability) وحفظ النتائج (memoization)
- إدارة السياق باستخدام
thisوالدوالbindوcallوapply - الـ IIFE وإنشاء الوحدات
- الدوال المولِّدة وasync/await للشيفرة غير المتزامنة
تقع الدوال حقًا في صميم JavaScript، وإتقانها ضروري لكتابة شيفرة حديثة وفعّالة وقابلة للصيانة. وباعتماد المقاربة الوظيفية، ستتمكن من بناء تطبيقات أكثر متانة وقابلية للاختبار والتوسع.
في الدرس القادم، سنستكشف التفكيك (destructuring) وعامل الانتشار (spread operator).
هل تحتاج إلى مراجعة المتغيرات؟ راجع الدرس السابق حول المتغيرات.
هل أنت مستعد للمتابعة؟ انتقل إلى الدرس التالي حول التفكيك (destructuring).
التعليقات
التعليقات مدعومة عبر GitHub Discussions
سجّل الدخول باستخدام GitHub للمشاركة في النقاش