Skip to content

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

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

10 أبريل 2023 | 08:30

سبق لي أن تكلمت عن الدوال السهمية في جافا سكريبت وذلك في مقال سابق وقديم على مدونة توتومينا. حينذات قدمت الميزة على أنها من المزايا الجديدة في إصدار ES2015 من لغة البرمجة JavaScript، وكذلك حاولت توضيح عدد من المميزات والإختلافات التي تتميز بها تلك الدوال عن غيرها من الدوال العادية التي تنشَأ عن طريق الكلمة function.

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

function doSomething() {
  console.log("hello world! I am from a function declaration :)");
}
const doSomething = function () {
  console.log("hello world! I am from a function expression :)");
};

أما الدوال السهمية كما هو معروف فيتم إنشاؤها بهذه الطريقة:

const doSomething = () => {
  console.log("hello world! I am from an arrow function :)");
};

كل هذه الطرق تستخدم لإنشاء الدوال في جافاسكريبت، ولكن لا يعني هذا أن الدوال السهمية تشتغل وتعمل 100% بنفس طريقة عمل الدوال الإعتيادية. على العكس من ذلك، هناك العديد من الإختلافات بينهما وقد لا تواجهها أو تلقي لها بالاً وأنت تكتب سطورك البرمجية الأولى مع Arrow functions. ولكن مع تقدمك وتعمقك أكثر، ومع مواجهة حالات معينة في عملك ستكتشف أن دالتك السهمية قد تصرفت على نحو غير متوقع منها، وحينذاك ستبدأ ربما في حك فروة رأسك وتسأل نفسك عن السبب.

هذه التدوينة جاء لتجيب على تلك التساؤلات، وتفتح لك الطريق نحو فهم أعمق وأفضل للدوال السهمية.

1. الإختلاف الأول: قيمة this أو Execution context

من المعروف لكل مطور جافا سكريبت أن قيمة الكائن this يتحدد لدى الدوال العادية (Regular functions) بناءً على عدد من المعطيات وحسب الكيفية التي تم بها استدعاء الدالة.

فعلى سبيل المثال، عندما نقوم بإنشاء دالة كوظيفة داخل كائن جافاسكريبت معين فإن this يشير إلى ذلك الكائن الذي يمثل سياق التنفيذ (Execution context) لتلك الدالة أو الوظيفة.

const user = {
  login: function () {
    console.log(this);
  },
};
 
user.login(); // user

أما عندما نستدعي الدالة في النطاق العام (Global scope) للبرنامج، أي تحت الكائن window مباشرة، فإن قيمة this داخل الدالة تشير إلى الكائن الأب window 👇

function login() {
  console.log(this);
}
 
login(); // window

أو تصبح undefined في حالة الوضع الصارم (Strict mode) 👇

function login() {
  "use strict";
  console.log(this);
}
 
login(); // undefined

أما في الحالة التي تستخدم فيها الدالة كَ constructor فإن قيمة this تشير إلى الكائن الجديد الذي تم إنشاؤه بواسطة new 👇

function User() {
  console.log(this);
}
 
const user = new User(); // user -> instance of User

ويمكن كذلك التحكم في سياق التنفيذ (Execution context) للدوال العادية في جافاسكريبت عن طريق استدعائها بواسطة call، apply أو حتى bind.

هذه الوظائف تقبل كمعامل أول الكائن المراد تنفيذ تلك الدالة في سياقه، وعندئد يصبح كل this داخل الدالة عائدا عليه.

function login() {
  console.log(this.username);
}
 
const user1 = { username: "ahmed" };
const user2 = { username: "fatima" };
 
login.call(user1); // ahmed
login.call(user2); // fatima

😄 الموضوع مختلف في الدوال السهمي

كل المميزات أعلاه للدوال العادية فيما يخص تحديد قيمة this ونطاق التنفيذ عليك نسيانها عند استخدام الدوال السهمية 👊

هذه الأخيرة لا تهتم أبدا بتحديد ماهية this أو في أي سياق تنفيذ هي موجودة من تلقاء نفسها أو بناءً على كيفية استدعائها، وإنما بناءً على مكان تواجدها في الكود (Lexically)، بنفس الطريقة التي تأخذ فيها المتغيرات الإعتيادية قيمها. أي بالإعتماد على تحليل Lexical Scope.

