الدوال في JavaScript

من موسوعة حسوب
مراجعة 09:04، 16 يناير 2018 بواسطة عبد اللطيف ايمش (نقاش | مساهمات)
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)

على وجه العموم، الدالة هي «برنامجٌ فرعي» (subprogram) الذي يمكن استدعاؤه من شيفرة خارجية (أو داخلية، في حال أردنا استدعاء الدالة تعاوديًا [recursively])؛ وتحتوي الدالة على سلسلةٍ من التعابير البرمجية التي تسمى «جسم الدالة»، ويمكن تمرير قيم إلى الدالة، وتستطيع الدالة أن تعيد قيمةً ما.

تُعدّ الدوال في JavaScript على أنَّها كائنات من الصنف الأول (first-class objects)، لأنها تملك خاصيات ودوال مثل بقية الكائنات، لكن ما يميّزها عن بقية الكائنات هو إمكانية استدعائها، إذ إنَّها كائنات من النوع Function.

الوصف

كل دالة في JavaScript هي كائن Function، راجع صفحة الكائن Function لمزيدٍ من المعلومات عن خاصيات ودوال كائنات Function.

لإعادة قيمة من الدالة فيجب استعمال التعبير البرمجي return داخلها، والدالة التي ليس فيها التعبير البرمجي return ستُعيد القيمة الافتراضية، وفي حال استدعاء دالة بانية باستخدام الكلمة المحجوزة new، فالقيمة الافتراضية المُعادة هي قيمة المعامل this، أما بقية الدوال فالقيمة الافتراضية التي ستعيدها هي undefined.

المعاملات (parameters) التي تستدعى معها الدالة تُسمى بالوسائط (arguments)، الوسائط تُمرَّر بقيمتها (by value) إلى الدوال، فلو غيّرت الدالة قيمة ذلك الوسيط، فلن ينعكس ذلك التأثير على ما خارج الدالة، لكن المرجعيات إلى الكائنات هي قيم أيضًا، لكنها خاصة، فلو عدّلت الدالة خاصيات الكائن المُشار إليه فسيكون تأثير التغيير ظاهرًا خارج الدالة، كما هو موضَّح في المثال الآتي:

function myFunc(theObject) {
   theObject.brand = "Toyota";
}
 
var mycar = {
   brand: "Honda",
   model: "Accord",
   year: 1998
};

console.log(mycar.brand); // Honda

myFunc(mycar); // تمرير مرجعية الكائن إلى الدالة

// لاحظ أثر تغيير قيمة الخاصية داخل الدالة
console.log(mycar.brand); // Toyota

الكلمة المحجوزة this لا تُشير إلى الدالة التي يجري تنفيذها الآن، لذا عليك الإشارة إلى كائنات Function باسمها، حتى ضمن جسم الدالة.

تعريف الدوال

هنالك عدِّة طرائق لتعريف الدوال.

التصريح عن الدوال

إحدى طرائق تعريف الدوال هي التصريح عنها عبر التعبير البرمجي function، راجع صفحته لمزيدٍ من التفاصيل.

function name([param[, param[, ... param]]]) {
   statements
}

تعابير تعريف الدوال

البنية العامة لتعابير تعريف الدوال تُشبه تعابير تصريح الدوال (راجع صفحتها لمزيدٍ من المعلومات). يمكن أن يكون تعبير تعريف الدوال جزءً من تعبيرٍ برمجيٍ أكبر، ويمكن تعريف دوال لها اسم، ويمكن حذف الاسم وإنشاء دوال مجهولة (anonymous)؛ لاحظ أنَّ من غير الممكن استدعاء الدوال المعرَّفة عبر تعابير تعريف الدوال قبل تعريفها.

function [name]([param[, param[, ... param]]]) {
   statements
}

هذا مثال عن تعريف دالة مجهولة (لاحظ عدم تحديد اسم [name] لها):

var myFunction = function() {
    // statements
}

