١٥ ديسمبر ٢٠٢١

النماذج البدائية Native prototypes

إن الخاصية "prototype" مستخدمة بشكل واسع من جافا سكريبت نفسها، حيث أن كل الدوال البانية (constructor functions) تستخدمها.

أولًا سنرى التفاصيل، ثم نتعلم كيف نستخدمها لإضافة إمكانيات جديدة للكائنات الموجودة بالفعل (built-in objects).

Object.prototype

دعنا نقول أننا سنطبع كائنًا فارغًا:

let obj = {};
alert(obj); // "[object Object]" ?

أين الكود المسؤول عن التحويل إلى النص "[object Object]"؟ إنها الدالة toString الموجودة بالفعل ، ولكن أين هى؟ الكائن obj فارغ !

…ولكن الصيغة obj = {} هي نفسها هذه الصيغة obj = new Object()، حيث أن Object هو دالة بانية للكائنات موجودة بالفعل والتى تحتوى على الخاصية prototype التى تحتوى على مرجع لكائن ضخم يحتوى على الدالة toString ودوال أخري.

إليك ما يحدث:

عندما يتم استدعاء new Object() (أو إنشاء الكائن العادى {...})، ستكون قيمة [[Prototype]] للكائن الناتج تشير إلى Object.prototype طبقًا للقاعدة التى ناقشناها فى الفصل السابق:

لذلك عندما يتم استدعاء obj.toString() فإن الدالة مأخوذة من Object.prototype.

يمكننا أن نختبر ذلك هكذا:

let obj = {};

alert(obj.__proto__ === Object.prototype); // true

alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true

لاحظ أنه لا يوجد المزيد من [[Prototype]] فى السلسة فوق Object.prototype:

alert(Object.prototype.__proto__); // null

نماذج أخرى موجودة بالفعل (built-in prototypes)

إن الكائنات الأخرى الموجوده بالفعل مثل Array, Date, Function وغيرهم يحتفظون بدوال فى النماذج (prototypes).

علي سبيل المثال، عندما نقوم بإنشاء قائمة [1, 2, 3]، فإن الدالة البانية الموجودة بالفعل new Array() يتم استخدامها داخليًا. ولذلك تصبح Array.prototype النموذج الخاص بها وتمنحها دوال خاصة وهذا شيئ جيد جدًّا للذاكرة.

كما ذُكر فى المصدر، فإن كل النماذج (prototypes) الموجودة بالفعل لديها Object.prototype على القمة فى الأعلى. وهذا مايدفع بعض الأشخاص للقول بأن “كل شيئ يرث من الكائنات”.

هنا الصورة الكاملة:

هيا نختبر الخاصية يدويًا:

let arr = [1, 2, 3];

// هل ترث من Array.prototype?
alert(arr.__proto__ === Array.prototype); // true

// ثم من Object.prototype?
alert(arr.__proto__.__proto__ === Object.prototype); // true

// والقيمة null فى الأعلى.
alert(arr.__proto__.__proto__.__proto__); // null

بعض الدوال فى النماذج يمكن أن تتداخل، فعلى سبيل المثال، تملك Array.prototype الدالة toString الخاصة بها والتى تقوم بإرجاع نص يحوى عناصر القائمة وبينها الفاصلة:

let arr = [1, 2, 3];
alert(arr); // 1,2,3 <-- نتيجة Array.prototype.toString

كما رأينا سابقًا، تملك Object.prototype أيضًا الدالة toString ولكن Array.prototype هي الأقرب فى السلسلة ولذلك يتم استخدام الدالة الخاصة بالقائمة.

تعرض الأدوات الموجودة فى المتصفح أيضًا الوراثة (يمكن أن يتم استخدام console.dir مع بعض الكائنات الموجودة بالفعل ):

تعمل الكائنات الموجودة بالفعل الأخرى بنفس الطريقة. حتى الدوال – هي عبارة عن كائنات مبنية عن طريق الدالة البانية Function والدوال الخاصه بها (مثل call/apply وغيرها) مأخوذة من Function.prototype. وتحتوى الدوال على الدالة toStringالخاصة بها أيضًا.

function f() {}

alert(f.__proto__ == Function.prototype); // true
alert(f.__proto__.__proto__ == Object.prototype); // true, ترث من الكائنات

القيم المفردة Primitives

أكثر الأشياء المعقدة تحدث مع النصوص والأرقام والقيم المطلقة.

كما نتذكر فإنهم ليسو عبارة عن كائنات، ولكن إذا حاولنا أن نصل إلى خصائصهم فسيتم إحاطتها بكائن باستخدام الدوال البانية String و Number و Boolean، حيث يمنحونهم الدوال ثم يختفون.

هذه الكائنات تم إنشاؤها لنا خفيةً وأغلب المحركات تقوم بتحسين ذلك، ولكن يصفها المصدر بهذه الطريقة بالضبط. ودوال هذه الكائنات توجد أيضًا فى النماذج وتكون متاحة كـ String.prototype و Number.prototype و Boolean.prototype.

القيم null و undefined ليس لها كائنات حاوية

القيم الخاصة null و undefined تقف بعيدًا عن هذا. حيث أنهم ليس لديهم كائنات حاوية (object wrappers)، ولذلك فإن الدوال والخصائص غير متاحة لهم وليس هناك نماذج لهم.

التعديل على النماذج البدائية (native prototypes)