فإذا كان Lexical scope الذي قمنا بإنشاء تلك الدالة فيه هو Global scope (أي window في حالة المتصفح) فإن this يكون هو window بغض النظر عن أين وكيف استدعينا الدالة. وإذا تم إنشاؤها داخل دالة أخرى فإنها ستأخذ قيمة this التي تفرضها عليها تلك الدالة الأم.

لا يهم إذا استدعيت الدالة بواسطة call أو apply، ال arrow function تتجاهل كل ذلك ولا تعترف إلا ب Lexical scope.

لا يمكن بعد ذلك للدالة السهمة أن تغير من القيمة المعطاة للكائن this من تلقاء نفسها إلا أن يأتي التغيير من السلطات العليا نفسها (Lexical scope) 👮

const notify = () => {
  console.log(this);
};
 
function login() {
  console.log(this.username);
  notify(); // window
}
 
const user = { username: "ahmed" };
 
login.call(user); // ahmed

لا حظ في المثال أعلاه، أنه رغم أنه تم استدعائها داخل الدالة login حيث قيمة this تشير إلى الكائن user إلا أن قيمة this بداخل الدالة السهمية notify مازالت تشير إلى window. والسبب أنها Lexically، أي من حيث تركيبة الكود، ما تزال داخل window وبالتالي فإن this بالنسبة لها هو window وليس user.

وكما قلت سابقا، فإن هذه العملية تتم بنفس الطريقة التي يتم بها إعطاء قيم للمتغيرات الإعتيادية:

const name = "أحمد";
 
const notify = () => {
  console.log(name);
};
 
function login() {
  const name = "فاطمة";
  notify(); // أحمد
}
 
login();

لسان حال الدالة السهمية يقول: بما أن نطاقي الأب (Parent scope) حيث تم إنشائي أول مرة هو Global scope (window في المتصفح أو globalThis في Node.js) فإن قيمة المتغير name التي أعرفها هي أحمد التي جاءت من Parent scope، ولا شأن لي في أي مكان تم فيه استدعائي بعد ذلك.

إذا أردنا للدالة السهمية أن تعطي للكائن this نفس القيمة المعطاة له في الدالة login فيجب علينا إنشاؤها بداخلها ليصبح نطاقها الأب هو الدالة login، وبالتالي ترث منها قيمة this كما أصبح مفهوما لدينا الآن 👇

function login() {
  console.log(this.username);
 
  const notify = () => {
    console.log(this);
  };
 
  notify(); // أحمد
}
 
const user = { username: "أحمد" };
 
login.call(user); // أحمد

من هذا كله، أصبح واضحا لدينا ضرورة الحذر عن استخدام هذين النوعين من الدوال في JavaScript، وضرورة معرفة خصوصية ومميزات كل نوع على حدة في هذه النقطة المتعلقة بالكائن this.

2. الإختلاف الثاني: استخدام الدالة كَ constructor

بما أن الدوال السهمية ليست معنية بإعطاء قيمة للكائن this بناء على طريقة استدعائها فإن ذلك يعني بأنها غير صالحة وغير مصممة لتلعب دوال ال class أو constructor.

لا يمكن إنشاء كائنات عن طريق new بواسطة الدوال السهمية كما هو ممكن مع الدوال العادية. إذا حاولت فعل ذلك فإن جافا سكريبت سيعتبر ذلك بمثابة خطأ برمجي.

function GoodUserClass(name) {
  this.name = name;
}
 
const BadUserClass = name => {
  this.name = name;
};
 
const ahmed = new GoodUserClass("أحمد"); // صحيح
const fatima = new BadUserClass("فاطمة"); // Uncaught TypeError: BadUserClass is not a constructor

3. الإختلاف الثالث: الكائن arguments

من المعروف لدى مطوري جافا سكريبت أنه يمكن الوصول إلى المعاملات التي مُرِّرَتْ للدالة العادية عند استدعائها، وذلك عن طريق الكائن السحري arguments.

لاحظ الدالة العادية في الكود أسفله 👇

function regFunction() {
  console.log(arguments);
}
 
regFunction("param1", "param2"); // [object Arguments] { 0: "param1", 1: "param2"}

هذه الميزة القديمة تعطينا إمكانية استخدام arguments لجعل الدالة ديناميكية ومرنة أكثر، وذلك عن طريق فعل أشياء محددة بحسبب عدد المعاملات الممررة.