لكن من الممكن توفير اسم للدالة لإنشاء تعبير تعريف دالة مسماة:

var myFunction = function namedFunction(){
    // statements
}

إحدى ميزات إنشاء دالة مسماة (بغض النظر عن إمكانية الإشارة إليها باسمها داخلها)، هي في حال صادفنا خطأً، فسيُذكَر فيها اسم الدالة، مما يُسهِّل من معرفة مصدر الخطأ.

وكما لاحظنا في المثالين السابقين، لا يبدأ تعبير تعريف الدوال بالكلمة المحجوزة function، فالتعابير البرمجية التي تُعرِّف الدوال لكن لا تبدأ بالكلمة المحجوزة function تسمى تعابير تعريف الدوال (function expressions).

عند استخدام دالة ما لمرة واحدة فقط، فمن الشائع إنشاء دالة ذاتية الاستدعاء (يسمى ذلك بالمصطلح Immediately Invokable Function Expression اختصارًا IIFE):

(function() {
    // statements
})();

التصريح عن الدوال المولِّدة

هنالك شكلٌ خاصٌ للتصريح عن الدوال المولِّدة عبر التعبير البرمجي function*‎ (راجع صفحته لمزيدٍ من التفاصيل):

function* name([param[, param[, ... param]]]) {
   // statements
}

تعابير تعريف الدوال المولِّدة

البنية العامة لتعابير تعريف الدوال المولِّدة تُشبه تعابير تصريح عن الدوال المولِّدة (راجع صفحتها لمزيدٍ من المعلومات).

function* [name]([param[, param[, ... param]]]) {
   statements
}

الدوال السهمية

تعبير الدالة السهمية له شكلٌ مختصر، ويملك خصائص فريدة (راجع صفحة الدوال السهمية للتفاصيل):

([param[, param]]) => {
   statements
}

param => expression

الدالة البانية Function

كما في بقية الكائنات، يمكن إنشاء كائنات من النوع Function باستخدام المعامل new:

new Function (arg1, arg2, ... argN, functionBody)

لاحظ أنَّ من غير المستحسن استخدام الدالة البانية Function لإنشاء دوال، لأنَّ جسم الدالة سيُمرَّر كسلسلة نصية وهذا سيمنع بعض التحسينات التي تجريها محرِّكات JS وقد يُسبِّب بعض المشاكل.

إذا استدعينا الدالة البانية Function كدالة عادية (أي دون استخدام المعامل new) فسيكون لها نفس تأثير استدعائها كدالة بانية (أي مع المعامل new).

الدالة البانية GeneratorFunction

كما في بقية الكائنات، يمكن إنشاء كائنات من النوع GeneratorFunction باستخدام المعامل new:

new GeneratorFunction (arg1, arg2, ... argN, functionBody)

من المهم أن تضع بالحسبان أنَّ GeneratorFunction ليس كائنًا عامًا، لكن يمكن الحصول عليه من نسخة من دالة مولِّدة (راجع صفحة GeneratorFunction للتفاصيل).

لاحظ أنَّ من غير المستحسن استخدام الدالة البانية GeneratorFunction لإنشاء دوال مولِّدة، لأنَّ جسم الدالة سيُمرَّر كسلسلة نصية وهذا سيمنع بعض التحسينات التي تجريها محرِّكات JS وقد يُسبِّب بعض المشاكل.

معاملات الدوال

المعاملات الافتراضية

المعاملات الافتراضية تسمح بتهيئة معاملات الدالة بقيمٍ افتراضية وذلك إذا لم تُمرَّر قيمةٌ إليها عند استدعاء الدالة أو مُرِّرَت القيمة undefined، لمزيدٍ من التفاصيل راجع صفحة المعاملات الافتراضية.

معامل البقية (rest)

يسمح معامل البقية (rest parameter) بتمثيل عدد غير مُحدِّد من الوسائط كمصفوفة. راجع صفحة معامل البقية للتفاصيل.

الكائن arguments