يمكن تعديل النماذج البدائية. فعلى سبيل المثال، إذا أضفنا دالة إلى String.prototype ستصبح متاحة إلى كل النصوص:

String.prototype.show = function() {
  alert(this);
};

"BOOM!".show(); // BOOM!

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

مهم:

إن النماذج متاحة بشكل عام، ولذلك فإنه من السهل أن يحدث تعارض. فإذا كان هناك مكتبتان أضافتا نفس الدالة String.prototype.show، إذن فإن واحدة منهن ستستبدل عمل الأخرى.

ولذلك، بشكل عام، فإن تعديل النماذج البدائية يُعد فكرة سيئة.

فى لغات البرمجة الحديثة، توجد حالة واحدة لتعديل النماذج البدائية. وهي تعدد الأشكال polyfilling

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

يمكننا إذن كتابتها يدويّاً وإضافتها للنموذج.

على سبيل المثال:

if (!String.prototype.repeat) {
  // إذا لم توجد هذه الدالة
  // أضفها للنموذج

  String.prototype.repeat = function (n) {
    // كرر النص n من المرات

    // فى الحقيقة، يجب أن يكون الكود أكثر تعقيدًا بقليل من هذا
    // (the full algorithm is in the specification)
    // ولكن حتي تعدد الأشكال الغير كامل غالبًا ما يكون كافيًا
    return new Array(n + 1).join(this);
  };
}

alert("La".repeat(3)); // LaLaLa

الإستعارة من النماذج

فى فصل المزخرفات decorators‌ والتمرير forwarding: التابعان call وapply تحدثنا عن استعارة الدوال.

وهذا يكون عندما نأخذ دالةً ما من كائن وننسخها لكائن آخر.

بعض الدوال غالبًا ما يتم استعارتها من النماذج البدائية.

على سبيل المثال، إذا كنا ننشئ كائنًا شبيهًا بالقائمة، فإننا يمكن أن نريد أن ننسخ بعذ دوال الكائن Array إليه.

مثال:

let obj = {
  0: "Hello",
  1: "world!",
  length: 2,
};

obj.join = Array.prototype.join;

alert( obj.join(',') ); // Hello,world!

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

وهناك إمكانية أخرى وهي الوراثة وذلك عن طريق أن نجعل obj.__proto__ تشير إلى Array.prototype ولذلك فإن كل دوال الكائن Array ستكون متاحة تلقائيًا للكائن obj.

ولكن هذا مستحيل إذا كان الكائن obj يرث بالفعل من كائن آخر، تذكر أننا يمكننا أن نرث من كائن واحد فقط فى المرة الواحدة.

إن استعارة الدوال مرن ويسمح بمزج الوظائف من كائنات مختلفة إذا أردنا ذلك.

الملخص

  • تتبع كل الكائنات الموجودة بالفعل نفس النمط:
    • يتم الإحتفاظ بالدوال فى النموذج (prototype) (Array.prototype, Object.prototype, Date.prototype, إلخ.)
    • يتم تخزين البيانات فقط فى الكائن (عناصر قائمة أو خصائص كائن أو تاريخ)
  • القيم المفردة (Primitives) تخزن الدوال فى نموذج خاص بالكائن الحاوي (wrapper object): Number.prototype و String.prototype و Boolean.prototype. ولا يوجد كائن حاوى للقيمتين undefined و null.
  • النماذج الموجودة بالفعل (Built-in prototypes) يمكن تعديلها أو إضافة دوال جديدة لها، ولكن هذا غير موصيً به، وإن الحالة الوحيدة المسموح فيها بذلك هي عندما نريد أن نذيف وظيفة جديدة موجودة فى المصدر ولكنها مازالت غير مدعومة من محرك جافا سكريبت معين.

مهمه

أضف إلى نموذج الدوال الدالة defer(ms) والتى تقوم بتشغيل الدالة بعد ms مللى ثانية.

بعد أن تفعلها، سيعمل الكود التالى:

function f() {
  alert("Hello!");
}

f.defer(1000); // تعرض كلمة "Hello!" بعد 1 ثانية
Function.prototype.defer = function(ms) {
  setTimeout(this, ms);
};

function f() {
  alert("Hello!");
}

f.defer(1000); // تعرض كلمة "Hello!" بعد 1 ثانية

أضف إلى النموذج الخاص بالدوال الدالة defer(ms)، والتى تقوم بإرجاع حاوى (wrapper) وتؤخر التنفيذ بعد ms مللى ثانية.

هاك مثال لكيفية استخدامها:

function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // تعرض 3 بعد ثانية واحدة

لاحظ أن المتغيرات يجب أن تُمرر إلى الدالة الأصلية.

Function.prototype.defer = function(ms) {
  let f = this;
  return function(...args) {
    setTimeout(() => f.apply(this, args), ms);
  }
};

// اختبر
function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // تعرض 3 بعد ثانية واحدة

لاحظ: استخدمنا this فى f.apply لتعمل مع دوال الكائنات.

ولذلك إذا تم استدعاء دالة كدالة كائن (method) فإن this سيتم تمرريرها إلى الدالة الأصلية f.

Function.prototype.defer = function(ms) {
  let f = this;
  return function(...args) {
    setTimeout(() => f.apply(this, args), ms);
  }
};

let user = {
  name: "John",
  sayHi() {
    alert(this.name);
  }
}

user.sayHi = user.sayHi.defer(1000);

user.sayHi();
خريطة الدورة التعليمية