ماذا عن الدوال السهمية ؟

في الدوال السهمية يعمل الكائن arguments بطريقة مختلفة.

يأخذ هذا الكائن قيمته من خلال Lexical scope، تماما كما رأينا قبل قليل مع الكائن this.

فإذا أنشأنا الدالة السهمية في Global scope فإن قيمة arguments ستكون غير معرفة (not defined) لأنه لا وجود لكائن اسمه arguments في النطاق العام. وعندما نحاول الوصول لمتغير غير معرف في جافا سكريبت فذلك يعتبر بمثابة خطأ برمجي.

const arrFunction = () => {
  console.log(arguments);
};
 
arrFunction("arrParam1", "arrParam2"); // Uncaught ReferenceError: arguments is not defined

أما إذا أنشأنا الدالة داخل دالة أخرى فإن الكائن arguments الخاص بها سيشير إلى ذات الكائن arguments الخاص بدالتها الأم حيث هي موجودة.

function regFunction() {
  console.log(arguments);
 
  const arrFunction = () => {
    console.log(arguments);
  };
 
  arrFunction("arrParam1", "arrParam2", "arrParam3"); // [object Arguments] { 0: "regParam1", 1: "regParam2"}
}
 
regFunction("regParam1", "regParam2"); // [object Arguments] { 0: "regParam1", 1: "regParam2"}

4. الإختلاف الرابع: الإرجاع

كما هو معلوم، نقوم بإرجاع قيمة معينة من الدالة في جافاسكريبت عن طريق الكلمة المفتاحية return.

// Return number
function sum(a, b) {
  return a + b;
}
 
// Return object
function getUser() {
  return {
    id: "134o-idka-867986",
    username: "tutomena",
  };
}

كانت هذه الطريقة الوحيدة لإرجاع شيء ما من دالة في لغة جافا سكريبت. ولكن بعد ظهور الدوال السهمية أصبح بإمكاننا الإرجاع بطرق مختلفة وبدون الحاجة لكلمة return إذا كانت الدالة تتكون من سطر واحد فقط.

// Return number
const sum = (a, b) => a + b;
 
// Return object
const getUser = () => ({
  id: "134o-idka-867986",
  username: "tutomena",
});

الكود أعلاه رغم اختلافه عن الذي قبله من ناحية Syntax إلا أنهما متطابقان من الناحية العملية.

إذا كانت الدالة متعددة الأسطر فيمكن استخدام الكلمة return للإرجاع.

const getUser = async userId => {
  const user = await fetchUser(userId);
  return user;
};

كما هو واضح من المثال، لا حاجة للأقواس في حال كانت الدالة تقبل معاملا واحدا فقط (userId في المثال).

5. الإختلاف الخامس: الإستخدام كوظائف

عادة تستخدم الدوال الإعتيادية كوظائف سواء في الكلاسات أو الكائنات (Literal Object) لأنها كما رأينا من قبل تكون مرتبطة بذلك الكائن حيث يمكن الوصول إليه من داخل الدالة عن طريق this.

const counter = {
  count: 0,
  increment: function () {
    this.count++;
    console.log(this.count);
  },
};
 
counter.increment(); // 1
class Counter {
  count = 0;
 
  increment() {
    this.count++;
    console.log(this.count);
  }
}
 
const counter = new Counter();
 
counter.increment(); // 1

ولكن ماذا لو استخدمنا تلك الوظيفة في مكان تكون فيه مربوطة بسياق (context) مختلف ؟ مثلا في حدث (Event) أو دالة علوية مثل setTimeout ؟

لاحظ الكود أسفله 👇

class Counter {
  count = 0;
 
  increment() {
    this.count++;
    console.log(this.count);
  }
}
 
const counter = new Counter();
 
setTimeout(counter.increment, 1000); // NaN

بتشغيل هذا الكود سنلاحظ بأنه سيطبع القيمة NaN، فلماذا ذلك ؟

السبب هو أن الدالة التي يتم تمريرها ل setTimeout يتم ربطها (bound) بسياق مختلف (window) عن السياق الذي كانت مرتبطة به في البداية وهو الكائن counter. وبالتالي فإن قيمة this.count تكون undefined (this أصبح هو window)، ما يفسر محصلة العملية undefined + 1 والتي أعطتنا NaN.

الحل

