اذهب إلى المحتوى

الباني والعامل "new" في جافاسكربت


Emq Mohammed

نُنشِئ الكائنات باستخدام الصيغة الاعتيادية المختصرة {...}. لكننا نحتاج لإنشاء العديد من الكائنات المتشابهة غالبًا، مثل العديد من المستخدمين، أو عناصر لقائمة وهكذا. يمكن القيام بذلك باستخدام الدوال البانية (constructor functions) لكائن والمُعامِل "new".

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

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

  1. أنها تبدأ بأحرف كبيرة.
  2. يجب تنفيذها مع المُعامِل "new" فقط.

إليك المثال التالي:

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // false

عند تنفيذ دالة مع الُعامِل new، تُنَفَّذ الخطوات التالية:

  1. يُنشَأ كائن فارغ ويُسنَد إلى this.
  2. يُنَفَّذ محتوى الدالة. تقوم غالبًا بتعديل this، وإضافة خاصيات إليه.
  3. تُرجَع قيمة this.

بمعنى آخر، تقوم new User(...)‎ بشيء يشبه ما يلي:

function User(name) {
  // this = {};  (implicitly)

  // this إضافة خاصيات إلى 
  this.name = name;
  this.isAdmin = false;

  // this;  (implicitly) إرجاع 
}

إذًا، تُعطي let user = new User("Jack")‎ النتيجة التالية ذاتها:

let user = {
  name: "Jack",
  isAdmin: false
};

الآن، إن أردنا إنشاء مستخدمين آخرين، يمكننا استدعاء new User("Ann")‎، و new User("Alice‎")‎ وهكذا. تعدُّ هذه الطريقة في بناء الكائنات أقصر من الطريقة الاعتيادية عبر الأقواس فقط، وأيضًا أسهل للقراءة. هذا هو الغرض الرئيسي للبانيات، وهي تطبيق شيفرة قابلة لإعادة الاستخدام لإنشاء الكائنات.

لاحظ أنَّه يمكن استخدام أي دالة لتكون دالة بانية تقنيًا. يعني أنه يمكن تنفيذ أي دالة مع new، وستُنَفَّذ باستخدام الخوارزمية أعلاه. استخدام الأحرف الكبيرة في البداية هو اتفاق شائع لتمييز الدالة البانية من غيرها وأنَّه يجب استدعاؤها مع new.

new function() { … }‎

إن كان لدينا العديد من الأسطر البرمجية، وجميعها عن إنشاء كائن واحد مُعَقَّد، فبإمكاننا تضمينها في دالة بانية، هكذا:

