أساسيات الواجهة الأمامية
أساسيات JavaScript: من الصفر إلى الاحتراف
دليل عملي كامل لكل ما يحتاجه المطوّر في JavaScript — كيف تعمل، والمتغيّرات والنطاق، والأنواع والتحويل، والدوال والإغلاقات، وthis، والكائنات والمصفوفات، والتفكيك، وحلقة الأحداث، والوعود وasync/await، والوحدات، وDOM، ومعالجة الأخطاء، والأخطاء الشائعة، وتمارين عملية مع حلولها.
JavaScript هي لغة الويب، لكن معظم الناس يتعلّمونها على شكل شظايا — حلقة هنا، وfetch هناك — دون أن يبنوا النموذج الذهني الذي يجعل الأجزاء الصعبة (this، والإغلاقات، وحلقة الأحداث) تبدو بديهية. هذا هو ذلك النموذج، مبنيًّا من الأساس. بنهاية المقال يُفترض أن تكون قادرًا على قراءة أي شيفرة JavaScript تقريبًا والتنبّؤ بما تفعله، لا تخمينه.
أكثر شيء مفيد أن تستوعبه: JavaScript أحادية الخيط (single-threaded)، وتعمل على حلقة أحداث (event loop)، وتعامل الدوال كقيم (functions as values). كل سلوك "غريب" تقريبًا — الرفع (hoisting)، والإغلاقات، وترتيب التنفيذ غير المتزامن، وthis — ينبع من هذه الحقائق الثلاث.
كيف تعمل JavaScript فعلًا
تعمل JavaScript داخل محرّك (V8 في Chrome وNode، وSpiderMonkey في Firefox). تُحلَّل شيفرتك وتُترجَم وقت التشغيل وتُنفَّذ على خيط واحد — شيء واحد في كل مرة. لا يوجد خط تنفيذ متوازٍ داخل سكربتك؛ التزامن يأتي من المضيف (المتصفح أو Node) الذي يعيد إليك العمل عبر حلقة الأحداث، وسنصل إليها.
بيئتان مهمّتان:
- المتصفح — تستطيع JavaScript لمس الصفحة (DOM)، والاستجابة للنقرات، واستدعاء الشبكة.
- Node.js — نفس اللغة مع الوصول إلى الملفات والخوادم ونظام التشغيل بدلًا من الصفحة.
نفس اللغة الأساسية، مع صناديق أدوات مختلفة مركّبة عليها.
المتغيّرات: let وconst وشبح var
const name = "Ada"; // لا يمكن إعادة إسناده
let age = 36; // يمكن إعادة إسناده
age = 37; // لا مشكلة
var legacy = true; // كلمة قديمة — تجنّبها في الشيفرة الجديدة
استخدم const افتراضيًّا، وlet فقط عند الحاجة الفعلية لإعادة الإسناد، ولا تستخدم var أبدًا في الشيفرة الجديدة. السبب هو النطاق:
letوconstنطاقهما كتلة (block-scoped) — توجد فقط داخل أقرب{ }.varنطاقها دالة (function-scoped) ومرفوعة (hoisted)، ما يجعلها تتسرّب خارج الكتل ويسبّب أخطاءً كلاسيكية.
{
let a = 1;
var b = 2;
}
console.log(b); // 2 — var تسرّبت خارج الكتلة
console.log(a); // ReferenceError — let بقيت في مكانها
const تعني أن الارتباط لا يمكن إعادة إسناده — وهي لا تجعل الكائنات غير قابلة للتغيير:
const user = { name: "Ada" };
user.name = "Grace"; // مسموح — غيّرنا الكائن، ولم نعد إسناد الارتباط
user = {}; // TypeError — إعادة إسناد الارتباط هو ما يُمنع
نظام الأنواع: القيم الأوّلية مقابل الكائنات
تملك JavaScript بالضبط سبعة أنواع أوّلية (primitives) ومظلّة واحدة لكل ما عداها (الكائنات):
typeof "hi" // "string"
typeof 42 // "number" — نوع رقم واحد للأعداد الصحيحة والعشرية
typeof 10n // "bigint" — أعداد صحيحة كبيرة بلا حدود
typeof true // "boolean"
typeof undefined // "undefined" — متغيّر بلا قيمة بعد
typeof null // "object" — خطأ تاريخي، لكن null تعني "فارغ عمدًا"
typeof Symbol() // "symbol" — معرّفات فريدة
typeof {} // "object"
typeof [] // "object" — المصفوفات كائنات
typeof function(){} // "function" — كائنات قابلة للاستدعاء
الفارق العميق: القيم الأوّلية تُنسخ بالقيمة، والكائنات تُشارَك بالمرجع.
let a = 1;
let b = a; // b يأخذ نسخة
b = 2;
console.log(a); // 1 — لم يتأثّر
let x = { n: 1 };
let y = x; // y يشير إلى نفس الكائن
y.n = 2;
console.log(x.n); // 2 — الاسمان يريان نفس الشيء
هذه الحقيقة وحدها تفسّر معظم مفاجآت "لماذا تغيّرت مصفوفتي؟".
المساواة والتحويل: استخدم ===
== تُجري تحويل أنواع (coercion) — تحوّل المعاملات كي تتطابق، بقواعد لا يحفظها أحد بدقّة:
0 == "" // true 😱
0 == "0" // true
"" == "0" // false
null == undefined // true
[] == false // true 😱😱
الحل بسيط: استخدم دائمًا === و!== (صارمة، بلا تحويل). الاستثناء الشائع الوحيد هو x == null، وهو اختصار مقصود لـ "null أو undefined".
فهم الصدقيّة (truthiness) أمر منفصل ومهم. هذه القيم الثماني كاذبة (falsy)؛ وكل ما عداها صادق (truthy):
false, 0, -0, 0n, "", null, undefined, NaN
// لاحظ: "0" و[] و{} كلها صادقة (TRUTHY)
لهذا يعمل if (array.length)، ويحرس if (user) من القيمة null.
السلاسل النصّية والقوالب النصّية والأرقام
const who = "world";
const greeting = `Hello, ${who}!`; // إدراج القيم
const multi = `سطر أول
سطر ثانٍ`; // أسطر جديدة حقيقية
" trim me ".trim(); // "trim me"
"a,b,c".split(","); // ["a", "b", "c"]
"hello".toUpperCase(); // "HELLO"
"hello".includes("ell"); // true
"hello".slice(1, 3); // "el"
الأرقام كلها من نوع IEEE-754 المزدوج، ما يؤدّي إلى المثال الشهير:
0.1 + 0.2 === 0.3 // false! القيمة 0.30000000000000004
(0.1 + 0.2).toFixed(2) // "0.30" — للعرض فقط
Number("42") // 42
parseInt("42px", 10) // 42 — يتوقّف عند أوّل حرف غير رقمي
Number.isNaN(NaN) // true — الفحص الموثوق لـ NaN
Math.round(4.5) // 5
للأموال، اعمل بالقروش الصحيحة أو استخدم مكتبة عشرية — لا تثق أبدًا بمجاميع الفاصلة العائمة.
الدوال قيم
هذا هو قلب JavaScript. الدالة مجرّد قيمة يمكنك تخزينها وتمريرها وإرجاعها.
// تصريح — مرفوع، قابل للاستخدام قبل سطره
function add(a, b) { return a + b; }
// تعبير — مُسنَد إلى متغيّر
const sub = function (a, b) { return a - b; };
// دالة سهمية — الأقصر، بلا `this` خاص بها
const mul = (a, b) => a * b;
const square = n => n * n; // معامل واحد، لا حاجة للأقواس
const make = () => ({ ok: true }); // لإرجاع كائن: لُفّه بأقواس
تأخذ الدوال وسائط مرنة:
function greet(name = "friend", ...rest) { // معامل افتراضي + بقية
console.log(name, rest);
}
greet(); // "friend" []
greet("Ada", 1, 2); // "Ada" [1, 2]
ولأن الدوال قيم، تمرّرها باستمرار — وهذا ما يجعل توابع المصفوفات وردود النداء ومعالِجات الأحداث تعمل:
[1, 2, 3].map(n => n * 2); // [2, 4, 6]
button.addEventListener("click", () => console.log("clicked"));
النطاق والإغلاقات
الإغلاق (closure) دالة تتذكّر المتغيّرات من المكان الذي عُرِّفت فيه، حتى بعد أن تنتهي الدالة الخارجية. إنه الآلية وراء الحالة الخاصّة، والمصانع، ومعظم خطّافات React.
function counter() {
let count = 0; // خاص بهذا الإغلاق
return () => ++count; // الدالة المُرجَعة "تُغلِق على" count
}
const next = counter();
next(); // 1
next(); // 2 — count يبقى، لكن لا شيء آخر يستطيع لمسه
كل استدعاء لـ counter() ينشئ count جديدًا. الدالة المُرجَعة تُبقيه حيًّا. هذا هو الإغلاق: بيانات مغلّفة بشكل خاص مع الدالة التي تستخدمها.
فخّ مقابلات كلاسيكي، صار محلولًا الآن بـ let:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2 ✅ (let = ارتباط جديد لكل تكرار)
}
// مع var ستحصل على 3, 3, 3 — ارتباط واحد مشترك
this — تحدّده طريقة الاستدعاء
this أكثر كلمة مفتاحية يُساء فهمها، لأنها في الدالة العادية تُحدَّد بـ طريقة استدعاء الدالة، لا بمكان تعريفها:
const obj = {
name: "Ada",
regular() { return this.name; }, // `this` = الكائن قبل النقطة
arrow: () => this, // السهمية ليس لها `this` خاص — ترث من الخارج
};
obj.regular(); // "Ada" — استُدعيت كـ obj.method()
const fn = obj.regular;
fn(); // undefined / خطأ — استُدعيت "مجرّدة"، فقدت `this`
القواعد بالترتيب:
- الدوال السهمية ليس لها
thisخاص — تستخدمthisالنطاق المحيط. لهذا تكون السهمية مثالية لردود النداء داخل التوابع. obj.method()—thisهوobj(ما على يسار النقطة).- استدعاء مجرّد
fn()—thisهوundefined(الوضع الصارم) أو الكائن العام. call/apply/bind— تضبطthisصراحةً.
const greet = function () { return `Hi ${this.name}`; };
greet.call({ name: "Ada" }); // "Hi Ada" — فُرض `this`
const bound = greet.bind({ name: "Grace" });
bound(); // "Hi Grace" — مرتبط دائمًا
قاعدة عملية: استخدم الدوال السهمية لردود النداء كي يتدفّق this، والدوال/التوابع العادية حين تريد this ديناميكيًّا.
الكائنات
const user = {
name: "Ada",
age: 36,
greet() { return `Hi, I'm ${this.name}`; }, // اختصار التابع
["dynamic" + "Key"]: true, // مفتاح محسوب
};
user.name; // وصول بالنقطة
user["name"]; // وصول بالأقواس (مطلوب للمفاتيح الديناميكية)
user.email ?? "—"; // nullish: "—" فقط إذا كان email يساوي null/undefined
// عمليات مفيدة على الكائنات
Object.keys(user); // ["name", "age", ...]
Object.values(user);
Object.entries(user); // [["name","Ada"], ...]
const copy = { ...user, age: 37 }; // نسخة سطحية مع تجاوز قيمة
التسلسل الاختياري ?. يقرأ بأمان مسارات عميقة قد لا توجد:
const city = user?.address?.city; // undefined بدلًا من رمي خطأ
user.save?.(); // يستدعي save فقط إن وُجدت
المصفوفات وتوابعها الأساسية
المصفوفات قوائم مرتّبة، وتوابعها هي عمال JavaScript اليومية. التمييز الأساسي: توابع التحويل تُرجِع مصفوفة جديدة (لا تغيّر الأصل)، بينما القليل منها يغيّر في المكان.
const nums = [1, 2, 3, 4, 5];
// تحويل (تُرجِع مصفوفات جديدة — فضّلها)
nums.map(n => n * 2); // [2, 4, 6, 8, 10]
nums.filter(n => n % 2 === 0); // [2, 4]
nums.reduce((sum, n) => sum + n, 0); // 15 — طيّ إلى قيمة واحدة
nums.slice(1, 3); // [2, 3] — نسخة من نطاق
// بحث / اختبار
nums.find(n => n > 3); // 4 (أول تطابق)
nums.some(n => n > 4); // true
nums.every(n => n > 0); // true
nums.includes(3); // true
nums.indexOf(3); // 2
// تكرار
nums.forEach(n => console.log(n));
// تغيير في المكان (تغيّر الأصل — استخدمها عن قصد)
nums.push(6); // إضافة في النهاية
nums.pop(); // حذف من النهاية
nums.shift(); // حذف من البداية
nums.sort((a, b) => a - b); // ترتيب عددي (الترتيب الافتراضي أبجدي!)
map وfilter وreduce تتسلسل بطبيعتها وتغطّي مدى هائلًا من العمل:
const total = orders
.filter(o => o.paid)
.map(o => o.amount)
.reduce((sum, a) => sum + a, 0);
التفكيك والنشر والبقية
ثلاث من أكثر بِنى الصياغة الحديثة استخدامًا — تعلّمها وستتقلّص شيفرتك بشكل كبير.
// تفكيك المصفوفات
const [first, second, ...others] = [1, 2, 3, 4];
// first=1, second=2, others=[3,4]
// تفكيك الكائنات (مع إعادة تسمية + قيمة افتراضية)
const { name, role = "user" } = { name: "Ada" };
// name="Ada", role="user"
// في معاملات الدوال — شائع جدًّا
function render({ title, items = [] }) { /* ... */ }
// النشر (spread) — توسيع في مصفوفة/كائن جديد (نسخة سطحية)
const merged = [...arr1, ...arr2];
const updated = { ...state, loading: false };
// البقية (rest) — جمع المتبقّي
function sum(...nums) { return nums.reduce((a, b) => a + b, 0); }
النشر هو الطريقة الاصطلاحية للنسخ والتحديث بشكل غير قابل للتغيير — النمط الذي تعتمد عليه حالة React.
التحكّم في التدفّق والحلقات
// الشروط
if (x > 0) { /* ... */ } else if (x < 0) { /* ... */ } else { /* ... */ }
const label = x > 0 ? "positive" : "non-positive"; // الثلاثي
switch (status) {
case "loading": return spinner();
case "error": return message();
default: return content();
}
// الحلقات
for (let i = 0; i < 3; i++) { /* الكلاسيكية */ }
for (const item of array) { /* القيم — استخدمها للمصفوفات */ }
for (const key in object) { /* المفاتيح — استخدمها للكائنات */ }
// منطق الدائرة القصيرة يعمل أيضًا كتحكّم في التدفّق
isReady && doThing(); // ينفّذ فقط إذا كان isReady
const port = config.port || 3000; // قيمة بديلة (انتبه: 0 كاذبة)
const port2 = config.port ?? 3000; // بديلة فقط عند null/undefined
فضّل for...of للمصفوفات وتوابع المصفوفات للتحويلات؛ ولا تلجأ للـ for الكلاسيكية إلا حين تحتاج المؤشر أو خطوة غير معتادة.
حلقة الأحداث: كيف يعمل اللاتزامن
إليك النموذج الذي يجعل JavaScript غير المتزامنة مفهومة. ينفّذ المحرّك شيفرتك على خيط واحد حتى تنتهي — مكدّس الاستدعاء (call stack). الأشياء البطيئة (المؤقّتات، الشبكة، قراءة الملفات) تُسلَّم إلى المضيف، الذي يعاودك لاحقًا بوضع دالة في طابور (queue). حلقة الأحداث تنقل ردود النداء من الطابور إلى المكدّس فقط حين يكون المكدّس فارغًا.
console.log("1");
setTimeout(() => console.log("2"), 0); // مؤجّل، حتى عند 0 مللي ثانية
Promise.resolve().then(() => console.log("3")); // مهمّة دقيقة — أولوية أعلى
console.log("4");
// الخرج: 1, 4, 3, 2
لماذا هذا الترتيب؟ الشيفرة المتزامنة (1، 4) تعمل أولًا. ثم المهام الدقيقة (microtasks) (ردود الوعود ← 3) تُفرَّغ قبل المهام الكبيرة (macrotasks) (المؤقّتات ← 2). الخلاصة: setTimeout(fn, 0) لا يعمل "الآن" — بل بعد انتهاء الشيفرة الحالية وكل الوعود المعلّقة.
ولهذا أيضًا فإن حلقة متزامنة طويلة تُجمّد الصفحة: لا شيء آخر — نقرات، رسم، مؤقّتات — يستطيع العمل حتى يفرغ المكدّس.
الوعود وasync/await
الوعد (Promise) يمثّل قيمة ستوجد لاحقًا. يكون في إحدى ثلاث حالات: معلّق (pending) ← مُنجَز (بقيمة) أو مرفوض (بخطأ).
fetch("/api/user")
.then(res => res.json()) // كل then يمرّر نتيجته للتالي
.then(user => console.log(user))
.catch(err => console.error(err)) // catch واحد يعالج أي فشل أعلاه
.finally(() => stopSpinner());
async/await تحلية صياغية فوق الوعود تتيح كتابة شيفرة غير متزامنة تُقرأ وكأنها متزامنة:
async function loadUser(id) {
try {
const res = await fetch(`/api/user/${id}`); // توقّف هنا حتى الإنجاز
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const user = await res.json();
return user;
} catch (err) {
console.error("Failed to load user:", err);
throw err; // أعد رميه إذا كان على المستدعي أن يعرف
}
}
const user = await loadUser(1); // أي دالة async تُرجِع وعدًا
حقائق أساسية:
awaitيعمل فقط داخل دالةasync(أو في المستوى الأعلى لوحدة).- دالة
asyncتُرجِع دائمًا وعدًا. - لتشغيل الأشياء بالتوازي، لا تستخدم
awaitداخل حلقة — أطلقها كلها وانتظرها معًا:
// بطيء: كل واحد ينتظر السابق (تتابعي)
const a = await getA();
const b = await getB();
// سريع: كلاهما يبدأ فورًا، ثم تنتظر كليهما
const [a2, b2] = await Promise.all([getA(), getB()]);
الوحدات: import / export
JavaScript الحديثة تقسّم الشيفرة إلى ملفات (وحدات)، لكلٍّ نطاقه. لا شيء عام إلا ما تصدّره.
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; }
// app.js
import multiply, { PI, add } from "./math.js";
import * as math from "./math.js"; // استيراد فضاء الأسماء
export default هو التصدير الرئيسي للملف (يُستورَد بلا أقواس، بالاسم الذي تختاره)؛ والتصديرات المسمّاة تُستورَد باسمها الدقيق بين أقواس. كما تعمل الوحدات في الوضع الصارم وتُحمَّل بشكل غير متزامن.
لمس الصفحة: أساسيات DOM
في المتصفح، DOM هو التمثيل الكائني الحيّ لـ HTML — وكلّما كان HTML أنظف ودلاليًّا، سهُل اختيار العناصر والتفكير فيها. تختار العناصر، وتقرأها أو تغيّرها، وتتفاعل مع الأحداث. هذه جولة سريعة؛ وللصورة الكاملة — التنقّل، ونموذج الأحداث، والتفويض، والأداء — انظر DOM من الصفر إلى الاحتراف.
// اختيار
const btn = document.querySelector(".submit"); // أول تطابق (محدّد CSS)
const items = document.querySelectorAll("li"); // كل التطابقات (NodeList)
// قراءة / تغيير
btn.textContent = "Saving…"; // نصّ فقط (آمن)
btn.classList.add("is-loading"); // تبديل الأصناف
btn.setAttribute("disabled", ""); // السمات
el.innerHTML = "<b>hi</b>"; // يحلّل HTML — لا تستخدمه أبدًا مع مدخلات غير موثوقة (XSS)
// إنشاء وإدراج
const li = document.createElement("li");
li.textContent = "New item";
list.append(li);
// التفاعل مع الأحداث
btn.addEventListener("click", (event) => {
event.preventDefault(); // إيقاف السلوك الافتراضي (مثل إرسال نموذج)
console.log("clicked", event.target);
});
ولأن النقرات قد تقع على آلاف العناصر، فإن تفويض الأحداث (event delegation) — مستمع واحد على الأب يفحص event.target — هو النمط القابل للتوسّع.
معالجة الأخطاء
try {
const data = JSON.parse(input); // يرمي خطأ عند JSON سيّئ
process(data);
} catch (err) {
console.error(err.message);
} finally {
cleanup(); // ينفّذ سواء رمى خطأ أم لا
}
// ارمِ خطأك الخاص — ارمِ دائمًا كائنات Error لا سلاسل نصّية
function withdraw(amount) {
if (amount <= 0) throw new Error("Amount must be positive");
}
في الشيفرة غير المتزامنة، try/catch يعمل مع await؛ ومع الوعود الخام تستخدم .catch(). الرفض غير المُعالَج يُعطّل Node ويحذّر بصوت عالٍ في المتصفح — عالِج مسار الفشل عن قصد.
الأخطاء الشائعة
- استخدام
==بدلًا من===والوقوع في التحويل (0 == ""تساويtrue). - الظنّ أن
constتجعل الكائنات غير قابلة للتغيير — هي تجمّد الارتباط فقط. - تغيير مصفوفة/كائن لم تقصد مشاركته، مع نسيان أن الكائنات بالمرجع.
- توقّع أن
setTimeout(fn, 0)يعمل فورًا — بل يعمل بعد الشيفرة الحالية والمهام الدقيقة. - استخدام
awaitداخل حلقة بينما الاستدعاءات مستقلّة — استخدمPromise.allللتوازي. - فقدان
thisبتمريرobj.methodكردّ نداء مجرّد — استخدم سهمية أو.bind. - ترتيب
[].sort()الافتراضي يرتّب الأرقام كنصوص ([10, 2].sort()←[10, 2]) — مرّر مُقارِنًا. - استخدام
for...inعلى المصفوفات — يكرّر المفاتيح (والموروثة)؛ استخدمfor...of. - بناء HTML من مدخلات المستخدم بـ
innerHTML— ثغرة XSS؛ استخدمtextContent. - نسيان أن دالة
asyncتُرجِع وعدًا، ثم استخدام قيمتها كأنها متزامنة.
تمارين
جرّب كلًّا منها قبل فتح الحل.
تمرين 1 — تنبّأ بالخرج (المراجع)
const a = [1, 2, 3];
const b = a;
b.push(4);
console.log(a.length);
إظهار الحل
4. المصفوفات كائنات، تُنسَخ بالمرجع — b وa يشيران إلى نفس المصفوفة، فالدفع عبر b مرئيّ عبر a. للحصول على نسخة مستقلّة: const b = [...a].
تمرين 2 — الجمع بـ reduce
بمعطى const prices = [9.99, 4.5, 12, 3.25]، احسب المجموع باستخدام reduce.
إظهار الحل
const total = prices.reduce((sum, p) => sum + p, 0); // 29.74
الوسيط الثاني 0 هو المراكِم الابتدائي — بدونه يستخدم reduce العنصر الأول وقد يسيء التصرّف مع المصفوفات الفارغة.
تمرين 3 — اصنع عدّاد إغلاق
اكتب makeCounter() يُرجِع دالة؛ كل استدعاء يُرجِع العدد الصحيح التالي بدءًا من 1، مع جعل العدّ خاصًّا تمامًا.
إظهار الحل
function makeCounter() {
let count = 0;
return () => ++count;
}
const next = makeCounter();
next(); // 1
next(); // 2
count يعيش في الإغلاق — يمكن الوصول إليه فقط عبر الدالة المُرجَعة، لا من الخارج أبدًا.
تمرين 4 — تنبّأ بترتيب اللاتزامن
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
إظهار الحل
A, D, C, B. الأسطر المتزامنة تعمل أولًا (A، D). ثم يُفرَّغ طابور المهام الدقيقة (الوعد ← C) قبل طابور المهام الكبيرة (المؤقّت ← B)، رغم أن المهلة 0.
تمرين 5 — أصلِح this المفقود
const timer = {
seconds: 0,
start() {
setInterval(function () {
this.seconds++; // معطوب — `this` ليس `timer` هنا
}, 1000);
},
};
إظهار الحل
start() {
setInterval(() => {
this.seconds++; // السهمية ترث `this` من start()
}, 1000);
}
ردّ النداء العادي يُستدعى "مجرّدًا"، فيكون this فيه undefined/عامًّا. أما الدالة السهمية فليس لها this خاص وترثه من start، حيث this هو كائن timer.
تمرين 6 — التوازي مقابل التتابع في await
عليك جلب موردين مستقلّين. أعد كتابة هذا ليعملا بالتوازي:
const user = await getUser();
const posts = await getPosts();
إظهار الحل
const [user, posts] = await Promise.all([getUser(), getPosts()]);
كلا الطلبين يبدأ فورًا؛ وPromise.all ينتظر كليهما ويُنجَز إلى مصفوفة نتائج. النسخة الأصلية تنتظر انتهاء getUser قبل حتى أن تبدأ getPosts.
النموذج الذهني الذي تحتفظ به
JavaScript أحادية الخيط، فالمهمة المتزامنة الطويلة تحجب كل شيء — ادفع العمل البطيء إلى الوعود ودع حلقة الأحداث تجدوله (المهام الدقيقة قبل الكبيرة). الدوال قيم، وهذا ما يجعل ردود النداء وتوابع المصفوفات والإغلاقات (حالة خاصّة مغلّفة مع دالة) ممكنة. المتغيّرات نطاقها كتلة مع let/const؛ والقيم الأوّلية تُنسَخ بالقيمة والكائنات تُشارَك بالمرجع — مصدر معظم المفاجآت. قارِن بـ ===، واقرأ بحذر بـ ?. و??، والجأ إلى map/filter/reduce والتفكيك/النشر لكتابة تحويلات بدل الحلقات. افهم أن this تحدّده طريقة استدعاء الدالة، وفضّل السهمية لردود النداء كي يتدفّق. ثبّت هذه الأفكار القليلة وسيتوقّف بقية اللغة — async/await، والوحدات، وDOM — عن كونه مجموعة حِيَل ويصبح أشياء تستطيع اشتقاقها.