كما نعلم بالفعل، فإن الوظيفة في JavaScript هي قيمة.
كل قيمة في JavaScript لها نوع. ما هو نوع الوظيفة؟
في JavaScript، الوظائف هي كائنات.
هناك طريقة جيدة لتخيل الوظائف وهي "كائنات إجراء" قابلة للاستدعاء. لا يمكننا تسميتها فحسب، بل يمكننا أيضًا التعامل معها ككائنات: إضافة/إزالة خصائص، والتمرير حسب المرجع، وما إلى ذلك.
تحتوي الكائنات الوظيفية على بعض الخصائص القابلة للاستخدام.
على سبيل المثال، يمكن الوصول إلى اسم الوظيفة كخاصية "الاسم":
الدالة sayHi() { تنبيه("مرحبا"); } تنبيه (sayHi.name)؛ // قل مرحبًا
الأمر المضحك هو أن منطق تحديد الاسم ذكي. كما أنه يعين الاسم الصحيح للوظيفة حتى لو تم إنشاؤها بدون اسم، ثم يتم تعيينه على الفور:
دعونا نقول مرحبا = وظيفة () { تنبيه("مرحبا"); }; تنبيه (sayHi.name)؛ // قل مرحبًا (هناك اسم!)
يعمل أيضًا إذا تم التعيين عبر قيمة افتراضية:
الدالة f(sayHi = function() {}) { تنبيه (sayHi.name)؛ // قل مرحبًا (يعمل!) } و ()؛
في المواصفات، تسمى هذه الميزة "الاسم السياقي". إذا لم توفر الوظيفة واحدًا، فسيتم اكتشافه في المهمة من السياق.
أساليب الكائن لها أسماء أيضًا:
السماح للمستخدم = { قل مرحبا () { // ... }, قل وداعا: وظيفة () { // ... } } تنبيه (user.sayHi.name)؛ // قل مرحبًا تنبيه (user.sayBye.name)؛ // قل وداعا
ليس هناك سحر بالرغم من ذلك. هناك حالات لا توجد فيها طريقة لمعرفة الاسم الصحيح. في هذه الحالة، تكون خاصية الاسم فارغة، كما هو الحال هنا:
// تم إنشاء الدالة داخل المصفوفة Let arr = [function() {}]; تنبيه (arr[0].name ); // <سلسلة فارغة> // المحرك ليس لديه طريقة لإعداد الاسم الصحيح، لذلك لا يوجد شيء
ومع ذلك، في الممارسة العملية، معظم الوظائف لها اسم.
هناك خاصية أخرى مضمنة هي "الطول" والتي تُرجع عدد معلمات الوظيفة، على سبيل المثال:
الدالة f1(أ) {} الدالة f2(أ، ب) {} دالة كثيرة (أ، ب، ... المزيد) {} تنبيه (f1.length)؛ // 1 تنبيه (f2.length)؛ // 2 تنبيه (العديد. الطول)؛ // 2
هنا يمكننا أن نرى أن معلمات الراحة لا يتم احتسابها.
تُستخدم خاصية length
أحيانًا للاستبطان في الوظائف التي تعمل على وظائف أخرى.
على سبيل المثال، في الكود الموجود أدناه، تقبل وظيفة ask
question
لطرحه وعددًا عشوائيًا من وظائف handler
للاتصال بها.
بمجرد أن يقدم المستخدم إجابته، تستدعي الوظيفة المعالجات. يمكننا تمرير نوعين من المعالجات:
دالة ذات وسيطة صفرية، والتي يتم استدعاؤها فقط عندما يعطي المستخدم إجابة إيجابية.
دالة ذات وسائط، يتم استدعاؤها في كلتا الحالتين وإرجاع إجابة.
لاستدعاء handler
بالطريقة الصحيحة، نقوم بفحص خاصية handler.length
.
الفكرة هي أن لدينا صيغة معالج بسيطة بدون وسيطات للحالات الإيجابية (الصيغة الأكثر شيوعًا)، ولكننا قادرون على دعم المعالجات العامة أيضًا:
وظيفة اسأل (سؤال، ... معالجات) { دع isYes = تأكيد(سؤال); ل(السماح لمعالج المعالجات) { إذا (handler.length == 0) { إذا (نعم) المعالج ()؛ } آخر { Handler(isYes); } } } // للحصول على إجابة إيجابية، يتم استدعاء كلا المعالجين // للإجابة السلبية، الثانية فقط Ask("سؤال؟", () => تنبيه('لقد قلت نعم'), result => تنبيه(نتيجة));
هذه حالة خاصة لما يسمى تعدد الأشكال - معالجة الحجج بشكل مختلف اعتمادًا على نوعها، أو في حالتنا اعتمادًا على length
. الفكرة لها استخدام في مكتبات JavaScript.
يمكننا أيضًا إضافة خصائص خاصة بنا.
هنا نضيف خاصية counter
لتتبع إجمالي عدد المكالمات:
الدالة sayHi() { تنبيه("مرحبا"); // دعونا نحسب عدد المرات التي ركضنا فيها sayHi.counter++; } sayHi.counter = 0; // القيمة الأولية sayHi(); // أهلاً sayHi(); // أهلاً تنبيه (`تم الاتصال بـ ${sayHi.counter} مرات`)؛ // تم الاتصال به مرتين
الخاصية ليست متغيرة
الخاصية المخصصة لدالة مثل sayHi.counter = 0
لا تحدد counter
متغير محلي بداخلها. بمعنى آخر، counter
الخاصية let counter
المتغير هما شيئان غير مرتبطين.
يمكننا التعامل مع الدالة ككائن، وتخزين الخصائص فيها، لكن هذا ليس له أي تأثير على تنفيذها. المتغيرات ليست خصائص دالة والعكس صحيح. هذه مجرد عوالم موازية.
يمكن لخصائص الوظيفة أن تحل محل عمليات الإغلاق في بعض الأحيان. على سبيل المثال، يمكننا إعادة كتابة مثال دالة العداد من الفصل نطاق المتغير، الإغلاق لاستخدام خاصية دالة:
وظيفة ماك كونتر () { // بدلاً من: // دع العد = 0 عداد الدالة () { إرجاع العداد.count++; }; العد = 0; عداد العودة } Let counter = makeCounter(); تنبيه (عداد ())؛ // 0 تنبيه (عداد ())؛ // 1
يتم الآن تخزين count
في الوظيفة مباشرة، وليس في بيئتها المعجمية الخارجية.
هل هو أفضل أم أسوأ من استخدام الإغلاق؟
والفرق الرئيسي هو أنه إذا كانت قيمة count
موجودة في متغير خارجي، فلن يتمكن الكود الخارجي من الوصول إليها. فقط الوظائف المتداخلة يمكنها تعديله. وإذا كان مرتبطًا بوظيفة، فهذا شيء ممكن:
وظيفة ماك كونتر () { عداد الدالة () { إرجاع العداد.count++; }; العد = 0; عداد العودة } Let counter = makeCounter(); العد = 10; تنبيه (عداد ())؛ // 10
لذا فإن اختيار التنفيذ يعتمد على أهدافنا.
التعبير الوظيفي المسمى، أو NFE، هو مصطلح للتعبيرات الوظيفية التي لها اسم.
على سبيل المثال، لنأخذ تعبير دالة عادي:
دعنا نقول مرحبا = وظيفة (من) { تنبيه("مرحبا، ${من}`); };
وأضيف لها اسما:
دع sayHi = وظيفة func(who) { تنبيه("مرحبا، ${من}`); };
هل حققنا شيئا هنا؟ ما هو الغرض من هذا الاسم الإضافي "func"
؟
أولاً دعونا نلاحظ أنه لا يزال لدينا تعبير دالة. إن إضافة الاسم "func"
بعد function
لم يجعله إعلانًا للوظيفة، لأنه لا يزال يتم إنشاؤه كجزء من تعبير المهمة.
إضافة مثل هذا الاسم أيضًا لم يكسر أي شيء.
لا تزال الوظيفة متاحة كـ sayHi()
:
دع sayHi = وظيفة func(who) { تنبيه("مرحبا، ${من}`); }; sayHi("جون"); // مرحبا جون
هناك شيئان مميزان في الاسم func
، وهما سببان في ذلك:
يسمح للوظيفة بالإشارة إلى نفسها داخليًا.
وهو غير مرئي خارج الوظيفة.
على سبيل المثال، الدالة sayHi
أدناه تستدعي نفسها مرة أخرى بكلمة "Guest"
إذا لم يتم توفير who
:
دع sayHi = وظيفة func(who) { إذا (من) { تنبيه("مرحبا، ${من}`); } آخر { func("ضيف"); // استخدم func لإعادة استدعاء نفسه } }; sayHi(); // مرحبا ضيف // لكن هذا لن ينجح: وظيفة(); // خطأ، لم يتم تعريف func (غير مرئي خارج الوظيفة)
لماذا نستخدم func
؟ ربما فقط استخدم sayHi
للمكالمة المتداخلة؟
في الواقع، في معظم الحالات يمكننا:
دعنا نقول مرحبا = وظيفة (من) { إذا (من) { تنبيه("مرحبا، ${من}`); } آخر { sayHi("ضيف"); } };
المشكلة في هذا الرمز هي أن sayHi
قد يتغير في الكود الخارجي. إذا تم تعيين الدالة لمتغير آخر بدلاً من ذلك، فسيبدأ الكود في إعطاء الأخطاء:
دعنا نقول مرحبا = وظيفة (من) { إذا (من) { تنبيه("مرحبا، ${من}`); } آخر { sayHi("ضيف"); // خطأ: sayHi ليست دالة } }; دع الترحيب = sayHi; sayHi = null; مرحباً()؛ // خطأ، استدعاء sayHi المتداخل لم يعد يعمل!
يحدث ذلك لأن الوظيفة تأخذ sayHi
من بيئتها المعجمية الخارجية. لا يوجد sayHi
محلي، لذلك يتم استخدام المتغير الخارجي. وفي لحظة المكالمة يكون sayHi
الخارجي null
.
الاسم الاختياري الذي يمكننا وضعه في التعبير الوظيفي يهدف إلى حل هذه الأنواع من المشكلات بالضبط.
دعنا نستخدمه لإصلاح الكود الخاص بنا:
دع sayHi = وظيفة func(who) { إذا (من) { تنبيه("مرحبا، ${من}`); } آخر { func("ضيف"); // الآن كل شيء على ما يرام } }; دع الترحيب = sayHi; sayHi = null; مرحباً()؛ // مرحبًا أيها الضيف (تعمل المكالمة المتداخلة)
الآن يعمل، لأن الاسم "func"
هو وظيفة محلية. لا يتم أخذها من الخارج (وغير مرئية هناك). تضمن المواصفات أنها ستشير دائمًا إلى الوظيفة الحالية.
لا يزال الكود الخارجي يحتوي على المتغير sayHi
أو welcome
. و func
هو "اسم وظيفة داخلي"، وهو الطريقة التي يمكن بها للوظيفة أن تطلق على نفسها اسمًا موثوقًا.
لا يوجد شيء من هذا القبيل لإعلان الوظيفة
ميزة "الاسم الداخلي" الموضحة هنا متاحة فقط لتعبيرات الوظائف، وليس لإعلانات الوظائف. بالنسبة لإعلانات الوظائف، لا يوجد بناء جملة لإضافة اسم "داخلي".
في بعض الأحيان، عندما نحتاج إلى اسم داخلي موثوق به، يكون هذا هو السبب وراء إعادة كتابة إعلان الوظيفة إلى نموذج التعبير عن الوظيفة المسماة.
الوظائف هي كائنات.
وقد تناولنا هنا خصائصهم:
name
- اسم الوظيفة. عادة ما يتم أخذها من تعريف الوظيفة، ولكن إذا لم يكن هناك تعريف، تحاول JavaScript تخمينها من السياق (على سبيل المثال مهمة).
length
- عدد الوسائط في تعريف الدالة. لا يتم احتساب معلمات الراحة.
إذا تم الإعلان عن الدالة كتعبير دالة (ليس في تدفق التعليمات البرمجية الرئيسي)، وكانت تحمل الاسم، فإنها تسمى تعبير دالة مسماة. يمكن استخدام الاسم في الداخل للإشارة إلى نفسه أو للمكالمات المتكررة أو ما شابه.
أيضًا، قد تحمل الوظائف خصائص إضافية. تستفيد العديد من مكتبات JavaScript المعروفة جيدًا من هذه الميزة.
يقومون بإنشاء وظيفة "رئيسية" ويربطون بها العديد من الوظائف "المساعدة" الأخرى. على سبيل المثال، تقوم مكتبة jQuery بإنشاء دالة باسم $
. تقوم مكتبة lodash بإنشاء دالة _
، ثم تضيف _.clone
و _.keyBy
وخصائص أخرى إليها (راجع المستندات عندما تريد معرفة المزيد عنها). في الواقع، يفعلون ذلك لتقليل تلوثهم للفضاء العالمي، بحيث توفر مكتبة واحدة متغيرًا عالميًا واحدًا فقط. وهذا يقلل من إمكانية تسمية الصراعات.
لذلك، يمكن للوظيفة أن تقوم بعمل مفيد بنفسها وتحمل أيضًا مجموعة من الوظائف الأخرى في الخصائص.
الأهمية: 5
قم بتعديل كود makeCounter()
بحيث يمكن للعداد أيضًا تقليل الرقم وتعيينه:
يجب أن يقوم counter()
بإرجاع الرقم التالي (كما كان من قبل).
يجب أن يقوم counter.set(value)
بتعيين العداد على value
.
يجب أن يؤدي counter.decrease()
إلى تقليل العداد بمقدار 1.
راجع رمز وضع الحماية للحصول على مثال الاستخدام الكامل.
ملاحظة: يمكنك استخدام خاصية الإغلاق أو خاصية الوظيفة للاحتفاظ بالعدد الحالي. أو اكتب كلا الخيارين.
افتح صندوق الرمل مع الاختبارات.
يستخدم الحل count
في المتغير المحلي، ولكن يتم كتابة طرق الجمع مباشرة في counter
. يتشاركون في نفس البيئة المعجمية الخارجية ويمكنهم أيضًا الوصول إلى count
الحالي.
وظيفة ماك كونتر () { دع العد = 0؛ عداد الدالة () { عدد الإرجاع++; } counter.set = value => count = value; counter.decrease = () => count--; عداد العودة }
افتح الحل بالاختبارات في وضع الحماية.
الأهمية: 2
اكتب sum
الدالة التي ستعمل على النحو التالي:
مجموع(1)(2) == 3; // 1 + 2 مجموع(1)(2)(3) == 6; // 1 + 2 + 3 المجموع(5)(-1)(2) == 6 المجموع(6)(-1)(-2)(-3) == 0 المجموع(0)(1)(2)(3)(4)(5) == 15
تلميح ملاحظة: قد تحتاج إلى إعداد كائن مخصص للتحويل البدائي لوظيفتك.
افتح صندوق الرمل مع الاختبارات.
لكي يعمل الأمر برمته على أي حال ، يجب أن تكون نتيجة sum
دالة.
يجب أن تحتفظ هذه الوظيفة بالقيمة الحالية بين المكالمات في الذاكرة.
وفقًا للمهمة، يجب أن تصبح الدالة الرقم عند استخدامها في ==
. الوظائف هي كائنات، لذلك يحدث التحويل كما هو موضح في الفصل من الكائن إلى التحويل البدائي، ويمكننا توفير طريقتنا الخاصة التي تُرجع الرقم.
الآن الكود:
مجموع الدالة (أ) { دع المجموع الحالي = أ؛ وظيفة و (ب) { currentSum += b; العودة و؛ } f.toString = وظيفة () { إرجاع المبلغ الحالي؛ }; العودة و؛ } تنبيه(مجموع(1)(2)); // 3 تنبيه(مجموع(5)(-1)(2) ); // 6 تنبيه(مجموع(6)(-1)(-2)(-3) ); // 0 تنبيه(مجموع(0)(1)(2)(3)(4)(5) ); // 15
يرجى ملاحظة أن دالة sum
تعمل مرة واحدة فقط. تقوم بإرجاع الدالة f
.
بعد ذلك، في كل استدعاء لاحق، يضيف f
المعلمة الخاصة به إلى المجموع currentSum
ويعيد نفسه.
لا يوجد تكرار في السطر الأخير من f
.
هنا هو ما يبدو العودية:
وظيفة و (ب) { currentSum += b; العودة و ()؛ // <-- مكالمة متكررة }
وفي حالتنا، نعيد الدالة فقط دون استدعائها:
وظيفة و (ب) { currentSum += b; العودة و؛ // <-- لا يستدعي نفسه، بل يُرجع نفسه }
سيتم استخدام هذا f
في المكالمة التالية، وسيعود مرة أخرى، عدة مرات حسب الحاجة. بعد ذلك، عند استخدامها كرقم أو سلسلة، تُرجع toString
القيمة currentSum
. يمكننا أيضًا استخدام Symbol.toPrimitive
أو valueOf
هنا للتحويل.
افتح الحل بالاختبارات في وضع الحماية.