يمكن الإشارة إلى وسائط الدالة من داخلها باستخدام الكائن arguments.

تعريف دوال تابعة لدالة

دوال الوصول والضبط

يمكنك تعريف دوال وصول (getters) ودوال ضبط (setters) على أيّ كائن مضمَّن في اللغة أو مُعرَّف من المستخدم الذي يدعم إضافة خاصيات جديدة إليه.

  • get: ربط خاصية الكائن إلى دالة التي ستُستدعى عند محاولة الحصول على تلك الخاصية.
  • set: ربط خاصية الكائن إلى دالة التي ستستدعى عند محاولة ضبط تلك الخاصية.

البنية المختصرة لتعريف الدوال

بدءًا من ECMAScript 2015 (أي ES6)، يمكن تعريف الدوال التابعة للكائن بطريقة مختصرة، راجع صفحة البنية المختصرة لتعريف الدوال (method definitions) لمزيدٍ من المعلومات.

var obj = {
  foo() {},
  bar() {}
};

تعريف الدوال عبر الدالة البانية Function وعبر تعابير تعريف وتعابير التصريح عن الدوال

هذه دالةٌ مُعرَّفةٌ بإسناد ما ستعيده الدالة البانية Function إلى المتغير multiply:

var multiply = new Function('x', 'y', 'return x * y');

أما الشيفرة الآتية فهي تُصرِّح عن دالة باسم multiply:

function multiply(x, y) {
   return x * y;
} // لا توجد فاصلة منقوطة هنا

أما الشيفرة الآتية فهي تستعمل تعابير تعريف الدوال لتعريف دالة مجهولة مُسنَدة إلى المتغير multiply:

var multiply = function(x, y) {
   return x * y;
};

أما هذا التعبير فهو يُعرِّف دالةً مسماةً باسم func_name ويُسنِدُها إلى المتغير multiply:

var multiply = function func_name(x, y) {
   return x * y;
};

الاختلافات بين طرائق تعريف الدوال

الأشكال السابقة لتعريف الدوال تقوم بالأمر نفسه لكن مع بعض الاختلافات التي سنوضِّحها في هذا القسم.

هناك فرقٌ بين اسم الدالة واسم المتغير الذي أُسنِدَت الدالة إليه، فلا يمكن تغيير اسم الدالة لكن يمكن إسناد وإعادة إسناد الدالة إلى المتغيرات، ولا يمكن أن يستعمل اسم الدالة إلا ضمن جسمها، ومحاولة استخدامه خارج جسم الدالة سيؤدي إلى حدوث خطأ:

var y = function x() {};
alert(x); // سيرمى خطأ

سيظهر اسم الدالة أيضًا عن تحويلها إلى سلسلة نصية عبر الدالة toString()‎ التابعة للكائن Function.

وكما يوضِّح المثال الرابع في القسم السابق، يمكن أن يختلف اسم الدالة عن اسم المتغير الذي أُسنِدَت إليه، إذ لا توجد علاقةٌ بينهما؛ إلا أنَّ التصريح عن الدالة (function declaration) سيُنشِئ متغيرًا يحمل اسم الدالة نفسه، وبالتالي يمكن الوصول إلى الدالة عبر اسمها في هذه الحالة.

الدوال المُعرَّفة عبر استدعاء الدالة البانية new Function ليس لها اسم، لكن في بعض محركات JavaScript ستظهر الدالة عند تحويلها إلى سلسلة نصية كما لو كان لها الاسم «anonymous»، فمثلًا التعبير البرمجي ‎‎‎alert(new Function‎()‎)‎‎‎ سيُخرِج:

function anonymous() {
}

ولمّا كانت الدالة لا تملك اسمًا، فإنَّ anonymous ليس متغيرًا يمكن الوصول إليه ضمن الدالة، فالمثال الآتي سيتسبب بظهور رسالة خطأ:

var foo = new Function("alert(anonymous);");
foo();