let user = new function() {
  this.name = "John";
  this.isAdmin = false;

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

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

وضع اختبار الباني: new.target

ميزة متقدمة: تُستخدم الصيغة في هذا الجزء نادرًا، ويمكنك تخطيها إلا إن كنت تُريد الإلمام بكل شيء.

يمكننا فحص ما إن كانت الدالة قد استدعيت باستخدام new أو دونه من داخل الدالة، وذلك باستخدام الخاصية الخاصة new.target.

تكون الخاصية فارغة في الاستدعاءات العادية، وتساوي الدالة البانية إذا استُدعِيَت باستخدام new:

function User() {
  alert(new.target);
}

//  "new" بدون:
User(); // undefined

// باستخدام "new":
new User(); // function User { ... }

يمكن استخدام ذلك بداخل الدالة لمعرفة إن استُدعِيَت مع new، "في وضع بناء كائن"، أو بدونه "في الوضع العادي".

يمكننا أيضًا جعل كلًا من الاستدعاء العادي و new ينفِّذان الأمر ذاته -بناء كائن- هكذا:

function User(name) {
  if (!new.target) { // new إن كنت تعمل بدون 
    return new User(name); // new ...سأضيف 
  }

  this.name = name;
}

let john = User("John"); // new User تُوَجِّه الاستدعاء إلى 
alert(john.name); // John

يستخدم هذا الاسلوب في بعض المكتبات أحيانًا لجعل الصيغة أكثر مرونة حتى يتمكن الأشخاص من استدعاء الدالة مع new أو بدونه، وتظل تعمل.

ربما ليس من الجيد استخدام ذلك في كل مكان، لأن حذف new يجعل ما يحدث أقل وضوحًا. لكن مع new، يعلم الجميع أن كائنًا جديدًا قد أُنشِئ.

ما تُرجِعه الدوال البانية

لا تملك الدوال البانية عادةً التعليمة return. فَمُهِمَتُهَا هي كتابة الأمور المهمة إلى this، وتصبح تلقائيًا هي النتيجة. لكن إن كان هناك التعليمة return فإن القاعدة بسيطة:

  • إن استُدعِيَت return مع كائن، يُرجَع الكائن بدلًا من this.
  • إن استُدعِيَت return مع متغير أولي، يُتَجاهَل.

بمعنىً آخر، return مع كائن يُرجَع الكائن، وفي الحالات الأخرى تُرجَع this. مثلًا، يعاد في المثال التالي الكائن المرفق بعد return ويهمل الكائن المسنَد إلى this:

function BigUser() {

  this.name = "John";

  return { name: "Godzilla" };  // <-- تُرجِع هذا الكائن
}

alert( new BigUser().name );  // Godzilla, حصلنا على الكائن

وهنا مثال على استعمال return فارغة (أو يمكننا وضع متغير أولي بعدها، لا فرق):

function SmallUser() {

  this.name = "John";

  return; // ← this تُرجِع 
}

alert( new SmallUser().name );  // John

لا تحوي الدوال البانية غالبًا على تعليمة الإعادة return. نذكر هنا هذا التصرف الخاص عند إرجاع الكائنات بغرض شمول جميع النواحي.

حذف الاقواس

بالمناسبة، يمكننا حذف أقواس new في حال غياب المعاملات مُعامِلات:

let user = new User; // <-- لا يوجد أقوس
// الغرض ذاته
let user = new User();

لا يُعد حذف الأقواس أسلوبًا جيدَا، لكن الصيغة مسموح بها من خلال المواصفات.

الدوال في الباني

استخدام الدوال البانية لإنشاء الكائنات يُعطي مرونة كبيرة. قد تحوي الدالة البانية على مُعامِلات ترشد في بناء الكائن ووضعه، إذ يمكننا إضافة خاصيات ودوال إلى this بالطبع. مثلًا، تُنشِئ new User(name)‎ في الأسفل كائنًا بالاسم المُعطَى name والدالة sayHi:

function User(name) {
  this.name = name;

  this.sayHi = function() {
    alert( "My name is: " + this.name );
  };
}

let john = new User("John");

john.sayHi(); // My name is: John

/*
john = {
   name: "John",
   sayHi: function() { ... }
}
*/

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

الخلاصة

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

يمكننا استخدام الدوال البانية لإنشاء كائنات متعددة متشابهة.

تزود JavaScript دوالًا بانية للعديد من الأنواع (الكائنات) المدمجة في اللغة: مثل النوع Date للتواريخ، و Set للمجموعات وغيرها من الكائنات التي نخطط لدراستها.

عودة قريبة

غطينا الأساسيات فقط عن الكائنات وبانياتها في هذا الفصل. هذه الأساسيات مهمة تمهيدًا لتعلم المزيد عن أنواع البيانات والدوال في الفصل التالي.

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

تمارين

دالتين - كائن واحد

الأهمية: 2

هل يمكن إنشاء الدالة A و B هكذا new A()==new B()‎؟

function A() { ... }
function B() { ... }

let a = new A;
let b = new B;

alert( a == b ); // true

إن كان ممكنًا، وضح ذلك بمثال برمجي.

الحل

نعم يمكن ذلك.

إن كان هناك دالة تُرجِع كائنًا فإن new تُرجِعه بدلًا من this. لذا فمن الممكن، مثلًا، إرجاع الكائن المعرف خارجيًا obj:

let obj = {};

function A() { return obj; }
function B() { return obj; }

alert( new A() == new B() ); // true

إنشاء حاسبة جديدة

الأهمية: 5

إنشِئ دالة بانية باسم Calculator تنشئ كائنًا بثلاث دوال:

  • read()‎ تطلب قيمتين باستخدام سطر الأوامر وتحفظها في خاصيات الكائن.
  • sum()‎ تُرجِع مجموع الخاصيتين.
  • mul()‎ تُرجِع حاصل ضرب الخاصيتين.

مثلًا:

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

الحل

function Calculator() {

  this.read = function() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  };

  this.sum = function() {
    return this.a + this.b;
  };

  this.mul = function() {
    return this.a * this.b;
  };
}

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

إنشاء مجمِّع

الأهمية: 5