لإجبار الوظيفة counter.increment على أن تظل وفية للكائن الذي تنتمي له، يجب علينا أن نطلب منها ذلك بشكل صريح وذلك عن طريق bind.

class Counter {
  count = 0;
 
  increment() {
    this.count++;
    console.log(this.count);
  }
}
 
const counter = new Counter();
 
setTimeout(counter.increment.bind(counter), 1000); // 1

وإذا كنت من مستخدمي ومطوري مكتبة React فبنسبة كبيرة سبق لك استخدام هذا الحل مرارا وتكرارا عندما كنت تحاول الوصول إلى this.setState أو this.state بداخل Event handlers في Class Components.

import React from "react";
 
export class App extends React.Component {
  constructor(props) {
    super(props);
  }
 
  handleClick(event) {
    console.log(this); // 'this' is undefined 👎
  }
 
  render() {
    return (
      <button type="button" onClick={this.handleClick}>
        Click Me
      </button>
    );
  }
}

الحل

import React from "react";
 
export class App extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
 
  handleClick(event) {
    console.log(this); // `this` is `App` component 👍
  }
 
  render() {
    return (
      <button type="button" onClick={this.handleClick}>
        Click Me
      </button>
    );
  }
}

ماذا عن الدوال السهمية ؟

لا وجود لهذه المشكلة في الدوال السهمية، فهي كما رأينا سابقا تظل وفية للسياق الذي أُنشأت فيه (لا تنسى موضوع Lexical scope) بغض النظر عن كيفية استدعائها.

وهذا نفسه السبب في أن الدوال السهمية غير ملائمة للإستخدام كوظائف (Methods).

فلو أنشأنا كائنا object يضم وظيفة على شكل دالة سهمية فإن الكائن this سيشير إلى window إذا اعتبرنا أن الكائن object تم إنشاؤه في Global scope.

هذا يعني بأن الدالة السهمية عديمة الفائدة كوظيفة في هذه الحالة.

const object = {
  someMethod: () => {
    console.log(this);
  },
};
 
object.someMethod(); // Window

أما داخل الكلاسات فيمكن استخدام الدالة السهمية كوظيفة على شكل Class field، وحينها يظل this داخلها يؤشر دائما على الكلاس أو instance نفسها.

class Counter {
  count = 0;
 
  increment = () => {
    this.count++;
    console.log(this.count);
  };
}
 
const counter = new Counter();
counter.increment(); // 1
setTimeout(counter.increment, 1000); // 2

هناك عيب وحيد لاستخدام الوظائف كَ Class field، وهي أن كل نسخة (instance) جديدة من الكلاس تنشئ دالة increment جديدة خاصة بها. على عكس الوظائف (Class methods) التي يتم إنشاؤها داخل الكائن prototype وبالتالي تصبح مشتركة بين جميع النسخ التي يتم إنشاؤها من هذا الكلاس.

class Counter {
  count = 0;
 
  increment = () => {
    this.count++;
    console.log(this.count);
  };
 
  doSomething() {
    // do something
  }
}
 
const counter1 = new Counter();
const counter2 = new Counter();
console.log(counter1.increment === counter2.increment); // false
console.log(counter1.doSomething === counter2.doSomething); // true

أقول هذا الكلام لأُبين لك أن الدوال السهمية لا تصلح للإستخدام كوظائف بصفة عامة، حتى وإن كان استخدامها داخل الكلاس على شكل Class Field يفي بالغرض إلا أنه حل لا ينصح به كثيرا خاصة إذا كان موضوع ال Performance يهمك كثيرا.

خاتمة

لا شك بأن الدوال السهمية ميزة عظيمة في لغة البرمجة جافا سكريبت، ولكنها مع ذلك لا يجب أن تكون بديلا فوريا للدوال التقليدية خاصة إذا لم نكن على بينة ومعرفة كاملتين بالفوارق الأساسية بينهما.

بعد كل ما تحدثنا عنه، من المفروض الآن أننا أصبحنا قادرين على التمييز بين الدوال السهمية والدوال التقليدية القديمة، وبتنا قادرين على معرفة متى يمكننا الإستعانة بأحد الخيارين على حساب الآخر.

;

عيسى محمد علي
عيسى محمد علي
مطور ويب متخصص في الواجهات الأمامية، أحب التدوين وإغناء المحتوى التقني للغة الضاد وهذا كان السبب الرئيسي في إنشائي لمدونة توتومينا.