وعلى النقيض من الدوال المُعرَّفة عبر تعابير أو عبر الدالة البانية Function، يمكن استخدام الدوال التي يُصرَّح عنها قبل التعبير البرمجي الذي يُصرِّح عنها. فمثلًا:

foo(); // alerts FOO!
function foo() {
   alert('FOO!');
}

الدوال التي تُعرَّف عبر تعبير أو عبر التصريح عن الدالة سترث المجال الحالي (current scope)، أي أنَّ الدالة ستُشكِّل تعبيرًا مغلقًا (closure)، أما الدوال المُعرَّفة عبر الدالة البانية Function لن ترث أيّ مجال عدا المجال العام (global scope) الذي ترثه جميع الدوال. سنصرِّح في المثال الآتي عن المتغير p في المجال العام، ثم سنُنشِئ دالةً باسم myFunc لتغيير المجال، وسنُعرِّف داخلها متغيرًا له نفس الاسم p، من ثم سنُعرِّف ثلاث دوال باستخدام طرائق مختلفة (تعبير، وتصريح، والدالة البانية Function)، وسنعرض قيمة المتغير p في كلٍ واحدةٍ منها:

var p = 5;
function myFunc() {
    var p = 9;

    function decl() {
        console.log(p);
    }
    var expr = function() {
        console.log(p);
    };
    var cons = new Function('\tconsole.log(p);');

    decl();
    expr();
    cons();
}
myFunc();

// الناتج
// 9 - decl (المجال الحالي)
// 9 - expr (المجال الحالي)
// 5 - cons (المجال العام)

الدوال التي تُعرَّف عبر تعابير تعريف الدوال أو التصريح عنها ستُفسَّر مرةً واحدة، بينما سيُفسَّر جسم الدالة المُمرَّر كسلسلة نصية إلى الدالة البانية Function في كل مرة ستستدعى فيها الدالة، وعلى الرغم من أنَّ تعابير تعريف الدوال ستُنشِئ تعبيرًا مغلقًا (closure) في كل مرة تستدعى فيها، لكن جسم الدالة لن يُعاد تفسيره؛ ما سبق يعني أنَّ تعابير تعريف الدوال أسرع من استخدام الدالة البانية Function، لذا من المُفضَّل تجنّب استخدام الدالة البانية Function حيثما استطعنا. لكن يجدر بالذكر أنَّ تعابير تعريف الدوال أو التصريح عنها الموجودة ضمن الدالة التي تولّد عبر تفسير السلسلة النصية المُمرَّرة إلى الدالة البانية Function لن يُعاد تفسيرها في كل مرة تستدعى فيها الدالة. فمثلًا:

var foo = (new Function("var bar = \'FOO!\';\nreturn(function() {\n\talert(bar);\n});"))();
foo(); // "function() {\n\talert(bar);\n}" لن يعاد تفسير الجزء

يمكن أن يحوَّل التصريح عن الدالة (function declaration) بسهولة (وعادةً دون قصد) إلى تعبير تعريف دالة؛ إذ يجب ألّا يتحقق ما يلي كيلا يتحول التصريح عن الدالة إلى تعبير تعريف دالة:

  • أصبح جزءًا من تعبيرٍ برمجي (expression).
  • لم يعد «عنصرًا مصدريًا» (source element) لدالة أو للسكربت كله، ويمكن تعريف «العنصر المصدري» (source element) على أنَّه تعبير برمجي ليس متشعبًا داخل السكربت أو جسم الدالة:
var x = 0;               // source element
if (x === 0) {           // source element
   x = 10;               // not a source element
   function boo() {}     // not a source element
}
function foo() {         // source element
   var y = 20;           // source element
   function bar() {}     // source element
   while (y === 10) {    // source element
      function blah() {} // not a source element
      y++;               // not a source element
   }
}

أمثلة عمّا سبق:

// تصريح عن الدالة
function foo() {}

// تعبير تعريف دالة
(function bar() {})

// تعبير تعريف دالة
x = function hello() {}