انشِئ دالة بانية باسم Accumulator(startingValue)‎، إذ يجب أن يتصف هذا الكائن بأنَّه:

  • يخزن القيمة الحالية في الخاصية value. تُعَيَّن قيمة البدء عبر المعامل startingValue المعطى من الدالة البانية.
  • يجب أن تستخدم الدالة read() الدالة prompt لقراءة رقم جديد وإضافته إلى value.

بمعنى آخر، الخاصية value هي مجموع القيم المدخلة بواسطة المستخدم بالإضافة إلى القيمة الأولية startingValue.

هنا مثال على ما يجب أن يُنَفَّذ:

let accumulator = new Accumulator(1); // القيمة الأولية 1

accumulator.read(); // يضيف قيمة مدخلة بواسطة المستخدم
accumulator.read(); // يضيف قيمة مدخلة بواسطة المستخدم

alert(accumulator.value); // يعرض مجموع القيم

الحل

function Accumulator(startingValue) {
  this.value = startingValue;

  this.read = function() {
    this.value += +prompt('How much to add?', 0);
  };

}

let accumulator = new Accumulator(1);
accumulator.read();
accumulator.read();
alert(accumulator.value);

ترجمة -وبتصرف- للفصل Constructor, operator "new" من كتاب The JavaScript Language

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

شكرا جزيلا على المقال الرائع!

عندي سؤال...لماذا لا يمكن اسناد قيمة خاصية بخواص أخرى في نفس التابع الباني؟

اشترط صاحب المقال لحل مسألة (إنشاء حاسبة جديدة) باستخدام الدوال للخواص this.sum و this.mul...لقد حاولت ان احولهما بدون استخدام الدوال ل this.sum = this.v1 + this.v2 و this.mul = this.v1 * this.v2 لكن كان يرجع لي القيمة NaN

فما السبب هنا؟..وما المنطق البرمجي وراء هذا؟

وشكرا جزيلا مرة أخرى

رابط هذا التعليق
شارك على الشبكات الإجتماعية

بتاريخ 1 ساعة قال Magdy Doze:

فما السبب هنا؟..وما المنطق البرمجي وراء هذا؟

مرحبا مجدي،

يمكنك استخدام this.v1 و this.v2ضمن الدالة البانية، لكن برأيك هل يكون لهم قيمة قبل الانتهاء من تنفيذ الدالة البانية بالأصل؟

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

function Calculator(a, b) {
  this.a = a;
  this.b = b;

  this.sum = this.a + this.b;
  this.mul = this.a * this.b;
}

let calculator = new Calculator(2, 3);

alert("Sum=" + calculator.sum);
alert("Mul=" + calculator.mul);

 

رابط هذا التعليق
شارك على الشبكات الإجتماعية

@Wael Aljamalمرحبا وائل وشكرا على ردك

فهمت الآن ما مشكلتي...وهي اني لم أتمكن من فهم كيفية تسلسل الأوامر في الدالة البانية, فالحمد لله ليست مشكلتي في الحل هنا ولا كيف حدث, انما لماذا لا يمكن حلها بطرق أخرى وهذا ما يكشف نقاط الضعف في فهمي

وسأعرض عليك ما حاولت فهمه من ردك ومن التعديل على بعض الأكواد في التمارين التي حللتها في هذه المقالة, لأتأكد ان فهمي صحيح بإذن الله

في تمرين (إنشاء حاسبة جديدة) هذا كان حلي:

function Calculator () {
    this.read = function () {
        this.v1 = +prompt ("please, enter the first number")
        this.v2 = +prompt ("please, enter the second number")
    }
    this.sum = function () {
        return this.v1 + this.v2
    }
    this.mul = function () {
        return this.v1 * this.v2
    }
}

let e7seb = new Calculator ()
e7seb.read ()
alert ("Sum = " + e7seb.sum ())
alert ("Mul = " + e7seb.mul ()) 

ولكن عندما انظر الى المثال المسؤول عنه

function Calculator () {
    this.read = function () {
        this.v1 = +prompt ("please, enter the first number")
        this.v2 = +prompt ("please, enter the second number")
    }
    this.sum = this.v1 + this.v2
    this.mul = this.v1 * this.v2
}

let e7seb = new Calculator ()
e7seb.read ()
alert ("Sum = " + e7seb.sum)
alert ("Mul = " + e7seb.mul)

