JavaScript: الدوال والمصفوفات والكائنات
درس 6 من 8
6JavaScript: الدوال والمصفوفات والكائنات
الصورة الكبيرةفي الدرس السابق كتبت أوامر تُنفَّذ مرة. الآن تتعلّم أن تغلّف الخطوات في دوال تُستخدم مرات، وأن تخزّن بيانات حقيقية: قائمة طلبات، مستخدم بخصائصه. هذه الثلاثة، الدوال والمصفوفات والكائنات، هي 80% مما ستكتبه في React والخادم لاحقًا.
ماذا تشاهد في الرسم: سلسلة تحويلات: تختار الطلبات المدفوعة، تحوّل كل طلب لمبلغه، ثم تجمعها. هذا الأسلوب ستراه في كل كود JavaScript حديث.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
الدالة آلة صغيرة: تضع فيها مدخلات (مثل عدد الأكواب)، تعمل عملها، وتخرج لك نتيجة (السعر). تبنيها مرة وتستخدمها ألف مرة. المصفوفة صفّ من الأدراج المرقّمة من صفر. والكائن بطاقة تعريف: «الاسم: نورة، المدينة: الرياض». وطرق المصفوفات مثل خط إنتاج في مصنع: محطة تستبعد التالف (filter)، ومحطة تغلّف كل قطعة (map)، ومحطة تعدّ المجموع (reduce).
الدوال قيم من الدرجة الأولى (first-class): تُخزَّن في متغيرات، وتُمرَّر لدوال أخرى (callbacks)، وتُعاد منها (closures تتذكّر نطاقها). الدوال السهمية أقصر ولا تملك this ولا arguments خاصّة بها. المصفوفات كائنات مرتّبة بفهارس، وطرقها غير المعدِّلة (map، filter، reduce، slice، toSorted) تعيد قيمًا جديدة بينما push وsort وsplice تعدّل الأصل. الكائنات تُمرَّر بالمرجع لا بالقيمة، فنسختان تشيران لنفس الكائن تتأثّران معًا، ولذلك النسخ بالنشر {...obj} (نسخ سطحي) أو structuredClone (عميق). التفكيك والقيم الافتراضية و?. و?? تختصر كثيرًا. وJSON صيغة نصية لنقل البيانات بين المتصفح والخادم، بـ JSON.stringify وJSON.parse.
قبل أن تبدأ
⚡JavaScript من الصفر
تعرف المتغيرات وif وfor.
الفكرة الجوهرية
1الدوال: وصفة لها اسم
الدالة تُعرَّف بـ function واسم، ثم معاملات بين قوسين (مدخلاتها)، ثم كتلة. وreturn يعيد النتيجة ويُنهي الدالة. تناديها باسمها وتعطيها وسائط (القيم الفعلية). والمعامل يقبل قيمة افتراضية إن لم تُعطَ.
function price(cups, each = 14) {
return cups * each;
}
function greet(name) {
return `Welcome back, ${name}!`;
}
console.log(price(3)); // uses the default 14
console.log(price(3, 18)); // overrides it
console.log(greet("Lina"));
const result = price(2);
console.log("stored result:", result);return. دالة تحسب لكنها لا تعيد شيئًا تعطيك undefined. function double(n) { n * 2 } ثم double(4) يعطي undefined، والصحيح return n * 2.2الدوال السهمية وتمرير الدوال
الدالة السهمية طريقة أقصر: (a, b) => a + b. إن كان جسمها تعبيرًا واحدًا فلا تحتاج أقواسًا ولا return. وفي JavaScript الدالة قيمة كأي قيمة: تخزّنها في متغير، وتمرّرها لدالة أخرى لتناديها لاحقًا. الدالة الممرّرة اسمها callback، وستراها في كل مكان: الأحداث، المؤقّتات، طرق المصفوفات.
const withVat = (amount) => Math.round(amount * 1.15);
const isCheap = (price) => price < 15;
console.log(withVat(100), isCheap(12), isCheap(20));
// a function that takes another function
function applyToAll(list, fn) {
const out = [];
for (const item of list) out.push(fn(item));
return out;
}
console.log(applyToAll([10, 20, 30], withVat));
console.log(applyToAll([10, 20, 30], (n) => n / 10));3المصفوفات: قوائم مرتّبة
المصفوفة قائمة بين أقواس مربّعة [ ]. كل عنصر له فهرس يبدأ من صفر: الأول list[0]، والأخير list[list.length - 1] أو list.at(-1). push يضيف للنهاية، وpop يحذف من النهاية، وincludes يسأل «هل هو موجود؟»، وindexOf «أين هو؟»، وslice يقتطع جزءًا بلا تعديل الأصل.
const menu = ["espresso", "latte", "mocha"];
console.log(menu[0], menu.at(-1), menu.length);
menu.push("v60");
console.log(menu);
console.log(menu.includes("tea"), menu.indexOf("mocha"));
console.log(menu.slice(1, 3), "| original:", menu.length, "items");
const last = menu.pop();
console.log("removed", last, "->", menu);menu[3] غير موجود ويعطي undefined بلا خطأ. أشهر خطأ عند المبتدئين اسمه «خطأ بفارق واحد» (off-by-one).4filter وmap وreduce: فكّر بالتحويل
بدل حلقة for بمتغيرات مؤقتة، صِف ما تريد: filter(fn) يختار العناصر التي تعيد لها الدالة true. map(fn) يحوّل كل عنصر لشيء جديد ويعيد مصفوفة بنفس الطول. reduce(fn, start) يجمع المصفوفة في قيمة واحدة. وكل واحدة تعيد قيمة جديدة ولا تغيّر الأصل. ومعها: find أول عنصر يطابق، وsome/every هل يطابق أحدها/كلها، وsort للترتيب.
const orders = [
{ id: 1, item: "latte", cups: 2, paid: true },
{ id: 2, item: "mocha", cups: 1, paid: false },
{ id: 3, item: "v60", cups: 3, paid: true },
{ id: 4, item: "latte", cups: 1, paid: true },
];
const PRICE = { latte: 14, mocha: 16, v60: 18 };
const paid = orders.filter((o) => o.paid);
const amounts = paid.map((o) => o.cups * PRICE[o.item]);
const revenue = amounts.reduce((sum, x) => sum + x, 0);
console.log("paid ids:", paid.map((o) => o.id));
console.log("amounts:", amounts);
console.log("revenue:", revenue);
console.log("first unpaid:", orders.find((o) => !o.paid).item);
console.log("any big order?", orders.some((o) => o.cups >= 3));كيف يعمل reduce خطوة خطوة؟ يبدأ المُجمِّع sum بالقيمة الثانية (0)، ثم لكل عنصر يعيد sum + x فتصير هي sum للعنصر التالي: 0 → 28 → 82 → 96. القيمة الأخيرة هي النتيجة.
5الكائنات: بطاقة خصائص
الكائن بين أقواس معقوفة { }، فيه أزواج مفتاح: قيمة. تقرأ الخاصية بنقطة user.name، أو بأقواس user["name"] حين يكون المفتاح في متغير. وتضيف أو تغيّر خاصية بالإسناد. والقيمة قد تكون أي شيء: نصًّا، رقمًا، مصفوفة، كائنًا آخر، أو دالة (وتسمّى حينها طريقة).
const user = {
name: "Noura",
city: "Riyadh",
points: 120,
favourites: ["latte", "v60"],
prefs: { milk: "oat", sugar: false },
greet() {
return `Hi, I am ${this.name}`;
},
};
console.log(user.name, user["city"], user.favourites[1], user.prefs.milk);
user.points += 30;
user.phone = "0500000000";
console.log(user.points, user.phone);
console.log(user.greet());
console.log(Object.keys(user));const a = user; a.city = "Jeddah"; غيّر user أيضًا! الكائنات تُنسخ بالمرجع: a وuser اسمان لنفس البطاقة. لنسخة مستقلة اكتب const a = { ...user }.6التفكيك والنشر و?. و??
التفكيك يسحب ما تحتاجه من كائن أو مصفوفة في سطر: const { name, city } = user. النشر ... يفرد محتوى مصفوفة أو كائن داخل آخر، فتنسخ مع تغيير دون أن تلمس الأصل. و?. يقرأ خاصية بأمان فيعطي undefined بدل أن يتوقف البرنامج إن كان ما قبله غير موجود. و?? يعطي قيمة بديلة إن كانت القيمة null أو undefined.
const user = { name: "Noura", city: "Riyadh", prefs: { milk: "oat" } };
const { name, city } = user;
const moved = { ...user, city: "Jeddah" };
console.log(name, city, "->", moved.city, "| original still", user.city);
const [first, ...rest] = ["latte", "mocha", "v60"];
console.log(first, rest);
console.log([...rest, "tea"]);
console.log(user.prefs?.milk);
console.log(user.address?.street); // undefined, no crash
console.log(user.address?.street ?? "no address");7JSON: البيانات حين تسافر
حين يرسل الخادم بيانات للمتصفح، أو تحفظ شيئًا في المتصفح، لا تُرسل الكائنات كما هي بل كنص بصيغة JSON، وهي تشبه كائنات JavaScript لكن المفاتيح بين علامات تنصيص مزدوجة دائمًا. JSON.stringify(obj) يحوّل كائنًا لنص، وJSON.parse(text) يعيده كائنًا.
const order = { id: 7, items: ["latte", "mocha"], total: 30, paid: true };
const text = JSON.stringify(order);
console.log(typeof text, text);
const back = JSON.parse(text);
console.log(typeof back, back.items[1], back.total + 5);
// what an API usually sends back
const reply = '{"menu":[{"name":"latte","price":14},{"name":"v60","price":18}]}';
const data = JSON.parse(reply);
console.log(data.menu.map((d) => `${d.name}: ${d.price}`).join(", "));مثال محلول
المسألة: تقرير مبيعات المقهى ليوم كامل. من قائمة الطلبات: (1) المدفوع فقط، (2) الإيراد لكل مشروب، (3) أكثر مشروب مبيعًا، (4) أسماء العملاء الذين طلبوا أكثر من مرة. الحل بدوال صغيرة لكل سؤال:
const PRICE = { latte: 14, mocha: 16, v60: 18 };
const orders = [
{ customer: "Ali", item: "latte", cups: 2, paid: true },
{ customer: "Sara", item: "v60", cups: 1, paid: true },
{ customer: "Ali", item: "mocha", cups: 1, paid: false },
{ customer: "Lina", item: "latte", cups: 3, paid: true },
{ customer: "Sara", item: "latte", cups: 1, paid: true },
];
const paidOnly = (list) => list.filter((o) => o.paid);
function revenueByDrink(list) {
return list.reduce((acc, o) => {
acc[o.item] = (acc[o.item] ?? 0) + o.cups * PRICE[o.item];
return acc;
}, {});
}
function bestSeller(byDrink) {
return Object.entries(byDrink).sort((a, b) => b[1] - a[1])[0];
}
function repeatCustomers(list) {
const counts = {};
for (const { customer } of list) counts[customer] = (counts[customer] ?? 0) + 1;
return Object.keys(counts).filter((name) => counts[name] > 1);
}
const paid = paidOnly(orders);
const byDrink = revenueByDrink(paid);
console.log("revenue by drink:", byDrink);
console.log("best seller:", bestSeller(byDrink));
console.log("came back:", repeatCustomers(orders));خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| الحلقة for أسرع فاستخدمها دائمًا. | الفرق لا يُلاحظ في أغلب البرامج، وmap وfilter أوضح للقراءة وأقل أخطاءً. |
| const يجعل الكائن ثابتًا. | يمنع إعادة ربط المتغير فقط، والخصائص داخله تتغيّر. |
| JSON هو JavaScript. | صيغة نصية مستوحاة منه، تقرؤها كل اللغات: بايثون وجافا وغيرهما. |
أين تقابله في العمل
🛍️سلة المشتريات
المنتجات مصفوفة كائنات، والمجموع reduce.
📊لوحات التقارير
تصفية وتجميع بيانات المبيعات قبل رسمها.
🔌التواصل مع الخادم
كل ما يأتي من API يصل JSON وتحوّله بـ map.
اختبر فهمك
أي طريقة تعيد مصفوفة بنفس الطول بعد تحويل كل عنصر؟
map تحوّل كل عنصر ولا تحذف شيئًا.
const a = [1]; a.push(2); ماذا يحدث؟
const يمنع a = ... لا تعديل محتوى المصفوفة.
ما ناتج ["a", "b", "c"][1]؟
الفهرس يبدأ من صفر، فالفهرس 1 هو العنصر الثاني.
دالة function add(a, b) { a + b } تعيد ماذا عند add(2, 3)؟
بلا return تعيد الدالة undefined رغم أنها حسبت.
كيف تنسخ كائنًا مع تغيير خاصية واحدة دون لمس الأصل؟
النشر ينشئ كائنًا جديدًا، والإسناد المباشر يشير لنفس الكائن.
بطاقات مراجعة سريعة
const { a, b } = obj يسحب خصائص في متغيرات.تحقق من فهمك
لماذا filter ثم map وليس العكس في مثال الإيرادات؟
لأن map تحوّل الطلب لرقم فتضيع خاصية paid، فلا تستطيع أن تصفّي بعدها.
ما الفرق بين المعامل والوسيط؟
المعامل الاسم في تعريف الدالة، والوسيط القيمة الفعلية التي تعطيها عند المناداة.
لماذا تغيّر الكائن الأصلي حين عدّلت نسخته؟
لأن b = a ينسخ المرجع لا الكائن؛ الاثنان يشيران لنفس البطاقة. انسخ بـ { ...a }.
متى تحتاج JSON.parse؟
حين يصلك نص JSON من خادم أو من التخزين وتريد التعامل معه ككائن.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.