if (x) {
   // function expression
   function world() {}
}


// تصريح عن الدالة
function a() {
   // تصريح عن الدالة
   function b() {}
   if (0) {
      // تعبير تعريف دالة
      function c() {}
   }
}

تعريف الدوال ضمن أقسام برمجية

في نمط strict وبدءًا من EMCAScript 2015 (أي ES6)، سيكون مجال الدوال المُعرَّفة ضمن أقسام برمجية هو مجال ذلك القسم، لكن قبل EMCAScript 2015 كان من غير المسموح تعريف الدوال ضمن أقسام برمجية في نمط strict.

'use strict';

function f() { 
  return 1; 
}

{  
  function f() { 
    return 2; 
  }
}

f() === 1; // true

// f() === 2 non-strict mode

تعريف الدوال ضمن أقسام برمجية في نمط non-strict

باختصار: لا تفعل ذلك.

في النمط non-strict ستسلك تعابير التصريح عن الدوال سلوكًا غريبًا، فمثلًا:

if (shouldDefineZero) {
   function zero() {     // تحذير! مشاكل في التوافقية
      console.log("This is zero.");
   }
}

تقول مواصفة ECMAScript 2015 (أي ES6) أنَّه إذا كانت قيمة shouldDefineZero تساوي false فلن تُعرَّف الدالة zero أبدًا، لأنَّ هذا القسم البرمجي لن يُنفَّذ أبدًا؛ لكن ما سبق هو جزءٌ جديدٌ أُضيف إلى المعيار؛ ولم يكن ذلك مُعرَّفًا قبل إطلاق النسخة النهائية منه، لذا أدى ذلك إلى تعريف بعض المتصفحات للدالة zero سواءً نُفِّذَ القسم البرمجي أم لم يُنفَّذ.

أما في نمط Strict، فجميع المتصفحات التي تدعم ECMAScript 2015 تتصرف بنفس الطريقة: يجب تعريف الدالة zero إذا كانت قيمة shouldDefineZero تساوي true، وستكون متاحةً في مجال قسم التعبير البرمجي if فقط.

طريقة أفضل لتعريف الدوال إذا تحقق شرطٌ معيّن هي إسناد تعبير تعريف الدالة إلى متغير:

var zero;
if (shouldDefineZero) {
   zero = function() {
      console.log("This is zero.");
   };
}

أمثلة

إعادة عدد مُنسَّق من دالة

الدالة الآتية تُعيد سلسلةً نصية تحتوي على عدد مُنسَّق عبر إضافة أصفار بادئة قبله:

function padZeros(num, totalLen) {
   var numStr = num.toString();             // تهيئة السلسلة النصية التي ستُعاد
   var numZeros = totalLen - numStr.length; // حساب عدد الأصفار التي يجب وضعها
   for (var i = 1; i <= numZeros; i++) {
      numStr = "0" + numStr;
   }
   return numStr;
}

التعابير البرمجية الآتية ستستدعي الدلة padZeros:

var result;
result = padZeros(42,4); // "0042"
result = padZeros(42,2); // "42"
result = padZeros(5,4);  // "0005"

تحديد إذا كان دالةٌ ما موجودةً

يمكنك تحديد إذا كان دالةٌ ما موجودةً عبر المعامل typeof، ففي المثال الآتي أجرينا اختيارًا لكن نعرف إن كان الخاصية noFunc التابعة للكائن window هي دالة:

if ('function' === typeof window.noFunc) {
   // استخدم الدالة
 } else {
   // افعل شيئًا آخر
 }

لاحظ أننا لم نضع أقواسًا () بعد اسم الدالة noFunc في التعبير الشرطي if، لأننا نريد الحصول على مرجعية إلى الدالة وليس استدعاؤها.

دعم المتصفحات

الميزة Chrome Firefox Internet Explorer Opera Safari
الدعم الأساسي نعم نعم نعم نعم نعم

مصادر ومواصفات