فانا بالفعل استدعيت الخاصية e7seb.sum/mul بعد استدعاء الدالة البانية (e7seb.read ()) أي بعد معرفة قيم كل من v1 و v2 بالفعل...فحسب فهمي من المفترض عند تنفيذ الشيفرة وإدخال قيم مثلا (2 و 5) ان تأتي المخرجات من e7seb.sum = 7 و e7seb.mul = 10

وبعد قراءة ردك الكريم فهمت ان الخواص يجب ان تكون معرفة مسبقا, اما التوابع فيمكن ان تنفذ شيفرة معينة لإرجاع قيمتها (الفرق بين e7seb.sum/mul كخاصية/متغير و e7seb.sum/mul كتابع/دالة)...فهل هذا بسبب ان الخاصية e7seb.sum/mul لم تعرف نوع قيمتها كstring او number؟ اما e7seb.sum ()/mul () فهي دوال تنفذ هذه العملية لي وترجع قيمة واحدة فقط فتسند نوع القيمة كnumber لها بناء عليها (بينما في e7seb.sum/mul فهي تتعامل مع قيمتين؟!)

وبناء على هذا حاولت ان اقسم العملية الى دالتين منفصلتين ونجحت

function values () {
    v1 = +prompt ("please, enter the first number")
    v2 = +prompt ("please, enter the second number")
}

function results () {
    sum = v1 + v2
    mul = v1 * v2
}

values ()
results ()
alert ("Sum = " + sum)
alert ("Mul = " + mul)

 

لننتقل الى التدريب (إنشاء مجمِّع)...وهذا كان حلي:

function Accumulator (startingValue) {
    this.startingValue = startingValue
    this.value = 0
    this.read = function () {
        this.value += +prompt ("please, enter a number")
    }
    this.value += this.startingValue
}
let mogame3 = new Accumulator (5)
mogame3.read ()
mogame3.read ()
mogame3.read ()
alert (mogame3.value)

هنا وضعت this.value = 0 خصيصا لأن الشيفرة ترجع لي قيمة NaN أيضا من دونها على الرغم من تعريف قيمة this.value في التابع this.read..واظن ان هذا بسبب ان this.value هي قيمة داخل الدالة فبالتالي تكون في مدى محلي فقط local scope وعند استدعائها في المدى المحلي الأوسع في الدالة البانية Accumulator لا يمكنها التعرف عليها؟

 

ارجو ان يكون كلامي واضحا, فقد حاولت بقدر الإمكان ان اوصف ما يدور في رأسي عند التفكير في هذه المسائل حتى نكشف موطن الزلل في الفهم ان وجد

شكرا لتفهمك

رابط هذا التعليق
شارك على الشبكات الإجتماعية

بتاريخ 4 ساعات قال Magdy Doze:

وبناء على هذا حاولت ان اقسم العملية الى دالتين منفصلتين ونجحت

في هذه الحالة أنت لم تستخدم مفهوم الصنف Class أو الأغراض Object بل هي دوال عادية ومتغيرات معرفة ضمن global scope.

بتاريخ 4 ساعات قال Magdy Doze:

بعد استدعاء الدالة البانية (e7seb.read ())

read لا تعتبر دالة بناء، هي دالة عادية تضيف خواص للكائن. دالة البناء . دالة البناء هي الدالة الرئيسية Calculator ومن تقوم بتعريف هيكلية الكائن. 

بتاريخ 4 ساعات قال Magdy Doze:

هنا وضعت this.value = 0 خصيصا لأن الشيفرة ترجع لي قيمة NaN أيضا من دونها على الرغم من تعريف قيمة this.value في التابع this.read..واظن ان هذا بسبب ان this.value هي قيمة داخل الدالة فبالتالي تكون في مدى محلي فقط local scope وعند استدعائها في المدى المحلي الأوسع في الدالة البانية Accumulator لا يمكنها التعرف عليها؟

إسناد قيمة ابتدائية هي فكرة سليمة this.value = 0 وضرورية لأنك تضيف ل value قيمة لاحقاً

ولكن شيفرتك بشكل عام يمكن تحسينها

function Accumulator (startingValue) {

    this.value = +startingValue;

    this.read = function () {
        this.value += +prompt ("please, enter a number")
    }
}

لاحظ، this هنا ضمن الدالة function تشير للكائن بشكل صحيح

fffffffffff-this.jpg

رابط هذا التعليق
شارك على الشبكات الإجتماعية



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...